Skip to content

Commit

Permalink
Merge branch 'mlshvdv-fix/request-resolver-mapped-output-names'
Browse files Browse the repository at this point in the history
  • Loading branch information
rubenvanassche committed Mar 30, 2023
2 parents 0f1de38 + fb8e829 commit a1175db
Show file tree
Hide file tree
Showing 11 changed files with 391 additions and 18 deletions.
File renamed without changes.
53 changes: 53 additions & 0 deletions docs/advanced-usage/mapping-rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
title: Mapping rules
weight: 13
---

It is possible to map the names properties going in and out of your data objects using: `MapOutputName`, `MapInputName`
and `MapName` attributes. But sometimes it can be quite hard to follow where which name can be used. Let's go through
some case:

In the data object:

```php
class UserData extends Data
{
public function __construct(
#[MapName('favorite_song')] // name mapping
public Lazy|SongData $song,
#[RequiredWith('song')] // In validation rules, use the original name
public string $title,
) {
}

public static function allowedRequestExcept(): ?array
{
return [
'song' // Use the original name when defining includes, excludes, excepts and only
];
}

// ...
}
```

When creating a data object:

```php
UserData::from([
'favorite_song' => ..., // You can use the mapped or original name here
'title' => 'some title'
]);
```

When adding an include, exclude, except or only:

```php
UserData::from(User::first())->except('song'); // Always use the original name here
```

Within a request query, you can use the mapped or original name:

```
https://spatie.be/my-account?except[]=favorite_song
```
2 changes: 1 addition & 1 deletion docs/advanced-usage/validation-attributes.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Validation attributes
weight: 13
weight: 14
---

It is possible to validate the request before a data object is constructed. This can be done by adding validation attributes to the properties of a data object like this:
Expand Down
37 changes: 23 additions & 14 deletions src/Resolvers/PartialsTreeFromRequestResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Spatie\LaravelData\Support\DataConfig;
use Spatie\LaravelData\Support\PartialsParser;
use Spatie\LaravelData\Support\PartialTrees;
use Spatie\LaravelData\Support\PartialTreesMapping;
use TypeError;

