Skip to content

Commit

Permalink
links handler
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Jun 21, 2024
1 parent d09188e commit 332b7ea
Show file tree
Hide file tree
Showing 29 changed files with 664 additions and 102 deletions.
13 changes: 0 additions & 13 deletions src/Doctrine/Orm/State/Options.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,4 @@ public function withEntityClass(?string $entityClass): self

return $self;
}

public function getHandleLinks(): mixed
{
return $this->handleLinks;
}

public function withHandleLinks(mixed $handleLinks): self
{
$self = clone $this;
$self->handleLinks = $handleLinks;

return $self;
}
}
103 changes: 68 additions & 35 deletions src/Laravel/ApiPlatformProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@

namespace ApiPlatform\Laravel;

use ApiPlatform\Action\NotExposedAction;
use ApiPlatform\Documentation\Action\DocumentationAction;
use ApiPlatform\Documentation\Action\EntrypointAction;
use ApiPlatform\Exception\NotExposedHttpException;
use ApiPlatform\Hydra\JsonSchema\SchemaFactory as HydraSchemaFactory;
use ApiPlatform\Hydra\Serializer\CollectionNormalizer as HydraCollectionNormalizer;
use ApiPlatform\Hydra\Serializer\DocumentationNormalizer as HydraDocumentationNormalizer;
Expand Down Expand Up @@ -43,18 +43,23 @@
use ApiPlatform\JsonSchema\TypeFactoryInterface;
use ApiPlatform\Laravel\ApiResource\Error;
use ApiPlatform\Laravel\Controller\ApiPlatformController;
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;
use ApiPlatform\Laravel\Eloquent\Metadata\IdentifiersExtractor as EloquentIdentifiersExtractor;
use ApiPlatform\Laravel\Eloquent\Metadata\ModelMetadata;
use ApiPlatform\Laravel\Eloquent\Metadata\ResourceClassResolver as EloquentResourceClassResolver;
use ApiPlatform\Laravel\Eloquent\PropertyAccess\PropertyAccessor as EloquentPropertyAccessor;
use ApiPlatform\Laravel\Eloquent\Serializer\SerializerContextBuilder as EloquentSerializerContextBuilder;
use ApiPlatform\Laravel\Eloquent\State\CollectionProvider;
use ApiPlatform\Laravel\Eloquent\State\ItemProvider;
use ApiPlatform\Laravel\Eloquent\State\LinksHandler;
use ApiPlatform\Laravel\Eloquent\State\LinksHandlerInterface;
use ApiPlatform\Laravel\Eloquent\State\PersistProcessor;
use ApiPlatform\Laravel\Eloquent\State\RemoveProcessor;
use ApiPlatform\Laravel\Exception\Handler;
use ApiPlatform\Laravel\Metadata\Property\EloquentPropertyMetadataFactory;
use ApiPlatform\Laravel\Metadata\Property\EloquentPropertyNameCollectionMetadataFactory;
use ApiPlatform\Laravel\Metadata\Resource\EloquentResourceCollectionMetadataFactory;
use ApiPlatform\Laravel\Routing\IriConverter;
use ApiPlatform\Laravel\Routing\Router as UrlGeneratorRouter;
use ApiPlatform\Laravel\Routing\SkolemIriConverter;
Expand All @@ -66,6 +71,7 @@
use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface;
use ApiPlatform\Metadata\Operation\PathSegmentNameGeneratorInterface;
use ApiPlatform\Metadata\Operation\UnderscorePathSegmentNameGenerator;
use ApiPlatform\Metadata\Property\Factory\AttributePropertyMetadataFactory;
use ApiPlatform\Metadata\Property\Factory\PropertyInfoPropertyMetadataFactory;
use ApiPlatform\Metadata\Property\Factory\PropertyInfoPropertyNameCollectionFactory;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
Expand Down Expand Up @@ -199,7 +205,7 @@ public function register(): void
return new PropertyInfoPropertyMetadataFactory(
$app->make(PropertyInfoExtractorInterface::class),
new EloquentPropertyMetadataFactory(
$app->make(ModelMetadata::class)
$app->make(ModelMetadata::class),
)
);
});
Expand All @@ -209,17 +215,23 @@ public function register(): void
$app->make(ResourceClassResolverInterface::class),
new SerializerPropertyMetadataFactory(
new SerializerClassMetadataFactory($app->make(ClassMetadataFactoryInterface::class)),
$inner,
new AttributePropertyMetadataFactory(
new EloquentAttributePropertyMetadataFactory(
$inner,
)
),
$app->make(ResourceClassResolverInterface::class)
),
);
});

