From 2ec06fbe3764e2a09ce9d7f7ef15f3a4dd9303a6 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Mon, 31 Mar 2025 00:00:34 +0100 Subject: [PATCH 01/15] Fix docblock (#18255) --- src/Mailer/Mailer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mailer/Mailer.php b/src/Mailer/Mailer.php index 5a8dd354234..56b5eb675eb 100644 --- a/src/Mailer/Mailer.php +++ b/src/Mailer/Mailer.php @@ -118,7 +118,7 @@ * @method $this setHeaders(array $headers) Sets headers for the message. {@see \Cake\Mailer\Message::setHeaders()} * @method $this addHeaders(array $headers) Add header for the message. {@see \Cake\Mailer\Message::addHeaders()} * @method $this getHeaders(array $include = []) Get list of headers. {@see \Cake\Mailer\Message::getHeaders()} - * @method $this setEmailFormat($format) Sets email format. {@see \Cake\Mailer\Message::getHeaders()} + * @method $this setEmailFormat($format) Sets email format. {@see \Cake\Mailer\Message::setEmailFormat()} * @method string getEmailFormat() Gets email format. {@see \Cake\Mailer\Message::getEmailFormat()} * @method $this setMessageId($message) Sets message ID. {@see \Cake\Mailer\Message::setMessageId()} * @method string|bool getMessageId() Gets message ID. {@see \Cake\Mailer\Message::getMessageId()} From 2cf175f24c02e3c68c43797b2cb81a92586477d9 Mon Sep 17 00:00:00 2001 From: ADmad Date: Mon, 31 Mar 2025 11:26:24 +0530 Subject: [PATCH 02/15] Fix errors reported by phpstan --- src/Cache/Engine/MemcachedEngine.php | 6 ++++- src/ORM/phpstan.neon.dist | 35 ++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/Cache/Engine/MemcachedEngine.php b/src/Cache/Engine/MemcachedEngine.php index 3180629a459..fc6149f16a8 100644 --- a/src/Cache/Engine/MemcachedEngine.php +++ b/src/Cache/Engine/MemcachedEngine.php @@ -355,7 +355,7 @@ public function get(string $key, mixed $default = null): mixed * @param iterable $keys An array of identifiers for the data * @param mixed $default Default value to return for keys that do not exist. * @return iterable An array containing, for each of the given $keys, the cached data or - * false if cached data could not be retrieved. + * `$default` if cached data could not be retrieved. */ public function getMultiple(iterable $keys, mixed $default = null): iterable { @@ -365,6 +365,10 @@ public function getMultiple(iterable $keys, mixed $default = null): iterable } $values = $this->_Memcached->getMulti($cacheKeys); + if ($values === false) { + return array_fill_keys(array_keys($cacheKeys), $default); + } + $return = []; foreach ($cacheKeys as $original => $prefixed) { $return[$original] = array_key_exists($prefixed, $values) ? $values[$prefixed] : $default; diff --git a/src/ORM/phpstan.neon.dist b/src/ORM/phpstan.neon.dist index 684a761c923..239541a0f84 100644 --- a/src/ORM/phpstan.neon.dist +++ b/src/ORM/phpstan.neon.dist @@ -18,3 +18,38 @@ parameters: - "#^PHPDoc tag @return with type Cake\\\\ORM\\\\Query\\\\SelectQuery\\ is not subtype of native type static\\(Cake\\\\ORM\\\\Query\\\\SelectQuery\\\\)\\.$#" - "#^Method Cake\\\\ORM\\\\Query\\\\SelectQuery\\:\\:find\\(\\) should return static\\(Cake\\\\ORM\\\\Query\\\\SelectQuery\\\\) but returns Cake\\\\ORM\\\\Query\\\\SelectQuery\\\\.$#" - '#^PHPDoc tag @var with type callable\(\): mixed is not subtype of native type Closure\(string\): string\.$#' + - + message: '#^Call to function method_exists\(\) with Cake\\Datasource\\EntityInterface and ''patch'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: Association/BelongsTo.php + + - + message: '#^Call to function method_exists\(\) with Cake\\Datasource\\EntityInterface and ''patch'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: Association/HasMany.php + + - + message: '#^Call to function method_exists\(\) with Cake\\Datasource\\EntityInterface and ''patch'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: Association/HasOne.php + + - + message: '#^Call to function method_exists\(\) with Cake\\Datasource\\EntityInterface and ''patch'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: Behavior/TreeBehavior.php + + - + message: '#^Call to function method_exists\(\) with Cake\\Datasource\\EntityInterface and ''patch'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 2 + path: Marshaller.php + + - + message: '#^Call to function method_exists\(\) with Cake\\Datasource\\EntityInterface and ''patch'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: Table.php From c40412206d6247897369709178f2489347c03c0e Mon Sep 17 00:00:00 2001 From: ADmad Date: Tue, 1 Apr 2025 00:37:16 +0530 Subject: [PATCH 03/15] Update composer branch aliases for split packages. (#18259) --- src/Cache/composer.json | 2 +- src/Collection/composer.json | 2 +- src/Console/composer.json | 2 +- src/Core/composer.json | 2 +- src/Database/composer.json | 2 +- src/Datasource/composer.json | 2 +- src/Event/composer.json | 2 +- src/Form/composer.json | 2 +- src/Http/composer.json | 2 +- src/I18n/composer.json | 2 +- src/Log/composer.json | 2 +- src/ORM/composer.json | 2 +- src/Utility/composer.json | 2 +- src/Validation/composer.json | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Cache/composer.json b/src/Cache/composer.json index 44562ce2a65..e978f708ce1 100644 --- a/src/Cache/composer.json +++ b/src/Cache/composer.json @@ -37,7 +37,7 @@ "prefer-stable": true, "extra": { "branch-alias": { - "dev-5.next": "5.2.x-dev" + "dev-5.x": "5.2.x-dev" } } } diff --git a/src/Collection/composer.json b/src/Collection/composer.json index 54ced32df0d..aa4fe5caef3 100644 --- a/src/Collection/composer.json +++ b/src/Collection/composer.json @@ -35,7 +35,7 @@ }, "extra": { "branch-alias": { - "dev-5.next": "5.2.x-dev" + "dev-5.x": "5.2.x-dev" } } } diff --git a/src/Console/composer.json b/src/Console/composer.json index 7793fba15a9..d3a061fe511 100644 --- a/src/Console/composer.json +++ b/src/Console/composer.json @@ -41,7 +41,7 @@ "prefer-stable": true, "extra": { "branch-alias": { - "dev-5.next": "5.2.x-dev" + "dev-5.x": "5.2.x-dev" } } } diff --git a/src/Core/composer.json b/src/Core/composer.json index cd8c5b232a6..55812cf1829 100644 --- a/src/Core/composer.json +++ b/src/Core/composer.json @@ -46,7 +46,7 @@ "prefer-stable": true, "extra": { "branch-alias": { - "dev-5.next": "5.2.x-dev" + "dev-5.x": "5.2.x-dev" } } } diff --git a/src/Database/composer.json b/src/Database/composer.json index bf367eb5225..00b65f86f94 100644 --- a/src/Database/composer.json +++ b/src/Database/composer.json @@ -46,7 +46,7 @@ "prefer-stable": true, "extra": { "branch-alias": { - "dev-5.next": "5.2.x-dev" + "dev-5.x": "5.2.x-dev" } } } diff --git a/src/Datasource/composer.json b/src/Datasource/composer.json index 31007314d88..26f53418b56 100644 --- a/src/Datasource/composer.json +++ b/src/Datasource/composer.json @@ -46,7 +46,7 @@ "prefer-stable": true, "extra": { "branch-alias": { - "dev-5.next": "5.2.x-dev" + "dev-5.x": "5.2.x-dev" } } } diff --git a/src/Event/composer.json b/src/Event/composer.json index c1efeb306fd..600b926c865 100644 --- a/src/Event/composer.json +++ b/src/Event/composer.json @@ -34,7 +34,7 @@ "prefer-stable": true, "extra": { "branch-alias": { - "dev-5.next": "5.2.x-dev" + "dev-5.x": "5.2.x-dev" } } } diff --git a/src/Form/composer.json b/src/Form/composer.json index 6e1b938aaff..9458465d8b9 100644 --- a/src/Form/composer.json +++ b/src/Form/composer.json @@ -33,7 +33,7 @@ "prefer-stable": true, "extra": { "branch-alias": { - "dev-5.next": "5.2.x-dev" + "dev-5.x": "5.2.x-dev" } } } diff --git a/src/Http/composer.json b/src/Http/composer.json index f79da51cf8f..2b58c60848a 100644 --- a/src/Http/composer.json +++ b/src/Http/composer.json @@ -64,7 +64,7 @@ "prefer-stable": true, "extra": { "branch-alias": { - "dev-5.next": "5.2.x-dev" + "dev-5.x": "5.2.x-dev" } } } diff --git a/src/I18n/composer.json b/src/I18n/composer.json index 4cb684eb1f4..19a27250d18 100644 --- a/src/I18n/composer.json +++ b/src/I18n/composer.json @@ -47,7 +47,7 @@ "prefer-stable": true, "extra": { "branch-alias": { - "dev-5.next": "5.2.x-dev" + "dev-5.x": "5.2.x-dev" } } } diff --git a/src/Log/composer.json b/src/Log/composer.json index 38119f6f6c4..593de4ff139 100644 --- a/src/Log/composer.json +++ b/src/Log/composer.json @@ -38,7 +38,7 @@ "prefer-stable": true, "extra": { "branch-alias": { - "dev-5.next": "5.2.x-dev" + "dev-5.x": "5.2.x-dev" } } } diff --git a/src/ORM/composer.json b/src/ORM/composer.json index cddef59fe9c..c3f67a12f45 100644 --- a/src/ORM/composer.json +++ b/src/ORM/composer.json @@ -51,7 +51,7 @@ "prefer-stable": true, "extra": { "branch-alias": { - "dev-5.next": "5.2.x-dev" + "dev-5.x": "5.2.x-dev" } } } diff --git a/src/Utility/composer.json b/src/Utility/composer.json index 98fbbac631f..563b1c34d7d 100644 --- a/src/Utility/composer.json +++ b/src/Utility/composer.json @@ -43,7 +43,7 @@ "prefer-stable": true, "extra": { "branch-alias": { - "dev-5.next": "5.2.x-dev" + "dev-5.x": "5.2.x-dev" } } } diff --git a/src/Validation/composer.json b/src/Validation/composer.json index 41cf76012f5..c5d6c13e93f 100644 --- a/src/Validation/composer.json +++ b/src/Validation/composer.json @@ -41,7 +41,7 @@ "prefer-stable": true, "extra": { "branch-alias": { - "dev-5.next": "5.2.x-dev" + "dev-5.x": "5.2.x-dev" } } } From 1bdb732857fcf04d72c2fcb146239ef00597de8e Mon Sep 17 00:00:00 2001 From: Mark Story Date: Tue, 1 Apr 2025 13:54:01 -0400 Subject: [PATCH 04/15] Include comments in generated column sql (#18256) --- src/Database/Schema/SqliteSchemaDialect.php | 3 +++ tests/TestCase/Database/Schema/SqliteSchemaDialectTest.php | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Database/Schema/SqliteSchemaDialect.php b/src/Database/Schema/SqliteSchemaDialect.php index b6efef13b98..9ae13f120cc 100644 --- a/src/Database/Schema/SqliteSchemaDialect.php +++ b/src/Database/Schema/SqliteSchemaDialect.php @@ -840,6 +840,9 @@ public function columnDefinitionSql(array $column): string if (isset($column['default'])) { $out .= ' DEFAULT ' . $this->_driver->schemaValue($column['default']); } + if (isset($column['comment']) && $column['comment']) { + $out .= " /* {$column['comment']} */"; + } return $out; } diff --git a/tests/TestCase/Database/Schema/SqliteSchemaDialectTest.php b/tests/TestCase/Database/Schema/SqliteSchemaDialectTest.php index 52be0200cdc..4e39a559809 100644 --- a/tests/TestCase/Database/Schema/SqliteSchemaDialectTest.php +++ b/tests/TestCase/Database/Schema/SqliteSchemaDialectTest.php @@ -917,8 +917,8 @@ public static function columnSqlProvider(): array // Text [ 'body', - ['type' => 'text', 'null' => false], - '"body" TEXT NOT NULL', + ['type' => 'text', 'null' => false, 'comment' => 'a comment'], + '"body" TEXT NOT NULL /* a comment */', ], [ 'body', From f8fa2a087d08bd79de782408c8138a1a966f1b5c Mon Sep 17 00:00:00 2001 From: ADmad Date: Wed, 2 Apr 2025 13:57:47 +0530 Subject: [PATCH 05/15] Fix method and finder map population when using BehaviorRegistry::set() Closes #18263 --- src/ORM/BehaviorRegistry.php | 18 ++++++++++++++++++ tests/TestCase/ORM/BehaviorRegistryTest.php | 8 ++++++++ 2 files changed, 26 insertions(+) diff --git a/src/ORM/BehaviorRegistry.php b/src/ORM/BehaviorRegistry.php index c531e4a493a..82db5cffec2 100644 --- a/src/ORM/BehaviorRegistry.php +++ b/src/ORM/BehaviorRegistry.php @@ -211,6 +211,24 @@ protected function _getMethods(Behavior $instance, string $class, string $alias) return compact('methods', 'finders'); } + /** + * Set an object directly into the registry by name. + * + * @param string $name The name of the object to set in the registry. + * @param \Cake\ORM\Behavior $object instance to store in the registry + * @return $this + */ + public function set(string $name, object $object) + { + parent::set($name, $object); + + $methods = $this->_getMethods($object, $object::class, $name); + $this->_methodMap += $methods['methods']; + $this->_finderMap += $methods['finders']; + + return $this; + } + /** * Remove an object from the registry. * diff --git a/tests/TestCase/ORM/BehaviorRegistryTest.php b/tests/TestCase/ORM/BehaviorRegistryTest.php index cbf9175703c..f977c6aa842 100644 --- a/tests/TestCase/ORM/BehaviorRegistryTest.php +++ b/tests/TestCase/ORM/BehaviorRegistryTest.php @@ -221,6 +221,14 @@ public function testLoadDuplicateFinderAliasing(): void $this->assertTrue($this->Behaviors->hasFinder('renamed')); } + public function testSet() + { + $this->Behaviors->set('Sluggable', new SluggableBehavior($this->Table, ['replacement' => '_'])); + + $this->assertEquals(['replacement' => '_'], $this->Behaviors->get('Sluggable')->getConfig()); + $this->assertTrue($this->Behaviors->hasMethod('slugify')); + } + /** * test hasMethod() */ From d2796f7d54c9394be60851f3f17f2fde0f9c8110 Mon Sep 17 00:00:00 2001 From: ADmad Date: Wed, 2 Apr 2025 17:32:03 +0530 Subject: [PATCH 06/15] Cleanup ObjectRegistry::set() and ObjectRegistry::unload(). Drop use of pluginSplit(). Plugin prefixed names are only used in ObjectRegistry::load() to expand the name to FQCN. After that the registry does not hold any info of the plugin name, so trying to split the provided name into plugin and object name in the above methods doesn't make sense. --- src/Core/ObjectRegistry.php | 9 +++------ tests/TestCase/Controller/ComponentRegistryTest.php | 5 +++-- tests/TestCase/View/HelperRegistryTest.php | 4 ++-- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/Core/ObjectRegistry.php b/src/Core/ObjectRegistry.php index 5b29f588255..6cd472edd68 100644 --- a/src/Core/ObjectRegistry.php +++ b/src/Core/ObjectRegistry.php @@ -344,8 +344,6 @@ public function reset() */ public function set(string $name, object $object) { - [, $objName] = pluginSplit($name); - // Just call unload if the object was loaded before if (array_key_exists($name, $this->_loaded)) { $this->unload($name); @@ -353,7 +351,7 @@ public function set(string $name, object $object) if ($this instanceof EventDispatcherInterface && $object instanceof EventListenerInterface) { $this->getEventManager()->on($object); } - $this->_loaded[$objName] = $object; + $this->_loaded[$name] = $object; return $this; } @@ -368,9 +366,8 @@ public function set(string $name, object $object) */ public function unload(string $name) { - if (empty($this->_loaded[$name])) { - [$plugin, $name] = pluginSplit($name); - $this->_throwMissingClassError($name, $plugin); + if (!isset($this->_loaded[$name])) { + throw new CakeException(sprintf('Object named `%s` is not loaded.', $name)); } $object = $this->_loaded[$name]; diff --git a/tests/TestCase/Controller/ComponentRegistryTest.php b/tests/TestCase/Controller/ComponentRegistryTest.php index 5223c79b03e..f0857379936 100644 --- a/tests/TestCase/Controller/ComponentRegistryTest.php +++ b/tests/TestCase/Controller/ComponentRegistryTest.php @@ -22,6 +22,7 @@ use Cake\Controller\Controller; use Cake\Controller\Exception\MissingComponentException; use Cake\Core\Container; +use Cake\Core\Exception\CakeException; use Cake\Event\EventManager; use Cake\Http\ServerRequest; use Cake\TestSuite\TestCase; @@ -262,8 +263,8 @@ public function testUnset(): void */ public function testUnloadUnknown(): void { - $this->expectException(MissingComponentException::class); - $this->expectExceptionMessage('Component class `FooComponent` could not be found.'); + $this->expectException(CakeException::class); + $this->expectExceptionMessage('Object named `Foo` is not loaded.'); $this->Components->unload('Foo'); } diff --git a/tests/TestCase/View/HelperRegistryTest.php b/tests/TestCase/View/HelperRegistryTest.php index 693f81f78c1..93e2dde8eb8 100644 --- a/tests/TestCase/View/HelperRegistryTest.php +++ b/tests/TestCase/View/HelperRegistryTest.php @@ -249,8 +249,8 @@ public function testUnload(): void */ public function testUnloadUnknown(): void { - $this->expectException(MissingHelperException::class); - $this->expectExceptionMessage('Helper class `FooHelper` could not be found.'); + $this->expectException(CakeException::class); + $this->expectExceptionMessage('Object named `Foo` is not loaded.'); $this->Helpers->unload('Foo'); } From 431c5bb955252ebcfe2e97a15a4efb6a13264350 Mon Sep 17 00:00:00 2001 From: mscherer Date: Thu, 3 Apr 2025 18:01:36 +0200 Subject: [PATCH 07/15] Fix regression on mapType(). --- src/Http/Response.php | 2 +- tests/TestCase/Http/ResponseTest.php | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/Http/Response.php b/src/Http/Response.php index 110fba34b80..96e9c1f065f 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -528,7 +528,7 @@ public function getMimeType(string $alias): array|string|false public function mapType(array|string $ctype): array|string|null { if (is_array($ctype)) { - return array_map('\Cake\Http\MimeType::getExtension', $ctype); + return array_map($this->mapType(...), $ctype); } return MimeType::getExtension($ctype); diff --git a/tests/TestCase/Http/ResponseTest.php b/tests/TestCase/Http/ResponseTest.php index a77a27d0937..828aeb09b24 100644 --- a/tests/TestCase/Http/ResponseTest.php +++ b/tests/TestCase/Http/ResponseTest.php @@ -355,9 +355,37 @@ public function testMapType(): void $this->assertSame('xml', $response->mapType('text/xml')); $this->assertSame('html', $response->mapType('*/*')); $this->assertSame('csv', $response->mapType('application/vnd.ms-excel')); + $expected = ['json', 'xhtml', 'css']; $result = $response->mapType(['application/json', 'application/xhtml+xml', 'text/css']); $this->assertEquals($expected, $result); + + $array = [ + '1.0' => [ + 'text/csv', + 'text/xml', + ], + '0.8' => [ + 'application/json', + ], + '0.7' => [ + 'application/xml', + ], + ]; + $expected = [ + '1.0' => [ + 'csv', + 'xml', + ], + '0.8' => [ + 'json', + ], + '0.7' => [ + 'xml', + ], + ]; + $result = $response->mapType($array); + $this->assertEquals($expected, $result); } /** From 8661e5156762f29e0d7eef34587f6deddce3228c Mon Sep 17 00:00:00 2001 From: ADmad Date: Fri, 4 Apr 2025 10:49:33 +0530 Subject: [PATCH 08/15] Update info regarding `Table::buildRules()` and `Model.buildRules` event. Refs #18270 --- src/ORM/Table.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ORM/Table.php b/src/ORM/Table.php index 24c7b7303e8..92252f8dbde 100644 --- a/src/ORM/Table.php +++ b/src/ORM/Table.php @@ -110,6 +110,8 @@ * for the provided named validator. * * - `Model.buildRules` Allows listeners to modify the rules checker by adding more rules. + * Behaviors or custom listerners can subscribe to this even. For tables you don't + * need to subscribe to this event, simply override the `Table::buildRules()` method. * * - `Model.beforeRules` Fired before an entity is validated using the rules checker. * By stopping this event, you can return the final value of the rules checking operation. @@ -142,7 +144,6 @@ * - `beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options)` * - `afterMarshal(EventInterface $event, EntityInterface $entity, ArrayObject $options)` * - `buildValidator(EventInterface $event, Validator $validator, string $name)` - * - `buildRules(RulesChecker $rules)` * - `beforeRules(EventInterface $event, EntityInterface $entity, ArrayObject $options, string $operation)` * - `afterRules(EventInterface $event, EntityInterface $entity, ArrayObject $options, bool $result, string $operation)` * - `beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)` From a39714c555d16edb6a738add3897d0b6c45cface Mon Sep 17 00:00:00 2001 From: fabian-mcfly <13197057+fabian-mcfly@users.noreply.github.com> Date: Fri, 4 Apr 2025 11:06:36 +0200 Subject: [PATCH 09/15] Merge pull request #18271 from fabian-mcfly/5.x Fixed incorrect session save path when the `TMP` constant is defined --- src/Http/Session.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Http/Session.php b/src/Http/Session.php index e8f45f412f1..0e84ebcc281 100644 --- a/src/Http/Session.php +++ b/src/Http/Session.php @@ -155,8 +155,8 @@ protected static function _defaultConfig(string $name): array 'session.use_trans_sid' => 0, 'session.serialize_handler' => 'php', 'session.use_cookies' => 1, - 'session.save_path' => defined('TMP') ? TMP : sys_get_temp_dir() - . DIRECTORY_SEPARATOR . 'sessions', + 'session.save_path' => (defined('TMP') ? TMP : sys_get_temp_dir() . DIRECTORY_SEPARATOR) + . 'sessions', 'session.save_handler' => 'files', ], ], From 90b9411ba2a9db713a8f3974a250d2fbd1d246a3 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Fri, 4 Apr 2025 16:31:43 +0200 Subject: [PATCH 10/15] Allow null for Number helper methods that are commonly used for bake. (#18272) * Allow null for Number helper methods that are commonly used for bake. * Sane default return value for empty string. * Docs. --- src/View/Helper/NumberHelper.php | 22 +++++++--- .../TestCase/View/Helper/NumberHelperTest.php | 40 +++++++++++++++++++ 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/View/Helper/NumberHelper.php b/src/View/Helper/NumberHelper.php index 081fa1c6824..a46c4c805e0 100644 --- a/src/View/Helper/NumberHelper.php +++ b/src/View/Helper/NumberHelper.php @@ -56,15 +56,20 @@ public function __call(string $method, array $params): mixed * - `locale` - The locale name to use for formatting the number, e.g. fr_FR * - `before` - The string to place before whole numbers, e.g. '[' * - `after` - The string to place after decimal numbers, e.g. ']' - * - `escape` - Whether to escape html in resulting string + * - `escape` - Whether to escape HTML in resulting string + * - `default` - The default value in case passed value is null * - * @param string|float|int $number A floating point number. + * @param string|float|int|null $number A floating point number. * @param array $options An array with options. * @return string Formatted number * @link https://book.cakephp.org/5/en/views/helpers/number.html#formatting-numbers */ - public function format(string|float|int $number, array $options = []): string + public function format(string|float|int|null $number, array $options = []): string { + if ($number === null) { + return $options['default'] ?? ''; + } + $formatted = Number::format($number, $options); $options += ['escape' => true]; @@ -90,15 +95,20 @@ public function format(string|float|int $number, array $options = []): string * - `pattern` - An ICU number pattern to use for formatting the number. e.g #,##0.00 * - `useIntlCode` - Whether to replace the currency symbol with the international * currency code. - * - `escape` - Whether to escape html in resulting string + * - `escape` - Whether to escape HTML in resulting string + * - `default` - The default value in case passed value is null * - * @param string|float $number Value to format. + * @param string|float|null $number Value to format. * @param string|null $currency International currency name such as 'USD', 'EUR', 'JPY', 'CAD' * @param array $options Options list. * @return string Number formatted as a currency. */ - public function currency(string|float $number, ?string $currency = null, array $options = []): string + public function currency(string|float|null $number, ?string $currency = null, array $options = []): string { + if ($number === null) { + return $options['default'] ?? ''; + } + $formatted = Number::currency($number, $currency, $options); $options += ['escape' => true]; diff --git a/tests/TestCase/View/Helper/NumberHelperTest.php b/tests/TestCase/View/Helper/NumberHelperTest.php index 753f5cd7768..1ffef5ad53b 100644 --- a/tests/TestCase/View/Helper/NumberHelperTest.php +++ b/tests/TestCase/View/Helper/NumberHelperTest.php @@ -77,4 +77,44 @@ public function testMethodProxying(string $method, mixed $arg): void $helper = new NumberHelper($this->View); $this->assertNotNull($helper->{$method}($arg)); } + + /** + * test format() and empty values + */ + public function testFormatEmpty(): void + { + $helper = new NumberHelper($this->View); + + $value = null; + $result = $helper->format($value); + $this->assertSame('', $result); + + $result = $helper->format($value, ['default' => '-']); + $this->assertSame('-', $result); + + // We should revisit this for 6.x + $value = ''; + $result = $helper->format($value); + $this->assertSame('0', $result); + } + + /** + * test currency() and empty values + */ + public function testCurrencyEmpty(): void + { + $helper = new NumberHelper($this->View); + + $value = null; + $result = $helper->currency($value); + $this->assertSame('', $result); + + $result = $helper->currency($value, null, ['default' => '-']); + $this->assertSame('-', $result); + + // We should revisit this for 6.x + $value = ''; + $result = $helper->currency($value); + $this->assertNotEmpty($result); + } } From d183cf39ccd6996c027748a02a51e2189ff458e4 Mon Sep 17 00:00:00 2001 From: ADmad Date: Fri, 4 Apr 2025 20:03:09 +0530 Subject: [PATCH 11/15] Fix passing context to validator method on behaviors. (#18277) Reflection is used to determine if the validation method has a $context arg. For table methods which are available by proxing to a behavior, we need to generate the callable using the behavior instance instead of the table, so that the actual behavior method is reflected. Fixes #18273 --- src/Validation/ValidationRule.php | 15 ++++++++++ tests/TestCase/ORM/TableTest.php | 28 +++++++++++++++++++ .../Model/Behavior/ValidationBehavior.php | 5 ++++ 3 files changed, 48 insertions(+) diff --git a/src/Validation/ValidationRule.php b/src/Validation/ValidationRule.php index 84a03b99434..822d45e65d8 100644 --- a/src/Validation/ValidationRule.php +++ b/src/Validation/ValidationRule.php @@ -20,6 +20,7 @@ */ namespace Cake\Validation; +use Cake\ORM\Table; use Closure; use ReflectionFunction; @@ -121,6 +122,20 @@ public function process(mixed $value, array $providers, array $context = []): ar if (is_string($this->_rule)) { $provider = $providers[$this->_provider]; + if ( + class_exists(Table::class) + && $provider instanceof Table + && !method_exists($provider, $this->_rule) + && $provider->behaviors()->hasMethod($this->_rule) + ) { + foreach ($provider->behaviors() as $behavior) { + if (in_array($this->_rule, $behavior->implementedMethods(), true)) { + $provider = $behavior; + break; + } + } + } + /** @phpstan-ignore-next-line */ $callable = [$provider, $this->_rule](...); } else { diff --git a/tests/TestCase/ORM/TableTest.php b/tests/TestCase/ORM/TableTest.php index af8193f6f36..ccd55f4b0ab 100644 --- a/tests/TestCase/ORM/TableTest.php +++ b/tests/TestCase/ORM/TableTest.php @@ -3480,6 +3480,34 @@ public function testValidatorBehavior(): void $this->assertArrayHasKey('behaviorRule', $set); } + /** + * https://github.com/cakephp/cakephp/issues/18273 + */ + public function testValidatorWithMethodInBehavior(): void + { + $table = new Table(); + $table->addBehavior('Validation'); + + $table->getValidator('default')->add( + 'name', + 'customValidationRule', + ['rule' => 'customValidationRule', 'provider' => 'table'], + ); + + $result = $table->getValidator('default')->validate([ + 'name' => 'test', + ], true); + + $this->assertSame( + [ + 'name' => [ + 'customValidationRule' => 'The provided value is invalid', + ], + ], + $result, + ); + } + /** * Tests that a InvalidArgumentException is thrown if the custom validator method does not exist. */ diff --git a/tests/test_app/TestApp/Model/Behavior/ValidationBehavior.php b/tests/test_app/TestApp/Model/Behavior/ValidationBehavior.php index 71455a6aba5..176b49fff2c 100644 --- a/tests/test_app/TestApp/Model/Behavior/ValidationBehavior.php +++ b/tests/test_app/TestApp/Model/Behavior/ValidationBehavior.php @@ -35,4 +35,9 @@ public function validationBehavior(Validator $validator) { return $validator->add('name', 'behaviorRule'); } + + public function customValidationRule(mixed $value, array $context): bool + { + return false; + } } From 2a1600122fa5285bbba87b05e40962466f2371ce Mon Sep 17 00:00:00 2001 From: mscherer Date: Sat, 5 Apr 2025 04:12:27 +0200 Subject: [PATCH 12/15] Fixed typos and cleanup. --- phpunit.xml.dist | 2 +- src/Cache/Engine/RedisEngine.php | 2 +- src/Database/Driver/Postgres.php | 4 ++-- src/Database/Expression/QueryExpression.php | 2 +- src/Database/Query.php | 2 +- src/Database/Query/SelectQuery.php | 2 +- src/Datasource/Paging/NumericPaginator.php | 2 +- src/Datasource/Paging/PaginatedResultSet.php | 2 +- src/Datasource/Paging/SimplePaginator.php | 4 ++-- src/Error/Debug/dumpHeader.html | 2 +- src/Http/Client/Exception/RequestException.php | 2 +- src/Http/Client/Response.php | 2 +- src/Http/ServerRequestFactory.php | 2 +- src/Http/Session.php | 2 +- src/ORM/Behavior/TranslateBehavior.php | 2 +- src/ORM/Query/SelectQuery.php | 6 +++--- src/ORM/ResultSetFactory.php | 2 +- src/ORM/Table.php | 6 +++--- src/Utility/Hash.php | 2 +- src/Validation/ValidationSet.php | 2 +- src/Validation/Validator.php | 2 +- src/View/Helper/FormHelper.php | 2 +- src/View/Helper/PaginatorHelper.php | 6 +++--- src/View/SerializedView.php | 2 +- src/View/View.php | 4 ++-- src/View/ViewBuilder.php | 2 +- tests/TestCase/ORM/Behavior/TranslateBehaviorEavTest.php | 2 +- tests/TestCase/ORM/ResultSetTest.php | 2 +- 28 files changed, 37 insertions(+), 37 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 9da2efe15eb..de94dccd0f9 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -45,7 +45,7 @@ -