Skip to content

Commit 2f70f17

Browse files
committed
Require attributes for property morphable and cast before passing to morph
1 parent 3129237 commit 2f70f17

9 files changed

+112
-47
lines changed

src/Resolvers/DataFromSomethingResolver.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Spatie\LaravelData\Resolvers;
44

55
use Illuminate\Http\Request;
6+
use Illuminate\Support\Arr;
67
use Spatie\LaravelData\Contracts\BaseData;
78
use Spatie\LaravelData\Contracts\PropertyMorphableData;
89
use Spatie\LaravelData\Enums\CustomCreationMethodType;
@@ -130,7 +131,7 @@ protected function dataFromArray(
130131
/**
131132
* @var class-string<PropertyMorphableData> $class
132133
*/
133-
if ($morph = $class::morph($properties)) {
134+
if ($morph = $class::morph(Arr::only($properties, $dataClass->propertyMorphablePropertyNames))) {
134135
return $this->execute($morph, $creationContext, ...$payloads);
135136
}
136137
}

src/Resolvers/DataValidationRulesResolver.php

+45-11
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
use Spatie\LaravelData\Attributes\MergeValidationRules;
99
use Spatie\LaravelData\Attributes\Validation\ArrayType;
1010
use Spatie\LaravelData\Attributes\Validation\Present;
11+
use Spatie\LaravelData\Contracts\BaseData;
1112
use Spatie\LaravelData\Contracts\PropertyMorphableData;
13+
use Spatie\LaravelData\Support\Creation\CreationContextFactory;
1214
use Spatie\LaravelData\Support\DataClass;
1315
use Spatie\LaravelData\Support\DataConfig;
1416
use Spatie\LaravelData\Support\DataProperty;
@@ -36,17 +38,11 @@ public function execute(
3638
DataRules $dataRules
3739
): array {
3840
$dataClass = $this->dataConfig->getDataClass($class);
39-
40-
if ($this->isPropertyMorphable($dataClass)) {
41-
/**
42-
* @var class-string<PropertyMorphableData> $class
43-
*/
44-
$morphedClass = $class::morph(
45-
$path->isRoot() ? $fullPayload : Arr::get($fullPayload, $path->get(), [])
46-
);
47-
48-
$dataClass = $this->dataConfig->getDataClass($morphedClass ?? $class);
49-
}
41+
$dataClass = $this->propertyMorphableDataClass(
42+
$dataClass,
43+
$fullPayload,
44+
$path
45+
) ?? $dataClass;
5046

5147
$withoutValidationProperties = [];
5248

@@ -96,6 +92,44 @@ public function execute(
9692
return $dataRules->rules;
9793
}
9894

95+
protected function propertyMorphableDataClass(
96+
DataClass $dataClass,
97+
array $fullPayload,
98+
ValidationPath $path
99+
): ?DataClass {
100+
if (! $dataClass->propertyMorphable) {
101+
return null;
102+
}
103+
104+
/**
105+
* @var class-string<PropertyMorphableData&BaseData> $class
106+
*/
107+
$class = $dataClass->name;
108+
$creationContext = CreationContextFactory::createFromConfig($class)->get();
109+
$pipeline = $this->dataConfig->getResolvedDataPipeline($class);
110+
111+
try {
112+
$properties = Arr::only($pipeline->execute(
113+
$path->isRoot() ? $fullPayload : Arr::get($fullPayload, $path->get(), []),
114+
$creationContext
115+
), $dataClass->propertyMorphablePropertyNames);
116+
} catch (\Throwable $exception) {
117+
return null;
118+
}
119+
120+
// Only morph if all properties are present
121+
if (count($properties) !== count($dataClass->propertyMorphablePropertyNames)) {
122+
return null;
123+
}
124+
125+
$morphedClass = $class::morph($properties);
126+
if ($morphedClass === null) {
127+
return null;
128+
}
129+
130+
return $this->dataConfig->getDataClass($morphedClass);
131+
}
132+
99133
protected function shouldSkipPropertyValidation(
100134
DataProperty $dataProperty,
101135
array $fullPayload,

src/Support/DataClass.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ public function __construct(
3535
public DataStructureProperty $allowedRequestOnly,
3636
public DataStructureProperty $allowedRequestExcept,
3737
public DataStructureProperty $outputMappedProperties,
38-
public DataStructureProperty $transformationFields
38+
public DataStructureProperty $transformationFields,
39+
public readonly array $propertyMorphablePropertyNames
3940
) {
4041
}
4142

src/Support/Factories/DataClassFactory.php

+4
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ public function build(ReflectionClass $reflectionClass): DataClass
104104
allowedRequestExcept: new LazyDataStructureProperty(fn (): ?array => $responsable ? $name::allowedRequestExcept() : null),
105105
outputMappedProperties: $outputMappedProperties,
106106
transformationFields: static::resolveTransformationFields($properties),
107+
propertyMorphablePropertyNames: $properties->filter(fn (DataProperty $property) => $property->isForMorph)
108+
->map(fn (DataProperty $property) => $property->name)
109+
->values()
110+
->all(),
107111
);
108112
}
109113

src/Support/Validation/RequiresPropertyMorphableClassRule.php

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

55
use Closure;
6+
use Exception;
67
use Illuminate\Contracts\Validation\ValidationRule;
78
use Spatie\LaravelData\Attributes\Validation\ObjectValidationAttribute;
89
use Spatie\LaravelData\Support\DataClass;
@@ -25,7 +26,7 @@ public static function keyword(): string
2526

2627
public static function create(string ...$parameters): static
2728
{
28-
return new static(...$parameters);
29+
throw new Exception('Cannot create a requires property morphable class rule');
2930
}
3031

3132
public function validate(string $attribute, mixed $value, Closure $fail): void

tests/CreationTest.php

+21-13
Original file line numberDiff line numberDiff line change
@@ -1455,17 +1455,25 @@ class TestAutoLazyClassAttributeData extends Data
14551455
});
14561456

