Skip to content

Commit c4584d6

Browse files
committed
[core] v4.4.0 - See CHANGELOG.md for details
1 parent 048ca9c commit c4584d6

14 files changed

Lines changed: 1943 additions & 1662 deletions

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
# CHANGELOG
22

3+
## [v4.4.0] - 2026-05-12
4+
5+
### Added
6+
7+
- **`#[ArrayOf]` now supports multiple types (polymorphic typed arrays).** Pass two or more class-strings or `Native` cases; each element is resolved against the declared types in declaration order — first successful match wins. Single-type usage is fully backwards compatible.
8+
9+
```php
10+
#[ArrayOf(ShippingDTO::class, PickupDTO::class)]
11+
public array $deliveries;
12+
13+
#[ArrayOf(DTO1::class, Native::int)]
14+
public array $mixed;
15+
```
16+
17+
> **Note:** The `ArrayOf` attribute's internal property has been renamed from `$class` (`string`) to `$classes` (`array`). If you inspect `ArrayOf` via `ReflectionAttribute::newInstance()`, update `->class` to `->classes[0]`.
18+
19+
### Fixed
20+
21+
- **`with()` silently ignored updates to nullable properties currently holding `null`.** The `isset($values[$path])` check in the flat-path loop returned `false` when an existing property value was `null`, causing the update to be skipped entirely. Replaced with `array_key_exists($path, $values)` so updates to null-valued properties are applied correctly.
22+
23+
- **`#[ArrayOf()]` with no arguments now throws `InvalidArrayOfTargetException` at scan time.** Previously, the empty-argument case was silently ignored because `getAttributeArgument` collapsed empty attribute arguments to `null` via a truthiness check. The check has been corrected to distinguish "attribute absent" (`null`) from "attribute present with no arguments" (`[]`).
24+
325
## [v4.3.2] - 2026-05-04
426

527
### Fixed

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,8 @@ CreateUserDTO::fromArray(['name' => 'Kip']); // role = 'member'
487487

488488
Marks an array property as a typed collection of ImmutableBase instances or primitive scalar values. Each element is automatically validated or instantiated. The target must be a subclass of DTO, VO, or SVO, or a `Native` enum case for scalar arrays.
489489

490+
Pass multiple types for polymorphic arrays — each element is resolved in declaration order, first match wins.
491+
490492
**Primitive scalar arrays** can be declared using `Native` enum cases instead of a class name:
491493

492494
| Case | PHP type |
@@ -501,7 +503,6 @@ use ReallifeKip\ImmutableBase\Attributes\ArrayOf;
501503

502504
readonly class SignUpUsersDTO extends DataTransferObject
503505
{
504-
505506
// ImmutableBase subclass
506507
#[ArrayOf(User::class)]
507508
public array $users;
@@ -511,6 +512,10 @@ readonly class SignUpUsersDTO extends DataTransferObject
511512
public array $tags;
512513
#[ArrayOf(Native::int)]
513514
public array $scores;
515+
516+
// Polymorphic — first match wins
517+
#[ArrayOf(ShippingDTO::class, Native::string, PickupDTO::class, Native::int)]
518+
public array $deliveries;
514519
}
515520
```
516521

README_TW.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,8 @@ CreateUserDTO::fromArray(['name' => 'Kip']); // role = 'member'
487487

488488
將陣列屬性標記為 ImmutableBase 實例或純量值的型別集合。每個元素會自動驗證或實例化。目標必須是 DTO、VO 或 SVO 的子類,或純量陣列可使用 `Native` enum case。
489489

490+
傳入多個型別可宣告多型陣列——每個元素依宣告順序逐一嘗試,首個成功配對的型別勝出。
491+
490492
**純量型別陣列**可使用 `Native` enum case 取代類別名稱:
491493

492494
| Case | PHP 型別 |
@@ -510,6 +512,10 @@ readonly class SignUpUsersDTO extends DataTransferObject
510512
public array $tags;
511513
#[ArrayOf(Native::int)]
512514
public array $scores;
515+
516+
// 多型——首個成功配對的型別勝出
517+
#[ArrayOf(ShippingDTO::class, Native::string, PickupDTO::class, Native::int)]
518+
public array $deliveries;
513519
}
514520
```
515521

