Skip to content
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] Add custom mutations #2447

Merged

Conversation

raoulclais
Copy link
Contributor

@raoulclais raoulclais commented Jan 11, 2019

Q A
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets
License MIT
Doc PR

Introduce a new MutationResolverInterface interface.

Create a service which implements this interface and declare your custom mutation on your entity:

@ApiResource(graphql={
    "create",
    "update",
    "increment"={"mutation"=IncrementMutation::class}
})

If null is returned from the resolver, the item will not be persisted.

@raoulclais raoulclais changed the title Add custom mutations to GraphQL [WIP] Add custom mutations to GraphQL Jan 11, 2019
if ('create' !== $operationName && 'update' !== $operationName) {
$mutationId = $resourceMetadata->getGraphqlAttribute($operationName, 'mutation');
if (!$this->mutationLocator->has($mutationId)) {
@trigger_error(sprintf('The mutation %s does not exist', $mutationId));
Copy link
Member

Choose a reason for hiding this comment

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

Why not throwing an exception instead?

} else {
$data['id'] = null;
}
break;
case 'create':
Copy link
Member

Choose a reason for hiding this comment

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

The two following lines can be removed then, right?

*
* @author Raoul Clais <raoul.clais@gmail.com>
*/
final class MutationPass implements CompilerPassInterface
Copy link
Member

@alanpoulain alanpoulain Jan 13, 2019

Choose a reason for hiding this comment

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

GraphQlMutationResolverPass instead?

*
* @author Raoul Clais <raoul.clais@gmail.com>
*/
interface MutationInterface
Copy link
Member

Choose a reason for hiding this comment

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

MutationResolverInterface instead?

@@ -111,6 +112,8 @@ public function load(array $configs, ContainerBuilder $container)
->addTag('api_platform.subresource_data_provider');
$container->registerForAutoconfiguration(FilterInterface::class)
->addTag('api_platform.filter');
$container->registerForAutoconfiguration(MutationInterface::class)
->addTag('api_platform.graphql_mutation');
Copy link
Member

Choose a reason for hiding this comment

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

api_platform.graphql.mutation_resolver instead?

use Symfony\Component\DependencyInjection\Reference;

/**
* Injects mutations.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* Injects mutations.
* Injects GraphQL Mutation resolvers.


<!-- Mutation -->

<service id="api_platform.graphql.mutation_locator" class="Symfony\Component\DependencyInjection\ServiceLocator">
Copy link
Member

Choose a reason for hiding this comment

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

api_platform.graphql.mutation_resolver_locator instead?

@alanpoulain
Copy link
Member

It should be great to have a custom input, but can be done later in another PR.

use GraphQL\Type\Definition\ResolveInfo;

/**
* Interface MutationInterface.
Copy link
Member

Choose a reason for hiding this comment

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

Should be replaced by an explicit definition or removed

private $resourceAccessChecker;
private $validator;

public function __construct(IriConverterInterface $iriConverter, DataPersisterInterface $dataPersister, NormalizerInterface $normalizer, ResourceMetadataFactoryInterface $resourceMetadataFactory, ResourceAccessCheckerInterface $resourceAccessChecker = null, ValidatorInterface $validator = null)
public function __construct(IriConverterInterface $iriConverter, DataPersisterInterface $dataPersister, NormalizerInterface $normalizer, ResourceMetadataFactoryInterface $resourceMetadataFactory, ContainerInterface $mutationLocator, ResourceAccessCheckerInterface $resourceAccessChecker = null, ValidatorInterface $validator = null)
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 a BC break. It's allowed for @experimental classes but it would be better to avoid it. Can we also made this argument optional? It doesn't have to be mandatory.

Copy link
Member

Choose a reason for hiding this comment

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

It can be made optional but do we really want to handle the "breaking" case (when it's null)?

*/
interface MutationResolverInterface
{
public function __invoke($item, array $input, ResolveInfo $info);
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 the mutation return something? How is it take into account by the Schema generator?

/**
* @Given there is a custom mutation dummy object
*/
public function thereIsACustomMutationDummyObjects()
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
public function thereIsACustomMutationDummyObjects()
public function thereIsACustomMutationDummyObject()

}

$mutationId = $resourceMetadata->getGraphqlAttribute($operationName, 'mutation');
if (!$this->mutationLocator->has($mutationId)) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (!$this->mutationLocator->has($mutationId)) {
if (!$this->mutationLocator || !$this->mutationLocator->has($mutationId)) {

$context = null === $item ? ['resource_class' => $resourceClass] : ['resource_class' => $resourceClass, 'object_to_populate' => $item];
$context += $resourceMetadata->getGraphqlAttribute($operationName, 'denormalization_context', [], true);
$item = $this->normalizer->denormalize($args['input'], $resourceClass, ItemNormalizer::FORMAT, $context);

if ('create' !== $operationName && 'update' !== $operationName) {
if (null === $this->mutationLocator) {
Copy link
Member

Choose a reason for hiding this comment

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

I don't really like to throw an exception here because the service is null. I have added my suggestion below.

/**
* @return mixed The mutated item
*/
public function __invoke($item, array $input, ResolveInfo $info);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
public function __invoke($item, array $input, ResolveInfo $info);
public function __invoke($item, array $context);

Maybe having a context is better to be independent of webonyx/graphql-php.
Then in context we have:

['input' => [], 'resolve_info' => $info]

*
* @author Raoul Clais <raoul.clais@gmail.com>
*/
class CustomMutationDummy
Copy link
Member

Choose a reason for hiding this comment

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

Should be added also as document for MongoDB.

/**
* {@inheritdoc}
*
* @throws RuntimeException
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 removed.

@alanpoulain alanpoulain force-pushed the feature/custom-mutation branch 4 times, most recently from effbb45 to bcff6f6 Compare April 1, 2019 20:48
@alanpoulain alanpoulain changed the title [WIP] Add custom mutations to GraphQL [GraphQL] Add custom mutations Apr 1, 2019
@alanpoulain
Copy link
Member

@SCIF, @lukasluecke I think this PR is good to go, could you take a look please?

Copy link
Contributor

@lukasluecke lukasluecke left a comment

Choose a reason for hiding this comment

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

Looks good to me 👍

@SCIF
Copy link

SCIF commented Apr 2, 2019

@alanpoulain , @lukasluecke ,

It looks pretty same as custom query PR's. And complains are generally the same:

  • id argument cannot be opt out
  • GraphQl custom operations provides different DX comparing to REST custom operations

I'm pretty sure, that getting rid of id will not be done in foreseeable future (and definitely not in this PR). So, I suggest one more time to consider using custom query resolvers and custom mutation resolvers in documentation, rather than custom operation. I would highly appreciate creation of issue and starting of process designing/discussing proper way and proper DX of implementation of custom operations (ready to invest some time into development in case if we would arrange desired DX fast).

Regardless mentioned above, it looks good.

@alanpoulain
Copy link
Member

alanpoulain commented Apr 2, 2019

id argument cannot be opt out

This is not right. You can with both custom mutation and query resolvers. I've added a Behat test in the custom query resolver PR and I will document it. Maybe I should add one here too.

GraphQl custom operations provides different DX comparing to REST custom operations

I'm not sure to follow you. The documentation about custom operations for REST is here: https://api-platform.com/docs/core/operations/#creating-custom-operations-and-controllers

It says:

API Platform automatically retrieves the appropriate PHP entity using the data provider then deserializes user data in it, and for POST and PUT requests updates the entity with data provided by the user.

This is exactly what this PR does for GraphQL.
I would like to see your proposal if it provides a better DX though.

@lukasluecke
Copy link
Contributor

@SCIF

I think it's pretty clear that the merged PR was "custom queries" and this one is "custom mutations", and that should and will be reflected in the docs. Anything beyond that can be implemented at any point in the future - if you have any suggestions on that, maybe you can start by creating the issue, and we can discuss there?

I believe the id is not required, which should solve almost any concerns regarding it - yes, you can not remove the argument right now, but it does not really matter that much.

@alanpoulain alanpoulain merged commit ea73061 into api-platform:master Apr 2, 2019
@alanpoulain
Copy link
Member

Thank you @raoulclais 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants