Skip to content

Callbacks enhancement #851

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

Draft
wants to merge 14 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/definitions/expression-language.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ MyType:
fields:
name:
type: String!
resolve: "@=service('my_private_service').formatName(value)"
resolve: "@=my_private_service.formatName(value)"
```

To use a vendor private services:
Expand Down
3 changes: 2 additions & 1 deletion src/Config/InterfaceTypeDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ public function getDefinition(): ArrayNodeDefinition
{
/** @var ArrayNodeDefinition $node */
$node = self::createNode('_interface_config');
$this->callbackNormalization($node, 'typeResolver', 'resolveType');

/** @phpstan-ignore-next-line */
$node
->children()
->append($this->nameSection())
->append($this->outputFieldsSection())
->append($this->resolveTypeSection())
->append($this->callbackSection('typeResolver', 'GraphQL type resolver'))
->append($this->descriptionSection())
->arrayNode('interfaces')
->prototype('scalar')->info('One of internal or custom interface types.')->end()
Expand Down
3 changes: 2 additions & 1 deletion src/Config/ObjectTypeDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public function getDefinition(): ArrayNodeDefinition

/** @var ArrayNodeDefinition $node */
$node = $builder->getRootNode();
$this->callbackNormalization($node, 'fieldResolver', 'resolveField');

/** @phpstan-ignore-next-line */
$node
Expand All @@ -29,7 +30,7 @@ public function getDefinition(): ArrayNodeDefinition
->prototype('scalar')->info('One of internal or custom interface types.')->end()
->end()
->variableNode('isTypeOf')->end()
->variableNode('resolveField')->end()
->append($this->callbackSection('fieldResolver', 'GraphQL field value resolver'))
->variableNode('fieldsDefaultAccess')
->info('Default access control to fields (expression language can be use here)')
->end()
Expand Down
78 changes: 73 additions & 5 deletions src/Config/TypeDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

namespace Overblog\GraphQLBundle\Config;

use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\Config\Definition\Builder\ScalarNodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\Builder\VariableNodeDefinition;
Expand Down Expand Up @@ -32,11 +34,6 @@ public static function create(): self
return new static();
}

protected function resolveTypeSection(): VariableNodeDefinition
{
return self::createNode('resolveType', 'variable');
}

protected function nameSection(): ScalarNodeDefinition
{
/** @var ScalarNodeDefinition $node */
Expand Down Expand Up @@ -156,6 +153,77 @@ protected function typeSection(bool $isRequired = false): ScalarNodeDefinition
return $node;
}

protected function callbackNormalization(NodeDefinition $node, string $new, string $old): void
{
$node
->beforeNormalization()
->ifTrue(fn ($options) => !empty($options[$old]) && empty($options[$new]))
->then(function ($options) use ($old, $new) {
if (is_callable($options[$old])) {
if (is_array($options[$old])) {
$options[$new]['function'] = implode('::', $options[$old]);
} else {
$options[$new]['function'] = $options[$old];
}
} elseif (is_string($options[$old])) {
$options[$new]['expression'] = ExpressionLanguage::stringHasTrigger($options[$old]) ?
ExpressionLanguage::unprefixExpression($options[$old]) :
json_encode($options[$old]);
} else {
$options[$new]['expression'] = json_encode($options[$old]);
}

return $options;
})
->end()
->beforeNormalization()
->ifTrue(fn ($options) => is_array($options) && array_key_exists($old, $options))
->then(function ($options) use ($old) {
unset($options[$old]);

return $options;
})
->end()
->validate()
->ifTrue(fn (array $v) => !empty($v[$new]) && !empty($v[$old]))
->thenInvalid(sprintf(
'"%s" and "%s" should not be used together in "%%s".',
$new,
$old,
))
->end()
;
}

protected function callbackSection(string $name, string $info): ArrayNodeDefinition
{
/** @var ArrayNodeDefinition $node */
$node = self::createNode($name);
/** @phpstan-ignore-next-line */
$node
->info($info)
->validate()
->ifTrue(fn (array $v) => !empty($v['function']) && !empty($v['expression']))
->thenInvalid('"function" and "expression" should not be use together.')
->end()
->beforeNormalization()
// Allow short syntax
->ifTrue(fn ($options) => is_string($options) && ExpressionLanguage::stringHasTrigger($options))
->then(fn ($options) => ['expression' => ExpressionLanguage::unprefixExpression($options)])
->end()
->beforeNormalization()
->ifTrue(fn ($options) => is_string($options) && !ExpressionLanguage::stringHasTrigger($options))
->then(fn ($options) => ['function' => $options])
->end()
->children()
->scalarNode('function')->end()
->scalarNode('expression')->end()
->end()
;

return $node;
}

/**
* @return mixed
*
Expand Down
5 changes: 2 additions & 3 deletions src/Config/TypeWithOutputFieldsDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ protected function outputFieldsSection(): NodeDefinition
$node->isRequired()->requiresAtLeastOneElement();

$prototype = $node->useAttributeAsKey('name', false)->prototype('array');
$this->callbackNormalization($prototype, 'resolver', 'resolve');

/** @phpstan-ignore-next-line */
$prototype
Expand Down Expand Up @@ -68,9 +69,7 @@ protected function outputFieldsSection(): NodeDefinition
->end()
->end()
->end()
->variableNode('resolve')
->info('Value resolver (expression language can be used here)')
->end()
->append($this->callbackSection('resolver', 'GraphQL value resolver'))
->append($this->descriptionSection())
->append($this->deprecationReasonSection())
->variableNode('access')
Expand Down
3 changes: 2 additions & 1 deletion src/Config/UnionTypeDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public function getDefinition(): ArrayNodeDefinition
{
/** @var ArrayNodeDefinition $node */
$node = self::createNode('_union_config');
$this->callbackNormalization($node, 'typeResolver', 'resolveType');

/** @phpstan-ignore-next-line */
$node
Expand All @@ -24,7 +25,7 @@ public function getDefinition(): ArrayNodeDefinition
->isRequired()
->requiresAtLeastOneElement()
->end()
->append($this->resolveTypeSection())
->append($this->callbackSection('typeResolver', 'GraphQL type resolver'))
->append($this->descriptionSection())
->end();

Expand Down
12 changes: 8 additions & 4 deletions src/Definition/GraphQLServices.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@

use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use Overblog\GraphQLBundle\Resolver\MutationResolver;
use Overblog\GraphQLBundle\Resolver\QueryResolver;
use Overblog\GraphQLBundle\Resolver\TypeResolver;
use Overblog\GraphQLBundle\Validator\InputValidator;
use Overblog\GraphQLBundle\Validator\InputValidatorFactory;
use Symfony\Component\DependencyInjection\ServiceLocator;

/**
Expand All @@ -21,7 +25,7 @@ final class GraphQLServices extends ServiceLocator
*/
public function query(string $alias, ...$args)
{
return $this->get('queryResolver')->resolve([$alias, $args]);
return $this->get(QueryResolver::class)->resolve([$alias, $args]);
}

/**
Expand All @@ -31,7 +35,7 @@ public function query(string $alias, ...$args)
*/
public function mutation(string $alias, ...$args)
{
return $this->get('mutationResolver')->resolve([$alias, $args]);
return $this->get(MutationResolver::class)->resolve([$alias, $args]);
}

/**
Expand All @@ -41,7 +45,7 @@ public function mutation(string $alias, ...$args)
*/
public function getType(string $typeName): ?Type
{
return $this->get('typeResolver')->resolve($typeName);
return $this->get(TypeResolver::class)->resolve($typeName);
}

/**
Expand All @@ -52,7 +56,7 @@ public function getType(string $typeName): ?Type
*/
public function createInputValidator($value, ArgumentInterface $args, $context, ResolveInfo $info): InputValidator
{
return $this->get('input_validator_factory')->create(
return $this->get(InputValidatorFactory::class)->create(
new ResolverArgs($value, $args, $context, $info)
);
}
Expand Down
31 changes: 17 additions & 14 deletions src/DependencyInjection/Compiler/GraphQLServicesPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@

namespace Overblog\GraphQLBundle\DependencyInjection\Compiler;

use InvalidArgumentException;
use Overblog\GraphQLBundle\Definition\GraphQLServices;
use Overblog\GraphQLBundle\Generator\TypeGenerator;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
use function is_string;
use function sprintf;
Expand All @@ -27,27 +26,31 @@ public function process(ContainerBuilder $container): void

foreach ($taggedServices as $id => $tags) {
foreach ($tags as $attributes) {
if (empty($attributes['alias']) || !is_string($attributes['alias'])) {
throw new InvalidArgumentException(
sprintf('Service "%s" tagged "overblog_graphql.service" should have a valid "alias" attribute.', $id)
);
}
$locateableServices[$attributes['alias']] = new Reference($id);
$locateableServices[] = new Reference($id);

if (array_key_exists('alias', $attributes)) {
if (empty($attributes['alias']) || !is_string($attributes['alias'])) {
throw new InvalidArgumentException(
sprintf('Service "%s" tagged "overblog_graphql.service" should have a valid "alias" attribute.', $id)
);
}

$isPublic = !isset($attributes['public']) || $attributes['public'];
if ($isPublic) {
$expressionLanguageDefinition->addMethodCall(
'addGlobalName',
'addExpressionVariableNameServiceId',
[
sprintf(TypeGenerator::GRAPHQL_SERVICES.'->get(\'%s\')', $attributes['alias']),
$attributes['alias'],
$id,
]
);
}
}
}
$locateableServices['container'] = new Reference('service_container');
$locateableServices[] = new Reference('service_container');
$expressionLanguageDefinition->addMethodCall(
'addExpressionVariableNameServiceId',
['container', 'service_container']
);

$container->findDefinition(GraphQLServices::class)->addArgument($locateableServices);
$container->findDefinition(GraphQLServices::class)->addArgument(array_unique($locateableServices));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

declare(strict_types=1);

namespace Overblog\GraphQLBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;

final class IdentifyCallbackServiceIdsPass implements CompilerPassInterface
{
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container): void
{
if (!$container->hasParameter('overblog_graphql_types.config')) {
return;
}
/** @var array $configs */
$configs = $container->getParameter('overblog_graphql_types.config');
foreach ($configs as &$typeConfig) {
switch ($typeConfig['type']) {
case 'object':
if (isset($typeConfig['config']['fieldResolver'])) {
$this->resolveServiceIdAndMethod($container, $typeConfig['config']['fieldResolver']);
}

foreach ($typeConfig['config']['fields'] as &$field) {
if (isset($field['resolver'])) {
$this->resolveServiceIdAndMethod($container, $field['resolver']);
}
}
break;

case 'interface':
case 'union':
if (isset($typeConfig['config']['typeResolver'])) {
$this->resolveServiceIdAndMethod($container, $typeConfig['config']['typeResolver']);
}
break;
}
}
$container->setParameter('overblog_graphql_types.config', $configs);
}

private function resolveServiceIdAndMethod(ContainerBuilder $container, ?array &$callback): void
{
if (!isset($callback['function'])) {
return;
}
[$id, $method] = explode('::', $callback['function'], 2) + [null, null];
if (str_starts_with($id, '\\')) {
$id = ltrim($id, '\\');
}

try {
$definition = $container->getDefinition($id);
} catch (ServiceNotFoundException $e) {
// get Alias real service ID
try {
$alias = $container->getAlias($id);
$id = (string) $alias;
$definition = $container->getDefinition($id);
} catch (ServiceNotFoundException|InvalidArgumentException $e) {
return;
}
}
if (
!$definition->hasTag('overblog_graphql.service')
&& !$definition->hasTag('overblog_graphql.global_variable')
) {
$definition->addTag('overblog_graphql.service');
}

$callback['function'] = "$id::$method";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ public function __construct($name = 'parameter')
{
parent::__construct(
$name,
fn (string $value) => "$this->gqlServices->get('container')->getParameter($value)",
static fn (array $arguments, $paramName) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get('container')->getParameter($paramName)
fn (string $value) => "$this->gqlServices->get('service_container')->getParameter($value)",
static fn (array $arguments, $paramName) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get('service_container')->getParameter($paramName)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ public function __construct($name = 'service')
{
parent::__construct(
$name,
fn (string $serviceId) => "$this->gqlServices->get('container')->get($serviceId)",
static fn (array $arguments, $serviceId) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get('container')->get($serviceId)
fn (string $serviceId) => "$this->gqlServices->get('service_container')->get($serviceId)",
static fn (array $arguments, $serviceId) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get('service_container')->get($serviceId)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ public function __construct()
{
parent::__construct(
'arguments',
fn ($mapping, $data) => "$this->gqlServices->get('container')->get('overblog_graphql.arguments_transformer')->getArguments($mapping, $data, \$info)",
static fn (array $arguments, $mapping, $data) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get('container')->get('overblog_graphql.arguments_transformer')->getArguments($mapping, $data, $arguments['info'])
fn ($mapping, $data) => "$this->gqlServices->get('service_container')->get('overblog_graphql.arguments_transformer')->getArguments($mapping, $data, \$info)",
static fn (array $arguments, $mapping, $data) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get('service_container')->get('overblog_graphql.arguments_transformer')->getArguments($mapping, $data, $arguments['info'])
);
}
}
Loading