api-platform / core

The server component of API Platform: hypermedia and GraphQL APIs in minutes
https://api-platform.com
MIT License
2.45k stars 875 forks source link

LinksHandler throwing: The class "\App\Entity\xxx" cannot be retrieved from "\App\ApiResource\xxx". When using stateOptions with stateOption entityClass and Collections - GraphQL Query #6590

Open KaiGrassnick opened 2 months ago

KaiGrassnick commented 2 months ago

API Platform version(s) affected: 3.3.12

Description
When converting from Entity ApiResource to DTOs with ApiResource and querying an API Resource which contains a OneToMany Relationship, an error is thrown by the LinksHandlerTrait which states: The class "\App\Entity\xxx" cannot be retrieved from "\App\ApiResource\xxx" which is unexpected, as the Entity should not be retrieved from the DTO Class. Note: This works fine using the Entity ApiResource.

Comparing the Processed Data inside the File: api/vendor/api-platform/core/src/Doctrine/Common/State/LinksHandlerTrait.php at Line 80 - 85 i could see, that the Handler is fed with resourceClass \App\Entity\xxx which is compared to \Api\Resource\xxx and obviously fails. Debugging the same Data when defining the API Resource above an Entity i could see, that \App\Entity\xxx was compared against \App\Entity\xxx which does obviously work.

How to reproduce

See my example Repository: https://github.com/KaiGrassnick/api-platform3-graphql-one-to-many-issue

Otherwise:

  1. Create an API Platform Project, add GraphQL Dependency (webonyx/graphql-php)
  2. Create 2 Entities and reference them One to Many
  3. Create 2 API Resources with stateOptions referring to the Entity ( example below )
  4. Run Query on the Resource which should contain the Array of the referenced Resources
    {
    serverApiResources {
    edges {
      node {
        name
        ips {
          edges {
            node {
              ip
              public
            }
          }
        }
      }
    }
    }
    }

Example API Resources:

#[ApiResource(
    graphQlOperations: [
        new Query(),
        new QueryCollection()
    ],
    provider: EntityClassDtoStateProvider::class,
    processor: EntityClassDtoStateProcessor::class,
    stateOptions: new Options(entityClass: ServerEntity::class),
)]
class ServerApiResource
{
    #[ApiProperty(readable: false, writable: false, identifier: true)]
    public ?int $id = null;

    public string $name;

    /**
     * @var IpApiResource[]
     */
    public array $ips = [];

    public function addIp(IpApiResource $ip): void
    {
        $this->ips[] = $ip;
    }

    public function removeIp(IpApiResource $ip): void
    {
        unset($this->ips[array_search($ip, $this->ips)]);
    }
}
#[ApiResource(
    graphQlOperations: [
        new Query(),
        new QueryCollection()
    ],
    provider: EntityClassDtoStateProvider::class,
    processor: EntityClassDtoStateProcessor::class,
    stateOptions: new Options(entityClass: IpEntity::class),
)]
class IpApiResource
{
    #[ApiProperty(readable: true, writable: false, identifier: true)]
    public ?int $id = null;

    public string $ip;

    public bool $public;
}

Possible Solution I've looked into the LinksHandlerTrait File (api/vendor/api-platform/core/src/Doctrine/Common/State/LinksHandlerTrait.php), it seems that we could replace the $resourceClass ( which seems to be populated with the Entity Class ) with $context['resource_class']. If we do that, the query works just fine, but so far i have not checked any side effects regarding mutations etc.

Additional Context

Error Output:

