Skip to content

Commit d0e9e17

Browse files
Merge pull request #409 from spatie/better-name-mapping-inpartials
Better name mapping inpartials
2 parents 322e71f + 908b76e commit d0e9e17

15 files changed

+436
-24
lines changed
File renamed without changes.

docs/advanced-usage/mapping-rules.md

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
title: Mapping rules
3+
weight: 13
4+
---
5+
6+
It is possible to map the names properties going in and out of your data objects using: `MapOutputName`, `MapInputName`
7+
and `MapName` attributes. But sometimes it can be quite hard to follow where which name can be used. Let's go through
8+
some case:
9+
10+
In the data object:
11+
12+
```php
13+
class UserData extends Data
14+
{
15+
public function __construct(
16+
#[MapName('favorite_song')] // name mapping
17+
public Lazy|SongData $song,
18+
#[RequiredWith('song')] // In validation rules, use the original name
19+
public string $title,
20+
) {
21+
}
22+
23+
public static function allowedRequestExcept(): ?array
24+
{
25+
return [
26+
'song' // Use the original name when defining includes, excludes, excepts and only
27+
];
28+
}
29+
30+
// ...
31+
}
32+
```
33+
34+
When creating a data object:
35+
36+
```php
37+
UserData::from([
38+
'favorite_song' => ..., // You can use the mapped or original name here
39+
'title' => 'some title'
40+
]);
41+
```
42+
43+
When adding an include, exclude, except or only:
44+
45+
```php
46+
UserData::from(User::first())->except('song'); // Always use the original name here
47+
```
48+
49+
Within a request query, you can use the mapped or original name:
50+
51+
```
52+
https://spatie.be/my-account?except[]=favorite_song
53+
```

docs/advanced-usage/validation-attributes.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Validation attributes
3-
weight: 13
3+
weight: 14
44
---
55

66
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:

src/Concerns/IncludeableData.php

+6-4
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,13 @@ public function getPartialTrees(): PartialTrees
130130
$only = collect($this->_only)->merge($this->onlyProperties())->filter($filter)->keys()->all();
131131
$except = collect($this->_except)->merge($this->exceptProperties())->filter($filter)->keys()->all();
132132

133+
$partialsParser = app(PartialsParser::class);
134+
133135
return new PartialTrees(
134-
(new PartialsParser())->execute($includes),
135-
(new PartialsParser())->execute($excludes),
136-
(new PartialsParser())->execute($only),
137-
(new PartialsParser())->execute($except),
136+
$partialsParser->execute($includes),
137+
$partialsParser->execute($excludes),
138+
$partialsParser->execute($only),
139+
$partialsParser->execute($except),
138140
);
139141
}
140142
}

src/Resolvers/PartialsTreeFromRequestResolver.php

