Skip to content

Conversation

alanpoulain
Copy link
Member

@alanpoulain alanpoulain commented Sep 11, 2017

Q A
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets api-platform/api-platform#230
License MIT
Doc PR

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.

@@ -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']);
Copy link
Member

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"
Copy link
Member

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')) {
Copy link
Member

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.

Copy link
Member

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)
Copy link
Member

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?
Copy link
Member

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.

*
* @author Alan Poulain <contact@alanpoulain.eu>
*/
class Executor
Copy link
Member

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?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$graphqlType = $this->getResourceType($className, $resourceMetadata);
break;

case Type::BUILTIN_TYPE_ARRAY:
Copy link
Member

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()
Copy link
Member

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)
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not marked as @internal

Copy link
Member

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.

Copy link
Member Author

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()]);
Copy link
Member

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 :)

Copy link
Member Author

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.

Copy link
Member Author

@alanpoulain alanpoulain Sep 12, 2017

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

Copy link
Member

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 :)

Copy link
Member Author

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?

Copy link
Member

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.

@dunglas
Copy link
Member

dunglas commented Sep 11, 2017

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).
Maybe should we provide in the future a different way to implement REST operations than mutations. I guess it will be very difficult to implement generic operations that work for both REST and GraphQL.

WDYT @alanpoulain @raoulclais @api-platform/core-team?

@dunglas
Copy link
Member

dunglas commented Sep 11, 2017

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);
Copy link
Member

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 no json_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)?

@theofidry
Copy link
Contributor

@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".

@alanpoulain alanpoulain force-pushed the feature/graphql branch 2 times, most recently from d39539d to 63e6fe6 Compare September 12, 2017 17:22
@mvrhov
Copy link

mvrhov commented Sep 13, 2017

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.

@dunglas
Copy link
Member

dunglas commented Sep 13, 2017

We must indeed make that clear in the documentation. And it's why it will be an optional disablable feature.

@alanpoulain alanpoulain force-pushed the feature/graphql branch 2 times, most recently from 7c05c1d to f1212fe Compare September 13, 2017 10:47
class GraphqlContext implements Context
{
/**
* @var \Behatch\Context\RestContext
Copy link
Member

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;
Copy link
Member

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
Copy link
Member

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) {
Copy link
Member

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?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes... Nice catch!

*/
public function executeQuery(...$args): ExecutionResult
{
return call_user_func_array(GraphQL::class.'::executeQuery', $args);
Copy link
Member

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);
Copy link
Member

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();
Copy link
Member

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.

Copy link
Member Author

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?

Copy link
Member

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!

Copy link
Member

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]);
Copy link
Member

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;

Copy link
Member Author

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.

foreach ($this->resourceNameCollectionFactory->create() as $resource) {
$resourceMetadata = $this->resourceMetadataFactory->create($resource);

$queryFields = array_merge($queryFields, $this->getQueryFields($resource, $resourceMetadata));
Copy link
Member

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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if ($property = $propertyMetadata->getType()) {

Copy link
Member

@dunglas dunglas Sep 13, 2017

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).

Copy link
Member

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.

Copy link
Member

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()) {
Copy link
Member

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)) {

Copy link
Member

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) {
Copy link
Member

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));
Copy link
Member

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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

|| !$propertyType

Copy link
Member

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 nullvalues. I would keep this as is.

}

/**
* Gets the field arguments of the identifier of a given resource.
Copy link
Member

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing @throws annotation.

@alanpoulain
Copy link
Member Author

Added: pagination support.

@alanpoulain alanpoulain force-pushed the feature/graphql branch 5 times, most recently from 2a01be4 to 9097958 Compare September 18, 2017 10:35
private $requestStack;
private $paginationEnabled;

public function __construct(CollectionDataProviderInterface $collectionDataProvider, SubresourceDataProviderInterface $subresourceDataProvider, NormalizerInterface $normalizer, IdentifiersExtractorInterface $identifiersExtractor, RequestStack $requestStack, bool $paginationEnabled)
Copy link
Member

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?

Copy link
Member Author

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;
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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) {
Copy link
Member

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) {
Copy link
Member

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.

@alanpoulain alanpoulain changed the title [WIP] GraphQL support GraphQL Query support Sep 20, 2017
@alanpoulain alanpoulain force-pushed the feature/graphql branch 3 times, most recently from 546d9e4 to d09ee00 Compare September 20, 2017 20:02
@@ -11,7 +11,7 @@ matrix:
include:
- php: '7.0'
- php: '7.1'
env: coverage=1 lint=1
env: coverage=0 lint=1
Copy link
Member

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.

Copy link
Member

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?

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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.

@@ -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')) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why?

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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);
Copy link
Member

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.

Copy link
Member

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) {
Copy link
Member

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.

Copy link
Member

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!

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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]);
Copy link
Member

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)
Copy link
Member

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

Copy link
Member

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...

Copy link
Member

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
Copy link
Member

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update-js.sh Outdated

dest=src/Bridge/Symfony/Bundle/Resources/public/react/

yarn add --production --no-lockfile react react-dom
Copy link
Member

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;
/**
Copy link
Member

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)
Copy link
Member

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')) {
Copy link
Member

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);
Copy link
Member

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

Copy link
Member

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', [])) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if ($variables = $request->...) {

foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $property) {
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property);
$propertyIsIdentifier = $propertyMetadata->isIdentifier() ?? false;
if (!$propertyIsIdentifier) {
Copy link
Member

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')) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

has() instead of get()?

Copy link
Member

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)

Copy link
Member

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
Copy link
Member

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),
Copy link
Member

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.

Copy link
Member

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
Copy link
Member

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;
Copy link
Member

@meyerbaptiste meyerbaptiste Sep 21, 2017

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) {
Copy link
Member

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?

Copy link
Member

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]);
Copy link
Member

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]);
Copy link
Member

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!

@alanpoulain
Copy link
Member Author

@dunglas dunglas merged commit 7852f10 into api-platform:master Oct 6, 2017
@theofidry
Copy link
Contributor

🎉 I will try it out ASAP but that's unlikely to be before 2-3 weeks

@alanpoulain alanpoulain deleted the feature/graphql branch October 15, 2017 14:30
cr3a7ure added a commit to cr3a7ure/core that referenced this pull request Oct 27, 2017
* 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
@ghost
Copy link

ghost commented Nov 19, 2017

Hello there!
There 's a little quick-start for grapqh and apiplatform!! Or something explaining how start!!
have a nice day

@dunglas
Copy link
Member

dunglas commented Nov 19, 2017

@jorgeluisacostaalonso, you need to use api-platform/core:dev-master. Then require webonyx/graphql-php, and you're down, the GraphQL API is available at the /graphql endpoint.

hoangnd25 pushed a commit to hoangnd25/core that referenced this pull request Feb 23, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants