Skip to content
Open
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
13 changes: 13 additions & 0 deletions CHANGELOG-1.6.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Changelog - Version 1.6

## [1.6.0] - 2025-01-28

### Added

- **Typed Arrays Support**: Added support for typed arrays in resource attributes, allowing type-safe array elements
- New `of()` method on `array()` descriptor to specify element type
- Support for class references (e.g., `ValueString::class`) in addition to descriptor instances
- Alternative `arrayOf()` helper method for more concise syntax
- Full support for nested typed arrays (multi-dimensional arrays)
- Compatible with all conditional methods (`when()`, `whenNotNull()`, `whenFilled()`, etc.)
- Type casting applied to all array elements (strings, integers, floats, booleans, etc.)
186 changes: 173 additions & 13 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ protected function toAttributes(Request $request): array
```

#### Described attributes
_**@see** [described notation](##described-notation)_
_**@see** [described notation](#described-notation)_

```php
protected function toAttributes(Request $request): array
Expand Down Expand Up @@ -283,7 +283,7 @@ protected function toRelationships(Request $request): array
```

#### Described attributes
_**@see** [described notation](##described-notation)_
_**@see** [described notation](#described-notation)_

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

### Value methods
| Method | Description |
|-----------|------------------------------------------|
| `bool` | Cast to boolean |
| `integer` | Cast to integer |
| `float` | Cast to float |
| `string` | Cast to string |
| `date` | Cast to date, allow to use custom format |
| `array` | Cast to array |
| `mixed` | Don't cast, return as is |
| `enum` | Get enum value |
| `struct` | Custom struct. Accept an array of values |
| Method | Description |
|-----------|-----------------------------------------------------------------|
| `bool` | Cast to boolean |
| `integer` | Cast to integer |
| `float` | Cast to float |
| `string` | Cast to string |
| `date` | Cast to date, allow to use custom format |
| `array` | Cast to array, supports typed arrays with `->of()` |
| `arrayOf` | Helper method for typed arrays (alternative to `array()->of()`) |
| `mixed` | Don't cast, return as is |
| `enum` | Get enum value |
| `struct` | Custom struct. Accept an array of values |

### Relation methods
| Method | Description |
Expand Down Expand Up @@ -454,3 +455,162 @@ Will return:
"role": "ADMIN"
]
```

### Typed Arrays

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.

#### Basic Usage

```php
// UserResource.php
protected function toAttributes(Request $request): array
{
return [
// Array of strings - all values will be cast to string
'tags' => $this->array('tags')->of($this->string()),

// Array of integers - all values will be cast to integer
'scores' => $this->array('scores')->of($this->integer()),

// Array of floats
'prices' => $this->array('prices')->of($this->float()),

// Array of booleans
'flags' => $this->array('flags')->of($this->bool()),
];
}
```

#### Using Class References

You can also use class references instead of descriptor instances:

```php
use Ark4ne\JsonApi\Descriptors\Values\ValueString;
use Ark4ne\JsonApi\Descriptors\Values\ValueInteger;

protected function toAttributes(Request $request): array
{
return [
'tags' => $this->array('tags')->of(ValueString::class),
'scores' => $this->array('scores')->of(ValueInteger::class),
];
}
```

#### Alternative Syntax

You can also use the `arrayOf()` helper method:

```php
protected function toAttributes(Request $request): array
{
return [
'tags' => $this->arrayOf($this->string(), 'tags'),
'scores' => $this->arrayOf($this->integer(), 'scores'),
];
}
```

#### Nested Typed Arrays

For multi-dimensional arrays, you can nest `array()->of()` calls:

```php
protected function toAttributes(Request $request): array
{
return [
// 2D array (matrix) of integers
'matrix' => $this->array('matrix')->of(
$this->array()->of($this->integer())
),
];
}
```

#### With Closures and Transformations

Combine typed arrays with closures for data transformation:

```php
protected function toAttributes(Request $request): array
{
return [
// Transform and type cast
'doubled' => $this->array(fn() => array_map(fn($n) => $n * 2, $this->numbers))
->of($this->integer()),

// Access nested properties
'user_ids' => $this->array(fn() => $this->users->pluck('id'))
->of($this->integer()),
];
}
```

#### With Conditions

Typed arrays support all conditional methods:

```php
protected function toAttributes(Request $request): array
{
return [
// Only include if not null
'tags' => $this->array('tags')->of($this->string())->whenNotNull(),

// Only include if array is not empty
'scores' => $this->array('scores')->of($this->integer())->whenFilled(),

// Conditional based on closure
'admin_notes' => $this->array('notes')->of($this->string())
->when(fn() => $request->user()->isAdmin()),
];
}
```

> **⚠️ 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.
>
> ```php
> // ❌ This will NOT filter individual items
> 'even-numbers' => $this->array('numbers')->of(
> $this->integer()->when(fn($request, $model, $attr) => $attr % 2 === 0)
> )
> // All items will be included, the when() doesn't filter per item
>
> // ✅ To filter items, do it before passing to the array
> 'even-numbers' => $this->array(
> fn() => array_filter($this->numbers, fn($n) => $n % 2 === 0)
> )->of($this->integer())
> ```

#### Example

Given a model with mixed-type arrays:

```php
$user = new User([
'tags' => ['php', 'laravel', 123, true],
'scores' => [95.5, '87', 92, '78.9'],
]);
```

The resource will ensure type consistency:

