diff --git a/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentAttributePropertyMetadataFactory.php b/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentAttributePropertyMetadataFactory.php index 51ddb890dd9..504dcab49f6 100644 --- a/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentAttributePropertyMetadataFactory.php +++ b/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentAttributePropertyMetadataFactory.php @@ -37,9 +37,12 @@ public function create(string $resourceClass, string $property, array $options = } $refl = new \ReflectionClass($resourceClass); - $model = $refl->newInstanceWithoutConstructor(); - $propertyMetadata = $this->decorated?->create($resourceClass, $property, $options); + if ($refl->isEnum() || !is_a($resourceClass, Model::class, true)) { + return $propertyMetadata ?? $this->throwNotFound($resourceClass, $property); + } + + $model = $refl->newInstanceWithoutConstructor(); if (!$model instanceof Model) { return $propertyMetadata ?? $this->throwNotFound($resourceClass, $property); } diff --git a/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyMetadataFactory.php b/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyMetadataFactory.php index 5703ca5a673..0fcfa13776c 100644 --- a/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyMetadataFactory.php +++ b/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyMetadataFactory.php @@ -88,7 +88,7 @@ public function create(string $resourceClass, string $property, array $options = 'collection', 'encrypted:collection' => Type::collection(Type::object(Collection::class)), 'encrypted:array' => Type::builtin(TypeIdentifier::ARRAY), 'encrypted:object' => Type::object(), - default => \in_array($builtinType, TypeIdentifier::values(), true) ? Type::builtin($builtinType) : Type::string(), + default => $this->resolveDefaultType($builtinType), }; if ($p['nullable']) { @@ -127,4 +127,19 @@ public function create(string $resourceClass, string $property, array $options = return $propertyMetadata; } + + private function resolveDefaultType(string $builtinType): Type + { + if (\in_array($builtinType, TypeIdentifier::values(), true)) { + return Type::builtin($builtinType); + } + + // Laravel allows passing parameters to class casts via "Class:param" syntax (e.g. AsEnumCollection). + $castClass = explode(':', $builtinType, 2)[0]; + if (enum_exists($castClass)) { + return Type::enum($castClass); + } + + return Type::string(); + } } diff --git a/src/Laravel/Eloquent/Metadata/Factory/Resource/EloquentResourceCollectionMetadataFactory.php b/src/Laravel/Eloquent/Metadata/Factory/Resource/EloquentResourceCollectionMetadataFactory.php index 97ef6dae952..706070d288c 100644 --- a/src/Laravel/Eloquent/Metadata/Factory/Resource/EloquentResourceCollectionMetadataFactory.php +++ b/src/Laravel/Eloquent/Metadata/Factory/Resource/EloquentResourceCollectionMetadataFactory.php @@ -71,9 +71,12 @@ public function create(string $resourceClass): ResourceMetadataCollection try { $refl = new \ReflectionClass($resourceClass); + if ($refl->isEnum()) { + return $resourceMetadataCollection; + } $model = $refl->newInstanceWithoutConstructor(); } catch (\ReflectionException) { - return $this->decorated->create($resourceClass); + return $resourceMetadataCollection; } $isModel = $model instanceof Model; diff --git a/src/Laravel/Tests/Eloquent/Metadata/Factory/Property/EloquentPropertyMetadataFactoryTest.php b/src/Laravel/Tests/Eloquent/Metadata/Factory/Property/EloquentPropertyMetadataFactoryTest.php new file mode 100644 index 00000000000..ff5723dd9cd --- /dev/null +++ b/src/Laravel/Tests/Eloquent/Metadata/Factory/Property/EloquentPropertyMetadataFactoryTest.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Eloquent\Metadata\Factory\Property; + +use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property\EloquentPropertyMetadataFactory; +use ApiPlatform\Laravel\Eloquent\Metadata\ModelMetadata; +use ApiPlatform\Laravel\workbench\app\Enums\BookStatus; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use Symfony\Component\TypeInfo\Type\BackedEnumType; +use Symfony\Component\TypeInfo\Type\EnumType; + +enum CastEnumIntStatus: int +{ + case ACTIVE = 1; + case INACTIVE = 0; +} + +enum CastEnumUnitStatus +{ + case ACTIVE; + case INACTIVE; +} + +class CastEnumStringStatusModel extends Model +{ + protected $table = 'books'; + + protected function casts(): array + { + return [ + 'status' => BookStatus::class, + ]; + } +} + +class CastEnumIntStatusModel extends Model +{ + protected $table = 'books'; + + protected function casts(): array + { + return [ + 'status' => CastEnumIntStatus::class, + ]; + } +} + +class CastEnumUnitStatusModel extends Model +{ + protected $table = 'books'; + + protected function casts(): array + { + return [ + 'status' => CastEnumUnitStatus::class, + ]; + } +} + +/** + * @see https://github.com/api-platform/core/issues/8138 + */ +final class EloquentPropertyMetadataFactoryTest extends TestCase +{ + use RefreshDatabase; + use WithWorkbench; + + public function testStringBackedEnumCastIsMappedToEnumType(): void + { + $factory = new EloquentPropertyMetadataFactory(new ModelMetadata()); + $metadata = $factory->create(CastEnumStringStatusModel::class, 'status'); + + $type = $metadata->getNativeType(); + $this->assertInstanceOf(BackedEnumType::class, $type); + $this->assertSame(BookStatus::class, $type->getClassName()); + } + + public function testIntBackedEnumCastIsMappedToEnumType(): void + { + $factory = new EloquentPropertyMetadataFactory(new ModelMetadata()); + $metadata = $factory->create(CastEnumIntStatusModel::class, 'status'); + + $type = $metadata->getNativeType(); + $this->assertInstanceOf(BackedEnumType::class, $type); + $this->assertSame(CastEnumIntStatus::class, $type->getClassName()); + } + + public function testUnitEnumCastIsMappedToEnumType(): void + { + $factory = new EloquentPropertyMetadataFactory(new ModelMetadata()); + $metadata = $factory->create(CastEnumUnitStatusModel::class, 'status'); + + $type = $metadata->getNativeType(); + $this->assertInstanceOf(EnumType::class, $type); + $this->assertSame(CastEnumUnitStatus::class, $type->getClassName()); + } +} diff --git a/src/Laravel/Tests/Eloquent/Metadata/Factory/Resource/EloquentResourceCollectionMetadataFactoryTest.php b/src/Laravel/Tests/Eloquent/Metadata/Factory/Resource/EloquentResourceCollectionMetadataFactoryTest.php new file mode 100644 index 00000000000..43ea4716d18 --- /dev/null +++ b/src/Laravel/Tests/Eloquent/Metadata/Factory/Resource/EloquentResourceCollectionMetadataFactoryTest.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Eloquent\Metadata\Factory\Resource; + +use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Resource\EloquentResourceCollectionMetadataFactory; +use ApiPlatform\Laravel\workbench\app\Enums\BookStatus; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; + +/** + * @see https://github.com/api-platform/core/issues/8138 + */ +final class EloquentResourceCollectionMetadataFactoryTest extends TestCase +{ + use WithWorkbench; + + public function testEnumClassIsNotInstantiated(): void + { + $decorated = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + $expected = new ResourceMetadataCollection(BookStatus::class); + $decorated->expects($this->once()) + ->method('create') + ->with(BookStatus::class) + ->willReturn($expected); + + $factory = new EloquentResourceCollectionMetadataFactory($decorated); + + $this->assertSame($expected, $factory->create(BookStatus::class)); + } +}