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
23 changes: 23 additions & 0 deletions docs/definitions/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,30 @@ overblog_graphql:
# by graphql-php during static schema analysis.
# These types names should be explicitly declare here
types: []
# when true, the built schema is discarded whenever the
# container is reset (e.g. between requests in long-running
# workers like Symfony Runtime, RoadRunner or FrankenPHP
# worker mode). Useful when the schema depends on request
# scoped state. Defaults to false.
resettable: false
```

## Resettable schemas

By default, schemas are built once per worker and reused across requests. When running Symfony in a long-running process (Symfony Runtime, RoadRunner, FrankenPHP worker mode, Swoole, etc.) and the schema depends on request scoped state, set `resettable: true` so the schema is rebuilt after each `kernel.reset`:

```yaml
overblog_graphql:
definitions:
schema:
default:
query: Query
mutation: Mutation
resettable: true
```

Non-resettable schemas (the default) are kept across resets, which preserves the build cost between requests.

## Batching


Expand All @@ -93,6 +115,7 @@ overblog_graphql:
bar:
query: barQuery
mutation: barMutation
resettable: true
```

**foo** schema endpoint can be access:
Expand Down
37 changes: 37 additions & 0 deletions docs/security/fields-public-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,40 @@ AnObject:
Have you noticed `typeName` and `fieldName` here? These variables are always set to the current
type name and current field name, meaning you can apply a per field `public` setting on all the
fields with one line of yaml.

## Input object fields

`public` is also supported on `input-object` fields. When the expression resolves to `false`,
the field is removed from the input type — it is hidden from introspection and rejected if a
client tries to submit it.

```yaml
HeroInput:
type: input-object
config:
fields:
name:
type: "String!"
internalNote:
type: "String"
public: "@=isGranted('ROLE_ADMIN')"
```

With attributes, combine `#[GQL\Field]` with `#[GQL\IsPublic]` on the property:

```php
<?php

use Overblog\GraphQLBundle\Annotation as GQL;

#[GQL\Input]
class HeroInput
{
#[GQL\Field(type: "String!")]
public string $name;

#[GQL\Field(type: "String")]
#[GQL\IsPublic("isGranted('ROLE_ADMIN')")]
public ?string $internalNote = null;
}
```
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 @@ -252,6 +252,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()
);
}
}
Loading
Loading