diff --git a/src/Illuminate/Validation/Rules/File.php b/src/Illuminate/Validation/Rules/File.php index b7589853e9d..0fbe1d81b27 100644 --- a/src/Illuminate/Validation/Rules/File.php +++ b/src/Illuminate/Validation/Rules/File.php @@ -13,10 +13,20 @@ use Illuminate\Support\Traits\Macroable; use InvalidArgumentException; -class File implements Rule, DataAwareRule, ValidatorAwareRule +class File implements DataAwareRule, Rule, ValidatorAwareRule { use Conditionable, Macroable; + /** + * Binary units flag used for size validation. + */ + protected const BINARY = 'binary'; + + /** + * International units flag used for size validation. + */ + protected const INTERNATIONAL = 'international'; + /** * The MIME types that the given file should match. This array may also contain file extensions. * @@ -32,19 +42,24 @@ class File implements Rule, DataAwareRule, ValidatorAwareRule protected $allowedExtensions = []; /** - * The minimum size in kilobytes that the file can be. + * The minimum file size that the file can be. * * @var null|int */ protected $minimumFileSize = null; /** - * The maximum size in kilobytes that the file can be. + * The maximum file size that the file can be. * * @var null|int */ protected $maximumFileSize = null; + /** + * The units used for size validation. + */ + protected string $units = self::INTERNATIONAL; + /** * An array of custom rules that will be merged into the validation rules. * @@ -112,7 +127,7 @@ public static function default() ? call_user_func(static::$defaultCallback) : static::$defaultCallback; - return $file instanceof Rule ? $file : new self(); + return $file instanceof Rule ? $file : new self; } /** @@ -134,7 +149,7 @@ public static function image($allowSvg = false) */ public static function types($mimetypes) { - return tap(new static(), fn ($file) => $file->allowedMimetypes = (array) $mimetypes); + return tap(new static, fn ($file) => $file->allowedMimetypes = (array) $mimetypes); } /** @@ -151,12 +166,29 @@ public function extensions($extensions) } /** - * Indicate that the uploaded file should be exactly a certain size in kilobytes. - * - * @param string|int $size - * @return $this + * Set the units for size validation to binary (1024-based). */ - public function size($size) + public function binary(): static + { + $this->units = self::BINARY; + + return $this; + } + + /** + * Set the units for size validation to international (1000-based). + */ + public function international(): static + { + $this->units = self::INTERNATIONAL; + + return $this; + } + + /** + * Indicate that the uploaded file should be exactly a certain size. + */ + public function size(string|int $size): static { $this->minimumFileSize = $this->toKilobytes($size); $this->maximumFileSize = $this->minimumFileSize; @@ -165,13 +197,9 @@ public function size($size) } /** - * Indicate that the uploaded file should be between a minimum and maximum size in kilobytes. - * - * @param string|int $minSize - * @param string|int $maxSize - * @return $this + * Indicate that the uploaded file should be between a minimum and maximum size. */ - public function between($minSize, $maxSize) + public function between(string|int $minSize, string|int $maxSize): static { $this->minimumFileSize = $this->toKilobytes($minSize); $this->maximumFileSize = $this->toKilobytes($maxSize); @@ -180,12 +208,9 @@ public function between($minSize, $maxSize) } /** - * Indicate that the uploaded file should be no less than the given number of kilobytes. - * - * @param string|int $size - * @return $this + * Indicate that the uploaded file should be no less than the given size. */ - public function min($size) + public function min(string|int $size): static { $this->minimumFileSize = $this->toKilobytes($size); @@ -193,12 +218,9 @@ public function min($size) } /** - * Indicate that the uploaded file should be no more than the given number of kilobytes. - * - * @param string|int $size - * @return $this + * Indicate that the uploaded file should be no more than the given size. */ - public function max($size) + public function max(string|int $size): static { $this->maximumFileSize = $this->toKilobytes($size); @@ -208,26 +230,143 @@ public function max($size) /** * Convert a potentially human-friendly file size to kilobytes. * - * @param string|int $size - * @return mixed + * Supports suffix detection with precedence over instance settings. */ - protected function toKilobytes($size) + protected function toKilobytes(string|int $size): float|int { if (! is_string($size)) { return $size; } - $size = strtolower(trim($size)); + if (($value = $this->parseSize($size)) === false || $value < 0) { + throw new InvalidArgumentException('Invalid numeric value in file size.'); + } + + return $this->detectUnits($size) === self::BINARY + ? $this->toBinaryKilobytes($size, $value) + : $this->toInternationalKilobytes($size, $value); + } + + /** + * Parse the numeric portion from a file size string. + */ + protected function parseSize($size): false|float + { + return filter_var( + is_numeric($size) + ? $size + : Str::before(trim($size), Str::match('/[a-zA-Z]/', trim($size))), + FILTER_VALIDATE_FLOAT, FILTER_FLAG_ALLOW_THOUSAND + ); + } + + /** + * Detect the suffix and determine appropriate units from a file size string + * + * @throws InvalidArgumentException + */ + protected function detectUnits(string $size): string + { + return match (true) { + is_numeric($size) => $this->units, + $this->isBinarySizeString($size) => self::BINARY, + $this->isInternationalSizeString($size) => self::INTERNATIONAL, + default => throw new InvalidArgumentException( + "Invalid file size; units must be one of [kib, mib, gib, tib, kb, mb, gb, tb]. Given: {$size}." + ), + }; + } + + protected function isBinarySizeString(string $size): bool + { + return in_array( + strtolower(substr(trim($size), -3)), + ['kib', 'mib', 'gib', 'tib'], + true + ); + } + + protected function isInternationalSizeString(string $size): bool + { + return in_array( + strtolower(substr(trim($size), -2)), + ['kb', 'mb', 'gb', 'tb'], + true + ); + } - $value = floatval($size); + /** + * Convert a human-friendly file size to kilobytes using the International System. + */ + protected function toInternationalKilobytes(string $size, float $value): float|int + { + return round($this->protectValueFromOverflow( + $this->prepareValueForPrecision($value), + ! is_numeric($size) ? $this->getInternationalMultiplier($size) : 1 + )); + } - return round(match (true) { - Str::endsWith($size, 'kb') => $value * 1, - Str::endsWith($size, 'mb') => $value * 1_000, - Str::endsWith($size, 'gb') => $value * 1_000_000, - Str::endsWith($size, 'tb') => $value * 1_000_000_000, - default => throw new InvalidArgumentException('Invalid file size suffix.'), - }); + /** + * Get the international multiplier for a given size string. + */ + protected function getInternationalMultiplier(string $size): int + { + return match (strtolower(substr(trim($size), -2))) { + 'kb' => 1, + 'mb' => 1_000, + 'gb' => 1_000_000, + 'tb' => 1_000_000_000, + }; + } + + /** + * Convert a human-friendly file size to kilobytes using the Binary System. + */ + protected function toBinaryKilobytes(string $size, float $value): float|int + { + return round($this->protectValueFromOverflow( + $this->prepareValueForPrecision($value), + ! is_numeric($size) ? $this->getBinaryMultiplier($size) : 1 + )); + } + + /** + * Get the binary multiplier for a given size string. + */ + protected function getBinaryMultiplier(string $size): int + { + return match (strtolower(substr(trim($size), -3))) { + 'kib' => 1, + 'mib' => 1_024, + 'gib' => 1_048_576, + 'tib' => 1_073_741_824, + }; + } + + /** + * Converts whole numbers to integers for exact arithmetic while keeping + * fractional numbers as floats; also provides overflow protection by + * falling back to float arithmetic for values too large for integer range. + */ + protected function prepareValueForPrecision(float $value): float|int + { + return $value > PHP_INT_MAX + || $value < PHP_INT_MIN + || ((float) (int) $value) !== $value + ? $value + : (int) $value; + } + + /** + * Protect calculations from integer overflow by switching to float arithmetic when necessary. + */ + protected function protectValueFromOverflow(float|int $value, int $multiplier): float|int + { + return $value > PHP_INT_MAX / $multiplier + || $value < PHP_INT_MIN / $multiplier + || is_float($value) + ? (float) $value * $multiplier + : (int) $value * $multiplier; } /** @@ -283,14 +422,18 @@ protected function buildValidationRules() $rules[] = 'extensions:'.implode(',', array_map(strtolower(...), $this->allowedExtensions)); } - $rules[] = match (true) { - is_null($this->minimumFileSize) && is_null($this->maximumFileSize) => null, - is_null($this->maximumFileSize) => "min:{$this->minimumFileSize}", - is_null($this->minimumFileSize) => "max:{$this->maximumFileSize}", - $this->minimumFileSize !== $this->maximumFileSize => "between:{$this->minimumFileSize},{$this->maximumFileSize}", - default => "size:{$this->minimumFileSize}", + $rule = match (true) { + $this->minimumFileSize === null && $this->maximumFileSize === null => null, + $this->maximumFileSize === null => "min:{$this->minimumFileSize}", + $this->minimumFileSize === null => "max:{$this->maximumFileSize}", + $this->minimumFileSize === $this->maximumFileSize => "size:{$this->minimumFileSize}", + default => "between:{$this->minimumFileSize},{$this->maximumFileSize}", }; + if ($rule) { + $rules[] = $rule; + } + return array_merge(array_filter($rules), $this->customRules); } diff --git a/tests/Validation/ValidationFileRuleTest.php b/tests/Validation/ValidationFileRuleTest.php index 0d95bb2a8e6..21908b091ca 100644 --- a/tests/Validation/ValidationFileRuleTest.php +++ b/tests/Validation/ValidationFileRuleTest.php @@ -12,11 +12,12 @@ use Illuminate\Validation\Rules\File; use Illuminate\Validation\ValidationServiceProvider; use Illuminate\Validation\Validator; +use InvalidArgumentException; use PHPUnit\Framework\TestCase; class ValidationFileRuleTest extends TestCase { - public function testBasic() + public function test_basic() { $this->fails( File::default(), @@ -62,7 +63,7 @@ protected function passes($rule, $values) $this->assertValidationRules($rule, $values, true, []); } - public function testSingleMimetype() + public function test_single_mimetype() { $this->fails( File::types('text/plain'), @@ -76,7 +77,7 @@ public function testSingleMimetype() ); } - public function testMultipleMimeTypes() + public function test_multiple_mime_types() { $this->fails( File::types(['text/plain', 'image/jpeg']), @@ -90,7 +91,7 @@ public function testMultipleMimeTypes() ); } - public function testSingleMime() + public function test_single_mime() { $this->fails( File::types('txt'), @@ -104,7 +105,7 @@ public function testSingleMime() ); } - public function testMultipleMimes() + public function test_multiple_mimes() { $this->fails( File::types(['png', 'jpg', 'jpeg', 'svg']), @@ -121,7 +122,7 @@ public function testMultipleMimes() ); } - public function testMixOfMimetypesAndMimes() + public function test_mix_of_mimetypes_and_mimes() { $this->fails( File::types(['png', 'image/png']), @@ -135,7 +136,7 @@ public function testMixOfMimetypesAndMimes() ); } - public function testSingleExtension() + public function test_single_extension() { $this->fails( File::default()->extensions('png'), @@ -161,7 +162,7 @@ public function testSingleExtension() ); } - public function testMultipleExtensions() + public function test_multiple_extensions() { $this->fails( File::default()->extensions(['png', 'jpeg', 'jpg']), @@ -181,7 +182,7 @@ public function testMultipleExtensions() ); } - public function testImage() + public function test_image() { $this->fails( File::image(), @@ -195,7 +196,7 @@ public function testImage() ); } - public function testImageFailsOnSvgByDefault() + public function test_image_fails_on_svg_by_default() { $maliciousSvgFileWithXSS = UploadedFile::fake()->createWithContent( name: 'foo.svg', @@ -228,7 +229,7 @@ public function testImageFailsOnSvgByDefault() ); } - public function testSize() + public function test_size() { $this->fails( File::default()->size(1024), @@ -245,7 +246,7 @@ public function testSize() ); } - public function testBetween() + public function test_between() { $this->fails( File::default()->between(1024, 2048), @@ -267,7 +268,7 @@ public function testBetween() ); } - public function testMin() + public function test_min() { $this->fails( File::default()->min(1024), @@ -285,7 +286,7 @@ public function testMin() ); } - public function testMinWithHumanReadableSize() + public function test_min_with_human_readable_size() { $this->fails( File::default()->min('1024kb'), @@ -303,7 +304,7 @@ public function testMinWithHumanReadableSize() ); } - public function testMax() + public function test_max() { $this->fails( File::default()->max(1024), @@ -321,7 +322,7 @@ public function testMax() ); } - public function testMaxWithHumanReadableSize() + public function test_max_with_human_readable_size() { $this->fails( File::default()->max('1024kb'), @@ -339,7 +340,7 @@ public function testMaxWithHumanReadableSize() ); } - public function testMaxWithHumanReadableSizeAndMultipleValue() + public function test_max_with_human_readable_size_and_multiple_value() { $this->fails( File::default()->max('1mb'), @@ -357,7 +358,7 @@ public function testMaxWithHumanReadableSizeAndMultipleValue() ); } - public function testMacro() + public function test_macro() { File::macro('toDocument', function () { return static::default()->rules('mimes:txt,csv'); @@ -378,7 +379,7 @@ public function testMacro() ); } - public function testItUsesTheCorrectValidationMessageForFile(): void + public function test_it_uses_the_correct_validation_message_for_file(): void { file_put_contents($path = __DIR__.'/test.json', 'this-is-a-test'); @@ -393,7 +394,7 @@ public function testItUsesTheCorrectValidationMessageForFile(): void unlink($path); } - public function testItCanSetDefaultUsing() + public function test_it_can_set_default_using() { $this->assertInstanceOf(File::class, File::default()); @@ -418,7 +419,7 @@ public function testItCanSetDefaultUsing() ); } - public function testFileSizeConversionWithDifferentUnits() + public function test_file_size_conversion_with_different_units() { $this->passes( File::image()->size('5MB'), @@ -439,6 +440,491 @@ public function testFileSizeConversionWithDifferentUnits() File::image()->size('10xyz'); } + public function test_global_binary_precedence(): void + { + $file1010 = UploadedFile::fake()->create('test.txt', 1010); + + $rule = File::default()->binary(); + + $this->passes( + $rule->max(1024), + $file1010 + ); + + $this->fails( + $rule->max(1000), + $file1010, + ['validation.max.file'] + ); + } + + public function test_global_international_precedence(): void + { + $file1010 = UploadedFile::fake()->create('test.txt', 1010); + + $rule = File::default()->international(); + + $this->passes( + $rule->min('1MB'), + $file1010 + ); + + $this->fails( + $rule->max('1MB'), + $file1010, + ['validation.size.file'] + ); + } + + public function test_defaults_to_international_when_no_global_setting(): void + { + $file1010 = UploadedFile::fake()->create('test.txt', 1010); + + $this->fails( + File::default()->max('1MB'), + $file1010, + ['validation.max.file'] + ); + } + + public function test_numeric_sizes_work_without_units(): void + { + $file1000 = UploadedFile::fake()->create('numeric.txt', 1000); + + $this->passes(File::default()->max(1000), $file1000); + $this->passes(File::default()->binary()->max(1000), $file1000); + $this->passes(File::default()->international()->max(1000), $file1000); + } + + public function test_binary_integer_precision_for_large_file_sizes(): void + { + $file999999 = UploadedFile::fake()->create('large999999.txt', 999999); + + $this->passes( + File::default()->binary()->max('1000000KB'), + $file999999 + ); + + $this->fails( + File::default()->binary()->max('999998KB'), + $file999999, + ['validation.max.file'] + ); + } + + public function test_international_integer_precision_for_large_file_sizes(): void + { + $file999999 = UploadedFile::fake()->create('large999999.txt', 999999); + + $this->passes( + File::default()->international()->max('1000000KB'), + $file999999 + ); + + $this->fails( + File::default()->international()->max('999998KB'), + $file999999, + ['validation.max.file'] + ); + } + + public function test_float_precision_for_fractional_sizes(): void + { + $file512 = UploadedFile::fake()->create('fractional512.txt', 512); + $file500 = UploadedFile::fake()->create('fractional500.txt', 500); + + $this->passes( + File::default()->size('0.5MiB'), + $file512 + ); + + $this->passes( + File::default()->size('0.5MB'), + $file500 + ); + + $this->fails( + File::default()->size('0.5MiB'), + $file500, + ['validation.size.file'] + ); + + $this->fails( + File::default()->size('0.5MB'), + $file512, + ['validation.size.file'] + ); + } + + public function test_binary_vs_international_calculation_accuracy(): void + { + $file1010 = UploadedFile::fake()->create('boundary1010.txt', 1010); + + $this->passes( + File::default()->max('1MiB'), + $file1010 + ); + + $this->fails( + File::default()->max('1MB'), + $file1010, + ['validation.max.file'] + ); + + $file900 = UploadedFile::fake()->create('small900.txt', 900); + + $this->passes( + File::default()->max('1MiB'), + $file900 + ); + + $this->passes( + File::default()->max('1MB'), + $file900 + ); + } + + public function test_binary_large_file_size_precision(): void + { + $file500000 = UploadedFile::fake()->create('huge500000.txt', 500000); + + $this->passes( + File::default()->binary()->between('400MB', '600MB'), + $file500000 + ); + + $this->passes( + File::default()->binary()->max('1GB'), + $file500000 + ); + + $this->fails( + File::default()->binary()->max('488MB'), + $file500000, + ['validation.max.file'] + ); + } + + public function test_international_large_file_size_precision(): void + { + $file500000 = UploadedFile::fake()->create('huge500000.txt', 500000); + + $this->passes( + File::default()->international()->between('400MB', '600MB'), + $file500000 + ); + + $this->passes( + File::default()->international()->max('1GB'), + $file500000 + ); + + $this->fails( + File::default()->international()->max('499MB'), + $file500000, + ['validation.max.file'] + ); + } + + public function test_overflow_protection_for_large_integer_values(): void + { + $fileLarge = UploadedFile::fake()->create('overflow.txt', 2000000000); + + $this->passes( + File::default()->binary()->max('2000000MB'), + $fileLarge + ); + + $this->passes( + File::default()->international()->max('2000000MB'), + $fileLarge + ); + } + + public function test_overflow_protection_with_fractional_values(): void + { + $file1536 = UploadedFile::fake()->create('fractional.txt', 1536); + + $this->passes( + File::default()->size('1.5MiB'), + $file1536 + ); + + $file1500 = UploadedFile::fake()->create('fractional.txt', 1500); + + $this->passes( + File::default()->size('1.5MB'), + $file1500 + ); + } + + public function test_case_insensitive_suffixes(): void + { + $file1024 = UploadedFile::fake()->create('case.txt', 1024); + + $this->passes(File::default()->size('1MiB'), $file1024); + $this->passes(File::default()->size('1mib'), $file1024); + $this->passes(File::default()->size('1Mib'), $file1024); + $this->passes(File::default()->size('1MIB'), $file1024); + } + + public function test_invalid_size_suffix_throws_exception(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'Invalid file size; units must be one of [kib, mib, gib, tib, kb, mb, gb, tb]. Given: 5xyz.' + ); + + File::default()->max('5xyz'); + } + + public function test_zero_and_very_small_file_sizes(): void + { + $fileZero = UploadedFile::fake()->create('zero.txt', 0); + $fileOne = UploadedFile::fake()->create('tiny.txt', 1); + + $this->passes( + File::default()->min('0KB'), + $fileZero + ); + + $this->passes( + File::default()->size('1KB'), + $fileOne + ); + + $this->passes( + File::default()->binary()->max('0.001MB'), + $fileOne + ); + } + + public function test_whitespace_handling_in_file_sizes(): void + { + $file2048 = UploadedFile::fake()->create('whitespace.txt', 2048); + $file2000 = UploadedFile::fake()->create('whitespace.txt', 2000); + + $this->passes( + File::default()->size(' 2MiB '), + $file2048 + ); + + $this->passes( + File::default()->size(' 2MB '), + $file2000 + ); + + $this->passes( + File::default()->size('2 MiB'), + $file2048 + ); + } + + public function test_comma_separated_number_parsing(): void + { + $file1024 = UploadedFile::fake()->create('comma.txt', 1024); + $file10240 = UploadedFile::fake()->create('large.txt', 10240); + + $this->passes( + File::default()->binary()->size('1,024KB'), + $file1024 + ); + + $this->passes( + File::default()->binary()->size('10,240KB'), + $file10240 + ); + + $this->passes( + File::default()->international()->size('1,024KB'), + $file1024 + ); + } + + public function test_negative_file_size_throws_exception(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid numeric value in file size.'); + + File::default()->max('-5MB'); + } + + public function test_binary_fluent_chaining(): void + { + $file1024 = UploadedFile::fake()->create('chain.txt', 1024); + + $rule = File::default()->binary() + ->types(['txt']) + ->min('1MB') + ->max('2MB'); + + $this->passes($rule, $file1024); + } + + public function test_international_fluent_chaining(): void + { + $file1000 = UploadedFile::fake()->create('chain.txt', 1000); + + $rule = File::default()->international() + ->types(['txt']) + ->min('1MB') + ->max('2MB'); + + $this->passes($rule, $file1000); + } + + public function test_units_methods_return_new_instances(): void + { + $binary1 = File::default()->binary(); + $binary2 = File::default()->binary(); + $international1 = File::default()->international(); + + $this->assertNotSame($binary1, $binary2); + $this->assertNotSame($binary1, $international1); + } + + public function test_units_backwards_compatibility(): void + { + $file1000 = UploadedFile::fake()->create('compat.txt', 1000); + + $this->passes( + File::types(['txt'])->max('1MB'), + $file1000 + ); + + $this->passes( + File::default()->between(500, 1500), + $file1000 + ); + + $this->passes( + File::default()->size(1000), + $file1000 + ); + } + + public function test_binary_fluent_method(): void + { + $file1024 = UploadedFile::fake()->create('binary.txt', 1024); + + $rule = File::default() + ->types(['txt']) + ->binary() + ->size('1MiB'); + + $this->passes($rule, $file1024); + } + + public function test_international_fluent_method(): void + { + $file1000 = UploadedFile::fake()->create('international.txt', 1000); + + $rule = File::default() + ->types(['txt']) + ->international() + ->size('1MB'); + + $this->passes($rule, $file1000); + } + + public function test_units_method_chaining(): void + { + $file1024 = UploadedFile::fake()->create('chaining.txt', 1024); + + $rule = File::default() + ->types(['txt']) + ->international() + ->binary() + ->size('1MiB'); + + $this->passes($rule, $file1024); + } + + public function test_instance_methods_return_same_object(): void + { + $originalRule = File::default()->types(['txt']); + + $binaryRule = $originalRule->binary(); + $internationalRule = $originalRule->international(); + + $this->assertSame($originalRule, $binaryRule); + $this->assertSame($originalRule, $internationalRule); + + $newBinary = File::default()->binary(); + $newInternational = File::default()->international(); + + $this->assertNotSame($originalRule, $newBinary); + $this->assertNotSame($originalRule, $newInternational); + } + + public function test_suffix_precedence_over_instance_methods(): void + { + $file1000 = UploadedFile::fake()->create('test.txt', 1000); + $file1030 = UploadedFile::fake()->create('test.txt', 1030); + + $this->passes( + File::default()->binary()->max('1MB'), + $file1000 + ); + + $this->fails( + File::default()->international()->max('1MiB'), + $file1030, + ['validation.max.file'] + ); + } + + public function test_naked_values_fallback_to_instance_methods(): void + { + $file1000 = UploadedFile::fake()->create('numeric.txt', 1000); + + $this->passes( + File::default()->binary()->max(1024), + $file1000 + ); + + $this->passes( + File::default()->international()->max(1000), + $file1000 + ); + + $this->fails( + File::default()->international()->max(999), + $file1000, + ['validation.max.file'] + ); + } + + public function test_comprehensive_binary_suffixes(): void + { + $file1 = UploadedFile::fake()->create('1kb.txt', 1); + $file1024 = UploadedFile::fake()->create('1mb.txt', 1024); + $file1048576 = UploadedFile::fake()->create('1gb.txt', 1048576); + + $this->passes(File::default()->size('1KiB'), $file1); + $this->passes(File::default()->size('1MiB'), $file1024); + $this->passes(File::default()->size('1GiB'), $file1048576); + } + + public function test_mixed_unit_constraints(): void + { + $file1500 = UploadedFile::fake()->create('mixed.txt', 1500); + + $rule = File::default() + ->min('1MB') + ->max('2MiB'); + + $this->passes($rule, $file1500); + + $file500 = UploadedFile::fake()->create('small.txt', 500); + + $this->fails( + $rule, + $file500, + ['validation.between.file'] + ); + } + protected function setUp(): void { $container = Container::getInstance(); @@ -461,5 +947,7 @@ protected function tearDown(): void Facade::clearResolvedInstances(); Facade::setFacadeApplication(null); + + File::$defaultCallback = null; } }