$this->app->singleton(PropertyNameCollectionFactoryInterface::class, function (Application $app) {
return new EloquentPropertyNameCollectionMetadataFactory(
$app->make(ModelMetadata::class),
new PropertyInfoPropertyNameCollectionFactory($app->make(PropertyInfoExtractorInterface::class)),
$app->make(ResourceClassResolverInterface::class)
return new EloquentAttributePropertyNameCollectionFactory(
new EloquentPropertyNameCollectionMetadataFactory(
$app->make(ModelMetadata::class),
new PropertyInfoPropertyNameCollectionFactory($app->make(PropertyInfoExtractorInterface::class)),
$app->make(ResourceClassResolverInterface::class)
)
);
});

Expand All @@ -233,36 +245,45 @@ public function register(): void

// TODO: add cached metadata factories
$this->app->singleton(ResourceMetadataCollectionFactoryInterface::class, function (Application $app) use ($config) {
return new EloquentResourceCollectionMetadataFactory(new AlternateUriResourceMetadataCollectionFactory(
new FiltersResourceMetadataCollectionFactory(
new FormatsResourceMetadataCollectionFactory(
new InputOutputResourceMetadataCollectionFactory(
new PhpDocResourceMetadataCollectionFactory(
new OperationNameResourceMetadataCollectionFactory(
new LinkResourceMetadataCollectionFactory(
$this->app->make(LinkFactoryInterface::class),
new UriTemplateResourceMetadataCollectionFactory(
$this->app->make(LinkFactoryInterface::class),
$this->app->make(PathSegmentNameGeneratorInterface::class),
new NotExposedOperationResourceMetadataCollectionFactory(
$this->app->make(LinkFactoryInterface::class),
// TODO: graphql
new AttributesResourceMetadataCollectionFactory(null, $app->make(LoggerInterface::class), ['routePrefix' => $config->get('api-platform.prefix') ?? '/'], false)
return new EloquentResourceCollectionMetadataFactory(
new AlternateUriResourceMetadataCollectionFactory(
new FiltersResourceMetadataCollectionFactory(
new FormatsResourceMetadataCollectionFactory(
new InputOutputResourceMetadataCollectionFactory(
new PhpDocResourceMetadataCollectionFactory(
new OperationNameResourceMetadataCollectionFactory(
new LinkResourceMetadataCollectionFactory(
$app->make(LinkFactoryInterface::class),
new UriTemplateResourceMetadataCollectionFactory(
$app->make(LinkFactoryInterface::class),
$app->make(PathSegmentNameGeneratorInterface::class),
new NotExposedOperationResourceMetadataCollectionFactory(
$app->make(LinkFactoryInterface::class),
new AttributesResourceMetadataCollectionFactory(
null,
$app->make(LoggerInterface::class),
[
'routePrefix' => $config->get('api-platform.prefix') ?? '/',
],
false
)
)
)
)
)
)
)
),
$config->get('api-platform.formats'),
$config->get('api-platform.patch_formats'),
),
$config->get('api-platform.formats'),
$config->get('api-platform.patch_formats'),
)
)
)
));
),
$app->make(ModelMetadata::class)
);
});

$this->app->bind(PropertyAccessorInterface::class, function () {
return PropertyAccess::createPropertyAccessor();
return new EloquentPropertyAccessor(PropertyAccess::createPropertyAccessor());
});

$this->app->bind(NameConverterInterface::class, function (Application $app) {
Expand All @@ -271,6 +292,16 @@ public function register(): void

$this->app->bind(OperationMetadataFactoryInterface::class, OperationMetadataFactory::class);

$this->app->singleton(ItemProvider::class, function (Application $app) {
$tagged = iterator_to_array($app->tagged(LinksHandlerInterface::class));

return new ItemProvider(new LinksHandler($app), new ServiceLocator($tagged));
});
$this->app->singleton(CollectionProvider::class, function (Application $app) {
$tagged = iterator_to_array($app->tagged(LinksHandlerInterface::class));

return new CollectionProvider($app->make(Pagination::class), new LinksHandler($app), new ServiceLocator($tagged));
});
$this->app->tag([ItemProvider::class, CollectionProvider::class], ProviderInterface::class);

$this->app->singleton(CallableProvider::class, function (Application $app) {
Expand Down Expand Up @@ -308,8 +339,8 @@ public function register(): void
return new WriteProcessor($app->make(SerializeProcessor::class), $app->make(CallableProcessor::class));
});

$this->app->singleton(SerializerContextBuilder::class, function (Application $app) {
return new SerializerContextBuilder($app->make(ResourceMetadataCollectionFactoryInterface::class));
$this->app->singleton(SerializerContextBuilder::class, function (Application $app) use ($config) {
return new SerializerContextBuilder($app->make(ResourceMetadataCollectionFactoryInterface::class), $config->get('app.debug') ?? false);
});
$this->app->bind(SerializerContextBuilderInterface::class, EloquentSerializerContextBuilder::class);
$this->app->singleton(EloquentSerializerContextBuilder::class, function (Application $app) {
Expand Down Expand Up @@ -746,7 +777,9 @@ public function boot(ResourceNameCollectionFactoryInterface $resourceNameCollect
});
$route->name('api_entrypoint')->middleware(ApiPlatformMiddleware::class);
$routeCollection->add($route);
$route = new Route(['GET'], $prefix.'/.well-known/genid/{id}', [NotExposedAction::class, '__invoke']);
$route = new Route(['GET'], $prefix.'/.well-known/genid/{id}', function (): void {
throw new NotExposedHttpException('This route is not exposed on purpose. It generates an IRI for a collection resource without identifier nor item operation.');
});
$route->name('api_genid')->middleware(ApiPlatformMiddleware::class);
$routeCollection->add($route);
$router->setRoutes($routeCollection);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?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\Eloquent\Metadata\Factory\Property;

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

final class EloquentAttributePropertyMetadataFactory implements PropertyMetadataFactoryInterface
{
public function __construct(private readonly ?PropertyMetadataFactoryInterface $decorated = null)
{
}

public function create(string $resourceClass, string $property, array $options = []): ApiProperty
{
$refl = new \ReflectionClass($resourceClass);
$model = $refl->newInstanceWithoutConstructor();

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

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);
}
}

return $propertyMetadata;
}

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}()) {
$propertyMetadata = $propertyMetadata->{"with{$matches[1]}"}($val);
}
}

return $this->handleUserDefinedSchema($propertyMetadata);
}

private function handleUserDefinedSchema(ApiProperty $propertyMetadata): ApiProperty
{
// can't know later if the schema has been defined by the user or by API Platform
// store extra key to make this difference
if (null !== $propertyMetadata->getSchema()) {
$extraProperties = $propertyMetadata->getExtraProperties() ?? [];
$propertyMetadata = $propertyMetadata->withExtraProperties([SchemaPropertyMetadataFactory::JSON_SCHEMA_USER_DEFINED => true] + $extraProperties);
}

return $propertyMetadata;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?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\Eloquent\Metadata\Factory\Property;

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

final class EloquentAttributePropertyNameCollectionFactory implements PropertyNameCollectionFactoryInterface
{
public function __construct(private readonly ?PropertyNameCollectionFactoryInterface $decorated = null)
{
}

public function create(string $resourceClass, array $options = []): PropertyNameCollection
{
$properties = $this->decorated ? iterator_to_array($this->decorated->create($resourceClass, $options)) : [];

$refl = new \ReflectionClass($resourceClass);
$attributes = $refl->getAttributes(ApiProperty::class);
foreach ($attributes as $attribute) {
$instance = $attribute->newInstance();
if ($property = $instance->getProperty()) {
$properties[] = $property;
}
}

return new PropertyNameCollection($properties);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

declare(strict_types=1);

namespace ApiPlatform\Laravel\Metadata\Property;
namespace ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property;

use ApiPlatform\Laravel\Eloquent\Metadata\ModelMetadata;
use ApiPlatform\Metadata\ApiProperty;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,12 @@

declare(strict_types=1);

namespace ApiPlatform\Laravel\Metadata\Property;
namespace ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property;

use ApiPlatform\Laravel\Eloquent\Metadata\ModelMetadata;
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Metadata\Property\PropertyNameCollection;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use Illuminate\Database\Console\ShowModelCommand;
use Illuminate\Database\Eloquent\Model;

final class EloquentPropertyNameCollectionMetadataFactory implements PropertyNameCollectionFactoryInterface
Expand Down Expand Up @@ -47,7 +46,7 @@ public function create(string $resourceClass, array $options = []): PropertyName
$properties = [];
// When it's an Eloquent model we read attributes from database (@see ShowModelCommand)
foreach ($this->modelMetadata->getAttributes($model) as $property) { // @phpstan-ignore-line
if ('id' !== $property['name'] && $property['hidden']) {
if (!$property['primary'] && $property['hidden']) {
continue;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@

declare(strict_types=1);

namespace ApiPlatform\Laravel\Metadata\Resource;
namespace ApiPlatform\Laravel\Eloquent\Metadata\Factory\Resource;

use ApiPlatform\Laravel\Eloquent\Metadata\ModelMetadata;
use ApiPlatform\Laravel\Eloquent\State\CollectionProvider;
use ApiPlatform\Laravel\Eloquent\State\ItemProvider;
use ApiPlatform\Laravel\Eloquent\State\PersistProcessor;
Expand All @@ -25,11 +26,10 @@

class EloquentResourceCollectionMetadataFactory implements ResourceMetadataCollectionFactoryInterface
{
private ResourceMetadataCollectionFactoryInterface $decorated;

public function __construct(ResourceMetadataCollectionFactoryInterface $decorated)
{
$this->decorated = $decorated;
public function __construct(
private readonly ResourceMetadataCollectionFactoryInterface $decorated,
private readonly ModelMetadata $modelMetadata
) {
}

/**
Expand Down
Loading

0 comments on commit 332b7ea

Please sign in to comment.