Skip to content

Commit a8dee86

Browse files
committed
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
1 parent 1b6d702 commit a8dee86

6 files changed

Lines changed: 467 additions & 2 deletions

File tree

docs/en/appendices/5-4-migration-guide.md

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,138 @@ $this->hasMany('Comments', [
4949

5050
## New Features
5151

52+
### Controller
53+
54+
#### RequestToDto Attribute
55+
56+
A new ``#[RequestToDto]`` attribute enables automatic mapping of request data to
57+
Data Transfer Objects in controller actions. This provides a clean way to handle
58+
form data with type safety:
59+
60+
```php
61+
use Cake\Controller\Attribute\RequestToDto;
62+
63+
class UsersController extends AppController
64+
{
65+
public function create(#[RequestToDto] UserCreateDto $dto): void
66+
{
67+
// $dto is automatically populated from request data
68+
$user = $this->Users->newEntity([
69+
'email' => $dto->email,
70+
'name' => $dto->name,
71+
]);
72+
}
73+
}
74+
```
75+
76+
Your DTO class must implement a static ``createFromArray()`` method:
77+
78+
```php
79+
class UserCreateDto
80+
{
81+
public function __construct(
82+
public string $email,
83+
public string $name,
84+
) {
85+
}
86+
87+
public static function createFromArray(array $data): self
88+
{
89+
return new self(
90+
email: $data['email'] ?? '',
91+
name: $data['name'] ?? '',
92+
);
93+
}
94+
}
95+
```
96+
97+
The attribute supports configuring the data source:
98+
99+
```php
100+
use Cake\Controller\Attribute\Enum\RequestToDtoSource;
101+
102+
// Use query string parameters
103+
public function search(
104+
#[RequestToDto(source: RequestToDtoSource::Query)] SearchDto $dto
105+
): void {}
106+
107+
// Use POST body data
108+
public function create(
109+
#[RequestToDto(source: RequestToDtoSource::Body)] CreateDto $dto
110+
): void {}
111+
112+
// Merge query and body (body takes precedence)
113+
public function update(
114+
#[RequestToDto(source: RequestToDtoSource::Request)] UpdateDto $dto
115+
): void {}
116+
117+
// Auto-detect based on request method (default)
118+
public function handle(
119+
#[RequestToDto(source: RequestToDtoSource::Auto)] DataDto $dto
120+
): void {}
121+
```
122+
123+
#### FormProtection Convenience Methods
124+
125+
``FormProtectionComponent`` now has convenience methods for unlocking actions
126+
and fields:
127+
128+
```php
129+
// In your controller's beforeFilter()
130+
$this->FormProtection->unlockActions(['api', 'webhook']);
131+
$this->FormProtection->unlockFields(['dynamic_field', 'optional_field']);
132+
133+
// With merge option (default is true)
134+
$this->FormProtection->unlockActions('newAction', merge: true);
135+
$this->FormProtection->unlockFields(['field1', 'field2'], merge: false);
136+
```
137+
138+
### Database
139+
140+
#### Query Expression Methods
141+
142+
New convenience methods have been added to ``QueryExpression``:
143+
144+
- ``notBetween()`` for ``NOT BETWEEN`` expressions:
145+
146+
```php
147+
$query = $articles->find()
148+
->where(function (QueryExpression $exp) {
149+
return $exp->notBetween('view_count', 100, 1000);
150+
});
151+
// WHERE view_count NOT BETWEEN 100 AND 1000
152+
```
153+
154+
- ``inOrNull()`` for ``(field IN (...) OR field IS NULL)`` patterns:
155+
156+
```php
157+
$query = $articles->find()
158+
->where(function (QueryExpression $exp) {
159+
return $exp->inOrNull('category_id', [1, 2, 3]);
160+
});
161+
// WHERE (category_id IN (1, 2, 3) OR category_id IS NULL)
162+
```
163+
164+
- ``isDistinctFrom()`` and ``isNotDistinctFrom()`` for null-safe comparisons:
165+
166+
```php
167+
$query = $articles->find()
168+
->where(function (QueryExpression $exp) {
169+
// True when values differ, treating NULL as a comparable value
170+
return $exp->isDistinctFrom('status', 'published');
171+
});
172+
// WHERE status IS DISTINCT FROM 'published'
173+
// MySQL uses: NOT (status <=> 'published')
174+
175+
$query = $articles->find()
176+
->where(function (QueryExpression $exp) {
177+
// True when values are equal, treating NULL = NULL as true
178+
return $exp->isNotDistinctFrom('category_id', null);
179+
});
180+
// WHERE category_id IS NOT DISTINCT FROM NULL
181+
// MySQL uses: category_id <=> NULL
182+
```
183+
52184
### I18n
53185

54186
- `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
58190
switch between the two units using a new optional boolean parameter in
59191
`Number::toReadableSize()`, as well as the new global setter `Number::setUseIecUnits()`.
60192

193+
### ORM
194+
195+
#### Nested Array Format for Marshalling
196+
197+
The ``associated`` option in ``newEntity()`` and ``patchEntity()`` now supports
198+
the same nested array format as ``contain()``:
199+
200+
```php
201+
// Nested arrays (new in 5.4)
202+
$entity = $articles->newEntity($data, [
203+
'associated' => [
204+
'Tags',
205+
'Comments' => [
206+
'Users',
207+
'Attachments',
208+
],
209+
],
210+
]);
211+
212+
// Mixed with options
213+
$entity = $articles->newEntity($data, [
214+
'associated' => [
215+
'Tags' => ['onlyIds' => true],
216+
'Comments' => [
217+
'Users',
218+
'validate' => 'special',
219+
],
220+
],
221+
]);
222+
```
223+
224+
CakePHP distinguishes associations from options using naming conventions:
225+
- Association names use PascalCase (e.g., ``Users``, ``Comments``)
226+
- Option keys use camelCase (e.g., ``onlyIds``, ``validate``)
227+
61228
### Utility
62229

63230
- New `Cake\Utility\Fs\Finder` class provides a fluent, iterator-based API for
64231
discovering files and directories with support for pattern matching, depth
65232
control, and custom filters. The `Cake\Utility\Fs\Path` class offers
66233
cross-platform utilities for path manipulation.
234+
235+
### View
236+
237+
#### FormHelper Template Variables
238+
239+
The ``inputContainer`` and ``error`` templates now receive an ``{{inputId}}``
240+
variable containing the input element's HTML id attribute. This is useful for
241+
generating related element IDs for ARIA attributes or custom JavaScript:
242+
243+
```php
244+
$this->Form->setTemplates([
245+
'inputContainer' => '<div class="input {{type}}{{required}}" id="{{inputId}}-container">{{content}}</div>',
246+
'error' => '<div class="error" id="{{inputId}}-error">{{content}}</div>',
247+
]);
248+
```

docs/en/controllers/components/form-protection.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,51 @@ class WidgetController extends AppController
140140

141141
This example would disable all security checks for the edit action.
142142

143+
You can also use the convenience method ``unlockActions()``:
144+
145+
```php
146+
public function beforeFilter(EventInterface $event): void
147+
{
148+
parent::beforeFilter($event);
149+
150+
// Unlock a single action
151+
$this->FormProtection->unlockActions('edit');
152+
153+
// Unlock multiple actions
154+
$this->FormProtection->unlockActions(['edit', 'api', 'webhook']);
155+
156+
// Replace existing unlocked actions instead of merging
157+
$this->FormProtection->unlockActions(['newAction'], merge: false);
158+
}
159+
```
160+
161+
::: info Added in version 5.4.0
162+
:::
163+
164+
## Unlocking fields
165+
166+
To unlock specific fields from validation, you can use the ``unlockFields()``
167+
convenience method:
168+
169+
```php
170+
public function beforeFilter(EventInterface $event): void
171+
{
172+
parent::beforeFilter($event);
173+
174+
// Unlock a single field
175+
$this->FormProtection->unlockFields('dynamic_field');
176+
177+
// Unlock multiple fields
178+
$this->FormProtection->unlockFields(['optional_field', 'ajax_field']);
179+
180+
// Dot notation for nested fields
181+
$this->FormProtection->unlockFields('user.preferences');
182+
}
183+
```
184+
185+
::: info Added in version 5.4.0
186+
:::
187+
143188
## Handling validation failure through callbacks
144189

145190
If form protection validation fails it will result in a 400 error by default.

docs/en/development/dependency-injection.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,106 @@ database. Because this service is injected into our controller, we can easily
425425
swap the implementation out with a mock object or a dummy sub-class when
426426
testing.
427427

428+
## Request to DTO Mapping
429+
430+
CakePHP supports automatic mapping of request data to Data Transfer Objects (DTOs)
431+
using the `#[RequestToDto]` attribute. This provides a clean, type-safe way to
432+
handle form data in controller actions:
433+
434+
```php
435+
use Cake\Controller\Attribute\RequestToDto;
436+
437+
class UsersController extends AppController
438+
{
439+
public function create(#[RequestToDto] UserCreateDto $dto): void
440+
{
441+
// $dto is automatically populated from request data
442+
$user = $this->Users->newEntity([
443+
'email' => $dto->email,
444+
'name' => $dto->name,
445+
]);
446+
447+
if ($this->Users->save($user)) {
448+
$this->Flash->success('User created');
449+
return $this->redirect(['action' => 'index']);
450+
}
451+
}
452+
}
453+
```
454+
455+
Your DTO class must implement a static `createFromArray()` method:
456+
457+
```php
458+
namespace App\Dto;
459+
460+
class UserCreateDto
461+
{
462+
public function __construct(
463+
public string $email,
464+
public string $name,
465+
public ?string $phone = null,
466+
) {
467+
}
468+
469+
public static function createFromArray(array $data): self
470+
{
471+
return new self(
472+
email: $data['email'] ?? '',
473+
name: $data['name'] ?? '',
474+
phone: $data['phone'] ?? null,
475+
);
476+
}
477+
}
478+
```
479+
480+
### Configuring the Data Source
481+
482+
By default, the attribute auto-detects the data source based on the request method
483+
(query params for GET, body data for POST/PUT/PATCH). You can explicitly configure
484+
the source using the `RequestToDtoSource` enum:
485+
486+
```php
487+
use Cake\Controller\Attribute\RequestToDto;
488+
use Cake\Controller\Attribute\Enum\RequestToDtoSource;
489+
490+
class ArticlesController extends AppController
491+
{
492+
// Use query string parameters
493+
public function search(
494+
#[RequestToDto(source: RequestToDtoSource::Query)] SearchCriteriaDto $criteria
495+
): void {
496+
$articles = $this->Articles->find()
497+
->where(['title LIKE' => "%{$criteria->query}%"])
498+
->limit($criteria->limit);
499+
}
500+
501+
// Use POST body data explicitly
502+
public function create(
503+
#[RequestToDto(source: RequestToDtoSource::Body)] ArticleCreateDto $dto
504+
): void {
505+
// ...
506+
}
507+
508+
// Merge query params and body data (body takes precedence)
509+
public function update(
510+
int $id,
511+
#[RequestToDto(source: RequestToDtoSource::Request)] ArticleUpdateDto $dto
512+
): void {
513+
// ...
514+
}
515+
}
516+
```
517+
518+
The available source options are:
519+
520+
- `RequestToDtoSource::Auto` - Auto-detect based on request method (default)
521+
- `RequestToDtoSource::Query` - Use query string parameters
522+
- `RequestToDtoSource::Body` - Use POST/PUT body data
523+
- `RequestToDtoSource::Request` - Merge query params and body data
524+
525+
::: info Added in version 5.4.0
526+
:::
527+
428528
## Command Example
429529

430530
```php

0 commit comments

Comments
 (0)