Skip to content

Commit 033b9ee

Browse files
committed
feat: add typed array support with new ValueArray class and related tests
1 parent 1f4da7a commit 033b9ee

File tree

6 files changed

+1316
-27
lines changed

6 files changed

+1316
-27
lines changed

CHANGELOG-1.6.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Changelog - Version 1.6
2+
3+
## [1.6.0] - 2025-01-28
4+
5+
### Added
6+
7+
- **Typed Arrays Support**: Added support for typed arrays in resource attributes, allowing type-safe array elements
8+
- New `of()` method on `array()` descriptor to specify element type
9+
- Support for class references (e.g., `ValueString::class`) in addition to descriptor instances
10+
- Alternative `arrayOf()` helper method for more concise syntax
11+
- Full support for nested typed arrays (multi-dimensional arrays)
12+
- Compatible with all conditional methods (`when()`, `whenNotNull()`, `whenFilled()`, etc.)
13+
- Type casting applied to all array elements (strings, integers, floats, booleans, etc.)

readme.md

Lines changed: 173 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ protected function toAttributes(Request $request): array
209209
```
210210

211211
#### Described attributes
212-
_**@see** [described notation](##described-notation)_
212+
_**@see** [described notation](#described-notation)_
213213

214214
```php
215215
protected function toAttributes(Request $request): array
@@ -283,7 +283,7 @@ protected function toRelationships(Request $request): array
283283
```
284284

285285
#### Described attributes
286-
_**@see** [described notation](##described-notation)_
286+
_**@see** [described notation](#described-notation)_
287287

288288
```php
289289
protected function toRelationships(Request $request): array
@@ -391,17 +391,18 @@ UserResource::collection(User::all()); // => JsonApiCollection
391391
## Described notation
392392

393393
### Value methods
394-
| Method | Description |
395-
|-----------|------------------------------------------|
396-
| `bool` | Cast to boolean |
397-
| `integer` | Cast to integer |
398-
| `float` | Cast to float |
399-
| `string` | Cast to string |
400-
| `date` | Cast to date, allow to use custom format |
401-
| `array` | Cast to array |
402-
| `mixed` | Don't cast, return as is |
403-
| `enum` | Get enum value |
404-
| `struct` | Custom struct. Accept an array of values |
394+
| Method | Description |
395+
|-----------|-----------------------------------------------------------------|
396+
| `bool` | Cast to boolean |
397+
| `integer` | Cast to integer |
398+
| `float` | Cast to float |
399+
| `string` | Cast to string |
400+
| `date` | Cast to date, allow to use custom format |
401+
| `array` | Cast to array, supports typed arrays with `->of()` |
402+
| `arrayOf` | Helper method for typed arrays (alternative to `array()->of()`) |
403+
| `mixed` | Don't cast, return as is |
404+
| `enum` | Get enum value |
405+
| `struct` | Custom struct. Accept an array of values |
405406

