Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(laravel): provide a trait in addition to the annotation #6543

Merged
merged 6 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 22 additions & 10 deletions src/Laravel/ApiPlatformProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
use ApiPlatform\Laravel\Eloquent\Filter\FilterInterface as EloquentFilterInterface;
use ApiPlatform\Laravel\Eloquent\Filter\SearchFilter;
use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property\EloquentAttributePropertyMetadataFactory;
use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property\EloquentAttributePropertyNameCollectionFactory;
use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property\EloquentPropertyMetadataFactory;
use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property\EloquentPropertyNameCollectionMetadataFactory;
use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Resource\EloquentResourceCollectionMetadataFactory;
Expand All @@ -62,6 +61,9 @@
use ApiPlatform\Laravel\Eloquent\State\PersistProcessor;
use ApiPlatform\Laravel\Eloquent\State\RemoveProcessor;
use ApiPlatform\Laravel\Exception\ErrorHandler;
use ApiPlatform\Laravel\Metadata\ConcernsPropertyNameCollectionMetadataFactory;
use ApiPlatform\Laravel\Metadata\ConcernsResourceMetadataCollectionFactory;
use ApiPlatform\Laravel\Metadata\ConcernsResourceNameCollectionFactory;
use ApiPlatform\Laravel\Routing\IriConverter;
use ApiPlatform\Laravel\Routing\Router as UrlGeneratorRouter;
use ApiPlatform\Laravel\Routing\SkolemIriConverter;
Expand All @@ -70,6 +72,7 @@
use ApiPlatform\Laravel\State\SwaggerUiProcessor;
use ApiPlatform\Laravel\State\ValidateProvider;
use ApiPlatform\Metadata\Exception\NotExposedHttpException;
use ApiPlatform\Metadata\Factory\Property\ClassLevelAttributePropertyNameCollectionFactory;
use ApiPlatform\Metadata\FilterInterface;
use ApiPlatform\Metadata\IdentifiersExtractor;
use ApiPlatform\Metadata\IdentifiersExtractorInterface;
Expand Down Expand Up @@ -205,7 +208,7 @@ public function register(): void
$refl = new \ReflectionClass(Error::class);
$paths[] = \dirname($refl->getFileName());

return new AttributesResourceNameCollectionFactory($paths);
return new ConcernsResourceNameCollectionFactory($paths, new AttributesResourceNameCollectionFactory($paths));
});

$this->app->bind(ResourceClassResolverInterface::class, ResourceClassResolver::class);
Expand Down Expand Up @@ -238,11 +241,13 @@ public function register(): void
});

