Skip to content
Open
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
8 changes: 8 additions & 0 deletions src/Config/InputObjectTypeDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Overblog\GraphQLBundle\Config;

use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\VariableNodeDefinition;

use function is_string;

Expand All @@ -31,6 +32,7 @@ public function getDefinition(): ArrayNodeDefinition
->append($this->typeSection(true))
->append($this->descriptionSection())
->append($this->defaultValueSection())
->append($this->publicSection())
->append($this->validationSection(self::VALIDATION_LEVEL_PROPERTY))
->append($this->deprecationReasonSection())
->end()
Expand All @@ -42,4 +44,10 @@ public function getDefinition(): ArrayNodeDefinition

return $node;
}

protected function publicSection(): VariableNodeDefinition
{
return self::createNode('public', 'variable')
->info('Visibility control to field (expression language can be used here)');
}
}
9 changes: 9 additions & 0 deletions src/Config/Parser/MetadataParser/MetadataParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -692,9 +692,14 @@ private static function getGraphQLInputFieldsFromMetadatas(ReflectionClass $refl

/** @var Metadata\Field|null $fieldMetadata */
$fieldMetadata = self::getFirstMetadataMatching($metadatas, Metadata\Field::class);
$publicMetadata = self::getFirstMetadataMatching($metadatas, Metadata\IsPublic::class);

// No field metadata found
if (null === $fieldMetadata) {
if (null !== $publicMetadata) {
throw new InvalidArgumentException(sprintf('The metadatas %s defined on "%s" are only usable in addition of metadata %s', self::formatMetadata('Visible'), $reflector->getName(), self::formatMetadata('Field')));
}

continue;
}

Expand Down Expand Up @@ -731,6 +736,10 @@ private static function getGraphQLInputFieldsFromMetadatas(ReflectionClass $refl
$fieldConfiguration['defaultValue'] = $reflector->getDefaultValue();
}

if ($publicMetadata) {
$fieldConfiguration['public'] = self::formatExpression($publicMetadata->value);
}

$fieldConfiguration = array_merge(self::getDescriptionConfiguration($metadatas, true), $fieldConfiguration);
$fields[$fieldName] = $fieldConfiguration;
}
Expand Down
26 changes: 18 additions & 8 deletions src/Definition/Builder/SchemaBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,43 +9,45 @@
use Overblog\GraphQLBundle\Definition\Type\ExtensibleSchema;
use Overblog\GraphQLBundle\Definition\Type\SchemaExtension\ValidatorExtension;
use Overblog\GraphQLBundle\Resolver\TypeResolver;
use Symfony\Contracts\Service\ResetInterface;

use function array_map;

