Skip to content

Custom InputType Validator for #[Input] Types #435

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

Merged
merged 12 commits into from
Mar 20, 2022
Merged
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 .github/workflows/continuous_integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
strategy:
matrix:
install-args: ['', '--prefer-lowest']
php-version: ['7.2', '7.3', '7.4', '8.0']
php-version: ['7.4', '8.0']
fail-fast: false

steps:
Expand Down
9 changes: 8 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
}
],
"require": {
"php": ">=7.2",
"php": ">=7.4",
"ext-json": "*",
"doctrine/annotations": "^1.13",
"composer/package-versions-deprecated": "^1.8",
Expand Down Expand Up @@ -67,5 +67,12 @@
"branch-alias": {
"dev-master": "5.0.x-dev"
}
},
"config": {
"allow-plugins": {
"composer/package-versions-deprecated": true,
"dealerdirect/phpcodesniffer-composer-installer": true,
"phpstan/extension-installer": true
}
}
}
10 changes: 10 additions & 0 deletions src/FactoryContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Psr\Container\ContainerInterface;
use Psr\SimpleCache\CacheInterface;
use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface;
use TheCodingMachine\GraphQLite\Types\InputTypeValidatorInterface;
use TheCodingMachine\GraphQLite\Types\TypeResolver;

/**
Expand Down Expand Up @@ -36,6 +37,8 @@ final class FactoryContext
private $container;
/** @var CacheInterface */
private $cache;
/** @var InputTypeValidatorInterface|null */
private $inputTypeValidator;
/** @var int|null */
private $globTTL;
/** @var int|null */
Expand All @@ -52,6 +55,7 @@ public function __construct(
RecursiveTypeMapperInterface $recursiveTypeMapper,
ContainerInterface $container,
CacheInterface $cache,
?InputTypeValidatorInterface $inputTypeValidator,
?int $globTTL,
?int $mapTTL = null
) {
Expand All @@ -65,6 +69,7 @@ public function __construct(
$this->recursiveTypeMapper = $recursiveTypeMapper;
$this->container = $container;
$this->cache = $cache;
$this->inputTypeValidator = $inputTypeValidator;
$this->globTTL = $globTTL;
$this->mapTTL = $mapTTL;
}
Expand Down Expand Up @@ -119,6 +124,11 @@ public function getCache(): CacheInterface
return $this->cache;
}

public function getInputTypeValidator(): ?InputTypeValidatorInterface
{
return $this->inputTypeValidator;
}