$this->app->singleton(PropertyNameCollectionFactoryInterface::class, function (Application $app) {
return new EloquentAttributePropertyNameCollectionFactory(
new EloquentPropertyNameCollectionMetadataFactory(
$app->make(ModelMetadata::class),
new PropertyInfoPropertyNameCollectionFactory($app->make(PropertyInfoExtractorInterface::class)),
$app->make(ResourceClassResolverInterface::class)
return new ClassLevelAttributePropertyNameCollectionFactory(
new ConcernsPropertyNameCollectionMetadataFactory(
new EloquentPropertyNameCollectionMetadataFactory(
$app->make(ModelMetadata::class),
new PropertyInfoPropertyNameCollectionFactory($app->make(PropertyInfoExtractorInterface::class)),
$app->make(ResourceClassResolverInterface::class)
)
)
);
});
Expand Down Expand Up @@ -273,13 +278,20 @@ public function register(): void
$app->make(PathSegmentNameGeneratorInterface::class),
new NotExposedOperationResourceMetadataCollectionFactory(
$app->make(LinkFactoryInterface::class),
new AttributesResourceMetadataCollectionFactory(
null,
new ConcernsResourceMetadataCollectionFactory(
new AttributesResourceMetadataCollectionFactory(
null,
$app->make(LoggerInterface::class),
[
'routePrefix' => $config->get('api-platform.routes.prefix') ?? '/',
],
false,
),
$app->make(LoggerInterface::class),
[
'routePrefix' => $config->get('api-platform.routes.prefix') ?? '/',
],
false
false,
)
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@

use ApiPlatform\JsonSchema\Metadata\Property\Factory\SchemaPropertyMetadataFactory;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\Exception\PropertyNotFoundException;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use Illuminate\Database\Eloquent\Model;

/**
* Handles Eloquent methods for relations.
*/
final class EloquentAttributePropertyMetadataFactory implements PropertyMetadataFactoryInterface
{
public function __construct(
Expand All @@ -28,45 +32,41 @@ public function __construct(
public function create(string $resourceClass, string $property, array $options = []): ApiProperty
{
if (!class_exists($resourceClass)) {
return $this->decorated?->create($resourceClass, $property, $options) ?? new ApiProperty();
return $this->decorated?->create($resourceClass, $property, $options) ??
$this->throwNotFound($resourceClass, $property);
}

$refl = new \ReflectionClass($resourceClass);
$model = $refl->newInstanceWithoutConstructor();

$propertyMetadata = $this->decorated?->create($resourceClass, $property, $options);
if (!$model instanceof Model) {
return $propertyMetadata ?? new ApiProperty();
return $propertyMetadata ?? $this->throwNotFound($resourceClass, $property);
}

try {
$method = $refl->getMethod($property);

if ($attributes = $method->getAttributes(ApiProperty::class)) {
return $this->createMetadata($attributes[0]->newInstance(), $propertyMetadata);
}
} catch (\ReflectionException) {
}

$attributes = $refl->getAttributes(ApiProperty::class);
foreach ($attributes as $attribute) {
$instance = $attribute->newInstance();
if ($instance->getProperty() === $property) {
return $this->createMetadata($instance, $propertyMetadata);
}
if ($refl->hasMethod($property) && $attributes = $refl->getMethod($property)->getAttributes(ApiProperty::class)) {
return $this->createMetadata($attributes[0]->newInstance(), $propertyMetadata);
}

return $propertyMetadata;
}

/**
* @throws PropertyNotFoundException
*/
private function throwNotFound(string $resourceClass, string $property): never
{
throw new PropertyNotFoundException(\sprintf('Property "%s" of class "%s" not found.', $property, $resourceClass));
}

private function createMetadata(ApiProperty $attribute, ?ApiProperty $propertyMetadata = null): ApiProperty
{
if (null === $propertyMetadata) {
return $this->handleUserDefinedSchema($attribute);
}

foreach (get_class_methods(ApiProperty::class) as $method) {
if (preg_match('/^(?:get|is)(.*)/', (string) $method, $matches) && null !== $val = $attribute->{$method}()) {
if (preg_match('/^(?:get|is)(.*)/', $method, $matches) && null !== $val = $attribute->{$method}()) {
$propertyMetadata = $propertyMetadata->{"with{$matches[1]}"}($val);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ final class EloquentPropertyNameCollectionMetadataFactory implements PropertyNam
{
public function __construct(
private readonly ModelMetadata $modelMetadata,
private readonly PropertyNameCollectionFactoryInterface $decorated,
private readonly ?PropertyNameCollectionFactoryInterface $decorated,
private readonly ResourceClassResolverInterface $resourceClassResolver,
) {
}
Expand All @@ -35,39 +35,41 @@ public function __construct(
*/
public function create(string $resourceClass, array $options = []): PropertyNameCollection
{
if (!class_exists($resourceClass)) {
return $this->decorated->create($resourceClass, $options);
if (!class_exists($resourceClass) || !is_a($resourceClass, Model::class, true)) {
return $this->decorated?->create($resourceClass, $options) ?? new PropertyNameCollection();
}

$refl = new \ReflectionClass($resourceClass);
try {
$refl = new \ReflectionClass($resourceClass);
$model = $refl->newInstanceWithoutConstructor();
} catch (\ReflectionException) {
return $this->decorated->create($resourceClass, $options);
}

if (!$model instanceof Model) {
return $this->decorated->create($resourceClass, $options);
return $this->decorated?->create($resourceClass, $options) ?? new PropertyNameCollection();
}

/**
* @var array<string, true> $properties
*/
$properties = [];

// When it's an Eloquent model we read attributes from database (@see ShowModelCommand)
foreach ($this->modelMetadata->getAttributes($model) as $property) {
if (!$property['primary'] && $property['hidden']) {
continue;
}

$properties[] = $property['name'];
$properties[$property['name']] = true;
}

foreach ($this->modelMetadata->getRelations($model) as $relation) {
if (!$this->resourceClassResolver->isResourceClass($relation['related'])) {
continue;
}

$properties[] = $relation['name'];
$properties[$relation['name']] = true;
}

return new PropertyNameCollection($properties);
return new PropertyNameCollection(
array_keys($properties) // @phpstan-ignore-line
);
}
}
27 changes: 27 additions & 0 deletions src/Laravel/IsApiResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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;

use ApiPlatform\Metadata\ApiResource;

/**
* @author Kévin Dunglas <kevin@dunglas.dev>
*/
trait IsApiResource
{
public static function apiResource(): ApiResource
{
return new ApiResource();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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\Metadata;

use ApiPlatform\Laravel\IsApiResource;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Metadata\Property\PropertyNameCollection;

/**
* Handles property defined with the {@see IsApiResource} concern.
*
* @author Kévin Dunglas <kevin@dunglas.dev>
*/
final class ConcernsPropertyNameCollectionMetadataFactory implements PropertyNameCollectionFactoryInterface
{
public function __construct(
private readonly ?PropertyNameCollectionFactoryInterface $decorated = null,
) {
}

/**
* {@inheritdoc}
*
* @param class-string $resourceClass
*/
public function create(string $resourceClass, array $options = []): PropertyNameCollection
{
$propertyNameCollection = $this->decorated?->create($resourceClass, $options);
if (!method_exists($resourceClass, 'apiResource')) {
return $propertyNameCollection ?? new PropertyNameCollection();
}

$refl = new \ReflectionClass($resourceClass);
$method = $refl->getMethod('apiResource');
if (!$method->isPublic() || !$method->isStatic()) {
return $propertyNameCollection ?? new PropertyNameCollection();
}

$metadataCollection = $method->invoke(null);
if (!\is_array($metadataCollection)) {
$metadataCollection = [$metadataCollection];
}

$properties = $propertyNameCollection ? array_flip(iterator_to_array($propertyNameCollection)) : [];

foreach ($metadataCollection as $apiProperty) {
if (!$apiProperty instanceof ApiProperty) {
continue;
}

if (null !== $propertyName = $apiProperty->getProperty()) {
$properties[$propertyName] = true;
}
}

return new PropertyNameCollection(array_keys($properties));
}
}
54 changes: 54 additions & 0 deletions src/Laravel/Metadata/ConcernsResourceMetadataCollectionFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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\Metadata;

use ApiPlatform\Laravel\IsApiResource;
use ApiPlatform\Metadata\Resource\Factory\MetadataCollectionFactoryTrait;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;

/**
* Creates a resource metadata from {@see IsApiResource} concerns.
*
* @author Kévin Dunglas <kevin@dunglas.dev>
*/
final class ConcernsResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface
{
use MetadataCollectionFactoryTrait;

/**
* {@inheritdoc}
*/
public function create(string $resourceClass): ResourceMetadataCollection
{
$resourceMetadataCollection = $this->decorated?->create($resourceClass) ?? new ResourceMetadataCollection(
$resourceClass
);

if (!method_exists($resourceClass, 'apiResource')) {
return $resourceMetadataCollection;
}

$metadataCollection = $resourceClass::apiResource();
if (!\is_array($metadataCollection)) {
$metadataCollection = [$metadataCollection];
}

foreach ($this->buildResourceOperations($metadataCollection, $resourceClass) as $resource) {
$resourceMetadataCollection[] = $resource;
}

return $resourceMetadataCollection;
}
}
Loading
Loading