From b1e17f63ebf00ed4a14b997c954145fe86529864 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Wed, 14 Oct 2026 14:17:46 +0300 Subject: [PATCH 1/6] Add "Custom hydrator type casters" article --- src/.vitepress/config.js | 1 + src/cookbook/custom-hydrator-type-casters.md | 443 +++++++++++++++++++ src/cookbook/index.md | 1 + 3 files changed, 445 insertions(+) create mode 100644 src/cookbook/custom-hydrator-type-casters.md diff --git a/src/.vitepress/config.js b/src/.vitepress/config.js index 36ebf9b4..66d62a2d 100644 --- a/src/.vitepress/config.js +++ b/src/.vitepress/config.js @@ -206,6 +206,7 @@ export default { {text: 'Disabling CSRF Protection', link: '/cookbook/disabling-csrf-protection'}, {text: 'Sentry Integration', link: '/cookbook/sentry-integration'}, {text: 'Using a Custom Migration Template', link: '/cookbook/custom-migration-template'}, + {text: 'Custom Hydrator Type Casters', link: '/cookbook/custom-hydrator-type-casters'}, {text: 'Using Yii in Third-Party Applications', link: '/cookbook/using-yii-in-third-party-apps'}, {text: 'Third-Party Integrations', link: '/cookbook/third-party-integrations/'} ] diff --git a/src/cookbook/custom-hydrator-type-casters.md b/src/cookbook/custom-hydrator-type-casters.md new file mode 100644 index 00000000..b0d5aa62 --- /dev/null +++ b/src/cookbook/custom-hydrator-type-casters.md @@ -0,0 +1,443 @@ +# Custom hydrator type casters + +[Yii Hydrator](https://github.com/yiisoft/hydrator) creates objects and fills them with data from arrays. It's used +directly and by packages built on top of it: + +- [Yii Input HTTP](https://github.com/yiisoft/input-http) fills request input objects from route arguments, query + parameters, and request body; +- [Yii Form Model](https://github.com/yiisoft/form-model) provides `FormHydrator` that fills form models from submitted + data. + +The incoming data is usually "raw": HTTP requests, database rows, and queue messages contain strings, numbers, and +arrays. Before assigning a value to a property or passing it to a constructor parameter, the hydrator converts it to +the declared type. This conversion is done by a *type caster*. By writing your own type caster, you teach the hydrator +to create your types, such as value objects, from raw data. + +## Why custom type casters + +Out of the box, the hydrator uses a composite type caster with two casters: + +- `PhpNativeTypeCaster` casts values to built-in PHP types (`int`, `string`, `bool`, `array`, and so on); +- `HydratorTypeCaster` creates nested objects from arrays using the hydrator itself. + +That's enough for DTOs with scalar properties, but not for a rich domain model. Consider a property typed as `UserId`, +`Email`, or `Money`. The incoming value is a string, and neither of the default casters knows how to turn it into such +an object. So you end up declaring the property as `string` and converting it by hand wherever it's used: + +```php +final class CreatePostForm extends FormModel +{ + public string $authorId = ''; +} + +// In every action that uses the form: +try { + $authorId = new UserId($form->authorId); +} catch (InvalidArgumentException) { + // Report the error somehow. +} +``` + +With a type caster for `UserId`, the property is declared with its real type, and the conversion happens during +hydration: + +```php +final class CreatePostForm extends FormModel +{ + public ?UserId $authorId = null; +} + +// $form->authorId is already an instance of UserId. +``` + +The conversion logic lives in a single place: once registered, the caster is applied to every property and constructor +parameter of the matching type, in every object the hydrator creates. + +Typical use cases are: + +- value objects for entity IDs, emails, phone numbers, money amounts, and so on; +- types from third-party libraries, such as UUIDs, dates, or currencies; +- consistent normalization of values, for example, parsing dates in a format specific to your application. + +## Anatomy of a type caster + +A type caster implements `Yiisoft\Hydrator\TypeCaster\TypeCasterInterface` with a single method: + +```php +public function cast(mixed $value, TypeCastContext $context): Result; +``` + +- `$value` is the raw value to convert. +- `$context` is a `Yiisoft\Hydrator\TypeCaster\TypeCastContext`. Its `getReflectionType()` method returns the declared + type of the target property or constructor parameter. Use it to decide whether the caster supports the target type. +- The method returns a `Yiisoft\Hydrator\Result`: `Result::success($convertedValue)` if the conversion succeeded or + `Result::fail()` if the caster can't handle the value. + +Follow these rules when writing a type caster: + +1. **Check the target type first.** If the declared type isn't the one your caster is responsible for, return + `Result::fail()` right away so other casters can handle the value. +2. **Handle both named and union types.** A nullable type such as `?Email` (or `Email|null`) is represented by + `ReflectionNamedType`, while `Email|string` is represented by `ReflectionUnionType`. In the latter case, look for + your type among its subtypes. +3. **Don't handle `null` yourself.** If the value is `null`, just return `Result::fail()`. `PhpNativeTypeCaster` + returns `null` for any type that allows it, so a `null` value assigned to `?Email` stays `null`. +4. **Accept already converted values.** If the value is already an instance of the target type, return it as is. +5. **Fail instead of throwing.** If the value can't be converted, for example, a malformed string, return + `Result::fail()`. An exception would break the whole hydration. + +Keep in mind that a failed cast is silent. If no caster succeeds, the hydrator doesn't set the property, so it keeps its +default value, and no error is reported. Reporting the problem to the user is the job of the validation that runs after +hydration. See [Null and invalid values](#null-and-invalid-values) for an example. + +## Registering a type caster + +Type casters are combined with `CompositeTypeCaster`. It tries the casters one by one and uses the result of the first +one that succeeds. To add your caster, redefine `TypeCasterInterface` in the DI container configuration, for example, +in `config/common/di/hydrator.php`: + +```php + static fn() => new CompositeTypeCaster( + new UuidValueTypeCaster(), + new PhpNativeTypeCaster(), + new HydratorTypeCaster(), + ), +]; +``` + +Since the first successful result wins, the order matters: + +- Put custom casters before `PhpNativeTypeCaster`. Otherwise, for a property declared as `UserId|string`, + `PhpNativeTypeCaster` accepts a string as is, and your caster never runs. +- Keep the default casters in the list. Without `PhpNativeTypeCaster`, scalar values won't be cast to built-in types, + and without `HydratorTypeCaster`, nested objects won't be created from arrays. + +If a caster has dependencies, take it from the container: + +```php +TypeCasterInterface::class => static fn(ContainerInterface $container) => new CompositeTypeCaster( + $container->get(MoneyTypeCaster::class), + new PhpNativeTypeCaster(), + new HydratorTypeCaster(), +), +``` + +## Example: UUID value objects + +Entity IDs are often represented as value objects instead of plain strings: `UserId`, `PostId`, `OrderId`, and so on. +Such objects make method signatures self-documenting and prevent passing a post ID where a user ID is expected. +Let's write a type caster that creates them from UUID strings. + +### Value objects + +All ID value objects extend an abstract `UuidValue` class. The example uses +[ramsey/uuid](https://github.com/ramsey/uuid) to validate and generate UUIDs: + +```shell +composer require ramsey/uuid +``` + +```php +value = strtolower($value); + } + + public static function generate(): static + { + return new static(Uuid::uuid7()->toString()); + } + + public function equals(self $other): bool + { + return $other instanceof static && $other->value === $this->value; + } + + public function __toString(): string + { + return $this->value; + } +} +``` + +The constructor is final, so `new static()` is safe and every child class can be created from a string the same way. +A concrete ID is just an empty child class: + +```php +findUuidValueClass($context->getReflectionType()); + if ($class === null) { + return Result::fail(); + } + + if ($value instanceof $class) { + return Result::success($value); + } + + if (!is_string($value) || !Uuid::isValid($value)) { + return Result::fail(); + } + + return Result::success(new $class($value)); + } + + /** + * @return class-string|null + */ + private function findUuidValueClass(?ReflectionType $type): ?string + { + $types = match (true) { + $type instanceof ReflectionNamedType => [$type], + $type instanceof ReflectionUnionType => $type->getTypes(), + default => [], + }; + + foreach ($types as $subType) { + if ( + $subType instanceof ReflectionNamedType + && !$subType->isBuiltin() + && is_subclass_of($subType->getName(), UuidValue::class) + ) { + return $subType->getName(); + } + } + + return null; + } +} +``` + +The caster follows the rules described above. Note that the class to create is taken from the declared type, not from +the value: all IDs share the same UUID format, and a string alone can't tell whether it's a user ID or a post ID. As +a result, a single caster serves all `UuidValue` child classes, and adding a new ID class doesn't require any changes in +it. + +Register `UuidValueTypeCaster` as shown in the [Registering a type caster](#registering-a-type-caster) section. + +### Using the value objects + +Now any class that's populated by the hydrator can declare ID value objects directly. + +#### Hydrator + +The same string becomes a different value object depending on the type of the property: + +```php +use App\Post\PostId; +use App\User\UserId; +use Yiisoft\Hydrator\HydratorInterface; + +final class CommentFilter +{ + public ?UserId $authorId = null; + public ?PostId $postId = null; +} + +/** @var HydratorInterface $hydrator */ +$filter = $hydrator->create(CommentFilter::class, [ + 'authorId' => '0192f2a4-7a4b-7c3e-9a1b-3f5c8d2e6a10', + 'postId' => '0192f2a4-7a4b-7c3e-9a1b-3f5c8d2e6a10', +]); + +// $filter->authorId is UserId, $filter->postId is PostId. +// $filter->authorId->equals($filter->postId) returns false. +``` + +#### Request input + +The [Yii Input HTTP](https://github.com/yiisoft/input-http) package uses the hydrator to fill request input objects, so +the same type caster works for route arguments, query parameters, and request body. Extend `AbstractInput` to get +the input validated after hydration: + +```php +getValidationResult()->isValid()) { + // Return an error response. + } + + // $input->id is PostId, $input->authorId is UserId. + // ... + } +} +``` + +#### Form model + +`FormHydrator` from [Yii Form Model](https://github.com/yiisoft/form-model) populates form models with the same +hydrator, so form properties can be typed as value objects too: + +```php +formHydrator->populateFromPostAndValidate($form, $request)) { + // $form->authorId is UserId. + // ... + } + + // ... + } +} +``` + +### Null and invalid values + +Here is what happens with a property declared as `?UserId` for different input values: + +| Input value | Handled by | Property value | +|------------------------------------------|------------------------|----------------------------------| +| `'0192f2a4-7a4b-7c3e-9a1b-3f5c8d2e6a10'` | `UuidValueTypeCaster` | `UserId` instance | +| `null` | `PhpNativeTypeCaster` | `null` | +| `'not-a-uuid'` | none, all casters fail | default value, `null` in example | + +The last row is the important one: the hydrator doesn't report the malformed value. With the `Required` rule, as in +the examples above, validation catches it with the "cannot be blank" error. But if the property is optional, an invalid +UUID is silently turned into `null` and is indistinguishable from a missing value. If that's not acceptable, keep +the raw value for validation: declare the property as `string`, validate its format, and convert it to the value object +after validation. + +## When to use a type caster + +A type caster fits well when the conversion: + +- depends only on the value and the declared type; +- is the same everywhere in the application; +- doesn't need I/O, such as database queries or HTTP calls. + +Value objects for IDs, emails, and money amounts match all three. Keep the conversion in a dedicated mapper or a domain +factory instead when it needs a database lookup (for example, turning an ID into a loaded entity), depends on context +such as the current user, combines several input fields into one object, or differs between use cases. diff --git a/src/cookbook/index.md b/src/cookbook/index.md index b2857f4b..b24f28f0 100644 --- a/src/cookbook/index.md +++ b/src/cookbook/index.md @@ -18,6 +18,7 @@ This book conforms to the [Terms of Yii Documentation](https://www.yiiframework. - [Disabling CSRF protection](disabling-csrf-protection.md) - [Sentry integration](sentry-integration.md) - [Using a custom migration template](custom-migration-template.md) +- [Custom hydrator type casters](custom-hydrator-type-casters.md) - [Using Yii in third-party applications](using-yii-in-third-party-apps.md) - [Third-party integrations](third-party-integrations/index.md) - [Working on Windows](working-on-windows.md) From 7b52179daeea076dfaf658d7681411ffed8ff517 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Wed, 14 Oct 2026 14:24:35 +0300 Subject: [PATCH 2/6] fix --- src/cookbook/custom-hydrator-type-casters.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/cookbook/custom-hydrator-type-casters.md b/src/cookbook/custom-hydrator-type-casters.md index b0d5aa62..f061eb8a 100644 --- a/src/cookbook/custom-hydrator-type-casters.md +++ b/src/cookbook/custom-hydrator-type-casters.md @@ -3,8 +3,8 @@ [Yii Hydrator](https://github.com/yiisoft/hydrator) creates objects and fills them with data from arrays. It's used directly and by packages built on top of it: -- [Yii Input HTTP](https://github.com/yiisoft/input-http) fills request input objects from route arguments, query - parameters, and request body; +- [Yii Input HTTP](https://github.com/yiisoft/input-http) fills request input objects from query parameters, request + body, and, with the help of [Yii Router](https://github.com/yiisoft/router), route arguments; - [Yii Form Model](https://github.com/yiisoft/form-model) provides `FormHydrator` that fills form models from submitted data. @@ -315,8 +315,9 @@ $filter = $hydrator->create(CommentFilter::class, [ #### Request input The [Yii Input HTTP](https://github.com/yiisoft/input-http) package uses the hydrator to fill request input objects, so -the same type caster works for route arguments, query parameters, and request body. Extend `AbstractInput` to get -the input validated after hydration: +the same type caster works for query parameters, request body, and route arguments. The `RouteArgument` attribute +comes from [Yii Router](https://github.com/yiisoft/router). Extend `AbstractInput` to get the input validated after +hydration: ```php Date: Wed, 14 Oct 2026 14:32:46 +0300 Subject: [PATCH 3/6] fix --- src/cookbook/custom-hydrator-type-casters.md | 89 +++++++++++++++++--- 1 file changed, 77 insertions(+), 12 deletions(-) diff --git a/src/cookbook/custom-hydrator-type-casters.md b/src/cookbook/custom-hydrator-type-casters.md index f061eb8a..0015ab52 100644 --- a/src/cookbook/custom-hydrator-type-casters.md +++ b/src/cookbook/custom-hydrator-type-casters.md @@ -94,7 +94,8 @@ hydration. See [Null and invalid values](#null-and-invalid-values) for an exampl Type casters are combined with `CompositeTypeCaster`. It tries the casters one by one and uses the result of the first one that succeeds. To add your caster, redefine `TypeCasterInterface` in the DI container configuration, for example, -in `config/common/di/hydrator.php`: +in `config/common/di/hydrator.php`. This chain is used by `HydratorInterface` from the container and by packages that +rely on it, such as Yii Input HTTP: ```php static fn(ContainerInterface $container) => new CompositeTypeCaster( - $container->get(MoneyTypeCaster::class), - new PhpNativeTypeCaster(), - new HydratorTypeCaster(), -), + static fn(ContainerInterface $container) => new CompositeTypeCaster( + $container->get(MoneyTypeCaster::class), + new PhpNativeTypeCaster(), + new HydratorTypeCaster(), + ), +]; +``` + +### Registering for form models + +`FormHydrator` from Yii Form Model doesn't use `TypeCasterInterface`. Its DI configuration creates a dedicated hydrator +with its own chain of type casters, so the caster has to be added there as well: + +```php + [ + '__construct()' => [ + 'hydrator' => DynamicReference::to([ + 'class' => Hydrator::class, + '__construct()' => [ + 'typeCaster' => new CompositeTypeCaster( + new NullTypeCaster(emptyString: true), + new UuidValueTypeCaster(), + new PhpNativeTypeCaster(), + new NonArrayTypeCaster(), + new HydratorTypeCaster(), + ), + ], + ]), + ], + ], +]; ``` +This definition replaces the one from the package, so keep its type casters. The list above is taken from +`config/di.php` of Yii Form Model 1.1; check it against the version you use. `NullTypeCaster` turns empty strings +submitted by forms into `null` for nullable properties, so it stays first. + ## Example: UUID value objects Entity IDs are often represented as value objects instead of plain strings: `UserId`, `PostId`, `OrderId`, and so on. @@ -227,6 +285,7 @@ use Yiisoft\Hydrator\Result; use Yiisoft\Hydrator\TypeCaster\TypeCastContext; use Yiisoft\Hydrator\TypeCaster\TypeCasterInterface; +use function count; use function is_string; use function is_subclass_of; @@ -251,6 +310,9 @@ final readonly class UuidValueTypeCaster implements TypeCasterInterface } /** + * Returns the `UuidValue` child class from the declared type, or `null` if there is none or there are several + * of them, such as in `UserId|PostId`. + * * @return class-string|null */ private function findUuidValueClass(?ReflectionType $type): ?string @@ -261,17 +323,18 @@ final readonly class UuidValueTypeCaster implements TypeCasterInterface default => [], }; + $classes = []; foreach ($types as $subType) { if ( $subType instanceof ReflectionNamedType && !$subType->isBuiltin() && is_subclass_of($subType->getName(), UuidValue::class) ) { - return $subType->getName(); + $classes[] = $subType->getName(); } } - return null; + return count($classes) === 1 ? $classes[0] : null; } } ``` @@ -279,9 +342,11 @@ final readonly class UuidValueTypeCaster implements TypeCasterInterface The caster follows the rules described above. Note that the class to create is taken from the declared type, not from the value: all IDs share the same UUID format, and a string alone can't tell whether it's a user ID or a post ID. As a result, a single caster serves all `UuidValue` child classes, and adding a new ID class doesn't require any changes in -it. +it. For the same reason, the caster fails for a type with several ID classes, such as `UserId|PostId`: there's no way +to choose the right one. -Register `UuidValueTypeCaster` as shown in the [Registering a type caster](#registering-a-type-caster) section. +Register `UuidValueTypeCaster` as shown in the [Registering a type caster](#registering-a-type-caster) section, +including the [form model configuration](#registering-for-form-models) if you use Yii Form Model. ### Using the value objects @@ -364,8 +429,8 @@ final readonly class Action #### Form model -`FormHydrator` from [Yii Form Model](https://github.com/yiisoft/form-model) populates form models with the same -hydrator, so form properties can be typed as value objects too: +`FormHydrator` from [Yii Form Model](https://github.com/yiisoft/form-model) populates form models. Once the caster is +[registered for form models](#registering-for-form-models), form properties can be typed as value objects too: ```php Date: Wed, 14 Oct 2026 14:38:07 +0300 Subject: [PATCH 4/6] fix --- src/cookbook/custom-hydrator-type-casters.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/cookbook/custom-hydrator-type-casters.md b/src/cookbook/custom-hydrator-type-casters.md index 0015ab52..1d0219fc 100644 --- a/src/cookbook/custom-hydrator-type-casters.md +++ b/src/cookbook/custom-hydrator-type-casters.md @@ -86,9 +86,14 @@ Follow these rules when writing a type caster: 5. **Fail instead of throwing.** If the value can't be converted, for example, a malformed string, return `Result::fail()`. An exception would break the whole hydration. -Keep in mind that a failed cast is silent. If no caster succeeds, the hydrator doesn't set the property, so it keeps its -default value, and no error is reported. Reporting the problem to the user is the job of the validation that runs after -hydration. See [Null and invalid values](#null-and-invalid-values) for an example. +Keep in mind what happens when no caster succeeds: + +- For a property, the cast fails silently. The hydrator doesn't set the property, so it keeps its default value, and + no error is reported. Reporting the problem to the user is the job of the validation that runs after hydration. + See [Null and invalid values](#null-and-invalid-values) for an example. +- For a constructor parameter, the hydrator omits the argument. If the parameter has a default value, it's used. + Otherwise, object creation fails with `WrongConstructorArgumentsCountException`, even for a nullable parameter + without a default value. ## Registering a type caster From d7c46f345da175171d16476393b86860eb8033bc Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 15 Oct 2026 13:30:36 +0300 Subject: [PATCH 5/6] Address Vale findings in type caster guide --- src/cookbook/custom-hydrator-type-casters.md | 92 +++++++++++--------- 1 file changed, 49 insertions(+), 43 deletions(-) diff --git a/src/cookbook/custom-hydrator-type-casters.md b/src/cookbook/custom-hydrator-type-casters.md index 1d0219fc..7228b9cd 100644 --- a/src/cookbook/custom-hydrator-type-casters.md +++ b/src/cookbook/custom-hydrator-type-casters.md @@ -4,20 +4,20 @@ directly and by packages built on top of it: - [Yii Input HTTP](https://github.com/yiisoft/input-http) fills request input objects from query parameters, request - body, and, with the help of [Yii Router](https://github.com/yiisoft/router), route arguments; + body, and, with the help of [Yii Router](https://github.com/yiisoft/router), route arguments. - [Yii Form Model](https://github.com/yiisoft/form-model) provides `FormHydrator` that fills form models from submitted data. The incoming data is usually "raw": HTTP requests, database rows, and queue messages contain strings, numbers, and arrays. Before assigning a value to a property or passing it to a constructor parameter, the hydrator converts it to -the declared type. This conversion is done by a *type caster*. By writing your own type caster, you teach the hydrator +the declared type. A *type caster* performs this conversion. By writing your own type caster, you teach the hydrator to create your types, such as value objects, from raw data. ## Why custom type casters Out of the box, the hydrator uses a composite type caster with two casters: -- `PhpNativeTypeCaster` casts values to built-in PHP types (`int`, `string`, `bool`, `array`, and so on); +- `PhpNativeTypeCaster` casts values to built-in PHP types, such as `int`, `string`, `bool`, and `array`. - `HydratorTypeCaster` creates nested objects from arrays using the hydrator itself. That's enough for DTOs with scalar properties, but not for a rich domain model. Consider a property typed as `UserId`, @@ -38,7 +38,7 @@ try { } ``` -With a type caster for `UserId`, the property is declared with its real type, and the conversion happens during +With a type caster for `UserId`, you can declare the property with its real type. The conversion happens during hydration: ```php @@ -50,13 +50,13 @@ final class CreatePostForm extends FormModel // $form->authorId is already an instance of UserId. ``` -The conversion logic lives in a single place: once registered, the caster is applied to every property and constructor -parameter of the matching type, in every object the hydrator creates. +The conversion logic lives in a single place. Once you register the caster, the hydrator applies it to every property +and constructor parameter of the matching type in every object it creates. Typical use cases are: -- value objects for entity IDs, emails, phone numbers, money amounts, and so on; -- types from third-party libraries, such as UUIDs, dates, or currencies; +- value objects, such as entity IDs, emails, phone numbers, and money amounts. +- types from third-party libraries, such as UUIDs, dates, or currencies. - consistent normalization of values, for example, parsing dates in a format specific to your application. ## Anatomy of a type caster @@ -77,19 +77,19 @@ Follow these rules when writing a type caster: 1. **Check the target type first.** If the declared type isn't the one your caster is responsible for, return `Result::fail()` right away so other casters can handle the value. -2. **Handle both named and union types.** A nullable type such as `?Email` (or `Email|null`) is represented by - `ReflectionNamedType`, while `Email|string` is represented by `ReflectionUnionType`. In the latter case, look for - your type among its subtypes. +2. **Handle both named and union types.** PHP represents a nullable type such as `?Email` (or `Email|null`) with + `ReflectionNamedType` and `Email|string` with `ReflectionUnionType`. In the latter case, look for your type among + its subtypes. 3. **Don't handle `null` yourself.** If the value is `null`, just return `Result::fail()`. `PhpNativeTypeCaster` returns `null` for any type that allows it, so a `null` value assigned to `?Email` stays `null`. 4. **Accept already converted values.** If the value is already an instance of the target type, return it as is. -5. **Fail instead of throwing.** If the value can't be converted, for example, a malformed string, return +5. **Fail instead of throwing.** If your caster can't convert the value, for example, a malformed string, return `Result::fail()`. An exception would break the whole hydration. Keep in mind what happens when no caster succeeds: -- For a property, the cast fails silently. The hydrator doesn't set the property, so it keeps its default value, and - no error is reported. Reporting the problem to the user is the job of the validation that runs after hydration. +- For a property, the cast fails silently. The hydrator doesn't set the property or report an error, so the property + keeps its default value. Reporting the problem to the user is the job of the validation that runs after hydration. See [Null and invalid values](#null-and-invalid-values) for an example. - For a constructor parameter, the hydrator omits the argument. If the parameter has a default value, it's used. Otherwise, object creation fails with `WrongConstructorArgumentsCountException`, even for a nullable parameter @@ -97,10 +97,10 @@ Keep in mind what happens when no caster succeeds: ## Registering a type caster -Type casters are combined with `CompositeTypeCaster`. It tries the casters one by one and uses the result of the first +Combine type casters with `CompositeTypeCaster`. It tries the casters one by one and uses the result of the first one that succeeds. To add your caster, redefine `TypeCasterInterface` in the DI container configuration, for example, -in `config/common/di/hydrator.php`. This chain is used by `HydratorInterface` from the container and by packages that -rely on it, such as Yii Input HTTP: +in `config/common/di/hydrator.php`. The `HydratorInterface` implementation from the container uses this chain, as do +packages that rely on it, such as Yii Input HTTP: ```php Date: Thu, 15 Oct 2026 13:44:55 +0300 Subject: [PATCH 6/6] Fix UUID normalization in cookbook [skip ci] --- src/cookbook/custom-hydrator-type-casters.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/cookbook/custom-hydrator-type-casters.md b/src/cookbook/custom-hydrator-type-casters.md index 7228b9cd..1b4c09bf 100644 --- a/src/cookbook/custom-hydrator-type-casters.md +++ b/src/cookbook/custom-hydrator-type-casters.md @@ -223,7 +223,6 @@ use Ramsey\Uuid\Uuid; use Stringable; use function sprintf; -use function strtolower; abstract readonly class UuidValue implements Stringable { @@ -234,7 +233,7 @@ abstract readonly class UuidValue implements Stringable if (!Uuid::isValid($value)) { throw new InvalidArgumentException(sprintf('"%s" is not a valid UUID.', $value)); } - $this->value = strtolower($value); + $this->value = Uuid::fromString($value)->toString(); } public static function generate(): static