| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198 |
- <?php
- declare(strict_types=1);
- namespace Webkul\BagistoApi\State;
- use ApiPlatform\Laravel\Eloquent\State\LinksHandlerInterface;
- use ApiPlatform\Metadata\Exception\OperationNotFoundException;
- use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation;
- use ApiPlatform\Metadata\GraphQl\Query;
- 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;
- use Illuminate\Database\Eloquent\Relations\BelongsTo;
- use Illuminate\Database\Eloquent\Relations\BelongsToMany;
- use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
- use Illuminate\Database\Eloquent\Relations\MorphTo;
- use Illuminate\Database\Eloquent\Relations\Relation;
- use Illuminate\Support\Str;
- use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
- /**
- * Fixes an ApiPlatform Laravel adapter bug where GraphQL sub-collections
- * return every row in the child table regardless of the parent.
- *
- * Root cause:
- * - ApiPlatform\Metadata\Resource\Factory\LinkFactory::createLinksFromRelations
- * uses snake_case Eloquent property names ('flexible_variants') for each
- * auto-discovered Link's $fromProperty.
- * - ApiPlatform\GraphQl\State\Provider\ReadProvider writes the raw GraphQL
- * field name ($info->fieldName, e.g. 'flexibleVariants') as
- * $context['linkProperty'] without applying any name converter.
- * - The default LinksHandler compares $linkProperty strictly against
- * $link->getFromProperty(); the camelCase/snake_case mismatch always
- * fails, so no WHERE clause is added and the CollectionProvider streams
- * the entire table back to the client.
- *
- * This handler replays the vendor logic but also matches Links by their
- * snake_case normalization, so 'flexibleVariants' finds the auto-generated
- * 'flexible_variants' Link and the relation is followed correctly.
- *
- * @implements LinksHandlerInterface<Model>
- */
- final class CamelCaseAwareLinksHandler implements LinksHandlerInterface
- {
- public function __construct(
- private readonly Application $application,
- private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory,
- ) {
- }
- public function handleLinks(Builder $builder, array $uriVariables, array $context): Builder
- {
- $operation = $context['operation'] ?? null;
- if (! ($linkClass = $context['linkClass'] ?? false) || ! $operation) {
- return $builder;
- }
- $linkProperty = $context['linkProperty'] ?? null;
- if (null === $linkProperty) {
- return $builder;
- }
- $linkedOperation = null;
- try {
- $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($linkClass);
- $linkedOperation = $resourceMetadataCollection->getOperation($operation->getName());
- } catch (OperationNotFoundException) {
- foreach ($resourceMetadataCollection as $resourceMetadata) {
- foreach ($resourceMetadata->getGraphQlOperations() as $op) {
- if ($op instanceof Query) {
- $linkedOperation = $op;
- }
- }
- }
- }
- if (! $linkedOperation instanceof GraphQlOperation) {
- return $builder;
- }
- $resourceClass = $builder->getModel()::class;
- $linkPropertySnake = Str::snake($linkProperty);
- $newLink = null;
- foreach ($linkedOperation->getLinks() ?? [] as $link) {
- if ($resourceClass !== $link->getToClass()) {
- continue;
- }
- $from = $link->getFromProperty();
- if ($from === $linkProperty || $from === $linkPropertySnake || Str::snake((string) $from) === $linkPropertySnake) {
- $newLink = $link;
- break;
- }
- }
- if (! $newLink) {
- return $builder;
- }
- $identifierKey = $newLink->getIdentifiers()[0] ?? null;
- if (! $identifierKey || ! array_key_exists($identifierKey, $uriVariables)) {
- return $builder;
- }
- return $this->buildQuery($builder, $newLink, $uriVariables[$identifierKey]);
- }
- /**
- * Mirrors the private LinksHandler::buildQuery from the vendor so that
- * callers get the same behaviour as the default handler once the link
- * has been resolved.
- *
- * @param Builder<Model> $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()) {
- /** @var Model $relatedInstance */
- $relatedInstance = $this->application->make($link->getFromClass());
- $identifierField = $link->getIdentifiers()[0];
- if ($identifierField !== $relatedInstance->getKeyName()) {
- $relatedInstance = $relatedInstance
- ->newQuery()
- ->where($identifierField, $identifier)
- ->first();
- } else {
- $relatedInstance->setAttribute($identifierField, $identifier);
- $relatedInstance->exists = true;
- }
- if (! $relatedInstance) {
- throw new NotFoundHttpException('Not Found');
- }
- // Resolve the actual method name on the related model. The Link's
- // $fromProperty is typically snake_case (Eloquent convention) but
- // GraphQL fieldnames (and therefore user-facing names) are
- // camelCase. Try both.
- $candidates = array_unique([$from, Str::camel($from)]);
- $relation = null;
- foreach ($candidates as $candidate) {
- if (method_exists($relatedInstance, $candidate)) {
- $relation = $relatedInstance->{$candidate}();
- if ($relation instanceof Relation) {
- break;
- }
- $relation = null;
- }
- }
- if (! $relation instanceof Relation) {
- return $builder;
- }
- if ($relation instanceof MorphTo) {
- return $builder;
- }
- if ($relation instanceof BelongsTo) {
- return $builder->getModel()
- ->join(
- $relation->getParent()->getTable(),
- $relation->getParent()->getQualifiedKeyName(),
- $identifier
- );
- }
- if ($relation instanceof HasOneOrMany || $relation instanceof BelongsToMany) {
- return $relation->getQuery();
- }
- if (method_exists($relation, 'getQualifiedForeignKeyName')) {
- return $relation->getQuery()->where(
- $relation->getQualifiedForeignKeyName(),
- $identifier
- );
- }
- return $builder;
- }
- return $builder->where(
- $builder->getModel()->qualifyColumn($link->getIdentifiers()[0]),
- $identifier
- );
- }
- }
|