Skip to content

Commit a6bdf71

Browse files
authored
fix(laravel): detect enum casts in eloquent property metadata factory (#8247)
Fixes #8138
1 parent 149adf7 commit a6bdf71

5 files changed

Lines changed: 179 additions & 4 deletions

File tree

src/Laravel/Eloquent/Metadata/Factory/Property/EloquentAttributePropertyMetadataFactory.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,12 @@ public function create(string $resourceClass, string $property, array $options =
3737
}
3838

3939
$refl = new \ReflectionClass($resourceClass);
40-
$model = $refl->newInstanceWithoutConstructor();
41-
4240
$propertyMetadata = $this->decorated?->create($resourceClass, $property, $options);
41+
if ($refl->isEnum() || !is_a($resourceClass, Model::class, true)) {
42+
return $propertyMetadata ?? $this->throwNotFound($resourceClass, $property);
43+
}
44+
45+
$model = $refl->newInstanceWithoutConstructor();
4346
if (!$model instanceof Model) {
4447
return $propertyMetadata ?? $this->throwNotFound($resourceClass, $property);
4548
}

src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyMetadataFactory.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public function create(string $resourceClass, string $property, array $options =
8888
'collection', 'encrypted:collection' => Type::collection(Type::object(Collection::class)),
8989
'encrypted:array' => Type::builtin(TypeIdentifier::ARRAY),
9090
'encrypted:object' => Type::object(),
91-
default => \in_array($builtinType, TypeIdentifier::values(), true) ? Type::builtin($builtinType) : Type::string(),
91+
default => $this->resolveDefaultType($builtinType),
9292
};
9393

9494
if ($p['nullable']) {
@@ -127,4 +127,19 @@ public function create(string $resourceClass, string $property, array $options =
127127

128128
return $propertyMetadata;
129129
}
130+
131+
private function resolveDefaultType(string $builtinType): Type
132+
{
133+
if (\in_array($builtinType, TypeIdentifier::values(), true)) {
134+
return Type::builtin($builtinType);
135+
}
136+
137+
// Laravel allows passing parameters to class casts via "Class:param" syntax (e.g. AsEnumCollection).
138+
$castClass = explode(':', $builtinType, 2)[0];
139+
if (enum_exists($castClass)) {
140+
return Type::enum($castClass);
141+
}
142+
143+
return Type::string();
144+
}
130145
}

src/Laravel/Eloquent/Metadata/Factory/Resource/EloquentResourceCollectionMetadataFactory.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,12 @@ public function create(string $resourceClass): ResourceMetadataCollection
7171

7272
try {
7373
$refl = new \ReflectionClass($resourceClass);
74+
if ($refl->isEnum()) {
75+
return $resourceMetadataCollection;
76+
}
7477
$model = $refl->newInstanceWithoutConstructor();
7578
} catch (\ReflectionException) {
76-
return $this->decorated->create($resourceClass);
79+
return $resourceMetadataCollection;
7780
}
7881

7982
$isModel = $model instanceof Model;
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Laravel\Tests\Eloquent\Metadata\Factory\Property;
15+
16+
use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property\EloquentPropertyMetadataFactory;
17+
use ApiPlatform\Laravel\Eloquent\Metadata\ModelMetadata;
18+
use ApiPlatform\Laravel\workbench\app\Enums\BookStatus;
19+
use Illuminate\Database\Eloquent\Model;
20+
use Illuminate\Foundation\Testing\RefreshDatabase;
21+
use Orchestra\Testbench\Concerns\WithWorkbench;
22+
use Orchestra\Testbench\TestCase;
23+
use Symfony\Component\TypeInfo\Type\BackedEnumType;
24+
use Symfony\Component\TypeInfo\Type\EnumType;
25+
26+
enum CastEnumIntStatus: int
27+
{
28+
case ACTIVE = 1;
29+
case INACTIVE = 0;
30+
}
31+
32+
enum CastEnumUnitStatus
33+
{
34+
case ACTIVE;
35+
case INACTIVE;
36+
}
37+
38+
class CastEnumStringStatusModel extends Model
39+
{
40+
protected $table = 'books';
41+
42+
protected function casts(): array
43+
{
44+
return [
45+
'status' => BookStatus::class,
46+
];
47+
}
48+
}
49+
50+
class CastEnumIntStatusModel extends Model
51+
{
52+
protected $table = 'books';
53+
54+
protected function casts(): array
55+
{
56+
return [
57+
'status' => CastEnumIntStatus::class,
58+
];
59+
}
60+
}
61+
62+
class CastEnumUnitStatusModel extends Model
63+
{
64+
protected $table = 'books';
65+
66+
protected function casts(): array
67+
{
68+
return [
69+
'status' => CastEnumUnitStatus::class,
70+
];
71+
}
72+
}
73+
74+
/**
75+
* @see https://github.com/api-platform/core/issues/8138
76+
*/
77+
final class EloquentPropertyMetadataFactoryTest extends TestCase
78+
{
79+
use RefreshDatabase;
80+
use WithWorkbench;
81+
82+
public function testStringBackedEnumCastIsMappedToEnumType(): void
83+
{
84+
$factory = new EloquentPropertyMetadataFactory(new ModelMetadata());
85+
$metadata = $factory->create(CastEnumStringStatusModel::class, 'status');
86+
87+
$type = $metadata->getNativeType();
88+
$this->assertInstanceOf(BackedEnumType::class, $type);
89+
$this->assertSame(BookStatus::class, $type->getClassName());
90+
}
91+
92+
public function testIntBackedEnumCastIsMappedToEnumType(): void
93+
{
94+
$factory = new EloquentPropertyMetadataFactory(new ModelMetadata());
95+
$metadata = $factory->create(CastEnumIntStatusModel::class, 'status');
96+
97+
$type = $metadata->getNativeType();
98+
$this->assertInstanceOf(BackedEnumType::class, $type);
99+
$this->assertSame(CastEnumIntStatus::class, $type->getClassName());
100+
}
101+
102+
public function testUnitEnumCastIsMappedToEnumType(): void
103+
{
104+
$factory = new EloquentPropertyMetadataFactory(new ModelMetadata());
105+
$metadata = $factory->create(CastEnumUnitStatusModel::class, 'status');
106+
107+
$type = $metadata->getNativeType();
108+
$this->assertInstanceOf(EnumType::class, $type);
109+
$this->assertSame(CastEnumUnitStatus::class, $type->getClassName());
110+
}
111+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Laravel\Tests\Eloquent\Metadata\Factory\Resource;
15+
16+
use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Resource\EloquentResourceCollectionMetadataFactory;
17+
use ApiPlatform\Laravel\workbench\app\Enums\BookStatus;
18+
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
19+
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
20+
use Orchestra\Testbench\Concerns\WithWorkbench;
21+
use Orchestra\Testbench\TestCase;
22+
23+
/**
24+
* @see https://github.com/api-platform/core/issues/8138
25+
*/
26+
final class EloquentResourceCollectionMetadataFactoryTest extends TestCase
27+
{
28+
use WithWorkbench;
29+
30+
public function testEnumClassIsNotInstantiated(): void
31+
{
32+
$decorated = $this->createMock(ResourceMetadataCollectionFactoryInterface::class);
33+
$expected = new ResourceMetadataCollection(BookStatus::class);
34+
$decorated->expects($this->once())
35+
->method('create')
36+
->with(BookStatus::class)
37+
->willReturn($expected);
38+
39+
$factory = new EloquentResourceCollectionMetadataFactory($decorated);
40+
41+
$this->assertSame($expected, $factory->create(BookStatus::class));
42+
}
43+
}

0 commit comments

Comments
 (0)