final class SchemaBuilder
final class SchemaBuilder implements ResetInterface
{
private TypeResolver $typeResolver;
private bool $enableValidation;
private array $builders = [];

public function __construct(TypeResolver $typeResolver, bool $enableValidation = false)
{
$this->typeResolver = $typeResolver;
$this->enableValidation = $enableValidation;
}

public function getBuilder(string $name, ?string $queryAlias, ?string $mutationAlias = null, ?string $subscriptionAlias = null, array $types = []): Closure
public function getBuilder(string $name, ?string $queryAlias, ?string $mutationAlias = null, ?string $subscriptionAlias = null, array $types = [], bool $resettable = false): Closure
{
return function () use ($name, $queryAlias, $mutationAlias, $subscriptionAlias, $types): ExtensibleSchema {
static $schema = null;
if (null === $schema) {
$schema = $this->create($name, $queryAlias, $mutationAlias, $subscriptionAlias, $types);
return function () use ($name, $queryAlias, $mutationAlias, $subscriptionAlias, $types, $resettable): ExtensibleSchema {
if (!isset($this->builders[$name])) {
$this->builders[$name] = $this->create($name, $queryAlias, $mutationAlias, $subscriptionAlias, $types, $resettable);
}

return $schema;
return $this->builders[$name];
};
}

/**
* @param string[] $types
*/
public function create(string $name, ?string $queryAlias, ?string $mutationAlias = null, ?string $subscriptionAlias = null, array $types = []): ExtensibleSchema
public function create(string $name, ?string $queryAlias, ?string $mutationAlias = null, ?string $subscriptionAlias = null, array $types = [], bool $resettable = false): ExtensibleSchema
{
$this->typeResolver->setCurrentSchemaName($name);
$query = $this->typeResolver->resolve($queryAlias);
$mutation = $this->typeResolver->resolve($mutationAlias);
$subscription = $this->typeResolver->resolve($subscriptionAlias);

$schema = new ExtensibleSchema($this->buildSchemaArguments($name, $query, $mutation, $subscription, $types));
$schema->setIsResettable($resettable);
$extensions = [];

if ($this->enableValidation) {
Expand Down Expand Up @@ -74,4 +76,12 @@ private function buildSchemaArguments(string $schemaName, Type $query, ?Type $mu
},
];
}

public function reset(): void
{
$this->builders = array_filter(
$this->builders,
fn (ExtensibleSchema $schema) => false === $schema->isResettable()
);
}
}
15 changes: 15 additions & 0 deletions src/Definition/Type/ExtensibleSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@

class ExtensibleSchema extends Schema
{
/**
* Need to reset when container reset called
*/
private bool $isResettable = false;

public function __construct($config)
{
parent::__construct(
Expand Down Expand Up @@ -51,4 +56,14 @@ public function processExtensions()

return $this;
}

public function isResettable(): bool
{
return $this->isResettable;
}

public function setIsResettable(bool $isResettable): void
{
$this->isResettable = $isResettable;
}
}
1 change: 1 addition & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ private function definitionsSchemaSection(): ArrayNodeDefinition
->scalarNode('query')->defaultNull()->end()
->scalarNode('mutation')->defaultNull()->end()
->scalarNode('subscription')->defaultNull()->end()
->scalarNode('resettable')->defaultFalse()->end()
->arrayNode('types')
->defaultValue([])
->prototype('scalar')->end()
Expand Down
1 change: 1 addition & 0 deletions src/DependencyInjection/OverblogGraphQLExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ private function setSchemaArguments(array $config, ContainerBuilder $container):
$schemaConfig['mutation'],
$schemaConfig['subscription'],
$schemaConfig['types'],
$schemaConfig['resettable'],
]);
// schema
$schemaID = sprintf('%s.schema_%s', $this->getAlias(), $schemaName);
Expand Down
38 changes: 27 additions & 11 deletions src/Request/Executor.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use GraphQL\Validator\Rules\DisableIntrospection;
use GraphQL\Validator\Rules\QueryComplexity;
use GraphQL\Validator\Rules\QueryDepth;
use Overblog\GraphQLBundle\Definition\Type\ExtensibleSchema;
use Overblog\GraphQLBundle\Event\Events;
use Overblog\GraphQLBundle\Event\ExecutorArgumentsEvent;
use Overblog\GraphQLBundle\Event\ExecutorContextEvent;
Expand All @@ -21,15 +22,22 @@
use RuntimeException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Contracts\Service\ResetInterface;

use function array_keys;
use function is_callable;
use function sprintf;

class Executor
class Executor implements ResetInterface
{
public const PROMISE_ADAPTER_SERVICE_ID = 'overblog_graphql.promise_adapter';

/**
* @var array<Closure>
*/
private array $schemaBuilders = [];
/**
* @var array<Schema>
*/
private array $schemas = [];
private EventDispatcherInterface $dispatcher;
private PromiseAdapter $promiseAdapter;
Expand Down Expand Up @@ -61,7 +69,7 @@ public function setExecutor(ExecutorInterface $executor): self

public function addSchemaBuilder(string $name, Closure $builder): self
{
$this->schemas[$name] = $builder;
$this->schemaBuilders[$name] = $builder;

return $this;
}
Expand All @@ -75,30 +83,34 @@ public function addSchema(string $name, Schema $schema): self

public function getSchema(?string $name = null): Schema
{
if (empty($this->schemas)) {
if (empty($this->schemaBuilders) && empty($this->schemas)) {
throw new RuntimeException('At least one schema should be declared.');
}

if (null === $name) {
$name = isset($this->schemas['default']) ? 'default' : array_key_first($this->schemas);
}

if (!isset($this->schemas[$name])) {
throw new NotFoundHttpException(sprintf('Could not find "%s" schema.', $name));
if (null === $name) {
$name = isset($this->schemaBuilders['default']) ? 'default' : array_key_first($this->schemaBuilders);
}

$schema = $this->schemas[$name];
if (is_callable($schema)) {
$schema = $schema();
if (isset($this->schemas[$name])) {
$schema = $this->schemas[$name];
} elseif (isset($this->schemaBuilders[$name])) {
$schema = call_user_func($this->schemaBuilders[$name]);

$this->addSchema((string) $name, $schema);
} else {
throw new NotFoundHttpException(sprintf('Could not find "%s" schema.', $name));
}

return $schema;
}

public function getSchemasNames(): array
{
return array_keys($this->schemas);
return array_merge(array_keys($this->schemaBuilders), array_keys($this->schemas));
}

public function setMaxQueryDepth(int $maxQueryDepth): void
Expand Down Expand Up @@ -199,6 +211,10 @@ private function postExecute(ExecutionResult $result, ExecutorArgumentsEvent $ex

public function reset(): void
{
$this->schemas = [];
// Remove only ExtensibleSchema and isResettable
$this->schemas = array_filter(
$this->schemas,
fn (Schema $schema) => $schema instanceof ExtensibleSchema && !$schema->isResettable()
);
}
}
26 changes: 16 additions & 10 deletions src/Resolver/AbstractResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,22 @@

namespace Overblog\GraphQLBundle\Resolver;

use Symfony\Contracts\Service\ResetInterface;

use function array_keys;

abstract class AbstractResolver implements FluentResolverInterface
abstract class AbstractResolver implements FluentResolverInterface, ResetInterface
{
private array $solutionsFactory = [];
private array $solutions = [];
private array $aliases = [];
private array $solutionOptions = [];
private array $fullyLoadedSolutions = [];

public function addSolution(string $id, callable $factory, array $aliases = [], array $options = []): self
{
$this->fullyLoadedSolutions[$id] = false;
$this->addAliases($id, $aliases);

$this->solutions[$id] = $factory;
$this->solutionsFactory[$id] = $factory;
$this->solutionOptions[$id] = $options;

return $this;
Expand All @@ -28,7 +29,7 @@ public function hasSolution(string $id): bool
{
$id = $this->resolveAlias($id);

return isset($this->solutions[$id]);
return isset($this->solutionsFactory[$id]);
}

/**
Expand Down Expand Up @@ -81,13 +82,13 @@ private function loadSolution(string $id)
return null;
}

if ($this->fullyLoadedSolutions[$id]) {
if (isset($this->solutions[$id])) {
return $this->solutions[$id];
}
$loader = $this->solutions[$id];

$loader = $this->solutionsFactory[$id];
$this->solutions[$id] = $solution = $loader();
$this->onLoadSolution($solution);
$this->fullyLoadedSolutions[$id] = true;

return $solution;
}
Expand All @@ -109,10 +110,15 @@ private function resolveAlias(string $alias): string
*/
private function loadSolutions(): array
{
foreach ($this->solutions as $name => &$solution) {
$solution = $this->loadSolution($name);
foreach (array_keys($this->solutionsFactory) as $name) {
$this->loadSolution($name);
}

return $this->solutions;
}

public function reset(): void
{
$this->solutions = [];
}
}
7 changes: 7 additions & 0 deletions src/Resolver/TypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ public function setCurrentSchemaName(?string $currentSchemaName): void
$this->currentSchemaName = $currentSchemaName;
}

public function getCurrentSchemaName(): ?string
{
return $this->currentSchemaName;
}

public function setIgnoreUnresolvableException(bool $ignoreUnresolvableException): void
{
$this->ignoreUnresolvableException = $ignoreUnresolvableException;
Expand Down Expand Up @@ -78,6 +83,8 @@ private function baseType(string $alias): ?Type

public function reset(): void
{
parent::reset();

$this->cache = [];
}

Expand Down
2 changes: 2 additions & 0 deletions src/Resources/config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ services:
arguments:
- '@Overblog\GraphQLBundle\Resolver\TypeResolver'
- false
tags:
- { name: 'kernel.reset', 'method': "reset" }

Overblog\GraphQLBundle\Definition\Builder\TypeFactory:
arguments:
Expand Down
25 changes: 25 additions & 0 deletions tests/Config/Parser/TestMetadataParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,31 @@ public function testInvalidProviderMutationOnQuery(): void
}
}

public function testInputWithIsPublic(): void
{
$this->expect('PublicFieldInput', 'input-object', [
'fields' => [
'restrictedField' => [
'type' => 'String!',
'public' => '@=isAuthenticated()',
],
'publicField' => ['type' => 'Int'],
],
]);
}

public function testInvalidIsPublicWithoutFieldOnInput(): void
{
try {
$file = __DIR__.'/fixtures/annotations/Invalid/InvalidIsPublicOnInput.php';
$this->parser('parse', new SplFileInfo($file), $this->containerBuilder, $this->parserConfig);
$this->fail('#[IsPublic] on an Input field without #[Field] should raise an exception');
} catch (Exception $e) {
$this->assertInstanceOf(InvalidArgumentException::class, $e);
$this->assertMatchesRegularExpression('/The metadatas '.$this->formatMetadata('Visible').' defined on "field"/', $e->getPrevious()->getMessage());
}
}

public function testInvalidPhpFiles(): void
{
$files = [
Expand Down
Loading
Loading