+22-14
Original file line numberDiff line numberDiff line change
@@ -25,29 +25,37 @@ public function execute(
2525
IncludeableData $data,
2626
Request $request,
2727
): PartialTrees {
28+
$dataClass = match (true) {
29+
$data instanceof BaseData => $data::class,
30+
$data instanceof BaseDataCollectable => $data->getDataClass(),
31+
default => throw new TypeError('Invalid type of data')
32+
};
33+
34+
$dataClass = $this->dataConfig->getDataClass($dataClass);
35+
36+
$mapping = $dataClass->outputNameMapping->resolve();
37+
2838
$requestedIncludesTree = $this->partialsParser->execute(
29-
$request->has('include') ? $this->arrayFromRequest($request, 'include') : []
39+
$request->has('include') ? $this->arrayFromRequest($request, 'include') : [],
40+
$mapping
3041
);
3142
$requestedExcludesTree = $this->partialsParser->execute(
32-
$request->has('exclude') ? $this->arrayFromRequest($request, 'exclude') : []
43+
$request->has('exclude') ? $this->arrayFromRequest($request, 'exclude') : [],
44+
$mapping
3345
);
3446
$requestedOnlyTree = $this->partialsParser->execute(
35-
$request->has('only') ? $this->arrayFromRequest($request, 'only') : []
47+
$request->has('only') ? $this->arrayFromRequest($request, 'only') : [],
48+
$mapping
3649
);
3750
$requestedExceptTree = $this->partialsParser->execute(
38-
$request->has('except') ? $this->arrayFromRequest($request, 'except') : []
51+
$request->has('except') ? $this->arrayFromRequest($request, 'except') : [],
52+
$mapping
3953
);
4054

41-
$dataClass = match (true) {
42-
$data instanceof BaseData => $data::class,
43-
$data instanceof BaseDataCollectable => $data->getDataClass(),
44-
default => throw new TypeError('Invalid type of data')
45-
};
46-
47-
$allowedRequestIncludesTree = $this->allowedPartialsParser->execute('allowedRequestIncludes', $this->dataConfig->getDataClass($dataClass));
48-
$allowedRequestExcludesTree = $this->allowedPartialsParser->execute('allowedRequestExcludes', $this->dataConfig->getDataClass($dataClass));
49-
$allowedRequestOnlyTree = $this->allowedPartialsParser->execute('allowedRequestOnly', $this->dataConfig->getDataClass($dataClass));
50-
$allowedRequestExceptTree = $this->allowedPartialsParser->execute('allowedRequestExcept', $this->dataConfig->getDataClass($dataClass));
55+
$allowedRequestIncludesTree = $this->allowedPartialsParser->execute('allowedRequestIncludes', $dataClass);
56+
$allowedRequestExcludesTree = $this->allowedPartialsParser->execute('allowedRequestExcludes', $dataClass);
57+
$allowedRequestOnlyTree = $this->allowedPartialsParser->execute('allowedRequestOnly', $dataClass);
58+
$allowedRequestExceptTree = $this->allowedPartialsParser->execute('allowedRequestExcept', $dataClass);
5159

5260
$partialTrees = $data->getPartialTrees();
5361

src/Support/DataClass.php

+30-1
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@
1717
use Spatie\LaravelData\Contracts\WrappableData;
1818
use Spatie\LaravelData\Mappers\ProvidedNameMapper;
1919
use Spatie\LaravelData\Resolvers\NameMappersResolver;
20+
use Spatie\LaravelData\Support\Lazy\CachedLazy;
21+
use Spatie\LaravelData\Support\NameMapping\DataClassNameMapping;
2022

2123
/**
2224
* @property class-string<DataObject> $name
2325
* @property Collection<string, DataProperty> $properties
2426
* @property Collection<string, DataMethod> $methods
2527
* @property Collection<string, object> $attributes
28+
* @property CachedLazy<DataClassNameMapping> $outputNameMapping
2629
*/
2730
class DataClass
2831
{
@@ -39,6 +42,7 @@ public function __construct(
3942
public readonly bool $validateable,
4043
public readonly bool $wrappable,
4144
public readonly Collection $attributes,
45+
public readonly CachedLazy $outputNameMapping,
4246
) {
4347
}
4448

@@ -70,7 +74,8 @@ public static function create(ReflectionClass $class): self
7074
transformable: $class->implementsInterface(TransformableData::class),
7175
validateable: $class->implementsInterface(ValidateableData::class),
7276
wrappable: $class->implementsInterface(WrappableData::class),
73-
attributes: $attributes,
77+
attributes: $attributes,
78+
outputNameMapping: new CachedLazy(fn () => self::resolveOutputNameMapping($properties)),
7479
);
7580
}
7681

@@ -125,4 +130,28 @@ protected static function resolveDefaultValues(
125130
$values
126131
);
127132
}
133+
134+
protected static function resolveOutputNameMapping(
135+
Collection $properties,
136+
): DataClassNameMapping {
137+
$mapped = [];
138+
$mappedDataObjects = [];
139+
140+
$properties->each(function (DataProperty $dataProperty) use (&$mapped, &$mappedDataObjects) {
141+
if ($dataProperty->type->isDataObject || $dataProperty->type->isDataCollectable) {
142+
$mappedDataObjects[$dataProperty->name] = $dataProperty->type->dataClass;
143+
}
144+
145+
if ($dataProperty->outputMappedName === null) {
146+
return;
147+
}
148+
149+
$mapped[$dataProperty->outputMappedName] = $dataProperty->name;
150+
});
151+
152+
return new DataClassNameMapping(
153+
$mapped,
154+
$mappedDataObjects
155+
);
156+
}
128157
}

