-
-
Notifications
You must be signed in to change notification settings - Fork 929
GraphQL Query support #1358
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
GraphQL Query support #1358
Conversation
@@ -142,6 +143,7 @@ private function handleConfig(ContainerBuilder $container, array $config, array | |||
$container->setParameter('api_platform.eager_loading.max_joins', $config['eager_loading']['max_joins']); | |||
$container->setParameter('api_platform.eager_loading.fetch_partial', $config['eager_loading']['fetch_partial']); | |||
$container->setParameter('api_platform.eager_loading.force_eager', $config['eager_loading']['force_eager']); | |||
$container->setParameter('api_platform.enable_graphql', $config['enable_graphql']); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer:
graphdql:
enabled: true
So that we try not to face the same issue as with swagger when adding more features.
composer.json
Outdated
@@ -23,7 +23,8 @@ | |||
"symfony/property-access": "^2.7 || ^3.0 || ^4.0", | |||
"symfony/property-info": "^3.1 || ^4.0", | |||
"symfony/serializer": "^3.1 || ^4.0", | |||
"willdurand/negotiation": "^2.0.3" | |||
"willdurand/negotiation": "^2.0.3", | |||
"webonyx/graphql-php": "^0.10.2" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This dep should be moved in require-dev
and added to the suggest
section.
@@ -253,7 +253,7 @@ private function getSerializerContext(string $resourceClass, string $contextType | |||
$request = $this->requestStack->getCurrentRequest(); | |||
} | |||
|
|||
if (null !== $this->serializerContextBuilder && null !== $request) { | |||
if (null !== $this->serializerContextBuilder && null !== $request && 'api_graphql_entrypoint' !== $request->attributes->get('_route')) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest to check for the application/graphql
MIME type instead. It will be more reliable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Forget about that, content-type can also be application/json
, so it's a bad idea. I'm still not a found of checking the route name (it creates a hard coupling with the Symfony Routing component).
Maybe can you just set a new _graphql
attributes to true in the action, and check it here. It will be easier for end users to hook into this.
|
||
public function __construct(ManagerRegistry $managerRegistry, RequestStack $requestStack, ResourceMetadataFactoryInterface $resourceMetadataFactory, bool $enabled = true, bool $clientEnabled = false, bool $clientItemsPerPage = false, int $itemsPerPage = 30, string $pageParameterName = 'page', string $enabledParameterName = 'pagination', string $itemsPerPageParameterName = 'itemsPerPage', int $maximumItemPerPage = null) | ||
public function __construct(ManagerRegistry $managerRegistry, RequestStack $requestStack, ResourceMetadataFactoryInterface $resourceMetadataFactory, bool $enabled = true, bool $clientEnabled = false, bool $clientItemsPerPage = false, int $itemsPerPage = 30, string $pageParameterName = 'page', string $enabledParameterName = 'pagination', string $itemsPerPageParameterName = 'itemsPerPage', int $maximumItemPerPage = null, bool $enableGraphql) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You must set a default value (false
?) to $enableGraphql
or it's a BC break.
@@ -76,6 +78,10 @@ public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGenerator | |||
} | |||
|
|||
$itemsPerPage = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_items_per_page', $this->itemsPerPage, true); | |||
if ($this->enableGraphql) { | |||
// @TODO Retrieve GraphQL 'first' argument using the request? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe can you create another PaginationExtension
class specific to GraphQL if the handling is really different than this one.
src/Bridge/Graphql/Executor.php
Outdated
* | ||
* @author Alan Poulain <contact@alanpoulain.eu> | ||
*/ | ||
class Executor |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
final
continue; | ||
} | ||
|
||
// @TODO Manage multiple identifiers? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe can you use just reuse this method: https://github.com/api-platform/core/blob/master/src/Bridge/Doctrine/Orm/Util/IdentifierManagerTrait.php#L40
$graphqlType = $this->getResourceType($className, $resourceMetadata); | ||
break; | ||
|
||
case Type::BUILTIN_TYPE_ARRAY: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can remove all those useless cases.
@@ -68,6 +68,7 @@ public function getConfigTreeBuilder() | |||
->booleanNode('enable_nelmio_api_doc')->defaultValue(false)->info('Enable the Nelmio Api doc integration.')->end() | |||
->booleanNode('enable_swagger')->defaultValue(true)->info('Enable the Swagger documentation and export.')->end() | |||
->booleanNode('enable_swagger_ui')->defaultValue(class_exists(TwigBundle::class))->info('Enable Swagger ui.')->end() | |||
->booleanNode('enable_graphql')->defaultValue(false)->info('Enable GraphQL.')->end() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest defaultValue(class_exists(GraphQL::class))
(as for Swagger UI the line before).
private $resourceClassDirectories; | ||
private $subresourceOperationFactory; | ||
|
||
public function __construct(KernelInterface $kernel, ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, OperationPathResolverInterface $operationPathResolver, ContainerInterface $container, array $formats, array $resourceClassDirectories = [], SubresourceOperationFactoryInterface $subresourceOperationFactory = null) | ||
public function __construct(KernelInterface $kernel, ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, OperationPathResolverInterface $operationPathResolver, ContainerInterface $container, array $formats, bool $enableGraphql, array $resourceClassDirectories = [], SubresourceOperationFactoryInterface $subresourceOperationFactory = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bool $enableGraphql
must be the last parameter and have a default value or it's a BC break.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you explain why? It seems to me this is an internal service. I don't see what it could break.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not marked as @internal
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some people (including me :D) use API Platform as a reusable library in other contexts (Silex, Laravel...). This change will break this class of uses.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK thanks for the explanation.
'resolve' => function ($root, $args, $context, ResolveInfo $resolveInfo) use ($resource, $operationName) { | ||
// @TODO Pagination | ||
$collection = $this->collectionDataProvider->getCollection($resource, $operationName); | ||
return $this->normalizer->normalize($collection, null, ['graphql_selected_fields' => $resolveInfo->getFieldSelection()]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cant' we use symfony/symfony#18834 instead of graphql_selected_fields
? I've introduced this option exactly for GraphQL support :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can replace graphql_selected_fields
by attributes
, yes. But it will not change the initial issue: to replace the generated URI (call to normalizeRelation
) by the subresource data.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not even sure this is a good idea. If I replace it, it will have consequences when using attributes
in a REST context. See its use here: https://github.com/alanpoulain/core/blob/2ed4e8c54ac8579857fc401ad32b18ece0f962a4/src/Serializer/AbstractItemNormalizer.php#L416
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, keep it as is for now :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using attributes
is not useful in this context because the fields are filtered by graphql-php. Maybe it can improves performance though?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will definitely improve performances, serialization is a costly operation, we should definitely prevent serializing useless data.
I'm wondering if we should not totally separate our current (REST) "operations" subsystem from GraphQL. They are not the same concept and aren't really compatible. CRUD fit well with REST (resource based API), but I don't think it fits well with GraphQL (more service oriented). WDYT @alanpoulain @raoulclais @api-platform/core-team? |
On the other end, it should be easy to provide basic CRUD mutations for GraphQL... Maybe can we just add a new option to operations to define if it's a rest operation, a graphql mutation, or both. |
$request = $this->requestStack->getCurrentRequest(); | ||
|
||
// @TODO Manage GET request. | ||
$input = json_decode($request->getContent(), true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should check that the incoming Content-Type
is application/json
or application/graphql
:
- if it's
application/json
, the current behavior is OK - if it's
application/graphql
, the raw content is a GraphQL query (so nojson_decode
needed) - throw an error for all other content types (maybe can you also serve https://github.com/graphql/graphiql if the content type is
text/html
)?
@dunglas in my experience what I find painfully laborious with GraphQL is to re-define those crudish operations. It's not like you need all the CRUD endpoints all the time, even with a REST API, but they are a PITA to re-define again and again. Another thing that API Platform nailed right is to provide a standardised structure whereas with a GraphQL API, you tend to have a bit of everything simply because there is no standard "enforced". |
d39539d
to
63e6fe6
Compare
Watch out. the server part of GraphQL has a patent on it! I don't care for software patents as I'm from EU, but others using this inside US should be careful. |
We must indeed make that clear in the documentation. And it's why it will be an optional disablable feature. |
7c05c1d
to
f1212fe
Compare
class GraphqlContext implements Context | ||
{ | ||
/** | ||
* @var \Behatch\Context\RestContext |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you import the class and use its short name instead?
|
||
public function __construct(RequestStack $requestStack, SchemaBuilderInterface $schemaBuilder, ExecutorInterface $executor) | ||
{ | ||
$this->requestStack = $requestStack; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is useless, just type hint Request
in your __invoke()
method definition:
public function __invoke(Request $request): Response
$this->executor = $executor; | ||
} | ||
|
||
public function __invoke(): JsonResponse |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Response
instead of JsonResponse
.
@@ -76,6 +78,10 @@ public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGenerator | |||
} | |||
|
|||
$itemsPerPage = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_items_per_page', $this->itemsPerPage, true); | |||
if ($this->graphqlEnabled) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be $request->attributes->get('_graphql')
, nop?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes... Nice catch!
src/Bridge/Graphql/Executor.php
Outdated
*/ | ||
public function executeQuery(...$args): ExecutionResult | ||
{ | ||
return call_user_func_array(GraphQL::class.'::executeQuery', $args); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return GraphQL::executeQuery(...$args);
$request = $this->requestStack->getCurrentRequest(); | ||
$collectionArgs = $request->attributes->get('_graphql_collections_args', []); | ||
$collectionArgs[$resourceClass] = $args; | ||
$request->attributes->set('_graphql_collections_args', $collectionArgs); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$request->attributes->set('_graphql_collections_args', [$resourceClass => $args] + $request->attributes->get('_graphql_collections_args', []));
{ | ||
return function ($root, $args, $context, ResolveInfo $resolveInfo) use ($resourceClass, $operationName) { | ||
// @TODO Pagination. | ||
$request = $this->requestStack->getCurrentRequest(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You have to test RequestStack::getCurrentRequest()
is not null.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In which cases it could occur?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When? We don't know but it could occur this is what the contract says and we have to respect the method signature!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It definitely occurs in when running this class in command line.
} | ||
$selectedFields = $resolveInfo->getFieldSelection(); | ||
|
||
return $this->normalizer->normalize($item, null, ['graphql_selected_fields' => $selectedFields]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One line:
return ($item = $itemDataProvider->getItem($resourceClass, $args['id'], $operationName))
? $normalizer->normalize($item, null, ['graphql_selected_fields' => $resolveInfo->getFieldSelection()])
: null;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sometimes, I prefer to be more verbose to avoid potential bugs and to modify more easily the code. But if it is preferred in API Platform, no problem.
f1212fe
to
ca321d8
Compare
foreach ($this->resourceNameCollectionFactory->create() as $resource) { | ||
$resourceMetadata = $this->resourceMetadataFactory->create($resource); | ||
|
||
$queryFields = array_merge($queryFields, $this->getQueryFields($resource, $resourceMetadata)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$queryFields = $this->getQueryFields($resource, $resourceMetadata) + $queryFields;
$propertyMetadata = $this->propertyMetadataFactory->create($resource, $property); | ||
|
||
$propertyType = $propertyMetadata->getType(); | ||
if (null !== $propertyType) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if ($property = $propertyMetadata->getType()) {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure @meyerbaptiste? We need an explicit check for null values (not for booleans).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even if the return type is object|null
?
null !== $property = $propertyMetadata->getType()
then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should double check.
$graphqlType = GraphQLType::string(); | ||
break; | ||
case Type::BUILTIN_TYPE_OBJECT: | ||
if (\DateTime::class === $type->getClassName()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (is_subclass_of($type->getClassName(), \DateTimeInterface::class)) {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can even nitpick with is_a($type->getClassName(), \DateTimeInterface::class, true)
*/ | ||
private function getOperations(ResourceMetadata $resourceMetadata, bool $isQuery, bool $isItem): array | ||
{ | ||
return array_filter($isItem ? $resourceMetadata->getItemOperations() : $resourceMetadata->getCollectionOperations(), function ($operation) use ($isQuery) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefer a foreach
with a generator here:
$operations = $isItem ? $resourceMetadata->getItemOperations() : $resourceMetadata->getCollectionOperations();
foreach ($operations as $operationName => $operation) {
if (isset($operation['controller']) || !isset($operation['method'])) {
continue;
}
if ($isQuery && Request::METHOD_GET !== $operation['method'] || !$isQuery && Request::METHOD_GET === $operation['method']) {
continue;
}
yield $operationName => $operation;
}
]; | ||
} | ||
|
||
throw new \LogicException(sprintf('Missing identifier field for resource "%s"', $resource)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Missing identifier field for resource \"$resource\"."
$propertyIsIdentifier = $propertyMetadata->isIdentifier() ?? false; | ||
$propertyType = $propertyMetadata->getType(); | ||
|
||
if (!$propertyIsIdentifier || null === $propertyType) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|| !$propertyType
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sructinizr (legitimately) add warnings when not checking explicitly the type when dealing with null
values. I would keep this as is.
} | ||
|
||
/** | ||
* Gets the field arguments of the identifier of a given resource. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing @throws
annotation.
} | ||
|
||
/** | ||
* Converts a built-in type to its GraphQL equivalent. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing @throws
annotation.
51b8fdd
to
7f30a56
Compare
Added: pagination support. |
2a01be4
to
9097958
Compare
private $requestStack; | ||
private $paginationEnabled; | ||
|
||
public function __construct(CollectionDataProviderInterface $collectionDataProvider, SubresourceDataProviderInterface $subresourceDataProvider, NormalizerInterface $normalizer, IdentifiersExtractorInterface $identifiersExtractor, RequestStack $requestStack, bool $paginationEnabled) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO the request should not be injected that deep. The low level GraphQL implementation should be agnostic of the transport layer (HTTP, AMQP...).
Can't we extract the offset and other data needed in the controller layer and pass the base64-encoded values to createCollectionResolver
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This resolver does not need the request to resolve the query. It's the PaginationExtension who needs to retrieve the offset and the limit.
The resolver adds the offset / limit argument of the GraphQL query to the request attributes to be used by the PaginationExtension.
if (isset($args['after'])) { | ||
$after = base64_decode($args['after'], true); | ||
$offset = (int) $after; | ||
$offset = false === $after ? $offset : ++$offset; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't we throw an error if base64_decode
returns an error?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can yes, if you prefer. I thought it was the responsibility of the client to use the cursors we give them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes but we cannot trust client input.
$data = ['edges' => [], 'pageInfo' => ['endCursor' => null, 'hasNextPage' => false]]; | ||
if ($collection instanceof PaginatorInterface) { | ||
$totalItems = $collection->getTotalItems(); | ||
if ($totalItems > 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can be merge with the previous if:
if ($collection instanceof PaginatorInterface && $totalItems = $collection->getTotalItems() > 0)
$collection = $this->collectionDataProvider->getCollection($resourceClass, $operationName); | ||
} | ||
|
||
if ($this->paginationEnabled) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would move all the following block in a new method to improve readability and decrease the cyclomatic complexity.
8da30cf
to
00ebee7
Compare
546d9e4
to
d09ee00
Compare
@@ -11,7 +11,7 @@ matrix: | |||
include: | |||
- php: '7.0' | |||
- php: '7.1' | |||
env: coverage=1 lint=1 | |||
env: coverage=0 lint=1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to restore this later, but I've no idea how to fix the problem for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the problem with the coverage?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We reach the ulimit
defined by Travis... Too many open files.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can't we ulimit -n 4000
before install? It happens on my local computer too when testing the Extension
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately no, Travis disallow to increase the default value.
73575a9
to
7a395ad
Compare
@@ -253,7 +253,7 @@ private function getSerializerContext(string $resourceClass, string $contextType | |||
$request = $this->requestStack->getCurrentRequest(); | |||
} | |||
|
|||
if (null !== $this->serializerContextBuilder && null !== $request) { | |||
if (null !== $this->serializerContextBuilder && null !== $request && !$request->attributes->get('_graphql')) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because of https://github.com/api-platform/core/blob/master/src/Serializer/SerializerContextBuilder.php#L42. There is no serialization context in the request.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't there be one? I mean, usually _api_resource_class
is declared on every route no?
Also maybe that the SerializerContextBuilder
shouldn't throw at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe can we find something expliciter, but as GraphQL isn't REST, it looks okish that _api_resource_class
doesn't exist.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay then!
if ($request->attributes->get('_graphql')) { | ||
$collectionArgs = $request->attributes->get('_graphql_collections_args', []); | ||
if (isset($collectionArgs[$resourceClass]['after'])) { | ||
$after = base64_decode($collectionArgs[$resourceClass]['after'], true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
\base64_decode
as below? Don't remember the policy on this but we need it to be consistent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should use \
everywhere, it's faster. IIRC, there is a PHP CS Fixer rule for that.
} | ||
|
||
if (\is_array($args[$rootIdentifier])) { | ||
if (\count($args[$rootIdentifier]) > 1) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same on these, we currently are not prefixing with \
in the base code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So we should add and enable this fixer in our php-cs-fixer config!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes but it is a risky one, IIRC, it breaks our tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wdyt we remove \
for now and we will do this in another pr.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed!
return null; | ||
} | ||
|
||
$item = $this->itemDataProvider->getItem($resourceClass, \count($identifiers) > 1 ? implode(';', $identifiers) : $uniqueIdentifier[0]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a note to myself but this would need refactoring if we can implement some kind of identifiers denormalization (#1309) not linked to any bridge (code duplication).
@@ -35,7 +35,7 @@ public function __construct(array $dataProviders) | |||
/** | |||
* {@inheritdoc} | |||
*/ | |||
public function getSubresource(string $resourceClass, array $identifiers, array $context, string $operationName) | |||
public function getSubresource(string $resourceClass, array $identifiers, array $context, string $operationName = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will conflict haha
mkdir -p $dest | ||
cp node_modules/graphiql/graphiql.min.js $dest | ||
cp node_modules/graphiql/graphiql.css $dest | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gosh 🐱 maybe we should kinda create a js repository for all the graphql stuff in the future and just une one module for all these.
This also means that we're now publishing react sources in the source code of api-platform, totally 👎 on this personally...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Creating our own JS repo will not help. AFAIK, it's not possible to upload artifacts on Packagist, so the JS must be included in the repo. It's still a dependency, the way it is downloaded doesn't change anything from a legal POV.
This script can be dramatically improved but we can still handle that later
rm -Rf $dest | ||
fi | ||
mkdir -p $dest | ||
cp node_modules/es6-promise/dist/es6-promise.auto.min.js $dest |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can assume devs will use a modern browser, and all modern browsers support promises now: https://caniuse.com/#feat=promises
I would just rely on the browser without downloading the polyfill.
rm -Rf $dest | ||
fi | ||
mkdir -p $dest | ||
cp node_modules/fetch/lib/fetch.js $dest |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same for fetch: https://caniuse.com/#feat=fetch
update-js.sh
Outdated
|
||
dest=src/Bridge/Symfony/Bundle/Resources/public/react/ | ||
|
||
yarn add --production --no-lockfile react react-dom |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't we just run yarn one time, copy all the files we need then remove node_modules
?
yarn add --production --no-lockfile swagger-ui-dist react react-dom graphiql
It will speed up the install and simplify this script.
* @var array | ||
*/ | ||
private $graphqlRequest; | ||
/** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing blank line before.
private $title; | ||
private $graphiqlEnabled; | ||
|
||
public function __construct(SchemaBuilderInterface $schemaBuilder, ExecutorInterface $executor, \Twig_Environment $twig, bool $debug, bool $graphiqlEnabled, string $title) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add default values to make it easier to use this class alone?
bool $debug = false, bool $graphiqlEnabled = false, string $title = ''
@@ -253,7 +253,7 @@ private function getSerializerContext(string $resourceClass, string $contextType | |||
$request = $this->requestStack->getCurrentRequest(); | |||
} | |||
|
|||
if (null !== $this->serializerContextBuilder && null !== $request) { | |||
if (null !== $this->serializerContextBuilder && null !== $request && !$request->attributes->get('_graphql')) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe can we find something expliciter, but as GraphQL isn't REST, it looks okish that _api_resource_class
doesn't exist.
if ($request->attributes->get('_graphql')) { | ||
$collectionArgs = $request->attributes->get('_graphql_collections_args', []); | ||
if (isset($collectionArgs[$resourceClass]['after'])) { | ||
$after = base64_decode($collectionArgs[$resourceClass]['after'], true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should use \
everywhere, it's faster. IIRC, there is a PHP CS Fixer rule for that.
mkdir -p $dest | ||
cp node_modules/graphiql/graphiql.min.js $dest | ||
cp node_modules/graphiql/graphiql.css $dest | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Creating our own JS repo will not help. AFAIK, it's not possible to upload artifacts on Packagist, so the JS must be included in the repo. It's still a dependency, the way it is downloaded doesn't change anything from a legal POV.
This script can be dramatically improved but we can still handle that later
{ | ||
$query = $request->query->get('query'); | ||
$operation = $request->query->get('operation'); | ||
if ([] !== $variables = $request->query->get('variables', [])) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if ($variables = $request->...) {
src/Api/IdentifiersExtractor.php
Outdated
foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $property) { | ||
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property); | ||
$propertyIsIdentifier = $propertyMetadata->isIdentifier() ?? false; | ||
if (!$propertyIsIdentifier) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if ($propertyMetadata->isIdentifier() ?? false) {
$identifiers[] = $property;
}
@@ -83,6 +83,10 @@ public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGenerator | |||
} | |||
|
|||
$itemsPerPage = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_items_per_page', $this->itemsPerPage, true); | |||
if ($request->attributes->get('_graphql')) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
has()
instead of get()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
get
is safer IMO (in case it is explicitly set to false
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, there are a lot of places where he uses has()
so it would also be safer to replace them!
* | ||
* @internal | ||
*/ | ||
class AbstractResolverFactory |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing abstract
here!
|
||
return $this->subresourceDataProvider->getSubresource($subresourceClass, $identifiers, [ | ||
'property' => $rootProperty, | ||
'identifiers' => array_map(function ($rootIdentifier) use ($rootClass) {return [$rootIdentifier, $rootClass]; }, $rootIdentifiers), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer a foreach
for performances.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Btw both loops can be merged. I take care of your comments.
} | ||
|
||
/** | ||
* @throws \InvalidArgumentException |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You have to remove the backslash.
if (false === $after) { | ||
throw new InvalidArgumentException(sprintf('Cursor %s is invalid', $args['after'])); | ||
} | ||
$offset = (int) $after; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1 + (int) $after;
} | ||
|
||
$data = ['edges' => [], 'pageInfo' => ['endCursor' => null, 'hasNextPage' => false]]; | ||
if ($collection instanceof PaginatorInterface && ($totalItems = $collection->getTotalItems()) > 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't we also support PartialPaginatorInterface
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 but in another PR please
throw new InvalidArgumentException('Composite identifiers are not allowed for a resource already used as a composite identifier'); | ||
} | ||
|
||
$identifiers[] = $rootIdentifier.'='.current($args[$rootIdentifier]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you use current()
to get the first element then it's not really safe!
throw new InvalidArgumentException('Composite identifiers are not allowed for a resource already used as a composite identifier'); | ||
} | ||
|
||
$identifiers[] = $rootIdentifier.'='.\current($args[$rootIdentifier]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you use current()
to get the first item then it's not really safe!
93759cb
to
5c10f7b
Compare
cfc6269
to
0277faf
Compare
29d6f90
to
6664085
Compare
🎉 I will try it out ASAP but that's unlikely to be before 2-3 weeks |
* GraphQL Query support (api-platform#1358) * Fix missing service when no Varnish URL is defined * [PropertyFilter] Fix whitelist comparison (api-platform#1379) * Remove wrong doc * Swagger subcollection documentation issue (api-platform#1395) * Make the requirements configurable Closes api-platform#1401 * Provide a better exception message and type Getting "Not found" on a route where you are getting an object can get really confusing. * Bump branch alias to 2.2.x-dev 2.1.x-dev is already taken by the 2.1 branch, and this should represent the next minor version anyway. * Fix tests * Reuse PriorityTaggedServiceTrait from symfony * Improve payload support and remove duplicate code in ConstraintViolationListNormalizer (api-platform#1416) * Filter Annotation implementation Parent class filters (needs test) Support for nested properties Tests Fix some comments and remove id=>id in compiler pass * Throw on abstract data providers / filters * Remove an unused var * Remove useless badges * Enable the coverage * Fix some quality issues * Add job to test upstream libs deprecations If api-platform uses upstream libs in a deprecated way, we should be aware of it. * Add job to test upstream libs deprecations If api-platform uses upstream libs in a deprecated way, we should be aware of it. * Fix missing cache tag on empty collections * Allow plain IDs with `allow_plain_identifiers` * Fix indentation for GraphQL features. * Add JSON API basic support (api-platform#785, api-platform#1036, api-platform#1175) * Clean Behat tests * Fix tags addition with an empty value * Document swagger-specific description options … and where to find them should there be newer ones. * Fix PHPUnit tests * Fix missing return statement * Support & compatibility for PHP7.2 * Add feature to update swagger context for properties * Generator compat improvements (api-platform#1429) * Add support for resource names without namespace * [SF 4.0] Make actions explicitly public * Allow phpdocumentor/reflection-docblock 4 * fix hydra documentation normalizer with subresources * Create a base collection normalizer * Fix request auto-runner * Update the changelog * Update changelog
Hello there! |
@jorgeluisacostaalonso, you need to use |
Support of GraphQL Queries by using https://github.com/webonyx/graphql-php.
Mutations will be done later.
Based on the work of @raoulclais.
Endpoint is
/graphql
.GraphiQL is used for the UI.