406407
### Relation methods
407408
| Method | Description |
@@ -454,3 +455,162 @@ Will return:
454455
"role": "ADMIN"
455456
]
456457
```
458+
459+
### Typed Arrays
460+
461+
The `array` descriptor supports typed arrays to ensure all elements are cast to a specific type. This is useful when you need to guarantee type consistency across array elements.
462+
463+
#### Basic Usage
464+
465+
```php
466+
// UserResource.php
467+
protected function toAttributes(Request $request): array
468+
{
469+
return [
470+
// Array of strings - all values will be cast to string
471+
'tags' => $this->array('tags')->of($this->string()),
472+
473+
// Array of integers - all values will be cast to integer
474+
'scores' => $this->array('scores')->of($this->integer()),
475+
476+
// Array of floats
477+
'prices' => $this->array('prices')->of($this->float()),
478+
479+
// Array of booleans
480+
'flags' => $this->array('flags')->of($this->bool()),
481+
];
482+
}
483+
```
484+
485+
#### Using Class References
486+
487+
You can also use class references instead of descriptor instances:
488+
489+
```php
490+
use Ark4ne\JsonApi\Descriptors\Values\ValueString;
491+
use Ark4ne\JsonApi\Descriptors\Values\ValueInteger;
492+
493+
protected function toAttributes(Request $request): array
494+
{
495+
return [
496+
'tags' => $this->array('tags')->of(ValueString::class),
497+
'scores' => $this->array('scores')->of(ValueInteger::class),
498+
];
499+
}
500+
```
501+
502+
#### Alternative Syntax
503+
504+
You can also use the `arrayOf()` helper method:
505+
506+
```php
507+
protected function toAttributes(Request $request): array
508+
{
509+
return [
510+
'tags' => $this->arrayOf($this->string(), 'tags'),
511+
'scores' => $this->arrayOf($this->integer(), 'scores'),
512+
];
513+
}
514+
```
515+
516+
#### Nested Typed Arrays
517+
518+
For multi-dimensional arrays, you can nest `array()->of()` calls:
519+
520+
```php
521+
protected function toAttributes(Request $request): array
522+
{
523+
return [
524+
// 2D array (matrix) of integers
525+
'matrix' => $this->array('matrix')->of(
526+
$this->array()->of($this->integer())
527+
),
528+
];
529+
}
530+
```
531+
532+
#### With Closures and Transformations
533+
534+
Combine typed arrays with closures for data transformation:
535+
536+
```php
537+
protected function toAttributes(Request $request): array
538+
{
539+
return [
540+
// Transform and type cast
541+
'doubled' => $this->array(fn() => array_map(fn($n) => $n * 2, $this->numbers))
542+
->of($this->integer()),
543+
544+
// Access nested properties
545+
'user_ids' => $this->array(fn() => $this->users->pluck('id'))
546+
->of($this->integer()),
547+
];
548+
}
549+
```
550+
551+
#### With Conditions
552+
553+
Typed arrays support all conditional methods:
554+
555+
```php
556+
protected function toAttributes(Request $request): array
557+
{
558+
return [
559+
// Only include if not null
560+
'tags' => $this->array('tags')->of($this->string())->whenNotNull(),
561+
562+
// Only include if array is not empty
563+
'scores' => $this->array('scores')->of($this->integer())->whenFilled(),
564+
565+
// Conditional based on closure
566+
'admin_notes' => $this->array('notes')->of($this->string())
567+
->when(fn() => $request->user()->isAdmin()),
568+
];
569+
}
570+
```
571+
572+
> **⚠️ Important Note:** Conditions applied to the item type (inside `of()`) are **not evaluated per-item**. They apply to the entire array descriptor, not individual elements.
573+
>
574+
> ```php
575+
> // ❌ This will NOT filter individual items
576+
> 'even-numbers' => $this->array('numbers')->of(
577+
> $this->integer()->when(fn($request, $model, $attr) => $attr % 2 === 0)
578+
> )
579+
> // All items will be included, the when() doesn't filter per item
580+
>
581+
> // ✅ To filter items, do it before passing to the array
582+
> 'even-numbers' => $this->array(
583+
> fn() => array_filter($this->numbers, fn($n) => $n % 2 === 0)
584+
> )->of($this->integer())
585+
> ```
586+
587+
#### Example
588+
589+
Given a model with mixed-type arrays:
590+
591+
```php
592+
$user = new User([
593+
'tags' => ['php', 'laravel', 123, true],
594+
'scores' => [95.5, '87', 92, '78.9'],
595+
]);
596+
```
597+
598+
The resource will ensure type consistency:
599+
600+
```php
601+
protected function toAttributes(Request $request): array
602+
{
603+
return [
604+
'tags' => $this->array('tags')->of($this->string()),
605+
'scores' => $this->array('scores')->of($this->integer()),
606+
];
607+
}
608+
```
609+
610+
Output:
611+
```json
612+
{
613+
"tags": ["php", "laravel", "123", "1"],
614+
"scores": [95, 87, 92, 78]
615+
}
616+
```

src/Descriptors/Values.php

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace Ark4ne\JsonApi\Descriptors;
44

5-
use Ark4ne\JsonApi\Descriptors\Values\{
5+
use Ark4ne\JsonApi\Descriptors\Values\{Value,
66
ValueArray,
77
ValueBool,
88
ValueDate,
@@ -11,8 +11,7 @@
1111
ValueInteger,
1212
ValueMixed,
1313
ValueString,
14-
ValueStruct
15-
};
14+
ValueStruct};
1615
use Closure;
1716

1817
/**
@@ -23,7 +22,7 @@ trait Values
2322
/**
2423
* @param null|string|Closure(T):mixed $attribute
2524
*
26-
* @return \Ark4ne\JsonApi\Descriptors\Values\ValueBool<T>
25+
* @return ValueBool<T>
2726
*/
2827
protected function bool(null|string|Closure $attribute = null): ValueBool
2928
{
@@ -33,7 +32,7 @@ protected function bool(null|string|Closure $attribute = null): ValueBool
3332
/**
3433
* @param null|string|Closure(T):mixed $attribute
3534
*
36-
* @return \Ark4ne\JsonApi\Descriptors\Values\ValueInteger<T>
35+
* @return ValueInteger<T>
3736
*/
3837
protected function integer(null|string|Closure $attribute = null): ValueInteger
3938
{
@@ -43,7 +42,7 @@ protected function integer(null|string|Closure $attribute = null): ValueInteger
4342
/**
4443
* @param null|string|Closure(T):mixed $attribute
4544
*
46-
* @return \Ark4ne\JsonApi\Descriptors\Values\ValueFloat<T>
45+
* @return ValueFloat<T>
4746
*/
4847
public function float(null|string|Closure $attribute = null): ValueFloat
4948
{
@@ -53,7 +52,7 @@ public function float(null|string|Closure $attribute = null): ValueFloat
5352
/**
5453
* @param null|string|Closure(T):mixed $attribute
5554
*
56-
* @return \Ark4ne\JsonApi\Descriptors\Values\ValueString<T>
55+
* @return ValueString<T>
5756
*/
5857
protected function string(null|string|Closure $attribute = null): ValueString
5958
{
@@ -63,7 +62,7 @@ protected function string(null|string|Closure $attribute = null): ValueString
6362
/**
6463
* @param null|string|Closure(T):(\DateTimeInterface|string|int|null) $attribute
6564
*
66-
* @return \Ark4ne\JsonApi\Descriptors\Values\ValueDate<T>
65+
* @return ValueDate<T>
6766
*/
6867
protected function date(null|string|Closure $attribute = null): ValueDate
6968
{
@@ -73,7 +72,7 @@ protected function date(null|string|Closure $attribute = null): ValueDate
7372
/**
7473
* @param null|string|Closure(T):(array<mixed>|null) $attribute
7574
*
76-
* @return \Ark4ne\JsonApi\Descriptors\Values\ValueArray<T>
75+
* @return ValueArray<T, mixed>
7776
*/
7877
protected function array(null|string|Closure $attribute = null): ValueArray
7978
{
@@ -83,7 +82,7 @@ protected function array(null|string|Closure $attribute = null): ValueArray
8382
/**
8483
* @param null|string|Closure(T):mixed $attribute
8584
*
86-
* @return \Ark4ne\JsonApi\Descriptors\Values\ValueMixed<T>
85+
* @return ValueMixed<T>
8786
*/
8887
protected function mixed(null|string|Closure $attribute = null): ValueMixed
8988
{
@@ -93,20 +92,31 @@ protected function mixed(null|string|Closure $attribute = null): ValueMixed
9392
/**
9493
* @param null|string|Closure(T):mixed $attribute
9594
*
96-
* @return \Ark4ne\JsonApi\Descriptors\Values\ValueEnum<T>
95+
* @return ValueEnum<T>
9796
*/
9897
protected function enum(null|string|Closure $attribute = null): ValueEnum
9998
{
10099
return new ValueEnum($attribute);
101100
}
102101

103102
/**
104-
* @param Closure(T):iterable<string, mixed|\Closure|\Ark4ne\JsonApi\Descriptors\Values\Value> $attribute
103+
* @param Closure(T):iterable<string, mixed|Closure|Value> $attribute
105104
*
106-
* @return \Ark4ne\JsonApi\Descriptors\Values\ValueStruct<T>
105+
* @return ValueStruct<T>
107106
*/
108107
protected function struct(Closure $attribute): ValueStruct
109108
{
110109
return new ValueStruct($attribute);
111110
}
111+
112+
/**
113+
* @template U
114+
* @param Value<U> $type
115+
* @param null|string|Closure(T):(array<mixed>|null) $attribute
116+
* @return ValueArray<T, U>
117+
*/
118+
protected function arrayOf(Value $type, null|string|Closure $attribute = null): ValueArray
119+
{
120+
return (new ValueArray($attribute))->of($type);
121+
}
112122
}

src/Descriptors/Values/ValueArray.php

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,45 @@
77

88
/**
99
* @template T
10+
* @template U
1011
* @extends Value<T>
1112
*/
1213
class ValueArray extends Value
1314
{
15+
/**
16+
* @var class-string<Value<U>>|Value<U>|null
17+
*/
18+
protected null|string|Value $type = null;
19+
20+
/**
21+
* Define the type of elements in the array
22+
*
23+
* @param class-string<Value<U>>|Value<U> $type
24+
* @return $this<T, U>
25+
*/
26+
public function of(string|Value $type): static
27+
{
28+
$this->type = $type;
29+
return $this;
30+
}
31+
1432
/**
1533
* @param mixed $of
1634
* @param Request $request
1735
* @return array<array-key, mixed>
1836
*/
1937
public function value(mixed $of, Request $request): array
2038
{
21-
return (new Collection($of))->toArray();
39+
if (!$this->type) {
40+
return (new Collection($of))->toArray();
41+
}
42+
43+
$type = is_string($this->type)
44+
? new ($this->type)(null)
45+
: $this->type;
46+
47+
return (new Collection($of))
48+
->map(fn($item) => $type->value($item, $request))
49+
->toArray();
2250
}
2351
}

0 commit comments

Comments
 (0)