{
  "errors": [
    {
      "message": "The class \"App\\Entity\\IpEntity\" cannot be retrieved from \"App\\ApiResource\\ServerApiResource\".",
      "locations": [
        {
          "line": 11,
          "column": 9
        }
      ],
      "path": [
        "serverApiResources",
        "edges",
        0,
        "node",
        "ips"
      ],
      "extensions": {
        "debugMessage": "The class \"App\\Entity\\IpEntity\" cannot be retrieved from \"App\\ApiResource\\ServerApiResource\".",
        "file": "/app/vendor/api-platform/core/src/Doctrine/Common/State/LinksHandlerTrait.php",
        "line": 88,
        "trace": [
          {
            "file": "/app/vendor/api-platform/core/src/Doctrine/Orm/State/LinksHandlerTrait.php",
            "line": 43,
            "call": "ApiPlatform\\Doctrine\\Orm\\State\\LinksHandler::getLinks('App\\Entity\\IpEntity', instance of ApiPlatform\\Metadata\\GraphQl\\QueryCollection, array(14))"
          },
          {
            "file": "/app/vendor/api-platform/core/src/Doctrine/Orm/State/LinksHandler.php",
            "line": 35,
            "call": "ApiPlatform\\Doctrine\\Orm\\State\\LinksHandler::handle(instance of Doctrine\\ORM\\QueryBuilder, array(1), instance of ApiPlatform\\Doctrine\\Orm\\Util\\QueryNameGenerator, array(14), 'App\\Entity\\IpEntity', instance of ApiPlatform\\Metadata\\GraphQl\\QueryCollection)"
          },
          {
            "file": "/app/vendor/api-platform/core/src/Doctrine/Orm/State/CollectionProvider.php",
            "line": 68,
            "call": "ApiPlatform\\Doctrine\\Orm\\State\\LinksHandler::handleLinks(instance of Doctrine\\ORM\\QueryBuilder, array(1), instance of ApiPlatform\\Doctrine\\Orm\\Util\\QueryNameGenerator, array(14))"
          },
          {
            "file": "/app/src/State/EntityClassDtoStateProvider.php",
            "line": 30,
            "call": "ApiPlatform\\Doctrine\\Orm\\State\\CollectionProvider::provide(instance of ApiPlatform\\Metadata\\GraphQl\\QueryCollection, array(1), array(13))"
          },
          {
            "file": "/app/vendor/api-platform/core/src/State/CallableProvider.php",
            "line": 43,
            "call": "App\\State\\EntityClassDtoStateProvider::provide(instance of ApiPlatform\\Metadata\\GraphQl\\QueryCollection, array(1), array(13))"
          },
          {
            "file": "/app/vendor/api-platform/core/src/GraphQl/State/Provider/ReadProvider.php",
            "line": 114,
            "call": "ApiPlatform\\State\\CallableProvider::provide(instance of ApiPlatform\\Metadata\\GraphQl\\QueryCollection, array(1), array(13))"
          },
          {
            "file": "/app/vendor/api-platform/core/src/Symfony/Security/State/AccessCheckerProvider.php",
            "line": 62,
            "call": "ApiPlatform\\GraphQl\\State\\Provider\\ReadProvider::provide(instance of ApiPlatform\\Metadata\\GraphQl\\QueryCollection, array(1), array(13))"
          },
          {
            "file": "/app/vendor/api-platform/core/src/State/Provider/ParameterProvider.php",
            "line": 99,
            "call": "ApiPlatform\\Symfony\\Security\\State\\AccessCheckerProvider::provide(instance of ApiPlatform\\Metadata\\GraphQl\\QueryCollection, array(0), array(6))"
          },
          {
            "file": "/app/vendor/api-platform/core/src/GraphQl/State/Provider/DenormalizeProvider.php",
            "line": 38,
            "call": "ApiPlatform\\State\\Provider\\ParameterProvider::provide(instance of ApiPlatform\\Metadata\\GraphQl\\QueryCollection, array(0), array(6))"
          },
          {
            "file": "/app/vendor/api-platform/core/src/Symfony/Security/State/AccessCheckerProvider.php",
            "line": 62,
            "call": "ApiPlatform\\GraphQl\\State\\Provider\\DenormalizeProvider::provide(instance of ApiPlatform\\Metadata\\GraphQl\\QueryCollection, array(0), array(5))"
          },
          {
            "file": "/app/vendor/api-platform/core/src/Symfony/Validator/State/ValidateProvider.php",
            "line": 32,
            "call": "ApiPlatform\\Symfony\\Security\\State\\AccessCheckerProvider::provide(instance of ApiPlatform\\Metadata\\GraphQl\\QueryCollection, array(0), array(5))"
          },
          {
            "file": "/app/vendor/api-platform/core/src/Symfony/Security/State/AccessCheckerProvider.php",
            "line": 62,
            "call": "ApiPlatform\\Symfony\\Validator\\State\\ValidateProvider::provide(instance of ApiPlatform\\Metadata\\GraphQl\\QueryCollection, array(0), array(5))"
          },
          {
            "file": "/app/vendor/api-platform/core/src/GraphQl/State/Provider/ResolverProvider.php",
            "line": 36,
            "call": "ApiPlatform\\Symfony\\Security\\State\\AccessCheckerProvider::provide(instance of ApiPlatform\\Metadata\\GraphQl\\QueryCollection, array(0), array(5))"
          },
          {
            "file": "/app/vendor/api-platform/core/src/Symfony/Validator/State/ValidateProvider.php",
            "line": 32,
            "call": "ApiPlatform\\GraphQl\\State\\Provider\\ResolverProvider::provide(instance of ApiPlatform\\Metadata\\GraphQl\\QueryCollection, array(0), array(5))"
          },
          {
            "file": "/app/vendor/api-platform/core/src/Symfony/Security/State/AccessCheckerProvider.php",
            "line": 62,
            "call": "ApiPlatform\\Symfony\\Validator\\State\\ValidateProvider::provide(instance of ApiPlatform\\Metadata\\GraphQl\\QueryCollection, array(0), array(5))"
          },
          {
            "file": "/app/vendor/api-platform/core/src/GraphQl/Resolver/Factory/ResolverFactory.php",
            "line": 82,
            "call": "ApiPlatform\\Symfony\\Security\\State\\AccessCheckerProvider::provide(instance of ApiPlatform\\Metadata\\GraphQl\\QueryCollection, array(0), array(5))"
          },
          {
            "file": "/app/vendor/api-platform/core/src/GraphQl/Resolver/Factory/ResolverFactory.php",
            "line": 66,
            "call": "ApiPlatform\\GraphQl\\Resolver\\Factory\\ResolverFactory::resolve(array(6), array(0), instance of GraphQL\\Type\\Definition\\ResolveInfo, 'App\\ApiResource\\ServerApiResource', instance of ApiPlatform\\Metadata\\GraphQl\\QueryCollection, null)"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
            "line": 737,
            "call": "ApiPlatform\\GraphQl\\Resolver\\Factory\\ResolverFactory::ApiPlatform\\GraphQl\\Resolver\\Factory\\{closure}(array(6), array(0), null, instance of GraphQL\\Type\\Definition\\ResolveInfo)"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
            "line": 653,
            "call": "GraphQL\\Executor\\ReferenceExecutor::resolveFieldValueOrError(instance of GraphQL\\Type\\Definition\\FieldDefinition, instance of GraphQL\\Language\\AST\\FieldNode, instance of Closure, array(6), instance of GraphQL\\Type\\Definition\\ResolveInfo, null)"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
            "line": 1361,
            "call": "GraphQL\\Executor\\ReferenceExecutor::resolveField(GraphQLType: ServerApiResource, array(6), instance of ArrayObject(1), 'ips', array(5), array(5), null)"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
            "line": 1301,
            "call": "GraphQL\\Executor\\ReferenceExecutor::executeFields(GraphQLType: ServerApiResource, array(6), array(4), array(4), instance of ArrayObject(4), null)"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
            "line": 1252,
            "call": "GraphQL\\Executor\\ReferenceExecutor::collectAndExecuteSubfields(GraphQLType: ServerApiResource, instance of ArrayObject(1), array(4), array(4), array(6), null)"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
            "line": 922,
            "call": "GraphQL\\Executor\\ReferenceExecutor::completeObjectValue(GraphQLType: ServerApiResource, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(4), array(4), array(6), null)"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
            "line": 777,
            "call": "GraphQL\\Executor\\ReferenceExecutor::completeValue(GraphQLType: ServerApiResource, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(4), array(4), array(6), null)"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
            "line": 662,
            "call": "GraphQL\\Executor\\ReferenceExecutor::completeValueCatchingError(GraphQLType: ServerApiResource, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(4), array(4), array(6), null)"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
            "line": 1361,
            "call": "GraphQL\\Executor\\ReferenceExecutor::resolveField(GraphQLType: ServerApiResourceEdge, array(1), instance of ArrayObject(1), 'node', array(4), array(4), null)"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
            "line": 1301,
            "call": "GraphQL\\Executor\\ReferenceExecutor::executeFields(GraphQLType: ServerApiResourceEdge, array(1), array(3), array(3), instance of ArrayObject(1), null)"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
            "line": 1252,
            "call": "GraphQL\\Executor\\ReferenceExecutor::collectAndExecuteSubfields(GraphQLType: ServerApiResourceEdge, instance of ArrayObject(1), array(3), array(3), array(1), null)"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
            "line": 922,
            "call": "GraphQL\\Executor\\ReferenceExecutor::completeObjectValue(GraphQLType: ServerApiResourceEdge, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(3), array(3), array(1), null)"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
            "line": 777,
            "call": "GraphQL\\Executor\\ReferenceExecutor::completeValue(GraphQLType: ServerApiResourceEdge, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(3), array(3), array(1), null)"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
            "line": 1019,
            "call": "GraphQL\\Executor\\ReferenceExecutor::completeValueCatchingError(GraphQLType: ServerApiResourceEdge, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(3), array(3), array(1), null)"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
            "line": 900,
            "call": "GraphQL\\Executor\\ReferenceExecutor::completeListValue(GraphQLType: [ServerApiResourceEdge], instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(2), array(2), array(1), null)"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
            "line": 777,
            "call": "GraphQL\\Executor\\ReferenceExecutor::completeValue(GraphQLType: [ServerApiResourceEdge], instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(2), array(2), array(1), null)"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
            "line": 662,
            "call": "GraphQL\\Executor\\ReferenceExecutor::completeValueCatchingError(GraphQLType: [ServerApiResourceEdge], instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(2), array(2), array(1), null)"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
            "line": 1361,
            "call": "GraphQL\\Executor\\ReferenceExecutor::resolveField(GraphQLType: ServerApiResourceCursorConnection, array(1), instance of ArrayObject(1), 'edges', array(2), array(2), null)"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
            "line": 1301,
            "call": "GraphQL\\Executor\\ReferenceExecutor::executeFields(GraphQLType: ServerApiResourceCursorConnection, array(1), array(1), array(1), instance of ArrayObject(1), null)"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
            "line": 1252,
            "call": "GraphQL\\Executor\\ReferenceExecutor::collectAndExecuteSubfields(GraphQLType: ServerApiResourceCursorConnection, instance of ArrayObject(1), array(1), array(1), array(1), null)"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
            "line": 922,
            "call": "GraphQL\\Executor\\ReferenceExecutor::completeObjectValue(GraphQLType: ServerApiResourceCursorConnection, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(1), array(1), array(1), null)"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
            "line": 777,
            "call": "GraphQL\\Executor\\ReferenceExecutor::completeValue(GraphQLType: ServerApiResourceCursorConnection, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(1), array(1), array(1), null)"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
            "line": 662,
            "call": "GraphQL\\Executor\\ReferenceExecutor::completeValueCatchingError(GraphQLType: ServerApiResourceCursorConnection, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(1), array(1), array(1), null)"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
            "line": 1361,
            "call": "GraphQL\\Executor\\ReferenceExecutor::resolveField(GraphQLType: Query, null, instance of ArrayObject(1), 'serverApiResources', array(1), array(1), null)"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
            "line": 299,
            "call": "GraphQL\\Executor\\ReferenceExecutor::executeFields(GraphQLType: Query, null, array(0), array(0), instance of ArrayObject(1), null)"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php",
            "line": 237,
            "call": "GraphQL\\Executor\\ReferenceExecutor::executeOperation(instance of GraphQL\\Language\\AST\\OperationDefinitionNode, null)"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/Executor/Executor.php",
            "line": 159,
            "call": "GraphQL\\Executor\\ReferenceExecutor::doExecute()"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/GraphQL.php",
            "line": 162,
            "call": "GraphQL\\Executor\\Executor::promiseToExecute(instance of GraphQL\\Executor\\Promise\\Adapter\\SyncPromiseAdapter, instance of GraphQL\\Type\\Schema, instance of GraphQL\\Language\\AST\\DocumentNode, null, null, array(0), null, null)"
          },
          {
            "file": "/app/vendor/webonyx/graphql-php/src/GraphQL.php",
            "line": 96,
            "call": "GraphQL\\GraphQL::promiseToExecute(instance of GraphQL\\Executor\\Promise\\Adapter\\SyncPromiseAdapter, instance of GraphQL\\Type\\Schema, '{\n  serverApiResources {\n    edges {\n      node {\n        name\n        memory\n        architecture {\n          id\n          name\n        }\n        ips {\n          edges {\n            node {\n              ip\n              public\n            }\n          }\n        }\n      }\n    }\n  }\n}', null, null, array(0), null, null, null)"
          },
          {
            "file": "/app/vendor/api-platform/core/src/GraphQl/Executor.php",
            "line": 43,
            "call": "GraphQL\\GraphQL::executeQuery(instance of GraphQL\\Type\\Schema, '{\n  serverApiResources {\n    edges {\n      node {\n        name\n        memory\n        architecture {\n          id\n          name\n        }\n        ips {\n          edges {\n            node {\n              ip\n              public\n            }\n          }\n        }\n      }\n    }\n  }\n}', null, null, array(0), null, null, null)"
          },
          {
            "file": "/app/vendor/api-platform/core/src/GraphQl/Action/EntrypointAction.php",
            "line": 79,
            "call": "ApiPlatform\\GraphQl\\Executor::executeQuery(instance of GraphQL\\Type\\Schema, '{\n  serverApiResources {\n    edges {\n      node {\n        name\n        memory\n        architecture {\n          id\n          name\n        }\n        ips {\n          edges {\n            node {\n              ip\n              public\n            }\n          }\n        }\n      }\n    }\n  }\n}', null, null, array(0), null)"
          },
          {
            "file": "/app/vendor/symfony/http-kernel/HttpKernel.php",
            "line": 181,
            "call": "ApiPlatform\\GraphQl\\Action\\EntrypointAction::__invoke(instance of Symfony\\Component\\HttpFoundation\\Request)"
          },
          {
            "file": "/app/vendor/symfony/http-kernel/HttpKernel.php",
            "line": 76,
            "call": "Symfony\\Component\\HttpKernel\\HttpKernel::handleRaw(instance of Symfony\\Component\\HttpFoundation\\Request, 1)"
          },
          {
            "file": "/app/vendor/symfony/http-kernel/Kernel.php",
            "line": 197,
            "call": "Symfony\\Component\\HttpKernel\\HttpKernel::handle(instance of Symfony\\Component\\HttpFoundation\\Request, 1, true)"
          },
          {
            "file": "/app/vendor/symfony/runtime/Runner/Symfony/HttpKernelRunner.php",
            "line": 35,
            "call": "Symfony\\Component\\HttpKernel\\Kernel::handle(instance of Symfony\\Component\\HttpFoundation\\Request)"
          },
          {
            "file": "/app/vendor/autoload_runtime.php",
            "line": 29,
            "call": "Symfony\\Component\\Runtime\\Runner\\Symfony\\HttpKernelRunner::run()"
          },
          {
            "file": "/app/public/index.php",
            "line": 5,
            "function": "require_once('/app/vendor/autoload_runtime.php')"
          }
        ]
      }
    }
  ],
  "data": {
    "serverApiResources": {
      "edges": [
        {
          "node": {
            "name": "server1",
            "memory": 1024,
            "architecture": {
              "id": "/architecture_api_resources/1",
              "name": "x86"
            },
            "ips": null
          }
        }
      ]
    }
  }
stale[bot] commented 1 week ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.