14571457
describe('property-morphable creation tests', function () {
1458+
enum TestPropertyMorphableEnum: string
1459+
{
1460+
case A = 'a';
1461+
case B = 'b';
1462+
};
1463+
14581464
abstract class TestAbstractPropertyMorphableData extends Data implements PropertyMorphableData
14591465
{
1460-
public function __construct(public string $variant)
1461-
{
1466+
public function __construct(
1467+
#[\Spatie\LaravelData\Attributes\PropertyForMorph]
1468+
public TestPropertyMorphableEnum $variant
1469+
) {
14621470
}
14631471

14641472
public static function morph(array $properties): ?string
14651473
{
14661474
return match ($properties['variant'] ?? null) {
1467-
'a' => TestPropertyMorphableDataA::class,
1468-
'b' => TestPropertyMorphableDataB::class,
1475+
TestPropertyMorphableEnum::A => TestPropertyMorphableDataA::class,
1476+
TestPropertyMorphableEnum::B => TestPropertyMorphableDataB::class,
14691477
default => null,
14701478
};
14711479
}
@@ -1475,15 +1483,15 @@ class TestPropertyMorphableDataA extends TestAbstractPropertyMorphableData
14751483
{
14761484
public function __construct(public string $a, public DummyBackedEnum $enum)
14771485
{
1478-
parent::__construct('a');
1486+
parent::__construct(TestPropertyMorphableEnum::A);
14791487
}
14801488
}
14811489

14821490
class TestPropertyMorphableDataB extends TestAbstractPropertyMorphableData
14831491
{
14841492
public function __construct(public string $b)
14851493
{
1486-
parent::__construct('b');
1494+
parent::__construct(TestPropertyMorphableEnum::B);
14871495
}
14881496
}
14891497

@@ -1496,7 +1504,7 @@ public function __construct(public string $b)
14961504

14971505
expect($dataA)
14981506
->toBeInstanceOf(TestPropertyMorphableDataA::class)
1499-
->variant->toEqual('a')
1507+
->variant->toEqual(TestPropertyMorphableEnum::A)
15001508
->a->toEqual('foo')
15011509
->enum->toEqual(DummyBackedEnum::FOO);
15021510

@@ -1507,7 +1515,7 @@ public function __construct(public string $b)
15071515

15081516
expect($dataB)
15091517
->toBeInstanceOf(TestPropertyMorphableDataB::class)
1510-
->variant->toEqual('b')
1518+
->variant->toEqual(TestPropertyMorphableEnum::B)
15111519
->b->toEqual('bar');
15121520
});
15131521

@@ -1519,7 +1527,7 @@ public function __construct(public string $b)
15191527

15201528
expect($dataA)
15211529
->toBeInstanceOf(TestPropertyMorphableDataA::class)
1522-
->variant->toEqual('a')
1530+
->variant->toEqual(TestPropertyMorphableEnum::A)
15231531
->a->toEqual('foo')
15241532
->enum->toEqual(DummyBackedEnum::FOO);
15251533
});
@@ -1543,13 +1551,13 @@ public function __construct(
15431551

15441552
expect($data->nestedCollection[0])
15451553
->toBeInstanceOf(TestPropertyMorphableDataA::class)
1546-
->variant->toEqual('a')
1554+
->variant->toEqual(TestPropertyMorphableEnum::A)
15471555
->a->toEqual('foo')
15481556
->enum->toEqual(DummyBackedEnum::FOO);
15491557

15501558
expect($data->nestedCollection[1])
15511559
->toBeInstanceOf(TestPropertyMorphableDataB::class)
1552-
->variant->toEqual('b')
1560+
->variant->toEqual(TestPropertyMorphableEnum::B)
15531561
->b->toEqual('bar');
15541562
});
15551563