```php
protected function toAttributes(Request $request): array
{
return [
'tags' => $this->array('tags')->of($this->string()),
'scores' => $this->array('scores')->of($this->integer()),
];
}
```

Output:
```json
{
"tags": ["php", "laravel", "123", "1"],
"scores": [95, 87, 92, 78]
}
```
36 changes: 23 additions & 13 deletions src/Descriptors/Values.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Ark4ne\JsonApi\Descriptors;

use Ark4ne\JsonApi\Descriptors\Values\{
use Ark4ne\JsonApi\Descriptors\Values\{Value,
ValueArray,
ValueBool,
ValueDate,
Expand All @@ -11,8 +11,7 @@
ValueInteger,
ValueMixed,
ValueString,
ValueStruct
};
ValueStruct};
use Closure;

/**
Expand All @@ -23,7 +22,7 @@ trait Values
/**
* @param null|string|Closure(T):mixed $attribute
*
* @return \Ark4ne\JsonApi\Descriptors\Values\ValueBool<T>
* @return ValueBool<T>
*/
protected function bool(null|string|Closure $attribute = null): ValueBool
{
Expand All @@ -33,7 +32,7 @@ protected function bool(null|string|Closure $attribute = null): ValueBool
/**
* @param null|string|Closure(T):mixed $attribute
*
* @return \Ark4ne\JsonApi\Descriptors\Values\ValueInteger<T>
* @return ValueInteger<T>
*/
protected function integer(null|string|Closure $attribute = null): ValueInteger
{
Expand All @@ -43,7 +42,7 @@ protected function integer(null|string|Closure $attribute = null): ValueInteger
/**
* @param null|string|Closure(T):mixed $attribute
*
* @return \Ark4ne\JsonApi\Descriptors\Values\ValueFloat<T>
* @return ValueFloat<T>
*/
public function float(null|string|Closure $attribute = null): ValueFloat
{
Expand All @@ -53,7 +52,7 @@ public function float(null|string|Closure $attribute = null): ValueFloat
/**
* @param null|string|Closure(T):mixed $attribute
*
* @return \Ark4ne\JsonApi\Descriptors\Values\ValueString<T>
* @return ValueString<T>
*/
protected function string(null|string|Closure $attribute = null): ValueString
{
Expand All @@ -63,7 +62,7 @@ protected function string(null|string|Closure $attribute = null): ValueString
/**
* @param null|string|Closure(T):(\DateTimeInterface|string|int|null) $attribute
*
* @return \Ark4ne\JsonApi\Descriptors\Values\ValueDate<T>
* @return ValueDate<T>
*/
protected function date(null|string|Closure $attribute = null): ValueDate
{
Expand All @@ -73,7 +72,7 @@ protected function date(null|string|Closure $attribute = null): ValueDate
/**
* @param null|string|Closure(T):(array<mixed>|null) $attribute
*
* @return \Ark4ne\JsonApi\Descriptors\Values\ValueArray<T>
* @return ValueArray<T, mixed>
*/
protected function array(null|string|Closure $attribute = null): ValueArray
{
Expand All @@ -83,7 +82,7 @@ protected function array(null|string|Closure $attribute = null): ValueArray
/**
* @param null|string|Closure(T):mixed $attribute
*
* @return \Ark4ne\JsonApi\Descriptors\Values\ValueMixed<T>
* @return ValueMixed<T>
*/
protected function mixed(null|string|Closure $attribute = null): ValueMixed
{
Expand All @@ -93,20 +92,31 @@ protected function mixed(null|string|Closure $attribute = null): ValueMixed
/**
* @param null|string|Closure(T):mixed $attribute
*
* @return \Ark4ne\JsonApi\Descriptors\Values\ValueEnum<T>
* @return ValueEnum<T>
*/
protected function enum(null|string|Closure $attribute = null): ValueEnum
{
return new ValueEnum($attribute);
}

/**
* @param Closure(T):iterable<string, mixed|\Closure|\Ark4ne\JsonApi\Descriptors\Values\Value> $attribute
* @param Closure(T):iterable<string, mixed|Closure|Value> $attribute
*
* @return \Ark4ne\JsonApi\Descriptors\Values\ValueStruct<T>
* @return ValueStruct<T>
*/
protected function struct(Closure $attribute): ValueStruct
{
return new ValueStruct($attribute);
}

/**
* @template U
* @param Value<U> $type
* @param null|string|Closure(T):(array<mixed>|null) $attribute
* @return ValueArray<T, U>
*/
protected function arrayOf(Value $type, null|string|Closure $attribute = null): ValueArray
{
return (new ValueArray($attribute))->of($type);
}
}
30 changes: 29 additions & 1 deletion src/Descriptors/Values/ValueArray.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,45 @@

/**
* @template T
* @template U
* @extends Value<T>
*/
class ValueArray extends Value
{
/**
* @var class-string<Value<U>>|Value<U>|null
*/
protected null|string|Value $type = null;

/**
* Define the type of elements in the array
*
* @param class-string<Value<U>>|Value<U> $type
* @return $this<T, U>
*/
public function of(string|Value $type): static
{
$this->type = $type;
return $this;
}

/**
* @param mixed $of
* @param Request $request
* @return array<array-key, mixed>
*/
public function value(mixed $of, Request $request): array
{
return (new Collection($of))->toArray();
if (!$this->type) {
return (new Collection($of))->toArray();
}

$type = is_string($this->type)
? new ($this->type)(null)
: $this->type;

return (new Collection($of))
->map(fn($item) => $type->value($item, $request))
->toArray();
}
}
Loading