src/Support/Lazy/CachedLazy.php

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace Spatie\LaravelData\Support\Lazy;
4+
5+
use Closure;
6+
use Spatie\LaravelData\Lazy;
7+
8+
/**
9+
* @internal
10+
* @template T
11+
*/
12+
class CachedLazy extends Lazy
13+
{
14+
/** @var T */
15+
protected mixed $resolved;
16+
17+
/**
18+
* @param Closure(): T $value
19+
*/
20+
public function __construct(
21+
protected Closure $value
22+
) {
23+
}
24+
25+
/**
26+
* @return T
27+
*/
28+
public function resolve(): mixed
29+
{
30+
if (isset($this->resolved)) {
31+
return $this->resolved;
32+
}
33+
34+
return $this->resolved = ($this->value)();
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace Spatie\LaravelData\Support\NameMapping;
4+
5+
use Spatie\LaravelData\Support\DataConfig;
6+
7+
class DataClassNameMapping
8+
{
9+
/*
10+
* @param array<string, string> $mapped
11+
* @param array<string, class-string<\Spatie\LaravelData\Support\DataClass>> $mappedDataObjects
12+
*/
13+
public function __construct(
14+
readonly array $mapped,
15+
readonly array $mappedDataObjects,
16+
) {
17+
}
18+
19+
public function getOriginal(string $mapped): ?string
20+
{
21+
return $this->mapped[$mapped] ?? null;
22+
}
23+
24+
public function resolveNextMapping(
25+
DataConfig $dataConfig,
26+
string $mappedOrOriginal
27+
): ?self {
28+
$dataClass = $this->mappedDataObjects[$mappedOrOriginal] ?? null;
29+
30+
if ($dataClass === null) {
31+
return null;
32+
}
33+
34+
$outputNameMapping = $dataConfig->getDataClass($dataClass)->outputNameMapping;
35+
36+
return $outputNameMapping->resolve();
37+
}
38+
}

src/Support/PartialsParser.php

+12-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Spatie\LaravelData\Support;
44

55
use Illuminate\Support\Str;
6+
use Spatie\LaravelData\Support\NameMapping\DataClassNameMapping;
67
use Spatie\LaravelData\Support\TreeNodes\AllTreeNode;
78
use Spatie\LaravelData\Support\TreeNodes\DisabledTreeNode;
89
use Spatie\LaravelData\Support\TreeNodes\ExcludedTreeNode;
@@ -11,7 +12,12 @@
1112

1213
class PartialsParser
1314
{
14-
public function execute(array $partials): TreeNode
15+
public function __construct(
16+
protected DataConfig $dataConfig,
17+
) {
18+
}
19+
20+
public function execute(array $partials, ?DataClassNameMapping $mapping = null): TreeNode
1521
{
1622
$nodes = new DisabledTreeNode();
1723

@@ -28,6 +34,7 @@ public function execute(array $partials): TreeNode
2834
if (Str::startsWith($field, '{') && Str::endsWith($field, '}')) {
2935
$children = collect(explode(',', substr($field, 1, -1)))
3036
->values()
37+
->map(fn (string $child) => $mapping?->getOriginal($child) ?? $child)
3138
->flip()
3239
->map(fn () => new ExcludedTreeNode())
3340
->all();
@@ -37,12 +44,14 @@ public function execute(array $partials): TreeNode
3744
continue;
3845
}
3946

47+
$fieldName = $mapping?->getOriginal($field) ?? $field;
48+
4049
$nestedNode = $nested === null
4150
? new ExcludedTreeNode()
42-
: $this->execute([$nested]);
51+
: $this->execute([$nested], $mapping?->resolveNextMapping($this->dataConfig, $fieldName));
4352

4453
$nodes = $nodes->merge(new PartialTreeNode([
45-
$field => $nestedNode,
54+
$fieldName => $nestedNode,
4655
]));
4756
}
4857

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Spatie\LaravelData\Tests\Fakes;
4+
5+
use Spatie\LaravelData\Attributes\MapOutputName;
6+
use Spatie\LaravelData\Data;
7+
8+
class SimpleChildDataWithMappedOutputName extends Data
9+
{
10+
public function __construct(
11+
public int $id,
12+
#[MapOutputName('child_amount')]
13+
public float $amount
14+
) {
15+
}
16+
17+
public static function allowedRequestExcept(): ?array
18+
{
19+
return ['amount'];
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace Spatie\LaravelData\Tests\Fakes;
4+
5+
use Spatie\LaravelData\Attributes\MapName;
6+
use Spatie\LaravelData\Attributes\MapOutputName;
7+
use Spatie\LaravelData\Data;
8+
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
9+
10+
#[MapName(SnakeCaseMapper::class)]
11+
class SimpleDataWithMappedOutputName extends Data
12+
{
13+
public function __construct(
14+
public int $id,
15+
#[MapOutputName('paid_amount')]
16+
public float $amount,
17+
public string $anyString,
18+
public SimpleChildDataWithMappedOutputName $child
19+
) {
20+
}
21+
22+
public static function allowedRequestExcept(): ?array
23+
{
24+
return [
25+
'amount',
26+
'anyString',
27+
'child',
28+
];
29+
}
30+
}

0 commit comments

Comments
 (0)