From a8dee866bfc5a0a7aceecfcbef83d9c40009ed40 Mon Sep 17 00:00:00 2001 From: mscherer Date: Thu, 26 Mar 2026 04:35:23 +0100 Subject: [PATCH 1/3] Document CakePHP 5.4 features - Migration guide: RequestToDto, FormProtection convenience methods, new query expression methods, nested array marshalling, inputId template variable - Query builder: notBetween(), inOrNull(), notInOrNull(), isDistinctFrom(), isNotDistinctFrom() - FormProtection: unlockActions() and unlockFields() methods - Dependency injection: Request to DTO mapping with #[RequestToDto] - ORM saving: nested array format for associated marshalling - FormHelper: {{inputId}} template variable --- docs/en/appendices/5-4-migration-guide.md | 182 ++++++++++++++++++ .../controllers/components/form-protection.md | 45 +++++ docs/en/development/dependency-injection.md | 100 ++++++++++ docs/en/orm/query-builder.md | 73 +++++++ docs/en/orm/saving-data.md | 39 +++- docs/en/views/helpers/form.md | 30 +++ 6 files changed, 467 insertions(+), 2 deletions(-) diff --git a/docs/en/appendices/5-4-migration-guide.md b/docs/en/appendices/5-4-migration-guide.md index c4e40b5c53..e289505a31 100644 --- a/docs/en/appendices/5-4-migration-guide.md +++ b/docs/en/appendices/5-4-migration-guide.md @@ -49,6 +49,138 @@ $this->hasMany('Comments', [ ## New Features +### Controller + +#### RequestToDto Attribute + +A new ``#[RequestToDto]`` attribute enables automatic mapping of request data to +Data Transfer Objects in controller actions. This provides a clean way to handle +form data with type safety: + +```php +use Cake\Controller\Attribute\RequestToDto; + +class UsersController extends AppController +{ + public function create(#[RequestToDto] UserCreateDto $dto): void + { + // $dto is automatically populated from request data + $user = $this->Users->newEntity([ + 'email' => $dto->email, + 'name' => $dto->name, + ]); + } +} +``` + +Your DTO class must implement a static ``createFromArray()`` method: + +```php +class UserCreateDto +{ + public function __construct( + public string $email, + public string $name, + ) { + } + + public static function createFromArray(array $data): self + { + return new self( + email: $data['email'] ?? '', + name: $data['name'] ?? '', + ); + } +} +``` + +The attribute supports configuring the data source: + +```php +use Cake\Controller\Attribute\Enum\RequestToDtoSource; + +// Use query string parameters +public function search( + #[RequestToDto(source: RequestToDtoSource::Query)] SearchDto $dto +): void {} + +// Use POST body data +public function create( + #[RequestToDto(source: RequestToDtoSource::Body)] CreateDto $dto +): void {} + +// Merge query and body (body takes precedence) +public function update( + #[RequestToDto(source: RequestToDtoSource::Request)] UpdateDto $dto +): void {} + +// Auto-detect based on request method (default) +public function handle( + #[RequestToDto(source: RequestToDtoSource::Auto)] DataDto $dto +): void {} +``` + +#### FormProtection Convenience Methods + +``FormProtectionComponent`` now has convenience methods for unlocking actions +and fields: + +```php +// In your controller's beforeFilter() +$this->FormProtection->unlockActions(['api', 'webhook']); +$this->FormProtection->unlockFields(['dynamic_field', 'optional_field']); + +// With merge option (default is true) +$this->FormProtection->unlockActions('newAction', merge: true); +$this->FormProtection->unlockFields(['field1', 'field2'], merge: false); +``` + +### Database + +#### Query Expression Methods + +New convenience methods have been added to ``QueryExpression``: + +- ``notBetween()`` for ``NOT BETWEEN`` expressions: + + ```php + $query = $articles->find() + ->where(function (QueryExpression $exp) { + return $exp->notBetween('view_count', 100, 1000); + }); + // WHERE view_count NOT BETWEEN 100 AND 1000 + ``` + +- ``inOrNull()`` for ``(field IN (...) OR field IS NULL)`` patterns: + + ```php + $query = $articles->find() + ->where(function (QueryExpression $exp) { + return $exp->inOrNull('category_id', [1, 2, 3]); + }); + // WHERE (category_id IN (1, 2, 3) OR category_id IS NULL) + ``` + +- ``isDistinctFrom()`` and ``isNotDistinctFrom()`` for null-safe comparisons: + + ```php + $query = $articles->find() + ->where(function (QueryExpression $exp) { + // True when values differ, treating NULL as a comparable value + return $exp->isDistinctFrom('status', 'published'); + }); + // WHERE status IS DISTINCT FROM 'published' + // MySQL uses: NOT (status <=> 'published') + + $query = $articles->find() + ->where(function (QueryExpression $exp) { + // True when values are equal, treating NULL = NULL as true + return $exp->isNotDistinctFrom('category_id', null); + }); + // WHERE category_id IS NOT DISTINCT FROM NULL + // MySQL uses: category_id <=> NULL + ``` + ### I18n - `Number::toReadableSize()` now calculates decimal units (KB, MB, GB and TB) @@ -58,9 +190,59 @@ to KiB, MiB, GiB, and TiB as defined in ISO/IEC 80000-13. It is possible to switch between the two units using a new optional boolean parameter in `Number::toReadableSize()`, as well as the new global setter `Number::setUseIecUnits()`. +### ORM + +#### Nested Array Format for Marshalling + +The ``associated`` option in ``newEntity()`` and ``patchEntity()`` now supports +the same nested array format as ``contain()``: + +```php +// Nested arrays (new in 5.4) +$entity = $articles->newEntity($data, [ + 'associated' => [ + 'Tags', + 'Comments' => [ + 'Users', + 'Attachments', + ], + ], +]); + +// Mixed with options +$entity = $articles->newEntity($data, [ + 'associated' => [ + 'Tags' => ['onlyIds' => true], + 'Comments' => [ + 'Users', + 'validate' => 'special', + ], + ], +]); +``` + +CakePHP distinguishes associations from options using naming conventions: +- Association names use PascalCase (e.g., ``Users``, ``Comments``) +- Option keys use camelCase (e.g., ``onlyIds``, ``validate``) + ### Utility - New `Cake\Utility\Fs\Finder` class provides a fluent, iterator-based API for discovering files and directories with support for pattern matching, depth control, and custom filters. The `Cake\Utility\Fs\Path` class offers cross-platform utilities for path manipulation. + +### View + +#### FormHelper Template Variables + +The ``inputContainer`` and ``error`` templates now receive an ``{{inputId}}`` +variable containing the input element's HTML id attribute. This is useful for +generating related element IDs for ARIA attributes or custom JavaScript: + +```php +$this->Form->setTemplates([ + 'inputContainer' => '
{{content}}
', + 'error' => '
{{content}}
', +]); +``` diff --git a/docs/en/controllers/components/form-protection.md b/docs/en/controllers/components/form-protection.md index 7aa0e76c00..a9f8c1c23e 100644 --- a/docs/en/controllers/components/form-protection.md +++ b/docs/en/controllers/components/form-protection.md @@ -140,6 +140,51 @@ class WidgetController extends AppController This example would disable all security checks for the edit action. +You can also use the convenience method ``unlockActions()``: + +```php +public function beforeFilter(EventInterface $event): void +{ + parent::beforeFilter($event); + + // Unlock a single action + $this->FormProtection->unlockActions('edit'); + + // Unlock multiple actions + $this->FormProtection->unlockActions(['edit', 'api', 'webhook']); + + // Replace existing unlocked actions instead of merging + $this->FormProtection->unlockActions(['newAction'], merge: false); +} +``` + +::: info Added in version 5.4.0 +::: + +## Unlocking fields + +To unlock specific fields from validation, you can use the ``unlockFields()`` +convenience method: + +```php +public function beforeFilter(EventInterface $event): void +{ + parent::beforeFilter($event); + + // Unlock a single field + $this->FormProtection->unlockFields('dynamic_field'); + + // Unlock multiple fields + $this->FormProtection->unlockFields(['optional_field', 'ajax_field']); + + // Dot notation for nested fields + $this->FormProtection->unlockFields('user.preferences'); +} +``` + +::: info Added in version 5.4.0 +::: + ## Handling validation failure through callbacks If form protection validation fails it will result in a 400 error by default. diff --git a/docs/en/development/dependency-injection.md b/docs/en/development/dependency-injection.md index 0de78be3f6..2d99061301 100644 --- a/docs/en/development/dependency-injection.md +++ b/docs/en/development/dependency-injection.md @@ -425,6 +425,106 @@ database. Because this service is injected into our controller, we can easily swap the implementation out with a mock object or a dummy sub-class when testing. +## Request to DTO Mapping + +CakePHP supports automatic mapping of request data to Data Transfer Objects (DTOs) +using the `#[RequestToDto]` attribute. This provides a clean, type-safe way to +handle form data in controller actions: + +```php +use Cake\Controller\Attribute\RequestToDto; + +class UsersController extends AppController +{ + public function create(#[RequestToDto] UserCreateDto $dto): void + { + // $dto is automatically populated from request data + $user = $this->Users->newEntity([ + 'email' => $dto->email, + 'name' => $dto->name, + ]); + + if ($this->Users->save($user)) { + $this->Flash->success('User created'); + return $this->redirect(['action' => 'index']); + } + } +} +``` + +Your DTO class must implement a static `createFromArray()` method: + +```php +namespace App\Dto; + +class UserCreateDto +{ + public function __construct( + public string $email, + public string $name, + public ?string $phone = null, + ) { + } + + public static function createFromArray(array $data): self + { + return new self( + email: $data['email'] ?? '', + name: $data['name'] ?? '', + phone: $data['phone'] ?? null, + ); + } +} +``` + +### Configuring the Data Source + +By default, the attribute auto-detects the data source based on the request method +(query params for GET, body data for POST/PUT/PATCH). You can explicitly configure +the source using the `RequestToDtoSource` enum: + +```php +use Cake\Controller\Attribute\RequestToDto; +use Cake\Controller\Attribute\Enum\RequestToDtoSource; + +class ArticlesController extends AppController +{ + // Use query string parameters + public function search( + #[RequestToDto(source: RequestToDtoSource::Query)] SearchCriteriaDto $criteria + ): void { + $articles = $this->Articles->find() + ->where(['title LIKE' => "%{$criteria->query}%"]) + ->limit($criteria->limit); + } + + // Use POST body data explicitly + public function create( + #[RequestToDto(source: RequestToDtoSource::Body)] ArticleCreateDto $dto + ): void { + // ... + } + + // Merge query params and body data (body takes precedence) + public function update( + int $id, + #[RequestToDto(source: RequestToDtoSource::Request)] ArticleUpdateDto $dto + ): void { + // ... + } +} +``` + +The available source options are: + +- `RequestToDtoSource::Auto` - Auto-detect based on request method (default) +- `RequestToDtoSource::Query` - Use query string parameters +- `RequestToDtoSource::Body` - Use POST/PUT body data +- `RequestToDtoSource::Request` - Merge query params and body data + +::: info Added in version 5.4.0 +::: + ## Command Example ```php diff --git a/docs/en/orm/query-builder.md b/docs/en/orm/query-builder.md index 2ea7e22f1d..bb5215dd9f 100644 --- a/docs/en/orm/query-builder.md +++ b/docs/en/orm/query-builder.md @@ -1248,6 +1248,29 @@ conditions: # WHERE country_id NOT IN ('AFG', 'USA', 'EST') ``` +- `inOrNull()` Create a condition for `IN` combined with `IS NULL`: + + ```php + $query = $cities->find() + ->where(function (QueryExpression $exp, SelectQuery $q) { + return $exp->inOrNull('country_id', ['AFG', 'USA', 'EST']); + }); + # WHERE (country_id IN ('AFG', 'USA', 'EST') OR country_id IS NULL) + ``` + + ::: info Added in version 5.4.0 + ::: + +- `notInOrNull()` Create a condition for `NOT IN` combined with `IS NULL`: + + ```php + $query = $cities->find() + ->where(function (QueryExpression $exp, SelectQuery $q) { + return $exp->notInOrNull('country_id', ['AFG', 'USA', 'EST']); + }); + # WHERE (country_id NOT IN ('AFG', 'USA', 'EST') OR country_id IS NULL) + ``` + - `gt()` Create a `>` condition: ```php @@ -1318,6 +1341,19 @@ conditions: # WHERE population BETWEEN 999 AND 5000000, ``` +- `notBetween()` Create a `NOT BETWEEN` condition: + + ```php + $query = $cities->find() + ->where(function (QueryExpression $exp, SelectQuery $q) { + return $exp->notBetween('population', 999, 5000000); + }); + # WHERE population NOT BETWEEN 999 AND 5000000 + ``` + + ::: info Added in version 5.4.0 + ::: + - `exists()` Create a condition using `EXISTS`: ```php @@ -1352,6 +1388,43 @@ conditions: # WHERE NOT EXISTS (SELECT id FROM cities WHERE countries.id = cities.country_id AND population > 5000000) ``` +- `isDistinctFrom()` Create a null-safe inequality comparison using `IS DISTINCT FROM`: + + ```php + $query = $cities->find() + ->where(function (QueryExpression $exp, SelectQuery $q) { + return $exp->isDistinctFrom('status', 'active'); + }); + # WHERE status IS DISTINCT FROM 'active' + # MySQL uses: NOT (status <=> 'active') + ``` + + This is useful when you need to compare values where `NULL` should be treated + as a distinct value. Unlike regular `!=` comparisons, `IS DISTINCT FROM` + returns `TRUE` when comparing `NULL` to a non-NULL value, and `FALSE` when + comparing `NULL` to `NULL`. + + ::: info Added in version 5.4.0 + ::: + +- `isNotDistinctFrom()` Create a null-safe equality comparison using `IS NOT DISTINCT FROM`: + + ```php + $query = $cities->find() + ->where(function (QueryExpression $exp, SelectQuery $q) { + return $exp->isNotDistinctFrom('category_id', null); + }); + # WHERE category_id IS NOT DISTINCT FROM NULL + # MySQL uses: category_id <=> NULL + ``` + + This is the null-safe equivalent of `=`. It returns `TRUE` when both values + are `NULL` (unlike regular `=` which returns `NULL`), making it useful for + comparing nullable columns. + + ::: info Added in version 5.4.0 + ::: + Expression objects should cover many commonly used functions and expressions. If you find yourself unable to create the required conditions with expressions you can may be able to use `bind()` to manually bind parameters into conditions: diff --git a/docs/en/orm/saving-data.md b/docs/en/orm/saving-data.md index 01b71a1130..4e5b93d8cd 100644 --- a/docs/en/orm/saving-data.md +++ b/docs/en/orm/saving-data.md @@ -187,12 +187,47 @@ $articles = $this->fetchTable('Articles'); $entity = $articles->newEntity($this->request->getData(), [ 'associated' => [ 'Tags', 'Comments' => ['associated' => ['Users']], - ] + ], ]); ``` The above indicates that the 'Tags', 'Comments' and 'Users' for the Comments -should be marshalled. Alternatively, you can use dot notation for brevity: +should be marshalled. + +You can also use a nested array format similar to ``contain()``: + +```php +// Nested arrays (same format as contain()) +$entity = $articles->newEntity($this->request->getData(), [ + 'associated' => [ + 'Tags', + 'Comments' => [ + 'Users', + 'Attachments', + ], + ], +]); + +// Mixed with options +$entity = $articles->newEntity($this->request->getData(), [ + 'associated' => [ + 'Tags' => ['onlyIds' => true], + 'Comments' => [ + 'Users', + 'validate' => 'special', + ], + ], +]); +``` + +CakePHP distinguishes associations from options using naming conventions: +association names use PascalCase (e.g., ``Users``), while option keys use +camelCase (e.g., ``onlyIds``). + +::: info Added in version 5.4.0 +::: + +Alternatively, you can use dot notation for brevity: ```php // In a controller diff --git a/docs/en/views/helpers/form.md b/docs/en/views/helpers/form.md index f4517dc5bb..41945bfe15 100644 --- a/docs/en/views/helpers/form.md +++ b/docs/en/views/helpers/form.md @@ -2440,6 +2440,36 @@ Output: ``` +### Built-in Template Variables + +The `inputContainer` and `error` templates have access to a built-in `{{inputId}}` +variable containing the input element's HTML id attribute. This is useful for +generating related element IDs for ARIA attributes or custom JavaScript: + +```php +$this->Form->setTemplates([ + 'inputContainer' => '
{{content}}
', + 'error' => '', +]); + +// When rendering a 'username' field: +echo $this->Form->control('username'); +``` + +Output: + +```html +
+ + +
+``` + +This enables use cases like field-specific error containers for AJAX form validation. + +::: info Added in version 5.4.0 +::: + ### Moving Checkboxes & Radios Outside of a Label By default, CakePHP nests checkboxes created via `control()` and radio buttons From 8c6adc78dfaf505a554e72af3c9db5a3c78d6893 Mon Sep 17 00:00:00 2001 From: mscherer Date: Thu, 26 Mar 2026 04:36:43 +0100 Subject: [PATCH 2/3] Fix markdown lint: add blank line before list --- docs/en/appendices/5-4-migration-guide.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/appendices/5-4-migration-guide.md b/docs/en/appendices/5-4-migration-guide.md index e289505a31..6109d253ee 100644 --- a/docs/en/appendices/5-4-migration-guide.md +++ b/docs/en/appendices/5-4-migration-guide.md @@ -222,6 +222,7 @@ $entity = $articles->newEntity($data, [ ``` CakePHP distinguishes associations from options using naming conventions: + - Association names use PascalCase (e.g., ``Users``, ``Comments``) - Option keys use camelCase (e.g., ``onlyIds``, ``validate``) From aa6060fd694c090fe289e0cc890436aa796c2f10 Mon Sep 17 00:00:00 2001 From: mscherer Date: Thu, 26 Mar 2026 04:41:49 +0100 Subject: [PATCH 3/3] Simplify migration guide, link to detailed docs --- docs/en/appendices/5-4-migration-guide.md | 225 +++------------------- 1 file changed, 24 insertions(+), 201 deletions(-) diff --git a/docs/en/appendices/5-4-migration-guide.md b/docs/en/appendices/5-4-migration-guide.md index 6109d253ee..f8db436ca7 100644 --- a/docs/en/appendices/5-4-migration-guide.md +++ b/docs/en/appendices/5-4-migration-guide.md @@ -18,30 +18,14 @@ bin/cake upgrade rector --rules cakephp54 ### I18n -``Number::parseFloat()`` now returns ``null`` instead of ``0.0`` when parsing -fails. Previously, when ``NumberFormatter::parse()`` failed it returned ``false``, -which was cast to ``0.0``. This silently converted invalid input like ``"abc"`` -to ``0.0``, making it impossible to distinguish from valid ``"0"`` input. - -This also affects ``FloatType`` and ``DecimalType`` database types which use -``Number::parseFloat()`` internally. Invalid locale-formatted form input will -now result in ``null`` entity values instead of ``0``. +`Number::parseFloat()` now returns `null` instead of `0.0` when parsing +fails. This also affects `FloatType` and `DecimalType` database types. ### ORM The default eager loading strategy for `HasMany` and `BelongsToMany` associations -has changed from ``select`` to ``subquery``. The ``subquery`` strategy performs -better for larger datasets as it avoids packet size limits from large ``WHERE IN`` -clauses and reduces PHP memory usage by keeping IDs in the database. - -If you need the previous behavior, you can explicitly set the strategy when -defining associations: - -```php -$this->hasMany('Comments', [ - 'strategy' => 'select', -]); -``` +has changed from `select` to `subquery`. If you need the previous behavior, +explicitly set `'strategy' => 'select'` when defining associations. ## Deprecations @@ -51,199 +35,38 @@ $this->hasMany('Comments', [ ### Controller -#### RequestToDto Attribute - -A new ``#[RequestToDto]`` attribute enables automatic mapping of request data to -Data Transfer Objects in controller actions. This provides a clean way to handle -form data with type safety: - -```php -use Cake\Controller\Attribute\RequestToDto; - -class UsersController extends AppController -{ - public function create(#[RequestToDto] UserCreateDto $dto): void - { - // $dto is automatically populated from request data - $user = $this->Users->newEntity([ - 'email' => $dto->email, - 'name' => $dto->name, - ]); - } -} -``` - -Your DTO class must implement a static ``createFromArray()`` method: - -```php -class UserCreateDto -{ - public function __construct( - public string $email, - public string $name, - ) { - } - - public static function createFromArray(array $data): self - { - return new self( - email: $data['email'] ?? '', - name: $data['name'] ?? '', - ); - } -} -``` - -The attribute supports configuring the data source: - -```php -use Cake\Controller\Attribute\Enum\RequestToDtoSource; - -// Use query string parameters -public function search( - #[RequestToDto(source: RequestToDtoSource::Query)] SearchDto $dto -): void {} - -// Use POST body data -public function create( - #[RequestToDto(source: RequestToDtoSource::Body)] CreateDto $dto -): void {} - -// Merge query and body (body takes precedence) -public function update( - #[RequestToDto(source: RequestToDtoSource::Request)] UpdateDto $dto -): void {} - -// Auto-detect based on request method (default) -public function handle( - #[RequestToDto(source: RequestToDtoSource::Auto)] DataDto $dto -): void {} -``` - -#### FormProtection Convenience Methods - -``FormProtectionComponent`` now has convenience methods for unlocking actions -and fields: - -```php -// In your controller's beforeFilter() -$this->FormProtection->unlockActions(['api', 'webhook']); -$this->FormProtection->unlockFields(['dynamic_field', 'optional_field']); - -// With merge option (default is true) -$this->FormProtection->unlockActions('newAction', merge: true); -$this->FormProtection->unlockFields(['field1', 'field2'], merge: false); -``` +- Added `#[RequestToDto]` attribute for automatic mapping of request data to + Data Transfer Objects in controller actions. + See [Request to DTO Mapping](../development/dependency-injection#request-to-dto-mapping). +- Added `unlockActions()` and `unlockFields()` convenience methods to + `FormProtectionComponent`. + See [Form Protection Component](../controllers/components/form-protection). ### Database -#### Query Expression Methods - -New convenience methods have been added to ``QueryExpression``: - -- ``notBetween()`` for ``NOT BETWEEN`` expressions: - - ```php - $query = $articles->find() - ->where(function (QueryExpression $exp) { - return $exp->notBetween('view_count', 100, 1000); - }); - // WHERE view_count NOT BETWEEN 100 AND 1000 - ``` - -- ``inOrNull()`` for ``(field IN (...) OR field IS NULL)`` patterns: - - ```php - $query = $articles->find() - ->where(function (QueryExpression $exp) { - return $exp->inOrNull('category_id', [1, 2, 3]); - }); - // WHERE (category_id IN (1, 2, 3) OR category_id IS NULL) - ``` - -- ``isDistinctFrom()`` and ``isNotDistinctFrom()`` for null-safe comparisons: - - ```php - $query = $articles->find() - ->where(function (QueryExpression $exp) { - // True when values differ, treating NULL as a comparable value - return $exp->isDistinctFrom('status', 'published'); - }); - // WHERE status IS DISTINCT FROM 'published' - // MySQL uses: NOT (status <=> 'published') - - $query = $articles->find() - ->where(function (QueryExpression $exp) { - // True when values are equal, treating NULL = NULL as true - return $exp->isNotDistinctFrom('category_id', null); - }); - // WHERE category_id IS NOT DISTINCT FROM NULL - // MySQL uses: category_id <=> NULL - ``` +- Added `notBetween()` method for `NOT BETWEEN` expressions. + See [Query Builder](../orm/query-builder#advanced-conditions). +- Added `inOrNull()` and `notInOrNull()` methods for combining `IN` conditions with `IS NULL`. +- Added `isDistinctFrom()` and `isNotDistinctFrom()` methods for null-safe comparisons. ### I18n -- `Number::toReadableSize()` now calculates decimal units (KB, MB, GB and TB) -using an exponent of ten, meaning that 1 KB is 1000 Bytes. The units from the -previous calculation method, where 1024 Bytes equaled 1 KB, have been changed -to KiB, MiB, GiB, and TiB as defined in ISO/IEC 80000-13. It is possible to -switch between the two units using a new optional boolean parameter in -`Number::toReadableSize()`, as well as the new global setter `Number::setUseIecUnits()`. +- `Number::toReadableSize()` now uses decimal units (KB = 1000 bytes) by default. + Binary units (KiB = 1024 bytes) can be enabled via parameter or `Number::setUseIecUnits()`. ### ORM -#### Nested Array Format for Marshalling - -The ``associated`` option in ``newEntity()`` and ``patchEntity()`` now supports -the same nested array format as ``contain()``: - -```php -// Nested arrays (new in 5.4) -$entity = $articles->newEntity($data, [ - 'associated' => [ - 'Tags', - 'Comments' => [ - 'Users', - 'Attachments', - ], - ], -]); - -// Mixed with options -$entity = $articles->newEntity($data, [ - 'associated' => [ - 'Tags' => ['onlyIds' => true], - 'Comments' => [ - 'Users', - 'validate' => 'special', - ], - ], -]); -``` - -CakePHP distinguishes associations from options using naming conventions: - -- Association names use PascalCase (e.g., ``Users``, ``Comments``) -- Option keys use camelCase (e.g., ``onlyIds``, ``validate``) +- The `associated` option in `newEntity()` and `patchEntity()` now supports + nested array format matching `contain()` syntax. + See [Converting Request Data into Entities](../orm/saving-data#converting-request-data-into-entities). ### Utility -- New `Cake\Utility\Fs\Finder` class provides a fluent, iterator-based API for - discovering files and directories with support for pattern matching, depth - control, and custom filters. The `Cake\Utility\Fs\Path` class offers - cross-platform utilities for path manipulation. +- Added `Cake\Utility\Fs\Finder` class for fluent file discovery with pattern matching, + depth control, and custom filters. Added `Cake\Utility\Fs\Path` for cross-platform + path manipulation. ### View -#### FormHelper Template Variables - -The ``inputContainer`` and ``error`` templates now receive an ``{{inputId}}`` -variable containing the input element's HTML id attribute. This is useful for -generating related element IDs for ARIA attributes or custom JavaScript: - -```php -$this->Form->setTemplates([ - 'inputContainer' => '
{{content}}
', - 'error' => '
{{content}}
', -]); -``` +- Added `{{inputId}}` template variable to `inputContainer` and `error` templates + in FormHelper. See [Built-in Template Variables](../views/helpers/form#built-in-template-variables).