From 2a12e7bdf45f800dfc911e5af5caaa88081765d8 Mon Sep 17 00:00:00 2001 From: Tobias Oitzinger Date: Thu, 14 Nov 2024 18:44:26 +0100 Subject: [PATCH] fix(laravel): graphql fix relationship loading fix loading relationships by only loading those related to the model Closes: #6791 Signed-off-by: Tobias Oitzinger --- src/Laravel/ApiPlatformProvider.php | 6 +- src/Laravel/Eloquent/State/LinksHandler.php | 73 ++++++++++++++++--- .../Resource/ResourceMetadataCollection.php | 3 +- 3 files changed, 67 insertions(+), 15 deletions(-) diff --git a/src/Laravel/ApiPlatformProvider.php b/src/Laravel/ApiPlatformProvider.php index e44f2f3ebf9..98d3ba9bba9 100644 --- a/src/Laravel/ApiPlatformProvider.php +++ b/src/Laravel/ApiPlatformProvider.php @@ -432,12 +432,12 @@ public function register(): void $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)); + return new ItemProvider(new LinksHandler($app, $app->make(ResourceMetadataCollectionFactoryInterface::class)), 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), $app->tagged(QueryExtensionInterface::class), new ServiceLocator($tagged)); + return new CollectionProvider($app->make(Pagination::class), new LinksHandler($app, $app->make(ResourceMetadataCollectionFactoryInterface::class)), $app->tagged(QueryExtensionInterface::class), new ServiceLocator($tagged)); }); $this->app->tag([ItemProvider::class, CollectionProvider::class], ProviderInterface::class); @@ -954,7 +954,7 @@ public function register(): void $app->make(ResourceMetadataCollectionFactoryInterface::class), $app->make(ResourceAccessCheckerInterface::class), null - // $app->make(TagCollectorInterface::class), + // $app->make(TagCollectorInterface::class), ); }); diff --git a/src/Laravel/Eloquent/State/LinksHandler.php b/src/Laravel/Eloquent/State/LinksHandler.php index 32256ab717c..559a506a5e7 100644 --- a/src/Laravel/Eloquent/State/LinksHandler.php +++ b/src/Laravel/Eloquent/State/LinksHandler.php @@ -13,7 +13,12 @@ namespace ApiPlatform\Laravel\Eloquent\State; +use ApiPlatform\Metadata\Exception\OperationNotFoundException; +use ApiPlatform\Metadata\GraphQl\Operation; +use ApiPlatform\Metadata\GraphQl\Query; use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use Illuminate\Contracts\Foundation\Application; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; @@ -25,6 +30,7 @@ final class LinksHandler implements LinksHandlerInterface { public function __construct( private readonly Application $application, + private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, ) { } @@ -34,27 +40,72 @@ public function handleLinks(Builder $builder, array $uriVariables, array $contex if ($operation instanceof HttpOperation) { foreach (array_reverse($operation->getUriVariables() ?? []) as $uriVariable => $link) { - $identifier = $uriVariables[$uriVariable]; + $builder = $this->buildQuery($builder, $link, $uriVariables[$uriVariable]); + } - if ($to = $link->getToProperty()) { - $builder = $builder->where($builder->getModel()->{$to}()->getQualifiedForeignKeyName(), $identifier); + return $builder; + } - continue; - } + if (!($linkClass = $context['linkClass'] ?? false)) { + return $builder; + } - if ($from = $link->getFromProperty()) { - $relation = $this->application->make($link->getFromClass()); - $builder = $builder->getModel()->where($relation->{$from}()->getQualifiedForeignKeyName(), $identifier); + $newLink = null; + $linkedOperation = null; + $linkProperty = $context['linkProperty'] ?? null; - continue; + try { + $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($linkClass); + $linkedOperation = $resourceMetadataCollection->getOperation($operation->getName()); + } catch (OperationNotFoundException) { + // Instead, we'll look for the first Query available. + foreach ($resourceMetadataCollection as $resourceMetadata) { + foreach ($resourceMetadata->getGraphQlOperations() as $op) { + if ($op instanceof Query) { + $linkedOperation = $op; + } } + } + } + + if (!$linkedOperation instanceof Operation) { + return $builder; + } - $builder->where($builder->getModel()->qualifyColumn($link->getIdentifiers()[0]), $identifier); + $resourceClass = $builder->getModel()::class; + foreach ($linkedOperation->getLinks() ?? [] as $link) { + if ($resourceClass === $link->getToClass() && $linkProperty === $link->getFromProperty()) { + $newLink = $link; + break; } + } + if (!$newLink) { return $builder; } - return $builder; + return $this->buildQuery($builder, $newLink, $uriVariables[$newLink->getIdentifiers()[0]]); + } + + /** + * @param Builder $builder + * + * @throws \Illuminate\Contracts\Container\BindingResolutionException + * + * @return Builder $builder + */ + private function buildQuery(Builder $builder, Link $link, mixed $identifier): Builder + { + if ($to = $link->getToProperty()) { + return $builder->where($builder->getModel()->{$to}()->getQualifiedForeignKeyName(), $identifier); + } + + if ($from = $link->getFromProperty()) { + $relation = $this->application->make($link->getFromClass()); + + return $builder->getModel()->where($relation->{$from}()->getQualifiedForeignKeyName(), $identifier); + } + + return $builder->where($builder->getModel()->qualifyColumn($link->getIdentifiers()[0]), $identifier); } } diff --git a/src/Metadata/Resource/ResourceMetadataCollection.php b/src/Metadata/Resource/ResourceMetadataCollection.php index e6c0523eff5..5ed151383ad 100644 --- a/src/Metadata/Resource/ResourceMetadataCollection.php +++ b/src/Metadata/Resource/ResourceMetadataCollection.php @@ -16,6 +16,7 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\CollectionOperationInterface; use ApiPlatform\Metadata\Exception\OperationNotFoundException; +use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation; use ApiPlatform\Metadata\Operation; /** @@ -35,7 +36,7 @@ public function __construct(private readonly string $resourceClass, array $input parent::__construct($input); } - public function getOperation(?string $operationName = null, bool $forceCollection = false, bool $httpOperation = false, bool $forceGraphQl = false): Operation + public function getOperation(?string $operationName = null, bool $forceCollection = false, bool $httpOperation = false, bool $forceGraphQl = false): Operation|GraphQlOperation { $operationName ??= ''; $cachePrefix = ($forceCollection ? self::FORCE_COLLECTION : '').($httpOperation ? self::HTTP_OPERATION : '');