src/Attributes/ArrayOf.php

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,39 @@
1-
<?php
2-
3-
declare (strict_types = 1);
4-
5-
namespace ReallifeKip\ImmutableBase\Attributes;
6-
7-
use ReallifeKip\ImmutableBase\Enums\Native;
8-
9-
/**
10-
* Declares that an array property contains typed elements of a specific
11-
* ImmutableBase subclass, or validated PHP scalar values via Native.
12-
*
13-
* Accepts either a class-string of an ImmutableBase subclass, or a
14-
* Native enum case (Native::string, Native::int, Native::float, Native::bool)
15-
* for primitive typed arrays.
16-
*
17-
* Must be applied to properties typed exactly as `array`. Union types
18-
* or non-array types will trigger InvalidArrayOfUsageException at scan time.
19-
*
20-
* @example #[ArrayOf(OrderItemDTO::class)] public array $items
21-
* @example #[ArrayOf(Native::string)] public array $tags
22-
* @example #[ArrayOf(Native::int)] public array $scores
23-
*/
24-
#[\Attribute(\Attribute::TARGET_PROPERTY)]
25-
final class ArrayOf
26-
{
27-
/**
28-
* @param class-string|Native $class FQCN of an ImmutableBase subclass, or a Native enum case for primitive typed arrays.
29-
*/
30-
private function __construct(
31-
public string $class
32-
) {}
33-
}
1+
<?php
2+
3+
declare (strict_types = 1);
4+
5+
namespace ReallifeKip\ImmutableBase\Attributes;
6+
7+
use ReallifeKip\ImmutableBase\Enums\Native;
8+
9+
/**
10+
* Declares that an array property contains typed elements of one or more
11+
* ImmutableBase subclasses, or validated PHP scalar values via Native.
12+
*
13+
* Accepts one or more class-strings of ImmutableBase subclasses, or Native
14+
* enum cases (Native::string, Native::int, Native::float, Native::bool)
15+
* for primitive typed arrays. When multiple types are given, each element
16+
* is resolved against the declared types in order (first match wins).
17+
*
18+
* Must be applied to properties typed exactly as `array`. Union types
19+
* or non-array types will trigger InvalidArrayOfUsageException at scan time.
20+
*
21+
* @example #[ArrayOf(OrderItemDTO::class)] public array $items
22+
* @example #[ArrayOf(Native::string)] public array $tags
23+
* @example #[ArrayOf(Native::int)] public array $scores
24+
* @example #[ArrayOf(ShippingDTO::class, PickupDTO::class)] public array $deliveries
25+
*/
26+
#[\Attribute(\Attribute::TARGET_PROPERTY)]
27+
final class ArrayOf
28+
{
29+
/** @var list<string> */
30+
public readonly array $classes;
31+
32+
/**
33+
* @param class-string|Native ...$classes One or more FQCNs of ImmutableBase subclasses, or Native enum cases.
34+
*/
35+
private function __construct(string | Native ...$classes)
36+
{
37+
$this->classes = array_values($classes);
38+
}
39+
}

src/BasicTrait.php

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ trait BasicTrait
1919
* of the first matching attribute instance. When false, returns the
2020
* full argument array of the first matching instance.
2121
*
22-
* If the attribute is not present, or present without constructor
23-
* arguments, `null` is returned.
22+
* If the attribute is not present, `null` is returned.
23+
* When `$getFirst` is true and the attribute has no arguments, `null` is returned.
24+
* When `$getFirst` is false and the attribute has no arguments, `[]` is returned.
2425
*
2526
* @param ReflectionClass|ReflectionProperty $target Reflection target to inspect.
2627
* @param class-string $name Fully-qualified attribute class name.
@@ -30,12 +31,9 @@ trait BasicTrait
3031
public static function getAttributeArgument(ReflectionClass | ReflectionProperty $target, string $name, bool $getFirst = true): mixed
3132
{
3233
if ($value = $target->getAttributes($name)) {
33-
$value = $value[0];
34-
if ($value = $value->getArguments()) {
35-
return $getFirst ? $value[0] : $value;
36-
}
34+
$value = $value[0]->getArguments();
3735

38-
return null;
36+
return $getFirst ? ($value[0] ?? null) : $value;
3937
}
4038

4139
return null;

0 commit comments

Comments
 (0)