public function getGlobTTL(): ?int
{
return $this->globTTL;
Expand Down
16 changes: 14 additions & 2 deletions src/InputTypeGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use ReflectionFunctionAbstract;
use ReflectionMethod;
use TheCodingMachine\GraphQLite\Types\InputType;
use TheCodingMachine\GraphQLite\Types\InputTypeValidatorInterface;
use TheCodingMachine\GraphQLite\Types\ResolvableMutableInputInterface;
use TheCodingMachine\GraphQLite\Types\ResolvableMutableInputObjectType;
use Webmozart\Assert\Assert;
Expand All @@ -28,13 +29,17 @@ class InputTypeGenerator
private $inputTypeUtils;
/** @var FieldsBuilder */
private $fieldsBuilder;
/** @var InputTypeValidatorInterface|null */
private $inputTypeValidator;

public function __construct(
InputTypeUtils $inputTypeUtils,
FieldsBuilder $fieldsBuilder
FieldsBuilder $fieldsBuilder,
?InputTypeValidatorInterface $inputTypeValidator = null
) {
$this->inputTypeUtils = $inputTypeUtils;
$this->fieldsBuilder = $fieldsBuilder;
$this->inputTypeValidator = $inputTypeValidator;
}

public function mapFactoryMethod(string $factory, string $methodName, ContainerInterface $container): ResolvableMutableInputObjectType
Expand Down Expand Up @@ -63,7 +68,14 @@ public function mapFactoryMethod(string $factory, string $methodName, ContainerI
public function mapInput(string $className, string $inputName, ?string $description, bool $isUpdate): InputType
{
if (! isset($this->inputCache[$inputName])) {
$this->inputCache[$inputName] = new InputType($className, $inputName, $description, $isUpdate, $this->fieldsBuilder);
$this->inputCache[$inputName] = new InputType(
$className,
$inputName,
$description,
$isUpdate,
$this->fieldsBuilder,
$this->inputTypeValidator
);
}

return $this->inputCache[$inputName];
Expand Down
112 changes: 68 additions & 44 deletions src/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
use TheCodingMachine\GraphQLite\Security\FailAuthorizationService;
use TheCodingMachine\GraphQLite\Security\SecurityExpressionLanguageProvider;
use TheCodingMachine\GraphQLite\Types\ArgumentResolver;
use TheCodingMachine\GraphQLite\Types\InputTypeValidatorInterface;
use TheCodingMachine\GraphQLite\Types\TypeResolver;
use TheCodingMachine\GraphQLite\Utils\NamespacedCache;
use TheCodingMachine\GraphQLite\Utils\Namespaces\NamespaceFactory;
Expand All @@ -63,48 +64,61 @@
*/
class SchemaFactory
{

public const GLOB_CACHE_SECONDS = 2;


/** @var string[] */
private $controllerNamespaces = [];
private array $controllerNamespaces = [];

/** @var string[] */
private $typeNamespaces = [];
private array $typeNamespaces = [];

/** @var QueryProviderInterface[] */
private $queryProviders = [];
private array $queryProviders = [];

/** @var QueryProviderFactoryInterface[] */
private $queryProviderFactories = [];
private array $queryProviderFactories = [];

/** @var RootTypeMapperFactoryInterface[] */
private $rootTypeMapperFactories = [];
private array $rootTypeMapperFactories = [];

/** @var TypeMapperInterface[] */
private $typeMappers = [];
private array $typeMappers = [];

/** @var TypeMapperFactoryInterface[] */
private $typeMapperFactories = [];
private array $typeMapperFactories = [];

/** @var ParameterMiddlewareInterface[] */
private $parameterMiddlewares = [];
/** @var Reader */
private $doctrineAnnotationReader;
/** @var AuthenticationServiceInterface|null */
private $authenticationService;
/** @var AuthorizationServiceInterface|null */
private $authorizationService;
/** @var CacheInterface */
private $cache;
/** @var NamingStrategyInterface|null */
private $namingStrategy;
/** @var ContainerInterface */
private $container;
/** @var ClassNameMapper */
private $classNameMapper;
/** @var SchemaConfig */
private $schemaConfig;
/** @var int|null */
private $globTTL = self::GLOB_CACHE_SECONDS;
private array $parameterMiddlewares = [];

private ?Reader $doctrineAnnotationReader = null;

private ?AuthenticationServiceInterface $authenticationService = null;

private ?AuthorizationServiceInterface $authorizationService = null;

private ?InputTypeValidatorInterface $inputTypeValidator = null;

private CacheInterface $cache;

private ?NamingStrategyInterface $namingStrategy = null;

private ContainerInterface $container;

private ?ClassNameMapper $classNameMapper = null;

private ?SchemaConfig $schemaConfig = null;

private ?int $globTTL = self::GLOB_CACHE_SECONDS;

/** @var array<int, FieldMiddlewareInterface> */
private $fieldMiddlewares = [];
/** @var ExpressionLanguage|null */
private $expressionLanguage;
/** @var string */
private $cacheNamespace;
private array $fieldMiddlewares = [];

private ?ExpressionLanguage $expressionLanguage = null;

private string $cacheNamespace;


public function __construct(CacheInterface $cache, ContainerInterface $container)
{
Expand Down Expand Up @@ -229,6 +243,14 @@ public function setAuthorizationService(AuthorizationServiceInterface $authoriza
return $this;
}

public function setInputTypeValidator(?InputTypeValidatorInterface $inputTypeValidator): self
{
$this->inputTypeValidator = $inputTypeValidator;

return $this;
}


public function setNamingStrategy(NamingStrategyInterface $namingStrategy): self
{
$this->namingStrategy = $namingStrategy;
Expand Down Expand Up @@ -305,20 +327,21 @@ public function setExpressionLanguage(ExpressionLanguage $expressionLanguage): s

public function createSchema(): Schema
{
$symfonyCache = new Psr16Adapter($this->cache, $this->cacheNamespace);
$annotationReader = new AnnotationReader($this->getDoctrineAnnotationReader($symfonyCache), AnnotationReader::LAX_MODE);
$authenticationService = $this->authenticationService ?: new FailAuthenticationService();
$authorizationService = $this->authorizationService ?: new FailAuthorizationService();
$typeResolver = new TypeResolver();
$namespacedCache = new NamespacedCache($this->cache);
$cachedDocBlockFactory = new CachedDocBlockFactory($namespacedCache);
$namingStrategy = $this->namingStrategy ?: new NamingStrategy();
$typeRegistry = new TypeRegistry();
$symfonyCache = new Psr16Adapter($this->cache, $this->cacheNamespace);
$annotationReader = new AnnotationReader($this->getDoctrineAnnotationReader($symfonyCache), AnnotationReader::LAX_MODE);
$authenticationService = $this->authenticationService ?: new FailAuthenticationService();
$authorizationService = $this->authorizationService ?: new FailAuthorizationService();
$typeResolver = new TypeResolver();
$namespacedCache = new NamespacedCache($this->cache);
$cachedDocBlockFactory = new CachedDocBlockFactory($namespacedCache);
$namingStrategy = $this->namingStrategy ?: new NamingStrategy();
$typeRegistry = new TypeRegistry();

$namespaceFactory = new NamespaceFactory($namespacedCache, $this->classNameMapper, $this->globTTL);
$nsList = array_map(static function (string $namespace) use ($namespaceFactory) {
return $namespaceFactory->createNamespace($namespace);
}, $this->typeNamespaces);
$nsList = array_map(
static fn (string $namespace) => $namespaceFactory->createNamespace($namespace),
$this->typeNamespaces
);

$expressionLanguage = $this->expressionLanguage ?: new ExpressionLanguage($symfonyCache);
$expressionLanguage->registerProvider(new SecurityExpressionLanguageProvider());
Expand Down Expand Up @@ -389,7 +412,7 @@ public function createSchema(): Schema

$typeGenerator = new TypeGenerator($annotationReader, $namingStrategy, $typeRegistry, $this->container, $recursiveTypeMapper, $fieldsBuilder);
$inputTypeUtils = new InputTypeUtils($annotationReader, $namingStrategy);
$inputTypeGenerator = new InputTypeGenerator($inputTypeUtils, $fieldsBuilder);
$inputTypeGenerator = new InputTypeGenerator($inputTypeUtils, $fieldsBuilder, $this->inputTypeValidator);

if (empty($this->typeNamespaces) && empty($this->typeMappers)) {
throw new GraphQLRuntimeException('Cannot create schema: no namespace for types found (You must call the SchemaFactory::addTypeNamespace() at least once).');
Expand Down Expand Up @@ -426,6 +449,7 @@ public function createSchema(): Schema
$recursiveTypeMapper,
$this->container,
$namespacedCache,
$this->inputTypeValidator,
$this->globTTL
);
}
Expand Down
19 changes: 16 additions & 3 deletions src/Types/InputType.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,24 @@
class InputType extends MutableInputObjectType implements ResolvableMutableInputInterface
{
/** @var InputTypeProperty[] */
private $fields;
private array $fields;

/** @var class-string<object> */
private $className;

private ?InputTypeValidatorInterface $inputTypeValidator;

/**
* @param class-string<object> $className
*/
public function __construct(string $className, string $inputName, ?string $description, bool $isUpdate, FieldsBuilder $fieldsBuilder)
{
public function __construct(
string $className,
string $inputName,
?string $description,
bool $isUpdate,
FieldsBuilder $fieldsBuilder,
?InputTypeValidatorInterface $inputTypeValidator = null
) {
$reflection = new ReflectionClass($className);
if (! $reflection->isInstantiable()) {
throw FailedResolvingInputType::createForNotInstantiableClass($className);
Expand Down Expand Up @@ -71,6 +79,7 @@ public function __construct(string $className, string $inputName, ?string $descr

parent::__construct($config);
$this->className = $className;
$this->inputTypeValidator = $inputTypeValidator;
}

/**
Expand All @@ -96,6 +105,10 @@ public function resolve(?object $source, array $args, $context, ResolveInfo $res
PropertyAccessor::setValue($instance, $property, $value);
}

if ($this->inputTypeValidator && $this->inputTypeValidator->isEnabled()) {
$this->inputTypeValidator->validate($instance);
}

return $instance;
}

Expand Down
26 changes: 26 additions & 0 deletions src/Types/InputTypeValidatorInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Types;

/**
* Used for validating InputTypes
* An implementation of this interface can be registered with the SchemaFactory.
*
* @author Jacob Thomason <jacob@thomason.xxx>
*/
interface InputTypeValidatorInterface
{
/**
* Checks to see if the Validator is currently enabled.
*/
public function isEnabled(): bool;

/**
* Performs the validation of the InputType.
*
* @param object $input The input type object to validate
*/
public function validate(object $input): void;
}
6 changes: 4 additions & 2 deletions tests/FactoryContextTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@

namespace TheCodingMachine\GraphQLite;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Psr16Cache;
use Symfony\Component\Cache\Simple\ArrayCache;
use TheCodingMachine\GraphQLite\Containers\EmptyContainer;
use TheCodingMachine\GraphQLite\Fixtures\Inputs\Validator;

class FactoryContextTest extends AbstractQueryProviderTest
{
Expand All @@ -17,6 +16,7 @@ public function testContext(): void
$namingStrategy = new NamingStrategy();
$container = new EmptyContainer();
$arrayCache = new Psr16Cache(new ArrayAdapter());
$validator = new Validator();

$context = new FactoryContext(
$this->getAnnotationReader(),
Expand All @@ -29,6 +29,7 @@ public function testContext(): void
$this->getTypeMapper(),
$container,
$arrayCache,
$validator,
self::GLOB_TTL_SECONDS
);

Expand All @@ -42,6 +43,7 @@ public function testContext(): void
$this->assertSame($this->getTypeMapper(), $context->getRecursiveTypeMapper());
$this->assertSame($container, $context->getContainer());
$this->assertSame($arrayCache, $context->getCache());
$this->assertSame($validator, $context->getInputTypeValidator());
$this->assertSame(self::GLOB_TTL_SECONDS, $context->getGlobTTL());
$this->assertNull($context->getMapTTL());
}
Expand Down
Loading