@@ -1562,13 +1570,13 @@ public function __construct(
15621570

15631571
expect($collection[0])
15641572
->toBeInstanceOf(TestPropertyMorphableDataA::class)
1565-
->variant->toEqual('a')
1573+
->variant->toEqual(TestPropertyMorphableEnum::A)
15661574
->a->toEqual('foo')
15671575
->enum->toEqual(DummyBackedEnum::FOO);
15681576

15691577
expect($collection[1])
15701578
->toBeInstanceOf(TestPropertyMorphableDataB::class)
1571-
->variant->toEqual('b')
1579+
->variant->toEqual(TestPropertyMorphableEnum::B)
15721580
->b->toEqual('bar');
15731581
});
15741582
});

tests/Support/EloquentCasts/DataCollectionEloquentCastTest.php

+4-2
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,10 @@
178178
it('can load and save an abstract property-morphable data collection', function () {
179179
abstract class TestCollectionCastAbstractPropertyMorphableData extends Data implements PropertyMorphableData
180180
{
181-
public function __construct(public string $variant)
182-
{
181+
public function __construct(
182+
#[\Spatie\LaravelData\Attributes\PropertyForMorph]
183+
public string $variant
184+
) {
183185
}
184186

185187
public static function morph(array $properties): ?string

tests/Support/EloquentCasts/DataEloquentCastTest.php

+12-10
Original file line numberDiff line numberDiff line change
@@ -191,24 +191,26 @@
191191
it('can load and save an abstract property-morphable data object', function () {
192192
abstract class TestCastAbstractPropertyMorphableData extends Data implements PropertyMorphableData
193193
{
194-
public function __construct(public string $variant)
195-
{
194+
public function __construct(
195+
#[\Spatie\LaravelData\Attributes\PropertyForMorph]
196+
public DummyBackedEnum $variant
197+
) {
196198
}
197199

198200
public static function morph(array $properties): ?string
199201
{
200202
return match ($properties['variant'] ?? null) {
201-
'a' => TestCastPropertyMorphableDataA::class,
203+
DummyBackedEnum::FOO => TestCastPropertyMorphableDataFoo::class,
202204
default => null,
203205
};
204206
}
205207
}
206208

207-
class TestCastPropertyMorphableDataA extends TestCastAbstractPropertyMorphableData
209+
class TestCastPropertyMorphableDataFoo extends TestCastAbstractPropertyMorphableData
208210
{
209-
public function __construct(public string $a, public DummyBackedEnum $enum)
211+
public function __construct(public string $a)
210212
{
211-
parent::__construct('a');
213+
parent::__construct(DummyBackedEnum::FOO);
212214
}
213215
}
214216

@@ -222,20 +224,20 @@ public function __construct(public string $a, public DummyBackedEnum $enum)
222224
public $timestamps = false;
223225
};
224226

225-
$abstractA = new TestCastPropertyMorphableDataA('foo', DummyBackedEnum::FOO);
227+
$abstractA = new TestCastPropertyMorphableDataFoo('foo');
226228

227229
$modelId = $modelClass::create([
228230
'data' => $abstractA,
229231
])->id;
230232

231233
assertDatabaseHas($modelClass::class, [
232-
'data' => json_encode(['a' => 'foo', 'enum' => 'foo', 'variant' => 'a']),
234+
'data' => json_encode(['a' => 'foo', 'variant' => 'foo']),
233235
]);
234236

235237
$model = $modelClass::find($modelId);
236238

237239
expect($model->data)
238-
->toBeInstanceOf(TestCastPropertyMorphableDataA::class)
240+
->toBeInstanceOf(TestCastPropertyMorphableDataFoo::class)
239241
->a->toBe('foo')
240-
->enum->toBe(DummyBackedEnum::FOO);
242+
->variant->toBe(DummyBackedEnum::FOO);
241243
});

tests/ValidationTest.php

+20-8
Original file line numberDiff line numberDiff line change
@@ -2609,19 +2609,25 @@ public static function rules(): array
26092609
});
26102610

26112611
describe('property-morphable validation tests', function () {
2612+
enum TestValidationPropertyMorphableEnum: string
2613+
{
2614+
case A = 'a';
2615+
case B = 'b';
2616+
};
2617+
26122618
abstract class TestValidationAbstractPropertyMorphableData extends Data implements PropertyMorphableData
26132619
{
26142620
public function __construct(
26152621
#[PropertyForMorph]
2616-
public string $variant,
2622+
public TestValidationPropertyMorphableEnum $variant,
26172623
) {
26182624
}
26192625

26202626
public static function morph(array $properties): ?string
26212627
{
2622-
return match ($properties['variant'] ?? null) {
2623-
'a' => TestValidationPropertyMorphableDataA::class,
2624-
'b' => TestValidationPropertyMorphableDataB::class,
2628+
return match ($properties['variant']) {
2629+
TestValidationPropertyMorphableEnum::A => TestValidationPropertyMorphableDataA::class,
2630+
TestValidationPropertyMorphableEnum::B => TestValidationPropertyMorphableDataB::class,
26252631
default => null,
26262632
};
26272633
}
@@ -2631,15 +2637,15 @@ class TestValidationPropertyMorphableDataA extends TestValidationAbstractPropert
26312637
{
26322638
public function __construct(public string $a, public DummyBackedEnum $enum)
26332639
{
2634-
parent::__construct('a');
2640+
parent::__construct(TestValidationPropertyMorphableEnum::A);
26352641
}
26362642
}
26372643

26382644
class TestValidationPropertyMorphableDataB extends TestValidationAbstractPropertyMorphableData
26392645
{
26402646
public function __construct(public string $b)
26412647
{
2642-
parent::__construct('b');
2648+
parent::__construct(TestValidationPropertyMorphableEnum::B);
26432649
}
26442650
}
26452651

@@ -2651,7 +2657,10 @@ public function __construct(public string $b)
26512657
->assertErrors([
26522658
'variant' => 'c',
26532659
], [
2654-
'variant' => ['The selected variant is invalid for morph.'],
2660+
'variant' => [
2661+
'The selected variant is invalid.',
2662+
'The selected variant is invalid for morph.',
2663+
],
26552664
])
26562665
->assertErrors([
26572666
'variant' => 'a',
@@ -2701,7 +2710,10 @@ public function __construct(
27012710
->assertErrors([
27022711
'nestedCollection' => [['variant' => 'c']],
27032712
], [
2704-
'nestedCollection.0.variant' => ['The selected nested collection.0.variant is invalid for morph.'],
2713+
'nestedCollection.0.variant' => [
2714+
'The selected nested collection.0.variant is invalid.',
2715+
'The selected nested collection.0.variant is invalid for morph.',
2716+
],
27052717
])
27062718
->assertErrors([
27072719
'nestedCollection' => [['variant' => 'a'], ['variant' => 'b']],

0 commit comments

Comments
 (0)