From 6eaa4b3a4d3961d90ba90def9f141367a82ac995 Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Wed, 15 Jan 2025 00:08:38 +0000 Subject: [PATCH 01/26] Update CHANGELOG --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61e779b0f1a..9bc1f72a672 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Release Notes for 11.x -## [Unreleased](https://github.com/laravel/framework/compare/v11.38.1...11.x) +## [Unreleased](https://github.com/laravel/framework/compare/v11.38.2...11.x) + +## [v11.38.2](https://github.com/laravel/framework/compare/v11.38.1...v11.38.2) - 2025-01-15 + +* [11.x] Simplify Codebase by Using `qualifyColumn` Helper Method by [@SanderMuller](https://github.com/SanderMuller) in https://github.com/laravel/framework/pull/54187 +* Revert "Add support for missing Postgres connection options" by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/54195 +* Revert "[11.x] Support DB aggregate by group (new methods)" by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/54196 ## [v11.38.1](https://github.com/laravel/framework/compare/v11.38.0...v11.38.1) - 2025-01-14 From 7594f44a5cb1e528d59cf64e2a6e4d9335e23a7f Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Wed, 15 Jan 2025 14:27:44 -0500 Subject: [PATCH 02/26] [11.x] Replace duplicate `ValidatedInput` functions with `InteractsWithData` trait (#54208) * Replace duplicate functions with InteractsWithData trait * Support keys parameter * Add all keys parameter test * Add enums test * Remove unused imports --- src/Illuminate/Support/ValidatedInput.php | 357 ++-------------------- tests/Support/ValidatedInputTest.php | 15 + 2 files changed, 32 insertions(+), 340 deletions(-) diff --git a/src/Illuminate/Support/ValidatedInput.php b/src/Illuminate/Support/ValidatedInput.php index 669ebdeadd0..f3ac30c79e2 100644 --- a/src/Illuminate/Support/ValidatedInput.php +++ b/src/Illuminate/Support/ValidatedInput.php @@ -4,13 +4,14 @@ use ArrayIterator; use Illuminate\Contracts\Support\ValidatedData; -use Illuminate\Support\Facades\Date; -use stdClass; +use Illuminate\Support\Traits\InteractsWithData; use Symfony\Component\VarDumper\VarDumper; use Traversable; class ValidatedInput implements ValidatedData { + use InteractsWithData; + /** * The underlying input. * @@ -29,149 +30,6 @@ public function __construct(array $input) $this->input = $input; } - /** - * Determine if the validated input has one or more keys. - * - * @param string|array $key - * @return bool - */ - public function exists($key) - { - return $this->has($key); - } - - /** - * Determine if the validated input has one or more keys. - * - * @param mixed $keys - * @return bool - */ - public function has($keys) - { - $keys = is_array($keys) ? $keys : func_get_args(); - - foreach ($keys as $key) { - if (! Arr::has($this->all(), $key)) { - return false; - } - } - - return true; - } - - /** - * Determine if the validated input contains any of the given keys. - * - * @param string|array $keys - * @return bool - */ - public function hasAny($keys) - { - $keys = is_array($keys) ? $keys : func_get_args(); - - $input = $this->all(); - - return Arr::hasAny($input, $keys); - } - - /** - * Determine if the validated input is missing one or more keys. - * - * @param mixed $keys - * @return bool - */ - public function missing($keys) - { - return ! $this->has($keys); - } - - /** - * Apply the callback if the validated input is missing the given input item key. - * - * @param string $key - * @param callable $callback - * @param callable|null $default - * @return $this|mixed - */ - public function whenMissing($key, callable $callback, ?callable $default = null) - { - if ($this->missing($key)) { - return $callback(data_get($this->all(), $key)) ?: $this; - } - - if ($default) { - return $default(); - } - - return $this; - } - - /** - * Retrieve input from the validated input as a Stringable instance. - * - * @param string $key - * @param mixed $default - * @return \Illuminate\Support\Stringable - */ - public function str($key, $default = null) - { - return $this->string($key, $default); - } - - /** - * Retrieve input from the validated input as a Stringable instance. - * - * @param string $key - * @param mixed $default - * @return \Illuminate\Support\Stringable - */ - public function string($key, $default = null) - { - return Str::of($this->input($key, $default)); - } - - /** - * Get a subset containing the provided keys with values from the input data. - * - * @param mixed $keys - * @return array - */ - public function only($keys) - { - $results = []; - - $input = $this->all(); - - $placeholder = new stdClass; - - foreach (is_array($keys) ? $keys : func_get_args() as $key) { - $value = data_get($input, $key, $placeholder); - - if ($value !== $placeholder) { - Arr::set($results, $key, $value); - } - } - - return $results; - } - - /** - * Get all of the input except for a specified array of items. - * - * @param mixed $keys - * @return array - */ - public function except($keys) - { - $keys = is_array($keys) ? $keys : func_get_args(); - - $results = $this->all(); - - Arr::forget($results, $keys); - - return $results; - } - /** * Merge the validated input with the given array of additional data. * @@ -183,137 +41,37 @@ public function merge(array $items) return new static(array_merge($this->all(), $items)); } - /** - * Get the input as a collection. - * - * @param array|string|null $key - * @return \Illuminate\Support\Collection - */ - public function collect($key = null) - { - return new Collection(is_array($key) ? $this->only($key) : $this->input($key)); - } - /** * Get the raw, underlying input array. * + * @param array|mixed|null $keys * @return array */ - public function all() + public function all($keys = null) { - return $this->input; - } - - /** - * Apply the callback if the validated inputs contains the given input item key. - * - * @param string $key - * @param callable $callback - * @param callable|null $default - * @return $this|mixed - */ - public function whenHas($key, callable $callback, ?callable $default = null) - { - if ($this->has($key)) { - return $callback(data_get($this->all(), $key)) ?: $this; - } - - if ($default) { - return $default(); - } - - return $this; - } - - /** - * Determine if the validated inputs contains a non-empty value for an input item. - * - * @param string|array $key - * @return bool - */ - public function filled($key) - { - $keys = is_array($key) ? $key : func_get_args(); - - foreach ($keys as $value) { - if ($this->isEmptyString($value)) { - return false; - } - } - - return true; - } - - /** - * Determine if the validated inputs contains an empty value for an input item. - * - * @param string|array $key - * @return bool - */ - public function isNotFilled($key) - { - $keys = is_array($key) ? $key : func_get_args(); - - foreach ($keys as $value) { - if (! $this->isEmptyString($value)) { - return false; - } + if (! $keys) { + return $this->input; } - return true; - } - - /** - * Determine if the validated inputs contains a non-empty value for any of the given inputs. - * - * @param string|array $keys - * @return bool - */ - public function anyFilled($keys) - { - $keys = is_array($keys) ? $keys : func_get_args(); - - foreach ($keys as $key) { - if ($this->filled($key)) { - return true; - } - } + $input = []; - return false; - } - - /** - * Apply the callback if the validated inputs contains a non-empty value for the given input item key. - * - * @param string $key - * @param callable $callback - * @param callable|null $default - * @return $this|mixed - */ - public function whenFilled($key, callable $callback, ?callable $default = null) - { - if ($this->filled($key)) { - return $callback(data_get($this->all(), $key)) ?: $this; - } - - if ($default) { - return $default(); + foreach (is_array($keys) ? $keys : func_get_args() as $key) { + Arr::set($input, $key, Arr::get($this->input, $key)); } - return $this; + return $input; } /** - * Determine if the given input key is an empty string for "filled". + * Retrieve data from the instance. * - * @param string $key - * @return bool + * @param string|null $key + * @param mixed $default + * @return mixed */ - protected function isEmptyString($key) + protected function data($key = null, $default = null) { - $value = $this->input($key); - - return ! is_bool($value) && ! is_array($value) && trim((string) $value) === ''; + return $this->input($key, $default); } /** @@ -340,87 +98,6 @@ public function input($key = null, $default = null) ); } - /** - * Retrieve input as a boolean value. - * - * Returns true when value is "1", "true", "on", and "yes". Otherwise, returns false. - * - * @param string|null $key - * @param bool $default - * @return bool - */ - public function boolean($key = null, $default = false) - { - return filter_var($this->input($key, $default), FILTER_VALIDATE_BOOLEAN); - } - - /** - * Retrieve input as an integer value. - * - * @param string $key - * @param int $default - * @return int - */ - public function integer($key, $default = 0) - { - return intval($this->input($key, $default)); - } - - /** - * Retrieve input as a float value. - * - * @param string $key - * @param float $default - * @return float - */ - public function float($key, $default = 0.0) - { - return floatval($this->input($key, $default)); - } - - /** - * Retrieve input from the validated inputs as a Carbon instance. - * - * @param string $key - * @param string|null $format - * @param string|null $tz - * @return \Illuminate\Support\Carbon|null - * - * @throws \Carbon\Exceptions\InvalidFormatException - */ - public function date($key, $format = null, $tz = null) - { - if ($this->isNotFilled($key)) { - return null; - } - - if (is_null($format)) { - return Date::parse($this->input($key), $tz); - } - - return Date::createFromFormat($format, $this->input($key), $tz); - } - - /** - * Retrieve input from the validated inputs as an enum. - * - * @template TEnum - * - * @param string $key - * @param class-string $enumClass - * @return TEnum|null - */ - public function enum($key, $enumClass) - { - if ($this->isNotFilled($key) || - ! enum_exists($enumClass) || - ! method_exists($enumClass, 'tryFrom')) { - return null; - } - - return $enumClass::tryFrom($this->input($key)); - } - /** * Dump the validated inputs items and end the script. * diff --git a/tests/Support/ValidatedInputTest.php b/tests/Support/ValidatedInputTest.php index 7b85c1ae30c..fe821604ba1 100644 --- a/tests/Support/ValidatedInputTest.php +++ b/tests/Support/ValidatedInputTest.php @@ -17,6 +17,7 @@ public function test_can_access_input() $this->assertSame('Taylor', $input->name); $this->assertSame('Taylor', $input['name']); + $this->assertEquals(['name' => 'Taylor'], $input->all(['name'])); $this->assertEquals(['name' => 'Taylor'], $input->only(['name'])); $this->assertEquals(['name' => 'Taylor'], $input->except(['votes'])); $this->assertEquals(['name' => 'Taylor', 'votes' => 100], $input->all()); @@ -478,6 +479,20 @@ public function test_enum_method() $this->assertNull($input->enum('invalid_enum_value', StringBackedEnum::class)); } + public function test_enums_method() + { + $input = new ValidatedInput([ + 'valid_enum_value' => 'Hello world', + 'invalid_enum_value' => 'invalid', + ]); + + $this->assertEmpty($input->enums('doesnt_exists', StringBackedEnum::class)); + + $this->assertEquals([StringBackedEnum::HELLO_WORLD], $input->enums('valid_enum_value', StringBackedEnum::class)); + + $this->assertEmpty($input->enums('invalid_enum_value', StringBackedEnum::class)); + } + public function test_collect_method() { $input = new ValidatedInput(['users' => [1, 2, 3]]); From 4ddd68e67e7b347089989479e42a53e26502ff3f Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Wed, 15 Jan 2025 20:32:47 +0100 Subject: [PATCH 03/26] [11.x] Improve `Email` validation rule custom translation messages (#54202) * Test validation messages * Simplify Email rule and make tests pass * Styleci --- src/Illuminate/Validation/Rules/Email.php | 70 +++------ tests/Validation/ValidationEmailRuleTest.php | 144 ++++++++++++++----- 2 files changed, 124 insertions(+), 90 deletions(-) diff --git a/src/Illuminate/Validation/Rules/Email.php b/src/Illuminate/Validation/Rules/Email.php index 850333dac66..fbfb6996ac3 100644 --- a/src/Illuminate/Validation/Rules/Email.php +++ b/src/Illuminate/Validation/Rules/Email.php @@ -2,22 +2,13 @@ namespace Illuminate\Validation\Rules; -use Egulias\EmailValidator\EmailValidator; -use Egulias\EmailValidator\Validation\DNSCheckValidation; -use Egulias\EmailValidator\Validation\Extra\SpoofCheckValidation; -use Egulias\EmailValidator\Validation\MultipleValidationWithAnd; -use Egulias\EmailValidator\Validation\NoRFCWarningsValidation; -use Egulias\EmailValidator\Validation\RFCValidation; -use Illuminate\Container\Container; use Illuminate\Contracts\Validation\DataAwareRule; use Illuminate\Contracts\Validation\Rule; use Illuminate\Contracts\Validation\ValidatorAwareRule; use Illuminate\Support\Arr; -use Illuminate\Support\Collection; use Illuminate\Support\Facades\Validator; use Illuminate\Support\Traits\Conditionable; use Illuminate\Support\Traits\Macroable; -use Illuminate\Validation\Concerns\FilterEmailValidation; use InvalidArgumentException; class Email implements Rule, DataAwareRule, ValidatorAwareRule @@ -199,29 +190,19 @@ public function passes($attribute, $value) return false; } - $emailValidator = Container::getInstance()->make(EmailValidator::class); + $validator = Validator::make( + $this->data, + [$attribute => $this->buildValidationRules()], + $this->validator->customMessages, + $this->validator->customAttributes + ); - $passes = $emailValidator->isValid((string) $value, new MultipleValidationWithAnd($this->buildValidationRules())); - - if (! $passes) { - $this->messages = [trans('validation.email', ['attribute' => $attribute])]; + if ($validator->fails()) { + $this->messages = array_merge($this->messages, $validator->messages()->all()); return false; } - if ($this->customRules) { - $validator = Validator::make( - $this->data, - [$attribute => $this->customRules], - $this->validator->customMessages, - $this->validator->customAttributes - ); - - if ($validator->fails()) { - return $this->fail($validator->messages()->all()); - } - } - return true; } @@ -235,51 +216,36 @@ protected function buildValidationRules() $rules = []; if ($this->rfcCompliant) { - $rules[] = new RFCValidation; + $rules[] = 'rfc'; } if ($this->strictRfcCompliant) { - $rules[] = new NoRFCWarningsValidation; + $rules[] = 'strict'; } if ($this->validateMxRecord) { - $rules[] = new DNSCheckValidation; + $rules[] = 'dns'; } if ($this->preventSpoofing) { - $rules[] = new SpoofCheckValidation; + $rules[] = 'spoof'; } if ($this->nativeValidation) { - $rules[] = new FilterEmailValidation; + $rules[] = 'filter'; } if ($this->nativeValidationWithUnicodeAllowed) { - $rules[] = FilterEmailValidation::unicode(); + $rules[] = 'filter_unicode'; } if ($rules) { - return $rules; + $rules = ['email:'.implode(',', $rules)]; + } else { + $rules = ['email']; } - return [new RFCValidation]; - } - - /** - * Adds the given failures, and return false. - * - * @param array|string $messages - * @return bool - */ - protected function fail($messages) - { - $messages = Collection::wrap($messages) - ->map(fn ($message) => $this->validator->getTranslator()->get($message)) - ->all(); - - $this->messages = array_merge($this->messages, $messages); - - return false; + return array_merge(array_filter($rules), $this->customRules); } /** diff --git a/tests/Validation/ValidationEmailRuleTest.php b/tests/Validation/ValidationEmailRuleTest.php index fb39934fdcd..a6e207910ac 100644 --- a/tests/Validation/ValidationEmailRuleTest.php +++ b/tests/Validation/ValidationEmailRuleTest.php @@ -15,17 +15,20 @@ class ValidationEmailRuleTest extends TestCase { + private const ATTRIBUTE = 'my_email'; + private const ATTRIBUTE_REPLACED = 'my email'; + public function testBasic() { $this->fails( Email::default(), 'foo', - ['validation.email'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], ); $this->fails( Rule::email(), 'foo', - ['validation.email'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], ); $this->passes( @@ -43,34 +46,57 @@ public function testBasic() $this->passes(Rule::email(), null); } - protected function fails($rule, $values, $messages) + /** + * @param mixed $rule + * @param string|array $values + * @param array $expectedMessages + * @param string|null $customValidationMessage + * @return void + */ + protected function fails($rule, $values, $expectedMessages, $customValidationMessage = null) { - $this->assertValidationRules($rule, $values, false, $messages); + $this->assertValidationRules($rule, $values, false, $expectedMessages, $customValidationMessage); } - protected function assertValidationRules($rule, $values, $result, $messages) + /** + * @param mixed $rule + * @param string|array $values + * @param bool $expectToPass + * @param array $expectedMessages + * @param string|null $customValidationMessage + * @return void + */ + protected function assertValidationRules($rule, $values, $expectToPass, $expectedMessages = [], $customValidationMessage = null) { $values = Arr::wrap($values); + $translator = resolve('translator'); + foreach ($values as $value) { $v = new Validator( - resolve('translator'), - ['my_email' => $value], - ['my_email' => is_object($rule) ? clone $rule : $rule] + $translator, + [self::ATTRIBUTE => $value], + [self::ATTRIBUTE => is_object($rule) ? clone $rule : $rule], + $customValidationMessage ? [self::ATTRIBUTE.'.email' => $customValidationMessage] : [] ); - $this->assertSame($result, $v->passes()); + $this->assertSame($expectToPass, $v->passes()); $this->assertSame( - $result ? [] : ['my_email' => $messages], + $expectToPass ? [] : [self::ATTRIBUTE => $expectedMessages], $v->messages()->toArray() ); } } + /** + * @param mixed $rule + * @param string|array $values + * @return void + */ protected function passes($rule, $values) { - $this->assertValidationRules($rule, $values, true, []); + $this->assertValidationRules($rule, $values, true); } public function testStrict() @@ -78,25 +104,25 @@ public function testStrict() $this->fails( (new Email())->rfcCompliant(true), 'invalid.@example.com', - ['validation.email'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], ); $this->fails( Rule::email()->rfcCompliant(true), 'invalid.@example.com', - ['validation.email'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], ); $this->fails( (new Email())->rfcCompliant(true), 'username@sub..example.com', - ['validation.email'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], ); $this->fails( Rule::email()->rfcCompliant(true), 'username@sub..example.com', - ['validation.email'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], ); $this->passes( @@ -115,13 +141,13 @@ public function testDns() $this->fails( (new Email())->validateMxRecord(), 'plainaddress@example.com', - ['validation.email'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], ); $this->fails( Rule::email()->validateMxRecord(), 'plainaddress@example.com', - ['validation.email'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], ); $this->passes( @@ -140,26 +166,26 @@ public function testSpoof() $this->fails( (new Email())->preventSpoofing(), 'admin@examрle.com',// Contains a Cyrillic 'р' (U+0440), not a Latin 'p' - ['validation.email'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], ); $this->fails( Rule::email()->preventSpoofing(), 'admin@examрle.com',// Contains a Cyrillic 'р' (U+0440), not a Latin 'p' - ['validation.email'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], ); $spoofingEmail = 'admin@exam'."\u{0440}".'le.com'; $this->fails( (new Email())->preventSpoofing(), $spoofingEmail, - ['validation.email'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], ); $this->fails( Rule::email()->preventSpoofing(), $spoofingEmail, - ['validation.email'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], ); $this->passes( @@ -188,13 +214,13 @@ public function testFilter() $this->fails( (new Email())->withNativeValidation(), 'tést@domain.com', - ['validation.email'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], ); $this->fails( Rule::email()->withNativeValidation(), 'tést@domain.com', - ['validation.email'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], ); $this->passes( @@ -213,13 +239,13 @@ public function testFilterUnicode() $this->fails( (new Email())->withNativeValidation(true), 'invalid.@example.com', - ['validation.email'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], ); $this->fails( Rule::email()->withNativeValidation(true), 'invalid.@example.com', - ['validation.email'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], ); $this->passes( @@ -248,25 +274,25 @@ public function testRfc() $this->fails( (new Email())->rfcCompliant(), 'invalid.@example.com', - ['validation.email'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], ); $this->fails( Rule::email()->rfcCompliant(), 'invalid.@example.com', - ['validation.email'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], ); $this->fails( (new Email())->rfcCompliant(), 'test👨‍💻@domain.com', - ['validation.email'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], ); $this->fails( Rule::email()->rfcCompliant(), 'test👨‍💻@domain.com', - ['validation.email'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], ); $this->passes( @@ -305,13 +331,13 @@ public function testCombiningRules() $this->fails( (new Email())->rfcCompliant(true)->preventSpoofing()->validateMxRecord(), 'test@example.com', - ['validation.email'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], ); $this->fails( Rule::email()->rfcCompliant(true)->preventSpoofing()->validateMxRecord(), 'test@example.com', - ['validation.email'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], ); $this->passes( @@ -327,13 +353,13 @@ public function testCombiningRules() $this->fails( (new Email())->preventSpoofing()->rfcCompliant(), 'test👨‍💻@domain.com', - ['validation.email'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], ); $this->fails( Rule::email()->preventSpoofing()->rfcCompliant(), 'test👨‍💻@domain.com', - ['validation.email'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], ); $spoofingEmail = 'admin@exam'."\u{0440}".'le.com'; @@ -351,13 +377,13 @@ public function testCombiningRules() $this->fails( (new Email())->rfcCompliant()->preventSpoofing(), $spoofingEmail, - ['validation.email'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], ); $this->fails( Rule::email()->rfcCompliant()->preventSpoofing(), $spoofingEmail, - ['validation.email'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], ); } @@ -408,7 +434,7 @@ public function testItCanSetDefaultUsing() $this->fails( Email::default(), $spoofingEmail, - ['validation.email'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], ); Email::defaults(function () { @@ -427,7 +453,43 @@ public function testItCanSetDefaultUsing() $this->fails( Email::default(), $spoofingEmail, - ['validation.email'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + ); + } + + public function testValidationMessages() + { + Email::defaults(function () { + return Rule::email()->preventSpoofing(); + }); + + $spoofingEmail = 'admin@exam'."\u{0440}".'le.com'; + + $this->fails( + Email::default(), + $spoofingEmail, + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + ); + + $this->fails( + Email::default(), + $spoofingEmail, + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + 'The :attribute must be a valid email address.', + ); + + $this->fails( + Email::default(), + $spoofingEmail, + ['Please check the entered '.self::ATTRIBUTE_REPLACED.", it must be a valid email address, {$spoofingEmail} given."], + 'Please check the entered :attribute, it must be a valid email address, :input given.' + ); + + $this->fails( + Email::default(), + $spoofingEmail, + ['Plain text value'], + 'Plain text value' ); } @@ -436,9 +498,15 @@ protected function setUp(): void $container = Container::getInstance(); $container->bind('translator', function () { - return new Translator( + $translator = new Translator( new ArrayLoader, 'en' ); + + $translator->addLines([ + 'validation.email' => 'The :attribute must be a valid email address.', + ], 'en'); + + return $translator; }); Facade::setFacadeApplication($container); From 51070667d99dadf0a6f1528b8dc61d293d67ed85 Mon Sep 17 00:00:00 2001 From: Luke Kuzmish <42181698+cosmastech@users.noreply.github.com> Date: Wed, 15 Jan 2025 14:36:31 -0500 Subject: [PATCH 04/26] [11.x] Fix deprecation warnings in `optimize:clear` and `optimize` (#54197) * Update OptimizeCommand.php * null coalesce in optimize clear && test --- src/Illuminate/Foundation/Console/OptimizeClearCommand.php | 2 +- src/Illuminate/Foundation/Console/OptimizeCommand.php | 2 +- .../Integration/Foundation/Console/OptimizeClearCommandTest.php | 2 ++ tests/Integration/Foundation/Console/OptimizeCommandTest.php | 2 ++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Foundation/Console/OptimizeClearCommand.php b/src/Illuminate/Foundation/Console/OptimizeClearCommand.php index 3e08ad2de1c..974af84f947 100644 --- a/src/Illuminate/Foundation/Console/OptimizeClearCommand.php +++ b/src/Illuminate/Foundation/Console/OptimizeClearCommand.php @@ -34,7 +34,7 @@ public function handle() { $this->components->info('Clearing cached bootstrap files.'); - $exceptions = Collection::wrap(explode(',', $this->option('except'))) + $exceptions = Collection::wrap(explode(',', $this->option('except') ?? '')) ->map(fn ($except) => trim($except)) ->filter() ->unique() diff --git a/src/Illuminate/Foundation/Console/OptimizeCommand.php b/src/Illuminate/Foundation/Console/OptimizeCommand.php index 2440ba8ab34..ef78e8c0af7 100644 --- a/src/Illuminate/Foundation/Console/OptimizeCommand.php +++ b/src/Illuminate/Foundation/Console/OptimizeCommand.php @@ -34,7 +34,7 @@ public function handle() { $this->components->info('Caching framework bootstrap, configuration, and metadata.'); - $exceptions = Collection::wrap(explode(',', $this->option('except'))) + $exceptions = Collection::wrap(explode(',', $this->option('except') ?? '')) ->map(fn ($except) => trim($except)) ->filter() ->unique() diff --git a/tests/Integration/Foundation/Console/OptimizeClearCommandTest.php b/tests/Integration/Foundation/Console/OptimizeClearCommandTest.php index 5df437cefeb..128ced3f529 100644 --- a/tests/Integration/Foundation/Console/OptimizeClearCommandTest.php +++ b/tests/Integration/Foundation/Console/OptimizeClearCommandTest.php @@ -15,6 +15,8 @@ protected function getPackageProviders($app): array public function testCanListenToOptimizingEvent(): void { + $this->withoutDeprecationHandling(); + $this->artisan('optimize:clear') ->assertSuccessful() ->expectsOutputToContain('ServiceProviderWithOptimizeClear'); diff --git a/tests/Integration/Foundation/Console/OptimizeCommandTest.php b/tests/Integration/Foundation/Console/OptimizeCommandTest.php index 3a1bcd5dd8e..2ea8d7a3f8b 100644 --- a/tests/Integration/Foundation/Console/OptimizeCommandTest.php +++ b/tests/Integration/Foundation/Console/OptimizeCommandTest.php @@ -23,6 +23,8 @@ protected function getPackageProviders($app): array public function testCanListenToOptimizingEvent(): void { + $this->withoutDeprecationHandling(); + $this->artisan('optimize') ->assertSuccessful() ->expectsOutputToContain('my package'); From 0b6d69917dd2b4111d1321b3e04f2409eadbd294 Mon Sep 17 00:00:00 2001 From: Petr Levtonov Date: Wed, 15 Jan 2025 20:57:28 +0100 Subject: [PATCH 05/26] [11.x] Add support for phpredis backoff and max retry config options (#54191) --- .../Redis/Connectors/PhpRedisConnector.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Illuminate/Redis/Connectors/PhpRedisConnector.php b/src/Illuminate/Redis/Connectors/PhpRedisConnector.php index 06618a2ff11..eb854279fe7 100644 --- a/src/Illuminate/Redis/Connectors/PhpRedisConnector.php +++ b/src/Illuminate/Redis/Connectors/PhpRedisConnector.php @@ -85,6 +85,22 @@ protected function createClient(array $config) ); } + if (array_key_exists('max_retries', $config)) { + $client->setOption(Redis::OPT_MAX_RETRIES, $config['max_retries']); + } + + if (array_key_exists('backoff_algorithm', $config)) { + $client->setOption(Redis::OPT_BACKOFF_ALGORITHM, $config['backoff_algorithm']); + } + + if (array_key_exists('backoff_base', $config)) { + $client->setOption(Redis::OPT_BACKOFF_BASE, $config['backoff_base']); + } + + if (array_key_exists('backoff_cap', $config)) { + $client->setOption(Redis::OPT_BACKOFF_CAP, $config['backoff_cap']); + } + $this->establishConnection($client, $config); if (! empty($config['password'])) { From ffa1a12b7ff81690a35487ac583eb8ec0c2660f5 Mon Sep 17 00:00:00 2001 From: Chris Arter Date: Wed, 15 Jan 2025 17:27:55 -0500 Subject: [PATCH 06/26] Introduces UseFactory attribute (#54065) * introduce UseFactory attribute * review updates * Update HasFactory.php --------- Co-authored-by: Taylor Otwell --- .../Eloquent/Attributes/UseFactory.php | 17 ++++++++++ .../Eloquent/Factories/HasFactory.php | 24 +++++++++++++- tests/Database/DatabaseEloquentModelTest.php | 31 +++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 src/Illuminate/Database/Eloquent/Attributes/UseFactory.php diff --git a/src/Illuminate/Database/Eloquent/Attributes/UseFactory.php b/src/Illuminate/Database/Eloquent/Attributes/UseFactory.php new file mode 100644 index 00000000000..9e6493b540a --- /dev/null +++ b/src/Illuminate/Database/Eloquent/Attributes/UseFactory.php @@ -0,0 +1,17 @@ +getAttributes(UseFactory::class); + + if ($attributes !== []) { + $useFactory = $attributes[0]->newInstance(); + + $factory = new $useFactory->factoryClass; + + $factory->guessModelNamesUsing(fn() => static::class); + + return $factory; + } } } diff --git a/tests/Database/DatabaseEloquentModelTest.php b/tests/Database/DatabaseEloquentModelTest.php index e74714ea3fe..1246a9b36a0 100755 --- a/tests/Database/DatabaseEloquentModelTest.php +++ b/tests/Database/DatabaseEloquentModelTest.php @@ -17,6 +17,7 @@ use Illuminate\Database\ConnectionResolverInterface as Resolver; use Illuminate\Database\Eloquent\Attributes\CollectedBy; use Illuminate\Database\Eloquent\Attributes\ObservedBy; +use Illuminate\Database\Eloquent\Attributes\UseFactory; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\ArrayObject; use Illuminate\Database\Eloquent\Casts\AsArrayObject; @@ -30,6 +31,8 @@ use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Concerns\HasUlids; use Illuminate\Database\Eloquent\Concerns\HasUuids; +use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\JsonEncodingException; use Illuminate\Database\Eloquent\MassAssignmentException; use Illuminate\Database\Eloquent\MissingAttributeException; @@ -51,6 +54,7 @@ use ReflectionClass; use stdClass; + include_once 'Enums.php'; class DatabaseEloquentModelTest extends TestCase @@ -3191,6 +3195,18 @@ public function testCollectedByAttribute() $this->assertInstanceOf(CustomEloquentCollection::class, $collection); } + + public function testUseFactoryAttribute() + { + $model = new EloquentModelWithUseFactoryAttribute; + $instance = EloquentModelWithUseFactoryAttribute::factory()->make(['name' => 'test name']); + $factory = EloquentModelWithUseFactoryAttribute::factory(); + $this->assertInstanceOf(EloquentModelWithUseFactoryAttribute::class, $instance); + $this->assertInstanceOf(EloquentModelWithUseFactoryAttributeFactory::class, $model::factory()); + $this->assertInstanceOf(EloquentModelWithUseFactoryAttributeFactory::class, $model::newFactory()); + $this->assertEquals(EloquentModelWithUseFactoryAttribute::class, $factory->modelName()); + $this->assertEquals('test name', $instance->name); // Small smoke test to ensure the factory is working + } } class EloquentTestObserverStub @@ -3990,3 +4006,18 @@ class EloquentModelWithCollectedByAttribute extends Model class CustomEloquentCollection extends Collection { } + + +class EloquentModelWithUseFactoryAttributeFactory extends Factory +{ + public function definition() + { + return []; + } +} + +#[UseFactory(EloquentModelWithUseFactoryAttributeFactory::class)] +class EloquentModelWithUseFactoryAttribute extends Model +{ + use HasFactory; +} \ No newline at end of file From 8f0b0270859ca7e76deeb1494bd1138f9d7fad8e Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Wed, 15 Jan 2025 22:28:22 +0000 Subject: [PATCH 07/26] Apply fixes from StyleCI --- .../Database/Eloquent/Attributes/UseFactory.php | 8 +++++--- src/Illuminate/Database/Eloquent/Factories/HasFactory.php | 3 ++- tests/Database/DatabaseEloquentModelTest.php | 4 +--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Attributes/UseFactory.php b/src/Illuminate/Database/Eloquent/Attributes/UseFactory.php index 9e6493b540a..a186a8d3709 100644 --- a/src/Illuminate/Database/Eloquent/Attributes/UseFactory.php +++ b/src/Illuminate/Database/Eloquent/Attributes/UseFactory.php @@ -10,8 +10,10 @@ class UseFactory /** * Create a new attribute instance. * - * @param class-string $factoryClass + * @param class-string $factoryClass * @return void */ - public function __construct(public string $factoryClass) {} -} \ No newline at end of file + public function __construct(public string $factoryClass) + { + } +} diff --git a/src/Illuminate/Database/Eloquent/Factories/HasFactory.php b/src/Illuminate/Database/Eloquent/Factories/HasFactory.php index d591d2f1751..2078d9f025f 100644 --- a/src/Illuminate/Database/Eloquent/Factories/HasFactory.php +++ b/src/Illuminate/Database/Eloquent/Factories/HasFactory.php @@ -1,6 +1,7 @@ factoryClass; - $factory->guessModelNamesUsing(fn() => static::class); + $factory->guessModelNamesUsing(fn () => static::class); return $factory; } diff --git a/tests/Database/DatabaseEloquentModelTest.php b/tests/Database/DatabaseEloquentModelTest.php index 1246a9b36a0..3271749ddb6 100755 --- a/tests/Database/DatabaseEloquentModelTest.php +++ b/tests/Database/DatabaseEloquentModelTest.php @@ -54,7 +54,6 @@ use ReflectionClass; use stdClass; - include_once 'Enums.php'; class DatabaseEloquentModelTest extends TestCase @@ -4007,7 +4006,6 @@ class CustomEloquentCollection extends Collection { } - class EloquentModelWithUseFactoryAttributeFactory extends Factory { public function definition() @@ -4020,4 +4018,4 @@ public function definition() class EloquentModelWithUseFactoryAttribute extends Model { use HasFactory; -} \ No newline at end of file +} From 206700a948b75b82a26441f214ba2ea8edb9170f Mon Sep 17 00:00:00 2001 From: Luke Kuzmish <42181698+cosmastech@users.noreply.github.com> Date: Thu, 16 Jan 2025 14:21:41 -0500 Subject: [PATCH 08/26] Update UseFactory.php (#54215) --- src/Illuminate/Database/Eloquent/Attributes/UseFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Attributes/UseFactory.php b/src/Illuminate/Database/Eloquent/Attributes/UseFactory.php index a186a8d3709..7f56804e213 100644 --- a/src/Illuminate/Database/Eloquent/Attributes/UseFactory.php +++ b/src/Illuminate/Database/Eloquent/Attributes/UseFactory.php @@ -10,7 +10,7 @@ class UseFactory /** * Create a new attribute instance. * - * @param class-string $factoryClass + * @param class-string<\Illuminate\Database\Eloquent\Factories\Factory> $factoryClass * @return void */ public function __construct(public string $factoryClass) From 1472088e7026f8d87d3c38dfc1bc7e023329a039 Mon Sep 17 00:00:00 2001 From: Ahmed Alaa <92916738+AhmedAlaa4611@users.noreply.github.com> Date: Thu, 16 Jan 2025 21:23:14 +0200 Subject: [PATCH 09/26] removing the middleman to simplify the call stack. (#54216) --- src/Illuminate/Cache/RedisStore.php | 4 ++-- src/Illuminate/Cache/RedisTagSet.php | 2 +- src/Illuminate/Database/Concerns/BuildsQueries.php | 4 ++-- src/Illuminate/Filesystem/Filesystem.php | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Illuminate/Cache/RedisStore.php b/src/Illuminate/Cache/RedisStore.php index c61e925f368..259f2c3fd64 100755 --- a/src/Illuminate/Cache/RedisStore.php +++ b/src/Illuminate/Cache/RedisStore.php @@ -313,7 +313,7 @@ protected function currentTags($chunkSize = 1000) $prefix = $connectionPrefix.$this->getPrefix(); - return LazyCollection::make(function () use ($connection, $chunkSize, $prefix, $defaultCursorValue) { + return (new LazyCollection(function () use ($connection, $chunkSize, $prefix, $defaultCursorValue) { $cursor = $defaultCursorValue; do { @@ -336,7 +336,7 @@ protected function currentTags($chunkSize = 1000) yield $tag; } } while (((string) $cursor) !== $defaultCursorValue); - })->map(fn (string $tagKey) => Str::match('/^'.preg_quote($prefix, '/').'tag:(.*):entries$/', $tagKey)); + }))->map(fn (string $tagKey) => Str::match('/^'.preg_quote($prefix, '/').'tag:(.*):entries$/', $tagKey)); } /** diff --git a/src/Illuminate/Cache/RedisTagSet.php b/src/Illuminate/Cache/RedisTagSet.php index c6aea191a83..267c11607cd 100644 --- a/src/Illuminate/Cache/RedisTagSet.php +++ b/src/Illuminate/Cache/RedisTagSet.php @@ -43,7 +43,7 @@ public function entries() default => '0', }; - return LazyCollection::make(function () use ($connection, $defaultCursorValue) { + return new LazyCollection(function () use ($connection, $defaultCursorValue) { foreach ($this->tagIds() as $tagKey) { $cursor = $defaultCursorValue; diff --git a/src/Illuminate/Database/Concerns/BuildsQueries.php b/src/Illuminate/Database/Concerns/BuildsQueries.php index cf1f6e2db92..385f3f964c4 100644 --- a/src/Illuminate/Database/Concerns/BuildsQueries.php +++ b/src/Illuminate/Database/Concerns/BuildsQueries.php @@ -236,7 +236,7 @@ public function lazy($chunkSize = 1000) $this->enforceOrderBy(); - return LazyCollection::make(function () use ($chunkSize) { + return new LazyCollection(function () use ($chunkSize) { $page = 1; while (true) { @@ -304,7 +304,7 @@ protected function orderedLazyById($chunkSize = 1000, $column = null, $alias = n $alias ??= $column; - return LazyCollection::make(function () use ($chunkSize, $column, $alias, $descending) { + return new LazyCollection(function () use ($chunkSize, $column, $alias, $descending) { $lastId = null; while (true) { diff --git a/src/Illuminate/Filesystem/Filesystem.php b/src/Illuminate/Filesystem/Filesystem.php index 8bba99a2f2e..67e31c0bf4f 100644 --- a/src/Illuminate/Filesystem/Filesystem.php +++ b/src/Illuminate/Filesystem/Filesystem.php @@ -168,7 +168,7 @@ public function lines($path) ); } - return LazyCollection::make(function () use ($path) { + return new LazyCollection(function () use ($path) { $file = new SplFileObject($path); $file->setFlags(SplFileObject::DROP_NEW_LINE); From 2e5054a19ba55b335bf8df43a8ae7e032244f671 Mon Sep 17 00:00:00 2001 From: newapx Date: Fri, 17 Jan 2025 02:31:40 +0400 Subject: [PATCH 10/26] support style file name hashes with query strings in manifest (#54219) support manifests with style file name hashes in query strings instead of file name (for example when using https://github.com/Infomaniak/vite-plugin-query-hash) --- src/Illuminate/Foundation/Vite.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Vite.php b/src/Illuminate/Foundation/Vite.php index 861905cfe0a..b77ea8ed0e5 100644 --- a/src/Illuminate/Foundation/Vite.php +++ b/src/Illuminate/Foundation/Vite.php @@ -800,7 +800,7 @@ protected function makeStylesheetTagWithAttributes($url, $attributes) */ protected function isCssPath($path) { - return preg_match('/\.(css|less|sass|scss|styl|stylus|pcss|postcss)$/', $path) === 1; + return preg_match('/\.(css|less|sass|scss|styl|stylus|pcss|postcss)(\?[^\.]*)?$/', $path) === 1; } /** From 537afdb7cf3b21eef04696b809c060de8050ccba Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Fri, 17 Jan 2025 16:15:36 +0100 Subject: [PATCH 11/26] [11.x] Solidify `Rule::email()` tests (#54226) * Update ValidationEmailRuleTest.php * Update ValidationEmailRuleTest.php * Styleci * styleci * Update ValidationEmailRuleTest.php * Update ValidationEmailRuleTest.php * Update to use TestWith instead of inline arrays * styleci --- tests/Validation/ValidationEmailRuleTest.php | 556 +++++++++++++++---- 1 file changed, 457 insertions(+), 99 deletions(-) diff --git a/tests/Validation/ValidationEmailRuleTest.php b/tests/Validation/ValidationEmailRuleTest.php index a6e207910ac..b6382f508bc 100644 --- a/tests/Validation/ValidationEmailRuleTest.php +++ b/tests/Validation/ValidationEmailRuleTest.php @@ -11,6 +11,7 @@ use Illuminate\Validation\Rules\Email; use Illuminate\Validation\ValidationServiceProvider; use Illuminate\Validation\Validator; +use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; class ValidationEmailRuleTest extends TestCase @@ -23,22 +24,23 @@ public function testBasic() $this->fails( Email::default(), 'foo', - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); + $this->fails( Rule::email(), 'foo', - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); $this->passes( Email::default(), - 'taylor@laravel.com', + 'taylor@laravel.com' ); $this->passes( Rule::email(), - 'taylor@laravel.com', + 'taylor@laravel.com' ); $this->passes(Email::default(), null); @@ -80,11 +82,12 @@ protected function assertValidationRules($rule, $values, $expectToPass, $expecte $customValidationMessage ? [self::ATTRIBUTE.'.email' => $customValidationMessage] : [] ); - $this->assertSame($expectToPass, $v->passes()); + $this->assertSame($expectToPass, $v->passes(), 'Expected email input '.$value.' to '.($expectToPass ? 'pass' : 'fail').'.'); $this->assertSame( $expectToPass ? [] : [self::ATTRIBUTE => $expectedMessages], - $v->messages()->toArray() + $v->messages()->toArray(), + 'Expected different message for email input '.$value, ); } } @@ -99,291 +102,646 @@ protected function passes($rule, $values) $this->assertValidationRules($rule, $values, true); } - public function testStrict() + public function testRfcCompliantStrict() { + $emailThatFailsBothNonStrictButFailsInStrict = 'username@sub..example.com'; + $emailThatPassesNonStrictButFailsInStrict = '"has space"@example.com'; + $emailThatPassesBothNonStrictAndInStrict = 'plainaddress@example.com'; + $this->fails( - (new Email())->rfcCompliant(true), - 'invalid.@example.com', - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + (new Email())->rfcCompliant(strict: true), + $emailThatPassesNonStrictButFailsInStrict, + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); $this->fails( - Rule::email()->rfcCompliant(true), - 'invalid.@example.com', - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + Rule::email()->rfcCompliant(strict: true), + $emailThatPassesNonStrictButFailsInStrict, + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); $this->fails( - (new Email())->rfcCompliant(true), - 'username@sub..example.com', - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + (new Email())->rfcCompliant(strict: true), + $emailThatFailsBothNonStrictButFailsInStrict, + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); $this->fails( - Rule::email()->rfcCompliant(true), - 'username@sub..example.com', - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + Rule::email()->rfcCompliant(strict: true), + $emailThatFailsBothNonStrictButFailsInStrict, + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); $this->passes( - (new Email())->rfcCompliant(true), - 'plainaddress@example.com', + (new Email())->rfcCompliant(strict: true), + $emailThatPassesBothNonStrictAndInStrict ); $this->passes( - Rule::email()->rfcCompliant(true), - 'plainaddress@example.com', + Rule::email()->rfcCompliant(strict: true), + $emailThatPassesBothNonStrictAndInStrict ); } - public function testDns() + public function testValidateMxRecord() { $this->fails( (new Email())->validateMxRecord(), 'plainaddress@example.com', - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); $this->fails( Rule::email()->validateMxRecord(), 'plainaddress@example.com', - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); $this->passes( (new Email())->validateMxRecord(), - 'taylor@laravel.com', + 'taylor@laravel.com' ); $this->passes( Rule::email()->validateMxRecord(), - 'taylor@laravel.com', + 'taylor@laravel.com' ); } - public function testSpoof() + public function testPreventSpoofing() { $this->fails( (new Email())->preventSpoofing(), 'admin@examрle.com',// Contains a Cyrillic 'р' (U+0440), not a Latin 'p' - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); $this->fails( Rule::email()->preventSpoofing(), 'admin@examрle.com',// Contains a Cyrillic 'р' (U+0440), not a Latin 'p' - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); $spoofingEmail = 'admin@exam'."\u{0440}".'le.com'; $this->fails( (new Email())->preventSpoofing(), $spoofingEmail, - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); $this->fails( Rule::email()->preventSpoofing(), $spoofingEmail, - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); $this->passes( (new Email())->preventSpoofing(), - 'admin@example.com', + 'admin@example.com' ); $this->passes( Rule::email()->preventSpoofing(), - 'admin@example.com', + 'admin@example.com' ); $this->passes( (new Email())->preventSpoofing(), - 'test👨‍💻@domain.com', + 'test👨‍💻@domain.com' ); $this->passes( Rule::email()->preventSpoofing(), - 'test👨‍💻@domain.com', + 'test👨‍💻@domain.com' ); } - public function testFilter() + public function testWithNativeValidation() { $this->fails( (new Email())->withNativeValidation(), 'tést@domain.com', - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); $this->fails( Rule::email()->withNativeValidation(), 'tést@domain.com', - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); $this->passes( (new Email())->withNativeValidation(), - 'admin@example.com', + 'admin@example.com' ); $this->passes( Rule::email()->withNativeValidation(), - 'admin@example.com', + 'admin@example.com' ); } - public function testFilterUnicode() + public function testWithNativeValidationAllowUnicode() { $this->fails( - (new Email())->withNativeValidation(true), + (new Email())->withNativeValidation(allowUnicode: true), 'invalid.@example.com', - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); $this->fails( - Rule::email()->withNativeValidation(true), + Rule::email()->withNativeValidation(allowUnicode: true), 'invalid.@example.com', - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); $this->passes( - (new Email())->withNativeValidation(true), - 'tést@domain.com', + (new Email())->withNativeValidation(allowUnicode: true), + 'tést@domain.com' ); $this->passes( - Rule::email()->withNativeValidation(true), - 'tést@domain.com', + Rule::email()->withNativeValidation(allowUnicode: true), + 'tést@domain.com' ); $this->passes( - (new Email())->withNativeValidation(true), - 'admin@example.com', + (new Email())->withNativeValidation(allowUnicode: true), + 'admin@example.com' ); $this->passes( - Rule::email()->withNativeValidation(true), - 'admin@example.com', + Rule::email()->withNativeValidation(allowUnicode: true), + 'admin@example.com' ); } - public function testRfc() + public function testRfcCompliantNonStrict() { $this->fails( (new Email())->rfcCompliant(), 'invalid.@example.com', - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); $this->fails( Rule::email()->rfcCompliant(), 'invalid.@example.com', - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); $this->fails( (new Email())->rfcCompliant(), 'test👨‍💻@domain.com', - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); $this->fails( Rule::email()->rfcCompliant(), 'test👨‍💻@domain.com', - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); $this->passes( (new Email())->rfcCompliant(), - 'admin@example.com', + 'admin@example.com' ); $this->passes( Rule::email()->rfcCompliant(), - 'admin@example.com', + 'admin@example.com' ); $this->passes( (new Email())->rfcCompliant(), - 'tést@domain.com', + 'tést@domain.com' ); $this->passes( Rule::email()->rfcCompliant(), - 'tést@domain.com', + 'tést@domain.com' + ); + } + + #[TestWith(['"has space"@example.com'])] // Quoted local part with space + #[TestWith(['some(comment)@example.com'])] // Comment in local part + #[TestWith(['abc."test"@example.com'])] // Mixed quoted/unquoted local part + #[TestWith(['"escaped\\\"quote"@example.com'])] // Escaped quote inside quoted local part + #[TestWith(['test@example'])] // Domain without TLD + #[TestWith(['test@localhost'])] // Domain without TLD + #[TestWith(['name@[127.0.0.1]'])] // Local-part with domain-literal IPv4 address + #[TestWith(['user@[IPv6:::1]'])] // Domain-literal with unusual IPv6 short form + #[TestWith(['a@[IPv6:2001:db8::1]'])] // Domain-literal with normal IPv6 + #[TestWith(['user@[IPv6:::]'])] // invalid shorthand IPv6 + #[TestWith(['"ab\\(c"@example.com'])] + public function testEmailsThatPassOnRfcCompliantButFailOnStrict($email) + { + $this->passes( + Rule::email()->rfcCompliant(), + $email + ); + + $this->fails( + Rule::email()->rfcCompliant(strict: true), + $email, + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] + ); + } + + #[TestWith(['plainaddress@example.com'])] + #[TestWith(['joe.smith@example.io'])] + #[TestWith(['custom-tag+dev@example.org'])] + #[TestWith(['hyphens--@example.org'])] + #[TestWith(['underscore_name@example.co.uk'])] + #[TestWith(['underscores__@example.org'])] + #[TestWith(['user@subdomain.example.com'])] + #[TestWith(['numbers123@domain.com'])] + #[TestWith(['john-doe@some-domain.com'])] + #[TestWith(['UPPERlower@example.org'])] + #[TestWith(['dots.ok@sub.domain.io'])] + #[TestWith(['some_email+tag@domain.dev'])] + #[TestWith(['a@b.c'])] + #[TestWith(['user@xn--bcher-kva.example'])] + #[TestWith(['user@bücher.example'])] + public function testEmailsThatPassOnBothRfcCompliantAndStrict($email) + { + $this->passes( + Rule::email()->rfcCompliant(), + $email + ); + + $this->passes( + Rule::email()->rfcCompliant(strict: true), + $email + ); + } + + #[TestWith(['invalid.@example.com'])] + #[TestWith(['invalid@.example.com'])] + #[TestWith(['.invalid@example.com'])] + #[TestWith(['invalid@example.com.'])] + #[TestWith(['some..dots@example.com'])] + #[TestWith(['username@sub..example.com'])] + #[TestWith(['test@example..com'])] + #[TestWith(['test@@example.com'])] + #[TestWith(['test👨‍💻@domain.com'])] + #[TestWith(['username@domain-with-hyphen-.com'])] + #[TestWith(['()<>[]:,;@example.com'])] + #[TestWith(['@example.com'])] + #[TestWith(['[test]@example.com'])] + #[TestWith(['user@example.com:3000'])] + #[TestWith(['"unescaped"quote@example.com'])] + #[TestWith(['https://example.com'])] + #[TestWith(['with\\escape@example.com'])] + public function testEmailsThatFailOnBothRfcCompliantAndStrict($email) + { + $this->fails( + Rule::email()->rfcCompliant(), + $email, + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] + ); + + $this->fails( + Rule::email()->rfcCompliant(strict: true), + $email, + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] + ); + } + + #[TestWith(['plainaddress@example.com'])] // Simple valid address + #[TestWith(['joe.smith@example.io'])] // Dotted local part with TLD + #[TestWith(['custom-tag+dev@example.org'])] // Plus tag in local part + #[TestWith(['hyphens--@example.org'])] // Hyphens in local part + #[TestWith(['underscore_name@example.co.uk'])] // Underscore in local part + #[TestWith(['underscores__@example.org'])] // Double underscores in local part + #[TestWith(['user@subdomain.example.com'])] // Subdomain in domain part + #[TestWith(['numbers123@domain.com'])] // Numbers in local part + #[TestWith(['john-doe@some-domain.com'])] // Hyphenated domain + #[TestWith(['UPPERlower@example.org'])] // Mixed case local part + #[TestWith(['dots.ok@sub.domain.io'])] // Dots in local and subdomain + #[TestWith(['some_email+tag@domain.dev'])] // Email with plus tag and underscore + #[TestWith(['a@b.c'])] // Minimal email + #[TestWith(['user@xn--bcher-kva.example'])] // Punycode domain (bücher) + #[TestWith(['user@bücher.example'])] // Unicode domain + public function testEmailsThatPassOnBothRfcCompliantAndRfcCompliantStrict($email) + { + $this->passes( + Rule::email()->rfcCompliant(), + $email + ); + + $this->passes( + Rule::email()->rfcCompliant(strict: true), + $email + ); + } + + #[TestWith(['déjà@example.com'])] + #[TestWith(['测试@example.com'])] + public function testEmailsThatFailWithNativeValidationAsciiPassUnicode($email) + { + $this->fails( + Rule::email()->withNativeValidation(), + $email, + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] + ); + + $this->passes( + Rule::email()->withNativeValidation(allowUnicode: true), + $email + ); + } + + #[TestWith(['test@üñîçødé.com'])] // Unicode domain + #[TestWith(['user@domain..com'])] // Double dots in domain + #[TestWith(['test@.example.com'])] // Domain starts with a dot + #[TestWith(['username@domain-with-hyphen-.com'])] + #[TestWith(['пример@пример.рф'])] // Cyrillic domain + #[TestWith(['例子@例子.公司'])] // Chinese domain + #[TestWith(['name@123.123.123.123'])] // Numeric domain + public function testEmailsThatFailOnBothWithNativeValidationAsciiAndUnicode($email) + { + $this->fails( + Rule::email()->withNativeValidation(), + $email, + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] + ); + + $this->fails( + Rule::email()->withNativeValidation(allowUnicode: true), + $email, + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] + ); + } + + #[TestWith(['user@example.com'])] + #[TestWith(['user.name+tag@example.co.uk'])] + #[TestWith(['joe_smith@example.org'])] + #[TestWith(['user@[IPv6:2001:db8:1ff::a0b:dbd0]'])] + #[TestWith(['test@xn--bcher-kva.com'])] // Punycode for bücher.com + public function testEmailsThatPassBothWithNativeValidationAsciiAndUnicode($email) + { + $this->passes( + Rule::email()->withNativeValidation(), + $email + ); + + $this->passes( + Rule::email()->withNativeValidation(allowUnicode: true), + $email + ); + } + + #[TestWith(['some(comment)@example.com'])] // Comment in local part + #[TestWith(['tést@example.com'])] // Accented local part + #[TestWith(['user@üñîçødé.com'])] // Unicode domain + #[TestWith(['user@bücher.example'])] // Unicode domain + #[TestWith(['"has space"@example.com'])] // Quoted local part with space + #[TestWith(['"escaped\\\"quote"@example.com'])] // Escaped quote inside quoted local part + #[TestWith(['test@localhost'])] // Domain without TLD + #[TestWith(['test@example'])] // Domain without TLD + #[TestWith(['пример@пример.рф'])] // Cyrillic local and domain + #[TestWith(['例子@例子.公司'])] // Chinese local and domain + #[TestWith(['name@123.123.123.123'])] // Numeric domain + public function testEmailsThatFailWithNativeValidationAsciiPassRfcCompliant($email) + { + $this->fails( + Rule::email()->withNativeValidation(), + $email, + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] + ); + + $this->passes( + Rule::email()->rfcCompliant(), + $email + ); + } + + #[TestWith(['plainaddress@example.com'])] // Simple valid address + #[TestWith(['joe.smith@example.io'])] // Dot in local part + #[TestWith(['custom-tag+dev@example.org'])] // Plus tag in local part + #[TestWith(['hyphens--@example.org'])] // Double hyphen in local part + #[TestWith(['underscore_name@example.co.uk'])] // Underscore in local part + #[TestWith(['underscores__@example.org'])] // Double underscores in local part + #[TestWith(['user@subdomain.example.com'])] // Subdomain in domain + #[TestWith(['numbers123@domain.com'])] // Numbers in local part + #[TestWith(['john-doe@some-domain.com'])] // Hyphen in domain + #[TestWith(['UPPERlower@example.org'])] // Mixed-case local part + #[TestWith(['dots.ok@sub.domain.io'])] // Subdomain with dot in local part + #[TestWith(['some_email+tag@domain.dev'])] // Underscore and tag in local part + #[TestWith(['a@b.c'])] // Minimal valid address + #[TestWith(['user@xn--bcher-kva.example'])] // Punycode domain (bücher.example) + #[TestWith(['user_name+tag@example.io'])] // Underscore with tag + #[TestWith(['UPPERCASE@EXAMPLE.IO'])] // All uppercase local and domain + #[TestWith(['abc."test"@example.com'])] // Mixed quoted/unquoted local part + #[TestWith(['name@[127.0.0.1]'])] // IPv4 domain literal + #[TestWith(['user@[IPv6:::1]'])] // IPv6 domain with unusual short form + #[TestWith(['a@[IPv6:2001:db8::1]'])] // IPv6 domain normal form + #[TestWith(['user@[IPv6:2001:db8:1ff::a0b:dbd0]'])] // Fully expanded IPv6 + public function testEmailsThatPassWithNativeValidationAndRfcCompliant($email) + { + $this->passes( + Rule::email()->withNativeValidation(), + $email + ); + + $this->passes( + Rule::email()->rfcCompliant(), + $email + ); + } + + #[TestWith(['test@@example.com'])] // Multiple @ symbols + #[TestWith(['user@domain..com'])] // Double dots in domain + #[TestWith(['.leadingdot@example.com'])] // Leading dot in local part + #[TestWith(['with\\escape@example.com'])] // Backslash in local part + #[TestWith(['@example.com'])] // Missing local part + #[TestWith(['some)@example.com'])] // Unmatched parenthesis in local part + #[TestWith([' space@domain.com'])] // Leading space in local part + #[TestWith(['user@domain:port.com'])] // Colon in domain (mimics a port) + #[TestWith(['username@domain-with-hyphen-.com'])] // Trailing hyphen in domain + public function testEmailsThatFailWithNativeValidationAndRfcCompliant($email) + { + $this->fails( + Rule::email()->withNativeValidation(), + $email, + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] + ); + + $this->fails( + Rule::email()->rfcCompliant(), + $email, + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] + ); + } + + public function testNativeValidationVsRfcCompliant() + { + $emailsThatPassNativeFailRfc = [ + // none I could find + ]; + + foreach ($emailsThatPassNativeFailRfc as $email) { + $this->passes( + Rule::email()->withNativeValidation(), + $email + ); + + $this->fails( + Rule::email()->rfcCompliant(), + $email, + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] + ); + } + } + + #[TestWith(['abc."test"@example.com'])] // Mixed quotes in local part + #[TestWith(['name@[127.0.0.1]'])] // Local-part with domain-literal IPv4 address + #[TestWith(['user@[IPv6:2001:db8::1]'])] // Domain-literal with normal IPv6 + #[TestWith(['user@[IPv6:2001:db8:1ff::a0b:dbd0]'])] // Domain-literal with full IPv6 address + #[TestWith(['"ab\\(c"@example.com'])] // Quoted local part with escaped character + public function testEmailsThatPassNativeValidationFailRfcCompliantStrict($email) + { + $this->passes( + Rule::email()->withNativeValidation(), + $email + ); + + $this->fails( + Rule::email()->rfcCompliant(true), + $email, + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] + ); + } + + #[TestWith(['пример@пример.рф'])] // Unicode domain in Cyrillic script + #[TestWith(['例子@例子.公司'])] // Unicode domain in Chinese script + #[TestWith(['name@123.123.123.123'])] // IP address in domain part + public function testEmailsThatFailNativeValidationPassRfcCompliantStrict($email) + { + $this->fails( + Rule::email()->withNativeValidation(), + $email, + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] + ); + + $this->passes( + Rule::email()->rfcCompliant(true), + $email + ); + } + + #[TestWith(['user@example.com'])] // Simple, valid email + #[TestWith(['joe.smith+dev@example.co.uk'])] // Plus-tagged email with subdomain TLD + #[TestWith(['user!#$%&\'*+/=?^_`{|}~@example.com'])] // Unusual valid characters in local part + public function testEmailsThatPassBothNativeValidationAndRfcCompliantStrict($email) + { + $this->passes( + Rule::email()->withNativeValidation(), + $email + ); + + $this->passes( + Rule::email()->rfcCompliant(true), + $email + ); + } + + #[TestWith(['test@@example.com'])] // Multiple @ + #[TestWith(['.leadingdot@example.com'])] // Leading dot in local part + #[TestWith(['user@domain..com'])] // Double dots in domain + #[TestWith(['test@'])] // Missing domain + #[TestWith(['abc"quote@example.com'])] // Unescaped quote in local part + #[TestWith(['some(comment)@example.com'])] // Local part comment + #[TestWith(['"has space"@example.com'])] // Quoted local part with space + #[TestWith(['user@domain(comment)'])] // Comment in domain + #[TestWith(['user@[127.0.0.1(comment)]'])] // Comment in domain-literal IPv4 address + #[TestWith(['some((double))comment@example.com'])] // Nested comment in local part + #[TestWith(['"test\\\"quote"@example.com'])] // Escaped quote in quoted local part + #[TestWith(['" leading.space"@example.com'])] // Leading space in quoted local part + public function testEmailsThatFailBothNativeValidationAndRfcCompliantStrict($email) + { + $this->fails( + Rule::email()->withNativeValidation(), + $email, + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] + ); + + $this->fails( + Rule::email()->rfcCompliant(true), + $email, + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); } public function testCombiningRules() { $this->passes( - (new Email())->rfcCompliant(true)->preventSpoofing(), - 'test@example.com', + (new Email())->rfcCompliant(strict: true)->preventSpoofing(), + 'test@example.com' ); $this->passes( - Rule::email()->rfcCompliant(true)->preventSpoofing(), - 'test@example.com', + Rule::email()->rfcCompliant(strict: true)->preventSpoofing(), + 'test@example.com' ); $this->fails( - (new Email())->rfcCompliant(true)->preventSpoofing()->validateMxRecord(), + (new Email())->rfcCompliant(strict: true)->preventSpoofing()->validateMxRecord(), 'test@example.com', - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); $this->fails( - Rule::email()->rfcCompliant(true)->preventSpoofing()->validateMxRecord(), + Rule::email()->rfcCompliant(strict: true)->preventSpoofing()->validateMxRecord(), 'test@example.com', - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); $this->passes( (new Email())->preventSpoofing(), - 'test👨‍💻@domain.com', + 'test👨‍💻@domain.com' ); $this->passes( Rule::email()->preventSpoofing(), - 'test👨‍💻@domain.com', + 'test👨‍💻@domain.com' ); $this->fails( (new Email())->preventSpoofing()->rfcCompliant(), 'test👨‍💻@domain.com', - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); $this->fails( Rule::email()->preventSpoofing()->rfcCompliant(), 'test👨‍💻@domain.com', - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); $spoofingEmail = 'admin@exam'."\u{0440}".'le.com'; $this->passes( (new Email())->rfcCompliant(), - $spoofingEmail, + $spoofingEmail ); $this->passes( Rule::email()->rfcCompliant(), - $spoofingEmail, + $spoofingEmail ); $this->fails( (new Email())->rfcCompliant()->preventSpoofing(), $spoofingEmail, - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); $this->fails( Rule::email()->rfcCompliant()->preventSpoofing(), $spoofingEmail, - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); } @@ -407,12 +765,12 @@ public function testMacro() $this->passes( Email::laravelEmployee(), - 'taylor@laravel.com', + 'taylor@laravel.com' ); $this->passes( Rule::email()->laravelEmployee(), - 'taylor@laravel.com', + 'taylor@laravel.com' ); } @@ -424,7 +782,7 @@ public function testItCanSetDefaultUsing() $this->passes( Email::default(), - $spoofingEmail, + $spoofingEmail ); Email::defaults(function () { @@ -434,7 +792,7 @@ public function testItCanSetDefaultUsing() $this->fails( Email::default(), $spoofingEmail, - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); Email::defaults(function () { @@ -443,7 +801,7 @@ public function testItCanSetDefaultUsing() $this->passes( Email::default(), - $spoofingEmail, + $spoofingEmail ); Email::defaults(function () { @@ -453,7 +811,7 @@ public function testItCanSetDefaultUsing() $this->fails( Email::default(), $spoofingEmail, - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); } @@ -468,28 +826,28 @@ public function testValidationMessages() $this->fails( Email::default(), $spoofingEmail, - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'] ); $this->fails( - Email::default(), - $spoofingEmail, - ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], - 'The :attribute must be a valid email address.', + rule: Email::default(), + values: $spoofingEmail, + expectedMessages: ['The '.self::ATTRIBUTE_REPLACED.' must be a valid email address.'], + customValidationMessage: 'The :attribute must be a valid email address.' ); $this->fails( - Email::default(), - $spoofingEmail, - ['Please check the entered '.self::ATTRIBUTE_REPLACED.", it must be a valid email address, {$spoofingEmail} given."], - 'Please check the entered :attribute, it must be a valid email address, :input given.' + rule: Email::default(), + values: $spoofingEmail, + expectedMessages: ['Please check the entered '.self::ATTRIBUTE_REPLACED.", it must be a valid email address, {$spoofingEmail} given."], + customValidationMessage: 'Please check the entered :attribute, it must be a valid email address, :input given.' ); $this->fails( - Email::default(), - $spoofingEmail, - ['Plain text value'], - 'Plain text value' + rule: Email::default(), + values: $spoofingEmail, + expectedMessages: ['Plain text value'], + customValidationMessage: 'Plain text value' ); } From ab0ee268326e95ffa22af16037ae939ff9641eea Mon Sep 17 00:00:00 2001 From: Ahmed Alaa <92916738+AhmedAlaa4611@users.noreply.github.com> Date: Fri, 17 Jan 2025 17:17:14 +0200 Subject: [PATCH 12/26] fixes the line ending error on windows test (#54222) --- tests/Foundation/Console/CliDumperTest.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/Foundation/Console/CliDumperTest.php b/tests/Foundation/Console/CliDumperTest.php index c92245f9a70..96775860c34 100644 --- a/tests/Foundation/Console/CliDumperTest.php +++ b/tests/Foundation/Console/CliDumperTest.php @@ -68,7 +68,10 @@ public function testArray() EOF; - $this->assertSame($expected, $output); + $this->assertSame( + str_replace("\r\n", "\n", $expected), + str_replace("\r\n", "\n", $output) + ); } public function testBoolean() @@ -96,7 +99,10 @@ public function testObject() EOF; - $this->assertSame($expected, $output); + $this->assertSame( + str_replace("\r\n", "\n", $expected), + str_replace("\r\n", "\n", $output) + ); } public function testNull() From 70f250fb8821b3a96be5a4992c573c0b7bb56eb8 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 17 Jan 2025 09:45:40 -0600 Subject: [PATCH 13/26] adjust connection host if it is prefixed with tls --- src/Illuminate/Redis/Connectors/PredisConnector.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Illuminate/Redis/Connectors/PredisConnector.php b/src/Illuminate/Redis/Connectors/PredisConnector.php index 8769fc53f53..50fc39462ce 100644 --- a/src/Illuminate/Redis/Connectors/PredisConnector.php +++ b/src/Illuminate/Redis/Connectors/PredisConnector.php @@ -6,6 +6,7 @@ use Illuminate\Redis\Connections\PredisClusterConnection; use Illuminate\Redis\Connections\PredisConnection; use Illuminate\Support\Arr; +use Illuminate\Support\Str; use Predis\Client; class PredisConnector implements Connector @@ -27,6 +28,11 @@ public function connect(array $config, array $options) $formattedOptions['prefix'] = $config['prefix']; } + if (isset($config['host']) && str_starts_with($config['host'], 'tls://')) { + $config['scheme'] = 'tls'; + $config['host'] = Str::after($config['host'], 'tls://'); + } + return new PredisConnection(new Client($config, $formattedOptions)); } From 0b5ad671bd90cf26ba4fe8f4f48fb4a70f056104 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1vio=20Resende?= Date: Fri, 17 Jan 2025 10:26:48 -0600 Subject: [PATCH 14/26] Add a report/log option to filesystem exceptions without throwing (#54212) * Added a report functionality to filesystem adapter, so filesystem operation errors are logged if wanted/needed. * Style adjustment in tests. * Avoid using report_if due to dependency, and use an optional exception handler from app container instead. * Added the new possible configuration to filesystems.php disks. * formatting * remove tests --------- Co-authored-by: Taylor Otwell --- config/filesystems.php | 3 + .../Filesystem/FilesystemAdapter.php | 49 +++++++ tests/Filesystem/FilesystemAdapterTest.php | 122 ++++++++++++++++++ tests/Filesystem/FilesystemManagerTest.php | 3 + 4 files changed, 177 insertions(+) diff --git a/config/filesystems.php b/config/filesystems.php index c5f244d7fca..c9bd1d09468 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -34,6 +34,7 @@ 'driver' => 'local', 'root' => storage_path('app'), 'throw' => false, + 'report' => false, ], 'public' => [ @@ -42,6 +43,7 @@ 'url' => env('APP_URL').'/storage', 'visibility' => 'public', 'throw' => false, + 'report' => false, ], 's3' => [ @@ -54,6 +56,7 @@ 'endpoint' => env('AWS_ENDPOINT'), 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 'throw' => false, + 'report' => false, ], ], diff --git a/src/Illuminate/Filesystem/FilesystemAdapter.php b/src/Illuminate/Filesystem/FilesystemAdapter.php index 18f14235ad0..1f8e4dc560c 100644 --- a/src/Illuminate/Filesystem/FilesystemAdapter.php +++ b/src/Illuminate/Filesystem/FilesystemAdapter.php @@ -3,6 +3,8 @@ namespace Illuminate\Filesystem; use Closure; +use Illuminate\Container\Container; +use Illuminate\Contracts\Debug\ExceptionHandler; use Illuminate\Contracts\Filesystem\Cloud as CloudFilesystemContract; use Illuminate\Contracts\Filesystem\Filesystem as FilesystemContract; use Illuminate\Http\File; @@ -35,6 +37,7 @@ use Psr\Http\Message\StreamInterface; use RuntimeException; use Symfony\Component\HttpFoundation\StreamedResponse; +use Throwable; /** * @mixin \League\Flysystem\FilesystemOperator @@ -288,6 +291,8 @@ public function get($path) return $this->driver->read($path); } catch (UnableToReadFile $e) { throw_if($this->throwsExceptions(), $e); + + $this->report($e); } } @@ -417,6 +422,8 @@ public function put($path, $contents, $options = []) } catch (UnableToWriteFile|UnableToSetVisibility $e) { throw_if($this->throwsExceptions(), $e); + $this->report($e); + return false; } @@ -502,6 +509,8 @@ public function setVisibility($path, $visibility) } catch (UnableToSetVisibility $e) { throw_if($this->throwsExceptions(), $e); + $this->report($e); + return false; } @@ -560,6 +569,8 @@ public function delete($paths) } catch (UnableToDeleteFile $e) { throw_if($this->throwsExceptions(), $e); + $this->report($e); + $success = false; } } @@ -581,6 +592,8 @@ public function copy($from, $to) } catch (UnableToCopyFile $e) { throw_if($this->throwsExceptions(), $e); + $this->report($e); + return false; } @@ -601,6 +614,8 @@ public function move($from, $to) } catch (UnableToMoveFile $e) { throw_if($this->throwsExceptions(), $e); + $this->report($e); + return false; } @@ -632,6 +647,8 @@ public function checksum(string $path, array $options = []) } catch (UnableToProvideChecksum $e) { throw_if($this->throwsExceptions(), $e); + $this->report($e); + return false; } } @@ -648,6 +665,8 @@ public function mimeType($path) return $this->driver->mimeType($path); } catch (UnableToRetrieveMetadata $e) { throw_if($this->throwsExceptions(), $e); + + $this->report($e); } return false; @@ -673,6 +692,8 @@ public function readStream($path) return $this->driver->readStream($path); } catch (UnableToReadFile $e) { throw_if($this->throwsExceptions(), $e); + + $this->report($e); } } @@ -686,6 +707,8 @@ public function writeStream($path, $resource, array $options = []) } catch (UnableToWriteFile|UnableToSetVisibility $e) { throw_if($this->throwsExceptions(), $e); + $this->report($e); + return false; } @@ -918,6 +941,8 @@ public function makeDirectory($path) } catch (UnableToCreateDirectory|UnableToSetVisibility $e) { throw_if($this->throwsExceptions(), $e); + $this->report($e); + return false; } @@ -937,6 +962,8 @@ public function deleteDirectory($directory) } catch (UnableToDeleteDirectory $e) { throw_if($this->throwsExceptions(), $e); + $this->report($e); + return false; } @@ -1026,6 +1053,28 @@ protected function throwsExceptions(): bool return (bool) ($this->config['throw'] ?? false); } + /** + * @param Throwable $exception + * @return void + * @throws Throwable + */ + protected function report($exception) + { + if ($this->shouldReport() && Container::getInstance()->bound(ExceptionHandler::class)) { + Container::getInstance()->make(ExceptionHandler::class)->report($exception); + } + } + + /** + * Determine if Flysystem exceptions should be reported. + * + * @return bool + */ + protected function shouldReport(): bool + { + return (bool) ($this->config['report'] ?? false); + } + /** * Pass dynamic methods call onto Flysystem. * diff --git a/tests/Filesystem/FilesystemAdapterTest.php b/tests/Filesystem/FilesystemAdapterTest.php index 3d509e0c774..9adba384baa 100644 --- a/tests/Filesystem/FilesystemAdapterTest.php +++ b/tests/Filesystem/FilesystemAdapterTest.php @@ -4,6 +4,8 @@ use Carbon\Carbon; use GuzzleHttp\Psr7\Stream; +use Illuminate\Container\Container; +use Illuminate\Contracts\Debug\ExceptionHandler; use Illuminate\Filesystem\FilesystemAdapter; use Illuminate\Filesystem\FilesystemManager; use Illuminate\Foundation\Application; @@ -545,6 +547,126 @@ public function testThrowExceptionsForMimeType() $this->fail('Exception was not thrown.'); } + public function testReportExceptionsForGet() + { + $container = Container::getInstance(); + + $exceptionHandler = m::mock(ExceptionHandler::class); + + $exceptionHandler->shouldReceive('report') + ->once() + ->andReturnUsing(function (UnableToReadFile $e) { + self::assertStringContainsString( + 'Unable to read file from location: foo.txt.', + $e->getMessage(), + ); + }); + + $container->bind(ExceptionHandler::class, function () use ($exceptionHandler) { + return $exceptionHandler; + }); + + $adapter = new FilesystemAdapter($this->filesystem, $this->adapter, ['report' => true]); + + try { + $adapter->get('/foo.txt'); + } catch (UnableToReadFile) { + $this->fail('Exception was thrown.'); + } + } + + public function testReportExceptionsForReadStream() + { + $container = Container::getInstance(); + + $exceptionHandler = m::mock(ExceptionHandler::class); + + $exceptionHandler->shouldReceive('report') + ->once() + ->andReturnUsing(function (UnableToReadFile $e) { + self::assertStringContainsString( + 'Unable to read file from location: foo.txt.', + $e->getMessage(), + ); + }); + + $container->bind(ExceptionHandler::class, function () use ($exceptionHandler) { + return $exceptionHandler; + }); + + $adapter = new FilesystemAdapter($this->filesystem, $this->adapter, ['report' => true], $exceptionHandler); + + try { + $adapter->readStream('/foo.txt'); + } catch (UnableToReadFile) { + $this->fail('Exception was thrown.'); + } + } + + public function testReportExceptionsForPut() + { + $container = Container::getInstance(); + + $exceptionHandler = m::mock(ExceptionHandler::class); + + $exceptionHandler->shouldReceive('report') + ->once() + ->andReturnUsing(function (UnableToWriteFile $e) { + self::assertStringContainsString( + 'Unable to write file at location: foo.txt.', + $e->getMessage(), + ); + }); + + $container->bind(ExceptionHandler::class, function () use ($exceptionHandler) { + return $exceptionHandler; + }); + + $this->filesystem->write('foo.txt', 'Hello World'); + + chmod(__DIR__.'/tmp/foo.txt', 0400); + + $adapter = new FilesystemAdapter($this->filesystem, $this->adapter, ['report' => true], $exceptionHandler); + + try { + $adapter->put('/foo.txt', 'Hello World!'); + } catch (UnableToWriteFile) { + $this->fail('Exception was thrown.'); + } finally { + chmod(__DIR__.'/tmp/foo.txt', 0600); + } + } + + public function testReportExceptionsForMimeType() + { + $container = Container::getInstance(); + + $exceptionHandler = m::mock(ExceptionHandler::class); + + $exceptionHandler->shouldReceive('report') + ->once() + ->andReturnUsing(function (UnableToRetrieveMetadata $e) { + self::assertStringContainsString( + 'Unable to retrieve the mime_type for file at location: unknown.mime-type.', + $e->getMessage(), + ); + }); + + $container->bind(ExceptionHandler::class, function () use ($exceptionHandler) { + return $exceptionHandler; + }); + + $this->filesystem->write('unknown.mime-type', ''); + + $adapter = new FilesystemAdapter($this->filesystem, $this->adapter, ['report' => true], $exceptionHandler); + + try { + $adapter->mimeType('unknown.mime-type'); + } catch (UnableToRetrieveMetadata) { + $this->fail('Exception was thrown.'); + } + } + public function testGetAllFiles() { $this->filesystem->write('body.txt', 'Hello World'); diff --git a/tests/Filesystem/FilesystemManagerTest.php b/tests/Filesystem/FilesystemManagerTest.php index 8850c5557a0..cfd51f6cb14 100644 --- a/tests/Filesystem/FilesystemManagerTest.php +++ b/tests/Filesystem/FilesystemManagerTest.php @@ -2,12 +2,15 @@ namespace Illuminate\Tests\Filesystem; +use Illuminate\Contracts\Debug\ExceptionHandler; use Illuminate\Contracts\Filesystem\Filesystem; use Illuminate\Filesystem\FilesystemManager; use Illuminate\Foundation\Application; use InvalidArgumentException; +use Mockery as m; use PHPUnit\Framework\Attributes\RequiresOperatingSystem; use PHPUnit\Framework\TestCase; +use Throwable; class FilesystemManagerTest extends TestCase { From c5e1c50fc427c8930e9173b2f4d075213772ae3a Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Fri, 17 Jan 2025 16:27:14 +0000 Subject: [PATCH 15/26] Apply fixes from StyleCI --- src/Illuminate/Filesystem/FilesystemAdapter.php | 1 + tests/Filesystem/FilesystemManagerTest.php | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Illuminate/Filesystem/FilesystemAdapter.php b/src/Illuminate/Filesystem/FilesystemAdapter.php index 1f8e4dc560c..46e23072c58 100644 --- a/src/Illuminate/Filesystem/FilesystemAdapter.php +++ b/src/Illuminate/Filesystem/FilesystemAdapter.php @@ -1056,6 +1056,7 @@ protected function throwsExceptions(): bool /** * @param Throwable $exception * @return void + * * @throws Throwable */ protected function report($exception) diff --git a/tests/Filesystem/FilesystemManagerTest.php b/tests/Filesystem/FilesystemManagerTest.php index cfd51f6cb14..8850c5557a0 100644 --- a/tests/Filesystem/FilesystemManagerTest.php +++ b/tests/Filesystem/FilesystemManagerTest.php @@ -2,15 +2,12 @@ namespace Illuminate\Tests\Filesystem; -use Illuminate\Contracts\Debug\ExceptionHandler; use Illuminate\Contracts\Filesystem\Filesystem; use Illuminate\Filesystem\FilesystemManager; use Illuminate\Foundation\Application; use InvalidArgumentException; -use Mockery as m; use PHPUnit\Framework\Attributes\RequiresOperatingSystem; use PHPUnit\Framework\TestCase; -use Throwable; class FilesystemManagerTest extends TestCase { From 4ae015b32b636c304ef59a7266d20caac09b0538 Mon Sep 17 00:00:00 2001 From: Petr Levtonov Date: Fri, 17 Jan 2025 17:50:05 +0100 Subject: [PATCH 16/26] [11.x] Fix Cache component to be aware of phpredis serialization and compression settings (#54221) * Add phpredis support to the cache component - Fix cache component not being able to handle serialized or compressed phpredis connections * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Cache/LuaScripts.php | 16 ++++ src/Illuminate/Cache/RedisStore.php | 82 ++++++++++++++++--- .../Redis/Connections/PacksPhpRedisValues.php | 11 +++ 3 files changed, 99 insertions(+), 10 deletions(-) diff --git a/src/Illuminate/Cache/LuaScripts.php b/src/Illuminate/Cache/LuaScripts.php index 6d22fcd4357..a61cffe3276 100644 --- a/src/Illuminate/Cache/LuaScripts.php +++ b/src/Illuminate/Cache/LuaScripts.php @@ -4,6 +4,22 @@ class LuaScripts { + /** + * Get the Lua script that sets a key only when it does not yet exist. + * + * KEYS[1] - The name of the key + * ARGV[1] - The value of the key + * ARGV[2] - The number of seconds the key should be valid + * + * @return string + */ + public static function add() + { + return <<<'LUA' +return redis.call('exists',KEYS[1])<1 and redis.call('setex',KEYS[1],ARGV[2],ARGV[1]) +LUA; + } + /** * Get the Lua script to atomically release a lock. * diff --git a/src/Illuminate/Cache/RedisStore.php b/src/Illuminate/Cache/RedisStore.php index 259f2c3fd64..d84c9a50596 100755 --- a/src/Illuminate/Cache/RedisStore.php +++ b/src/Illuminate/Cache/RedisStore.php @@ -68,9 +68,11 @@ public function __construct(Redis $redis, $prefix = '', $connection = 'default') */ public function get($key) { - $value = $this->connection()->get($this->prefix.$key); + $connection = $this->connection(); + + $value = $connection->get($this->prefix.$key); - return ! is_null($value) ? $this->unserialize($value) : null; + return ! is_null($value) ? $this->connectionAwareUnserialize($value, $connection) : null; } /** @@ -89,12 +91,14 @@ public function many(array $keys) $results = []; - $values = $this->connection()->mget(array_map(function ($key) { + $connection = $this->connection(); + + $values = $connection->mget(array_map(function ($key) { return $this->prefix.$key; }, $keys)); foreach ($values as $index => $value) { - $results[$keys[$index]] = ! is_null($value) ? $this->unserialize($value) : null; + $results[$keys[$index]] = ! is_null($value) ? $this->connectionAwareUnserialize($value, $connection) : null; } return $results; @@ -110,8 +114,10 @@ public function many(array $keys) */ public function put($key, $value, $seconds) { - return (bool) $this->connection()->setex( - $this->prefix.$key, (int) max(1, $seconds), $this->serialize($value) + $connection = $this->connection(); + + return (bool) $connection->setex( + $this->prefix.$key, (int) max(1, $seconds), $this->connectionAwareSerialize($value, $connection) ); } @@ -165,10 +171,10 @@ public function putMany(array $values, $seconds) */ public function add($key, $value, $seconds) { - $lua = "return redis.call('exists',KEYS[1])<1 and redis.call('setex',KEYS[1],ARGV[2],ARGV[1])"; + $connection = $this->connection(); - return (bool) $this->connection()->eval( - $lua, 1, $this->prefix.$key, $this->serialize($value), (int) max(1, $seconds) + return (bool) $connection->eval( + LuaScripts::add(), 1, $this->prefix.$key, $this->pack($value, $connection), (int) max(1, $seconds) ); } @@ -205,7 +211,9 @@ public function decrement($key, $value = 1) */ public function forever($key, $value) { - return (bool) $this->connection()->set($this->prefix.$key, $this->serialize($value)); + $connection = $this->connection(); + + return (bool) $connection->set($this->prefix.$key, $this->connectionAwareSerialize($value, $connection)); } /** @@ -414,6 +422,28 @@ public function setPrefix($prefix) $this->prefix = $prefix; } + /** + * Prepare a value to be used with the Redis cache store when used by eval scripts. + * + * @param mixed $value + * @param \Illuminate\Redis\Connections\Connection $connection + * @return mixed + */ + protected function pack($value, $connection) + { + if ($connection instanceof PhpRedisConnection) { + if ($connection->serialized()) { + return $connection->pack([$value])[0]; + } + + if ($connection->compressed()) { + return $connection->pack([$this->serialize($value)])[0]; + } + } + + return $this->serialize($value); + } + /** * Serialize the value. * @@ -435,4 +465,36 @@ protected function unserialize($value) { return is_numeric($value) ? $value : unserialize($value); } + + /** + * Handle connection specific considerations when a value needs to be serialized. + * + * @param mixed $value + * @param \Illuminate\Redis\Connections\Connection $connection + * @return mixed + */ + protected function connectionAwareSerialize($value, $connection) + { + if ($connection instanceof PhpRedisConnection && $connection->serialized()) { + return $value; + } + + return $this->serialize($value); + } + + /** + * Handle connection specific considerations when a value needs to be unserialized. + * + * @param mixed $value + * @param \Illuminate\Redis\Connections\Connection $connection + * @return mixed + */ + protected function connectionAwareUnserialize($value, $connection) + { + if ($connection instanceof PhpRedisConnection && $connection->serialized()) { + return $value; + } + + return $this->unserialize($value); + } } diff --git a/src/Illuminate/Redis/Connections/PacksPhpRedisValues.php b/src/Illuminate/Redis/Connections/PacksPhpRedisValues.php index 4d27ff59aeb..cc7f3109488 100644 --- a/src/Illuminate/Redis/Connections/PacksPhpRedisValues.php +++ b/src/Illuminate/Redis/Connections/PacksPhpRedisValues.php @@ -82,6 +82,17 @@ public function pack(array $values): array return array_map($processor, $values); } + /** + * Determine if serialization is enabled. + * + * @return bool + */ + public function serialized(): bool + { + return defined('Redis::OPT_SERIALIZER') && + $this->client->getOption(Redis::OPT_SERIALIZER) !== Redis::SERIALIZER_NONE; + } + /** * Determine if compression is enabled. * From 1f174fc3cb81849058ae5c35c1b0c29f0d09aa0d Mon Sep 17 00:00:00 2001 From: Mathias Grimm Date: Fri, 17 Jan 2025 14:15:03 -0300 Subject: [PATCH 17/26] [11.x] fix: Forcing DB Session driver to always use the write connection (#54231) * Forcing DB Session driver to always use the write connection * Delete tests/Session/DatabaseSessionHandlerTest.php --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Session/DatabaseSessionHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Session/DatabaseSessionHandler.php b/src/Illuminate/Session/DatabaseSessionHandler.php index 0770c22f46e..f4c1e944132 100644 --- a/src/Illuminate/Session/DatabaseSessionHandler.php +++ b/src/Illuminate/Session/DatabaseSessionHandler.php @@ -288,7 +288,7 @@ public function gc($lifetime): int */ protected function getQuery() { - return $this->connection->table($this->table); + return $this->connection->table($this->table)->useWritePdo(); } /** From eb3918e2e1a1149be94980518ba838a8194d7e3e Mon Sep 17 00:00:00 2001 From: Ahmed Alaa <92916738+AhmedAlaa4611@users.noreply.github.com> Date: Fri, 17 Jan 2025 21:22:37 +0200 Subject: [PATCH 18/26] fixes the line ending error on windows test (#54233) --- .../Blade/BladeComponentTagCompilerTest.php | 45 +++++++++++++++---- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/tests/View/Blade/BladeComponentTagCompilerTest.php b/tests/View/Blade/BladeComponentTagCompilerTest.php index 9461edc14e5..5ebfbb10464 100644 --- a/tests/View/Blade/BladeComponentTagCompilerTest.php +++ b/tests/View/Blade/BladeComponentTagCompilerTest.php @@ -21,7 +21,10 @@ public function testSlotsCanBeCompiled() $result = $this->compiler()->compileSlots(' '); - $this->assertSame("@slot('foo', null, []) \n".' @endslot', trim($result)); + $this->assertSame( + "@slot('foo', null, []) \n".' @endslot', + str_replace("\r\n", "\n", trim($result)) + ); } public function testInlineSlotsCanBeCompiled() @@ -30,7 +33,10 @@ public function testInlineSlotsCanBeCompiled() $result = $this->compiler()->compileSlots(' '); - $this->assertSame("@slot('foo', null, []) \n".' @endslot', trim($result)); + $this->assertSame( + "@slot('foo', null, []) \n".' @endslot', + str_replace("\r\n", "\n", trim($result)) + ); } public function testDynamicSlotsCanBeCompiled() @@ -39,7 +45,10 @@ public function testDynamicSlotsCanBeCompiled() $result = $this->compiler()->compileSlots(' '); - $this->assertSame("@slot(\$foo, null, []) \n".' @endslot', trim($result)); + $this->assertSame( + "@slot(\$foo, null, []) \n".' @endslot', + str_replace("\r\n", "\n", trim($result)) + ); } public function testDynamicSlotsCanBeCompiledWithKeyOfObjects() @@ -48,7 +57,10 @@ public function testDynamicSlotsCanBeCompiledWithKeyOfObjects() $result = $this->compiler()->compileSlots(' '); - $this->assertSame("@slot(\$foo->name, null, []) \n".' @endslot', trim($result)); + $this->assertSame( + "@slot(\$foo->name, null, []) \n".' @endslot', + str_replace("\r\n", "\n", trim($result)) + ); } public function testSlotsWithAttributesCanBeCompiled() @@ -57,7 +69,10 @@ public function testSlotsWithAttributesCanBeCompiled() $result = $this->compiler()->compileSlots(' '); - $this->assertSame("@slot('foo', null, ['class' => 'font-bold']) \n".' @endslot', trim($result)); + $this->assertSame( + "@slot('foo', null, ['class' => 'font-bold']) \n".' @endslot', + str_replace("\r\n", "\n", trim($result)) + ); } public function testInlineSlotsWithAttributesCanBeCompiled() @@ -66,7 +81,10 @@ public function testInlineSlotsWithAttributesCanBeCompiled() $result = $this->compiler()->compileSlots(' '); - $this->assertSame("@slot('foo', null, ['class' => 'font-bold']) \n".' @endslot', trim($result)); + $this->assertSame( + "@slot('foo', null, ['class' => 'font-bold']) \n".' @endslot', + str_replace("\r\n", "\n", trim($result)) + ); } public function testSlotsWithDynamicAttributesCanBeCompiled() @@ -75,7 +93,10 @@ public function testSlotsWithDynamicAttributesCanBeCompiled() $result = $this->compiler()->compileSlots(' '); - $this->assertSame("@slot('foo', null, ['class' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(\$classes)]) \n".' @endslot', trim($result)); + $this->assertSame( + "@slot('foo', null, ['class' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(\$classes)]) \n".' @endslot', + str_replace("\r\n", "\n", trim($result)) + ); } public function testSlotsWithClassDirectiveCanBeCompiled() @@ -84,7 +105,10 @@ public function testSlotsWithClassDirectiveCanBeCompiled() $result = $this->compiler()->compileSlots(' '); - $this->assertSame("@slot('foo', null, ['class' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(\Illuminate\Support\Arr::toCssClasses(\$classes))]) \n".' @endslot', trim($result)); + $this->assertSame( + "@slot('foo', null, ['class' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(\Illuminate\Support\Arr::toCssClasses(\$classes))]) \n".' @endslot', + str_replace("\r\n", "\n", trim($result)) + ); } public function testSlotsWithStyleDirectiveCanBeCompiled() @@ -93,7 +117,10 @@ public function testSlotsWithStyleDirectiveCanBeCompiled() $result = $this->compiler()->compileSlots(' '); - $this->assertSame("@slot('foo', null, ['style' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(\Illuminate\Support\Arr::toCssStyles(\$styles))]) \n".' @endslot', trim($result)); + $this->assertSame( + "@slot('foo', null, ['style' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(\Illuminate\Support\Arr::toCssStyles(\$styles))]) \n".' @endslot', + str_replace("\r\n", "\n", trim($result)) + ); } public function testBasicComponentParsing() From 08c2101b41e1c74826741b9a121428bd958b7c6e Mon Sep 17 00:00:00 2001 From: decaylala Date: Sat, 18 Jan 2025 03:43:24 +0800 Subject: [PATCH 19/26] [11.x] Fix job not logged in failed_jobs table if timeout occurs within database transaction (#54173) * Rollback timeout job transaction to ensure failed jobs are logged in the failed_jobs table * Rollback timeout job transaction to root level * format * refactor * add new stubs instead of changing the old stub * formatting * foramtting --------- Co-authored-by: Taylor Otwell --- .../Bus/DatabaseBatchRepository.php | 2 +- src/Illuminate/Queue/Jobs/Job.php | 20 ++++++++++++++++ .../TimeOutJobWithNestedTransactions.php | 24 +++++++++++++++++++ ...tNonBatchableJobWithNestedTransactions.php | 23 ++++++++++++++++++ .../TimeOutNonBatchableJobWithTransaction.php | 21 ++++++++++++++++ .../Database/Queue/QueueTransactionTest.php | 16 +++++++++++-- 6 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 tests/Integration/Database/Queue/Fixtures/TimeOutJobWithNestedTransactions.php create mode 100644 tests/Integration/Database/Queue/Fixtures/TimeOutNonBatchableJobWithNestedTransactions.php create mode 100644 tests/Integration/Database/Queue/Fixtures/TimeOutNonBatchableJobWithTransaction.php diff --git a/src/Illuminate/Bus/DatabaseBatchRepository.php b/src/Illuminate/Bus/DatabaseBatchRepository.php index d6130ede6c6..2e0c7c68a9e 100644 --- a/src/Illuminate/Bus/DatabaseBatchRepository.php +++ b/src/Illuminate/Bus/DatabaseBatchRepository.php @@ -319,7 +319,7 @@ public function transaction(Closure $callback) */ public function rollBack() { - $this->connection->rollBack(); + $this->connection->rollBack(toLevel: 0); } /** diff --git a/src/Illuminate/Queue/Jobs/Job.php b/src/Illuminate/Queue/Jobs/Job.php index a41b401dddb..8c5c7c76279 100755 --- a/src/Illuminate/Queue/Jobs/Job.php +++ b/src/Illuminate/Queue/Jobs/Job.php @@ -204,6 +204,12 @@ public function fail($e = null) } } + if ($this->shouldRollBackDatabaseTransaction($e)) { + $this->container->make('db') + ->connection($this->container['config']['queue.failed.database']) + ->rollBack(toLevel: 0); + } + try { // If the job has failed, we will delete it, call the "failed" method and then call // an event indicating the job has failed so it can be logged if needed. This is @@ -218,6 +224,20 @@ public function fail($e = null) } } + /** + * Determine if the current database transaction should be rolled back to level zero. + * + * @param \Throwable $e + * @return bool + */ + protected function shouldRollBackDatabaseTransaction($e) + { + return ($e instanceof TimeoutExceededException && + $this->container['config']['queue.failed.database'] && + in_array($this->container['config']['queue.failed.driver'], ['database', 'database-uuids']) && + $this->container->bound('db')); + } + /** * Process an exception that caused the job to fail. * diff --git a/tests/Integration/Database/Queue/Fixtures/TimeOutJobWithNestedTransactions.php b/tests/Integration/Database/Queue/Fixtures/TimeOutJobWithNestedTransactions.php new file mode 100644 index 00000000000..821b7bcac34 --- /dev/null +++ b/tests/Integration/Database/Queue/Fixtures/TimeOutJobWithNestedTransactions.php @@ -0,0 +1,24 @@ + sleep(20)); + }); + } +} diff --git a/tests/Integration/Database/Queue/Fixtures/TimeOutNonBatchableJobWithNestedTransactions.php b/tests/Integration/Database/Queue/Fixtures/TimeOutNonBatchableJobWithNestedTransactions.php new file mode 100644 index 00000000000..1d383e90b8b --- /dev/null +++ b/tests/Integration/Database/Queue/Fixtures/TimeOutNonBatchableJobWithNestedTransactions.php @@ -0,0 +1,23 @@ + sleep(20)); + }); + } +} diff --git a/tests/Integration/Database/Queue/Fixtures/TimeOutNonBatchableJobWithTransaction.php b/tests/Integration/Database/Queue/Fixtures/TimeOutNonBatchableJobWithTransaction.php new file mode 100644 index 00000000000..494e9fbdd70 --- /dev/null +++ b/tests/Integration/Database/Queue/Fixtures/TimeOutNonBatchableJobWithTransaction.php @@ -0,0 +1,21 @@ + sleep(20)); + } +} diff --git a/tests/Integration/Database/Queue/QueueTransactionTest.php b/tests/Integration/Database/Queue/QueueTransactionTest.php index 9bfa3ca87a6..eac0c3fc452 100644 --- a/tests/Integration/Database/Queue/QueueTransactionTest.php +++ b/tests/Integration/Database/Queue/QueueTransactionTest.php @@ -7,6 +7,7 @@ use Illuminate\Tests\Integration\Database\DatabaseTestCase; use Orchestra\Testbench\Attributes\WithConfig; use Orchestra\Testbench\Attributes\WithMigration; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhpExtension; use Symfony\Component\Process\Exception\ProcessSignaledException; use Throwable; @@ -29,9 +30,10 @@ protected function setUp(): void } } - public function testItCanHandleTimeoutJob() + #[DataProvider('timeoutJobs')] + public function testItCanHandleTimeoutJob($job) { - dispatch(new Fixtures\TimeOutJobWithTransaction); + dispatch($job); $this->assertSame(1, DB::table('jobs')->count()); $this->assertSame(0, DB::table('failed_jobs')->count()); @@ -49,4 +51,14 @@ public function testItCanHandleTimeoutJob() $this->assertSame(0, DB::table('jobs')->count()); $this->assertSame(1, DB::table('failed_jobs')->count()); } + + public static function timeoutJobs(): array + { + return [ + [new Fixtures\TimeOutJobWithTransaction()], + [new Fixtures\TimeOutJobWithNestedTransactions()], + [new Fixtures\TimeOutNonBatchableJobWithTransaction()], + [new Fixtures\TimeOutNonBatchableJobWithNestedTransactions()], + ]; + } } From aa01ceddb4f41d618a78d01dd515455b7df3a4fe Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Fri, 17 Jan 2025 19:43:39 +0000 Subject: [PATCH 20/26] Apply fixes from StyleCI --- src/Illuminate/Queue/Jobs/Job.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Queue/Jobs/Job.php b/src/Illuminate/Queue/Jobs/Job.php index 8c5c7c76279..f690b64963b 100755 --- a/src/Illuminate/Queue/Jobs/Job.php +++ b/src/Illuminate/Queue/Jobs/Job.php @@ -232,10 +232,10 @@ public function fail($e = null) */ protected function shouldRollBackDatabaseTransaction($e) { - return ($e instanceof TimeoutExceededException && + return $e instanceof TimeoutExceededException && $this->container['config']['queue.failed.database'] && in_array($this->container['config']['queue.failed.driver'], ['database', 'database-uuids']) && - $this->container->bound('db')); + $this->container->bound('db'); } /** From 66eeb8236dd61a111a2c014d1e6caf2c37149433 Mon Sep 17 00:00:00 2001 From: Zakaria AKTOUF <101515566+zackAJ@users.noreply.github.com> Date: Fri, 17 Jan 2025 23:56:39 +0100 Subject: [PATCH 21/26] [11.x] Fix unique job lock is not released on model not found exception, lock gets stuck. (#54000) * added the test * rewrote the test * release unique job lock without instantiating a job instance using a hidden context wrapper, only when ModelNotFound exception is thrown. * refactor: test * refactor: changed the line for ensureUniqueJobLockIsReleasedWithoutInstance() * refactor: changed the name of context keys to a framework specific one * refactor: oneliners * refactor: code style * changed test to mock real queues * test was not working as expected in local, changed the location of the releasing of the lock to be before all returns * adding runQueueWorkerCommand for ci * formatting * formatting * formatting * formatting --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Bus/UniqueLock.php | 2 +- .../Foundation/Bus/PendingDispatch.php | 9 +++ .../Queue/InteractsWithUniqueJobs.php | 55 +++++++++++++++++++ src/Illuminate/Queue/CallQueuedHandler.php | 33 +++++++++++ tests/Integration/Queue/UniqueJobTest.php | 37 +++++++++++++ 5 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 src/Illuminate/Foundation/Queue/InteractsWithUniqueJobs.php diff --git a/src/Illuminate/Bus/UniqueLock.php b/src/Illuminate/Bus/UniqueLock.php index dea12303b71..9a2726e9d8a 100644 --- a/src/Illuminate/Bus/UniqueLock.php +++ b/src/Illuminate/Bus/UniqueLock.php @@ -64,7 +64,7 @@ public function release($job) * @param mixed $job * @return string */ - protected function getKey($job) + public static function getKey($job) { $uniqueId = method_exists($job, 'uniqueId') ? $job->uniqueId() diff --git a/src/Illuminate/Foundation/Bus/PendingDispatch.php b/src/Illuminate/Foundation/Bus/PendingDispatch.php index fb85b52b7a6..d7a299d42f8 100644 --- a/src/Illuminate/Foundation/Bus/PendingDispatch.php +++ b/src/Illuminate/Foundation/Bus/PendingDispatch.php @@ -7,9 +7,12 @@ use Illuminate\Contracts\Bus\Dispatcher; use Illuminate\Contracts\Cache\Repository as Cache; use Illuminate\Contracts\Queue\ShouldBeUnique; +use Illuminate\Foundation\Queue\InteractsWithUniqueJobs; class PendingDispatch { + use InteractsWithUniqueJobs; + /** * The job. * @@ -207,12 +210,18 @@ public function __call($method, $parameters) */ public function __destruct() { + $this->addUniqueJobInformationToContext($this->job); + if (! $this->shouldDispatch()) { + $this->removeUniqueJobInformationFromContext($this->job); + return; } elseif ($this->afterResponse) { app(Dispatcher::class)->dispatchAfterResponse($this->job); } else { app(Dispatcher::class)->dispatch($this->job); } + + $this->removeUniqueJobInformationFromContext($this->job); } } diff --git a/src/Illuminate/Foundation/Queue/InteractsWithUniqueJobs.php b/src/Illuminate/Foundation/Queue/InteractsWithUniqueJobs.php new file mode 100644 index 00000000000..064c3f09423 --- /dev/null +++ b/src/Illuminate/Foundation/Queue/InteractsWithUniqueJobs.php @@ -0,0 +1,55 @@ + $this->getUniqueJobCacheStore($job), + 'laravel_unique_job_key' => UniqueLock::getKey($job), + ]); + } + } + + /** + * Remove the unique job information from the context. + * + * @param mixed $job + * @return void + */ + public function removeUniqueJobInformationFromContext($job): void + { + if ($job instanceof ShouldBeUnique) { + Context::forgetHidden([ + 'laravel_unique_job_cache_store', + 'laravel_unique_job_key', + ]); + } + } + + /** + * Determine the cache store used by the unique job to acquire locks. + * + * @param mixed $job + * @return string|null + */ + protected function getUniqueJobCacheStore($job): ?string + { + return method_exists($job, 'uniqueVia') + ? $job->uniqueVia()->getName() + : config('cache.default'); + } +} diff --git a/src/Illuminate/Queue/CallQueuedHandler.php b/src/Illuminate/Queue/CallQueuedHandler.php index 4f2e9e9ce9e..4bd5aa30feb 100644 --- a/src/Illuminate/Queue/CallQueuedHandler.php +++ b/src/Illuminate/Queue/CallQueuedHandler.php @@ -6,6 +6,7 @@ use Illuminate\Bus\Batchable; use Illuminate\Bus\UniqueLock; use Illuminate\Contracts\Bus\Dispatcher; +use Illuminate\Contracts\Cache\Factory as CacheFactory; use Illuminate\Contracts\Cache\Repository as Cache; use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Encryption\Encrypter; @@ -13,6 +14,7 @@ use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; use Illuminate\Database\Eloquent\ModelNotFoundException; +use Illuminate\Log\Context\Repository as ContextRepository; use Illuminate\Pipeline\Pipeline; use Illuminate\Queue\Attributes\DeleteWhenMissingModels; use ReflectionClass; @@ -227,6 +229,8 @@ protected function handleModelNotFound(Job $job, $e) $shouldDelete = false; } + $this->ensureUniqueJobLockIsReleasedViaContext(); + if ($shouldDelete) { return $job->delete(); } @@ -234,6 +238,35 @@ protected function handleModelNotFound(Job $job, $e) return $job->fail($e); } + /** + * Ensure the lock for a unique job is released via context. + * + * This is required when we can't unserialize the job due to missing models. + * + * @return void + */ + protected function ensureUniqueJobLockIsReleasedViaContext() + { + if (! $this->container->bound(ContextRepository::class) || + ! $this->container->bound(CacheFactory::class)) { + return; + } + + $context = $this->container->make(ContextRepository::class); + + [$store, $key] = [ + $context->getHidden('laravel_unique_job_cache_store'), + $context->getHidden('laravel_unique_job_key'), + ]; + + if ($store && $key) { + $this->container->make(CacheFactory::class) + ->store($store) + ->lock($key) + ->forceRelease(); + } + } + /** * Call the failed method on the job instance. * diff --git a/tests/Integration/Queue/UniqueJobTest.php b/tests/Integration/Queue/UniqueJobTest.php index 36eb9aeb5cb..82e567282a6 100644 --- a/tests/Integration/Queue/UniqueJobTest.php +++ b/tests/Integration/Queue/UniqueJobTest.php @@ -8,10 +8,14 @@ use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Database\Eloquent\ModelNotFoundException; +use Illuminate\Foundation\Auth\User; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Bus; use Orchestra\Testbench\Attributes\WithMigration; +use Orchestra\Testbench\Factories\UserFactory; #[WithMigration] #[WithMigration('cache')] @@ -130,6 +134,28 @@ public function testLockCanBeReleasedBeforeProcessing() $this->assertTrue($this->app->get(Cache::class)->lock($this->getLockKey($job), 10)->get()); } + public function testLockIsReleasedOnModelNotFoundException() + { + UniqueTestSerializesModelsJob::$handled = false; + + /** @var \Illuminate\Foundation\Auth\User */ + $user = UserFactory::new()->create(); + $job = new UniqueTestSerializesModelsJob($user); + + $this->expectException(ModelNotFoundException::class); + + try { + $user->delete(); + dispatch($job); + $this->runQueueWorkerCommand(['--once' => true]); + unserialize(serialize($job)); + } finally { + $this->assertFalse($job::$handled); + $this->assertModelMissing($user); + $this->assertTrue($this->app->get(Cache::class)->lock($this->getLockKey($job), 10)->get()); + } + } + protected function getLockKey($job) { return 'laravel_unique_job:'.(is_string($job) ? $job : get_class($job)).':'; @@ -185,3 +211,14 @@ class UniqueUntilStartTestJob extends UniqueTestJob implements ShouldBeUniqueUnt { public $tries = 2; } + +class UniqueTestSerializesModelsJob extends UniqueTestJob +{ + use SerializesModels; + + public $deleteWhenMissingModels = true; + + public function __construct(public User $user) + { + } +} From ad20dd51b4d00c625aa7462cc9a1672beca19f85 Mon Sep 17 00:00:00 2001 From: Ahmed Alaa <92916738+AhmedAlaa4611@users.noreply.github.com> Date: Sat, 18 Jan 2025 01:00:23 +0200 Subject: [PATCH 22/26] fixes the line ending error on windows test (#54236) --- tests/Http/HttpClientTest.php | 5 ++++- tests/View/Blade/BladeComponentsTest.php | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index 334fe3eb947..64745bf4998 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -838,7 +838,10 @@ public function testSequenceBuilder() $this->assertSame(200, $response->status()); $response = $this->factory->get('https://example.com'); - $this->assertSame("This is a story about something that happened long ago when your grandfather was a child.\n", $response->body()); + $this->assertSame( + "This is a story about something that happened long ago when your grandfather was a child.\n", + str_replace("\r\n", "\n", $response->body()) + ); $this->assertSame(200, $response->status()); $response = $this->factory->get('https://example.com'); diff --git a/tests/View/Blade/BladeComponentsTest.php b/tests/View/Blade/BladeComponentsTest.php index 64231b4fd09..92d89e6ef5b 100644 --- a/tests/View/Blade/BladeComponentsTest.php +++ b/tests/View/Blade/BladeComponentsTest.php @@ -16,12 +16,12 @@ public function testComponentsAreCompiled() public function testClassComponentsAreCompiled() { - $this->assertSame(' + $this->assertSame(str_replace("\r\n", "\n", ' "bar"] + (isset($attributes) && $attributes instanceof Illuminate\View\ComponentAttributeBag ? $attributes->all() : [])); ?> withName(\'test\'); ?> shouldRender()): ?> -startComponent($component->resolveView(), $component->data()); ?>', $this->compiler->compileString('@component(\'Illuminate\Tests\View\Blade\ComponentStub::class\', \'test\', ["foo" => "bar"])')); +startComponent($component->resolveView(), $component->data()); ?>'), $this->compiler->compileString('@component(\'Illuminate\Tests\View\Blade\ComponentStub::class\', \'test\', ["foo" => "bar"])')); } public function testEndComponentsAreCompiled() @@ -35,7 +35,7 @@ public function testEndComponentClassesAreCompiled() { $this->compiler->newComponentHash('foo'); - $this->assertSame('renderComponent(); ?> + $this->assertSame(str_replace("\r\n", "\n", 'renderComponent(); ?> @@ -44,7 +44,7 @@ public function testEndComponentClassesAreCompiled() -', $this->compiler->compileString('@endcomponentClass')); +'), $this->compiler->compileString('@endcomponentClass')); } public function testSlotsAreCompiled() From fffdd53a608c79b64d18a72398435c634d5db6ac Mon Sep 17 00:00:00 2001 From: Rainer Bendig Date: Sat, 18 Jan 2025 21:59:17 +0100 Subject: [PATCH 23/26] =?UTF-8?q?Added=20support=20in=20DB::prohibitDestru?= =?UTF-8?q?ctiveCommands=20to=20preventing=20destructive=20Rollback?= =?UTF-8?q?=E2=80=A6=20(#54238)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added support in DB::prohibitDestructiveCommands to prohibit RollbackCommand Signed-off-by: Rainer Bendig * fixed use of Illuminate\Console\Command; Signed-off-by: Rainer Bendig --------- Signed-off-by: Rainer Bendig --- .../Database/Console/Migrations/RollbackCommand.php | 9 ++++++--- src/Illuminate/Support/Facades/DB.php | 2 ++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Database/Console/Migrations/RollbackCommand.php b/src/Illuminate/Database/Console/Migrations/RollbackCommand.php index 0a88ec5a1a0..8846a5e376c 100755 --- a/src/Illuminate/Database/Console/Migrations/RollbackCommand.php +++ b/src/Illuminate/Database/Console/Migrations/RollbackCommand.php @@ -2,7 +2,9 @@ namespace Illuminate\Database\Console\Migrations; +use Illuminate\Console\Command; use Illuminate\Console\ConfirmableTrait; +use Illuminate\Console\Prohibitable; use Illuminate\Database\Migrations\Migrator; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputOption; @@ -10,7 +12,7 @@ #[AsCommand('migrate:rollback')] class RollbackCommand extends BaseCommand { - use ConfirmableTrait; + use ConfirmableTrait, Prohibitable; /** * The console command name. @@ -53,8 +55,9 @@ public function __construct(Migrator $migrator) */ public function handle() { - if (! $this->confirmToProceed()) { - return 1; + if ($this->isProhibited() || + ! $this->confirmToProceed()) { + return Command::FAILURE; } $this->migrator->usingConnection($this->option('database'), function () { diff --git a/src/Illuminate/Support/Facades/DB.php b/src/Illuminate/Support/Facades/DB.php index cc282903ec7..c2d4249727d 100644 --- a/src/Illuminate/Support/Facades/DB.php +++ b/src/Illuminate/Support/Facades/DB.php @@ -5,6 +5,7 @@ use Illuminate\Database\Console\Migrations\FreshCommand; use Illuminate\Database\Console\Migrations\RefreshCommand; use Illuminate\Database\Console\Migrations\ResetCommand; +use Illuminate\Database\Console\Migrations\RollbackCommand; use Illuminate\Database\Console\WipeCommand; /** @@ -132,6 +133,7 @@ public static function prohibitDestructiveCommands(bool $prohibit = true) FreshCommand::prohibit($prohibit); RefreshCommand::prohibit($prohibit); ResetCommand::prohibit($prohibit); + RollbackCommand::prohibit($prohibit); WipeCommand::prohibit($prohibit); } From 122e5c2a9ac6680bd0e2a4fe4db6756f01393dcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bat=C4=B1n=20Mu=C5=9Ftu?= <74476363+batinmustu@users.noreply.github.com> Date: Mon, 20 Jan 2025 19:23:48 +0300 Subject: [PATCH 24/26] [11.x] Add applyAfterQueryCallbacks Support to Non-Mutator Cases in pluck Method (#54268) This PR enhances the pluck method by introducing support for applyAfterQueryCallbacks in cases where mutators are not involved. --- src/Illuminate/Database/Eloquent/Builder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index 9a4642d3fea..78bad127854 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -950,7 +950,7 @@ public function pluck($column, $key = null) if (! $this->model->hasAnyGetMutator($column) && ! $this->model->hasCast($column) && ! in_array($column, $this->model->getDates())) { - return $results; + return $this->applyAfterQueryCallbacks($results); } return $this->applyAfterQueryCallbacks( From 4389c7f870e25360c56940c625bbcc42fee2ff51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sel=C3=A7uk=20=C3=87ukur?= <5716652+selcukcukur@users.noreply.github.com> Date: Tue, 21 Jan 2025 01:35:02 +0300 Subject: [PATCH 25/26] [11.x] `addPath()` Allow adding new path for translation loader. (#54277) * It allows adding new directories with the `addLocation` method. * Update FileLoader.php * Update FileLoader.php * Update FileLoader.php --------- Co-authored-by: Taylor Otwell --- src/Illuminate/Translation/FileLoader.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Illuminate/Translation/FileLoader.php b/src/Illuminate/Translation/FileLoader.php index b324e7aaecc..65c4c4abc32 100755 --- a/src/Illuminate/Translation/FileLoader.php +++ b/src/Illuminate/Translation/FileLoader.php @@ -182,6 +182,17 @@ public function namespaces() return $this->hints; } + /** + * Add a new path to the loader. + * + * @param string $path + * @return void + */ + public function addPath($path) + { + $this->paths[] = $path; + } + /** * Add a new JSON path to the loader. * From 996c96955f78e8a2b26a24c490a1721cfb14574f Mon Sep 17 00:00:00 2001 From: taylorotwell <463230+taylorotwell@users.noreply.github.com> Date: Tue, 21 Jan 2025 15:02:43 +0000 Subject: [PATCH 26/26] Update version to v11.39.0 --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index c3c78a0c514..8489ace4a4c 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -45,7 +45,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '11.38.2'; + const VERSION = '11.39.0'; /** * The base path for the Laravel installation.