class PartialsTreeFromRequestResolver
Expand All @@ -25,29 +26,37 @@ public function execute(
IncludeableData $data,
Request $request,
): PartialTrees {
$dataClass = match (true) {
$data instanceof BaseData => $data::class,
$data instanceof BaseDataCollectable => $data->getDataClass(),
default => throw new TypeError('Invalid type of data')
};

$dataClass = $this->dataConfig->getDataClass($dataClass);

$mapping = PartialTreesMapping::fromRootDataClass($dataClass);

$requestedIncludesTree = $this->partialsParser->execute(
$request->has('include') ? $this->arrayFromRequest($request, 'include') : []
$request->has('include') ? $this->arrayFromRequest($request, 'include') : [],
$mapping
);
$requestedExcludesTree = $this->partialsParser->execute(
$request->has('exclude') ? $this->arrayFromRequest($request, 'exclude') : []
$request->has('exclude') ? $this->arrayFromRequest($request, 'exclude') : [],
$mapping
);
$requestedOnlyTree = $this->partialsParser->execute(
$request->has('only') ? $this->arrayFromRequest($request, 'only') : []
$request->has('only') ? $this->arrayFromRequest($request, 'only') : [],
$mapping
);
$requestedExceptTree = $this->partialsParser->execute(
$request->has('except') ? $this->arrayFromRequest($request, 'except') : []
$request->has('except') ? $this->arrayFromRequest($request, 'except') : [],
$mapping
);

$dataClass = match (true) {
$data instanceof BaseData => $data::class,
$data instanceof BaseDataCollectable => $data->getDataClass(),
default => throw new TypeError('Invalid type of data')
};

$allowedRequestIncludesTree = $this->allowedPartialsParser->execute('allowedRequestIncludes', $this->dataConfig->getDataClass($dataClass));
$allowedRequestExcludesTree = $this->allowedPartialsParser->execute('allowedRequestExcludes', $this->dataConfig->getDataClass($dataClass));
$allowedRequestOnlyTree = $this->allowedPartialsParser->execute('allowedRequestOnly', $this->dataConfig->getDataClass($dataClass));
$allowedRequestExceptTree = $this->allowedPartialsParser->execute('allowedRequestExcept', $this->dataConfig->getDataClass($dataClass));
$allowedRequestIncludesTree = $this->allowedPartialsParser->execute('allowedRequestIncludes', $dataClass);
$allowedRequestExcludesTree = $this->allowedPartialsParser->execute('allowedRequestExcludes', $dataClass);
$allowedRequestOnlyTree = $this->allowedPartialsParser->execute('allowedRequestOnly', $dataClass);
$allowedRequestExceptTree = $this->allowedPartialsParser->execute('allowedRequestExcept', $dataClass);

$partialTrees = $data->getPartialTrees();

Expand Down
67 changes: 67 additions & 0 deletions src/Support/PartialTreesMapping.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

namespace Spatie\LaravelData\Support;

class PartialTreesMapping
{
/**
* @param array<string, \Spatie\LaravelData\Support\PartialTreesMapping> $children
*/
public function __construct(
readonly public string $original,
readonly public string $mapped,
readonly public array $children = [],
) {
}

public function getChild(string $mapped): ?PartialTreesMapping
{
foreach ($this->children as $child) {
if ($child->mapped === $mapped || $child->original === $mapped) {
return $child;
}
}

return null;
}

public static function fromRootDataClass(DataClass $dataClass): self
{
return self::fromDataClass('root', 'root', $dataClass);
}

public static function fromDataClass(
string $original,
string $mapped,
DataClass $dataClass
): self {
$children = [];

$dataClass->properties->each(function (DataProperty $dataProperty) use (&$children) {
if ($dataProperty->type->isDataObject || $dataProperty->type->isDataCollectable) {
$children[] = self::fromDataClass(
$dataProperty->name,
$dataProperty->outputMappedName ?? $dataProperty->name,
app(DataConfig::class)->getDataClass($dataProperty->type->dataClass),
);

return;
}

if ($dataProperty->outputMappedName === null) {
return;
}

$children[] = new self(
$dataProperty->name,
$dataProperty->outputMappedName,
);
});

return new self(
$original,
$mapped,
$children
);
}
}
11 changes: 8 additions & 3 deletions src/Support/PartialsParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

class PartialsParser
{
public function execute(array $partials): TreeNode
public function execute(array $partials, ?PartialTreesMapping $mapping = null): TreeNode
{
$nodes = new DisabledTreeNode();

Expand All @@ -28,6 +28,7 @@ public function execute(array $partials): TreeNode
if (Str::startsWith($field, '{') && Str::endsWith($field, '}')) {
$children = collect(explode(',', substr($field, 1, -1)))
->values()
->map(fn (string $child) => $mapping?->getChild($child)?->original ?? $child)
->flip()
->map(fn () => new ExcludedTreeNode())
->all();
Expand All @@ -37,12 +38,16 @@ public function execute(array $partials): TreeNode
continue;
}

$mappingForField = $mapping?->getChild($field);

$nestedNode = $nested === null
? new ExcludedTreeNode()
: $this->execute([$nested]);
: $this->execute([$nested], $mappingForField);

$fieldName = $mappingForField?->original ?? $field;

$nodes = $nodes->merge(new PartialTreeNode([
$field => $nestedNode,
$fieldName => $nestedNode,
]));
}

Expand Down
21 changes: 21 additions & 0 deletions tests/Fakes/SimpleChildDataWithMappedOutputName.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Spatie\LaravelData\Tests\Fakes;

use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Data;

class SimpleChildDataWithMappedOutputName extends Data
{
public function __construct(
public int $id,
#[MapOutputName('child_amount')]
public float $amount
) {
}

public static function allowedRequestExcept(): ?array
{
return ['amount'];
}
}
30 changes: 30 additions & 0 deletions tests/Fakes/SimpleDataWithMappedOutputName.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace Spatie\LaravelData\Tests\Fakes;

use Spatie\LaravelData\Attributes\MapName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;

#[MapName(SnakeCaseMapper::class)]
class SimpleDataWithMappedOutputName extends Data
{
public function __construct(
public int $id,
#[MapOutputName('paid_amount')]
public float $amount,
public string $anyString,
public SimpleChildDataWithMappedOutputName $child
) {
}

public static function allowedRequestExcept(): ?array
{
return [
'amount',
'anyString',
'child',
];
}
}
30 changes: 30 additions & 0 deletions tests/Resolvers/PartialsTreeFromRequestResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
use Spatie\LaravelData\Support\TreeNodes\TreeNode;
use Spatie\LaravelData\Tests\Fakes\LazyData;
use Spatie\LaravelData\Tests\Fakes\MultiLazyData;
use Spatie\LaravelData\Tests\Fakes\SimpleChildDataWithMappedOutputName;
use Spatie\LaravelData\Tests\Fakes\SimpleDataWithMappedOutputName;

beforeEach(function () {
$this->resolver = resolve(PartialsTreeFromRequestResolver::class);
Expand Down Expand Up @@ -213,3 +215,31 @@ public static function allowedRequestIncludes(): ?array
'expected' => ['artist', 'name'],
];
});

it('handles parsing except from request with mapped output name', function (array $input, array $expectedArray) {
$dataclass = SimpleDataWithMappedOutputName::from([
'id' => 1,
'amount' => 1000,
'any_string' => 'test',
'child' => SimpleChildDataWithMappedOutputName::from([
'id' => 2,
'amount' => 2000,
]),
]);

$request = request()->merge($input);

$data = $dataclass->toResponse($request)->getData(assoc: true);

expect($data)->toMatchArray($expectedArray);
})->with(function () {
yield 'input as array' => [
'input' => ['except' => ['paid_amount', 'any_string', 'child.child_amount']],
'expectedArray' => [
'id' => 1,
'child' => [
'id' => 2,
],
],
];
});
Loading

0 comments on commit a1175db

Please sign in to comment.