Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 36 additions & 30 deletions docs/en/appendices/5-4-migration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,49 +18,55 @@ bin/cake upgrade rector --rules cakephp54 <path/to/app/src>

### 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

- WIP

## New Features

### Controller

- 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

- 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

- 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

- Added `{{inputId}}` template variable to `inputContainer` and `error` templates
in FormHelper. See [Built-in Template Variables](../views/helpers/form#built-in-template-variables).
45 changes: 45 additions & 0 deletions docs/en/controllers/components/form-protection.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
100 changes: 100 additions & 0 deletions docs/en/development/dependency-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
73 changes: 73 additions & 0 deletions docs/en/orm/query-builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
39 changes: 37 additions & 2 deletions docs/en/orm/saving-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading