Skip to content

Commit 011c7e0

Browse files
authored
Custom InputType Validator for #[Input] Types (#435)
* Due to typing that exists in numerous files, PHP 7.4 is required * PHPUnit 9.3 introduced configuration changes - updated config and required version * Implemented an InputTypeValidatorInterface * Removed assignment of InputTypeValidator to GlobTypeMapper * Upgraded Docusaurus version * Added and updated documentation * Remove PHP 7.2 and 7.3 from CI workflows * Revert "PHPUnit 9.3 introduced configuration changes - updated config and required version" This reverts commit 9411f6b. * Revert Docusaurus version * Upgraded Docusaurus: facebook/docusaurus#6337 * Improved documentation * Duplicate class name typo
1 parent 5f06f8e commit 011c7e0

16 files changed

+502
-259
lines changed

.github/workflows/continuous_integration.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
strategy:
1919
matrix:
2020
install-args: ['', '--prefer-lowest']
21-
php-version: ['7.2', '7.3', '7.4', '8.0']
21+
php-version: ['7.4', '8.0']
2222
fail-fast: false
2323

2424
steps:

composer.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
}
1111
],
1212
"require": {
13-
"php": ">=7.2",
13+
"php": ">=7.4",
1414
"ext-json": "*",
1515
"doctrine/annotations": "^1.13",
1616
"composer/package-versions-deprecated": "^1.8",
@@ -67,5 +67,12 @@
6767
"branch-alias": {
6868
"dev-master": "5.0.x-dev"
6969
}
70+
},
71+
"config": {
72+
"allow-plugins": {
73+
"composer/package-versions-deprecated": true,
74+
"dealerdirect/phpcodesniffer-composer-installer": true,
75+
"phpstan/extension-installer": true
76+
}
7077
}
7178
}

src/FactoryContext.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Psr\Container\ContainerInterface;
88
use Psr\SimpleCache\CacheInterface;
99
use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface;
10+
use TheCodingMachine\GraphQLite\Types\InputTypeValidatorInterface;
1011
use TheCodingMachine\GraphQLite\Types\TypeResolver;
1112

1213
/**
@@ -36,6 +37,8 @@ final class FactoryContext
3637
private $container;
3738
/** @var CacheInterface */
3839
private $cache;
40+
/** @var InputTypeValidatorInterface|null */
41+
private $inputTypeValidator;
3942
/** @var int|null */
4043
private $globTTL;
4144
/** @var int|null */
@@ -52,6 +55,7 @@ public function __construct(
5255
RecursiveTypeMapperInterface $recursiveTypeMapper,
5356
ContainerInterface $container,
5457
CacheInterface $cache,
58+
?InputTypeValidatorInterface $inputTypeValidator,
5559
?int $globTTL,
5660
?int $mapTTL = null
5761
) {
@@ -65,6 +69,7 @@ public function __construct(
6569
$this->recursiveTypeMapper = $recursiveTypeMapper;
6670
$this->container = $container;
6771
$this->cache = $cache;
72+
$this->inputTypeValidator = $inputTypeValidator;
6873
$this->globTTL = $globTTL;
6974
$this->mapTTL = $mapTTL;
7075
}
@@ -119,6 +124,11 @@ public function getCache(): CacheInterface
119124
return $this->cache;
120125
}
121126

127+
public function getInputTypeValidator(): ?InputTypeValidatorInterface
128+
{
129+
return $this->inputTypeValidator;
130+
}
131+
122132
public function getGlobTTL(): ?int
123133
{
124134
return $this->globTTL;

src/InputTypeGenerator.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use ReflectionFunctionAbstract;
1010
use ReflectionMethod;
1111
use TheCodingMachine\GraphQLite\Types\InputType;
12+
use TheCodingMachine\GraphQLite\Types\InputTypeValidatorInterface;
1213
use TheCodingMachine\GraphQLite\Types\ResolvableMutableInputInterface;
1314
use TheCodingMachine\GraphQLite\Types\ResolvableMutableInputObjectType;
1415
use Webmozart\Assert\Assert;
@@ -28,13 +29,17 @@ class InputTypeGenerator
2829
private $inputTypeUtils;
2930
/** @var FieldsBuilder */
3031
private $fieldsBuilder;
32+
/** @var InputTypeValidatorInterface|null */
33+
private $inputTypeValidator;
3134

3235
public function __construct(
3336
InputTypeUtils $inputTypeUtils,
34-
FieldsBuilder $fieldsBuilder
37+
FieldsBuilder $fieldsBuilder,
38+
?InputTypeValidatorInterface $inputTypeValidator = null
3539
) {
3640
$this->inputTypeUtils = $inputTypeUtils;
3741
$this->fieldsBuilder = $fieldsBuilder;
42+
$this->inputTypeValidator = $inputTypeValidator;
3843
}
3944

4045
public function mapFactoryMethod(string $factory, string $methodName, ContainerInterface $container): ResolvableMutableInputObjectType
@@ -63,7 +68,14 @@ public function mapFactoryMethod(string $factory, string $methodName, ContainerI
6368
public function mapInput(string $className, string $inputName, ?string $description, bool $isUpdate): InputType
6469
{
6570
if (! isset($this->inputCache[$inputName])) {
66-
$this->inputCache[$inputName] = new InputType($className, $inputName, $description, $isUpdate, $this->fieldsBuilder);
71+
$this->inputCache[$inputName] = new InputType(
72+
$className,
73+
$inputName,
74+
$description,
75+
$isUpdate,
76+
$this->fieldsBuilder,
77+
$this->inputTypeValidator
78+
);
6779
}
6880

6981
return $this->inputCache[$inputName];

src/SchemaFactory.php

Lines changed: 68 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
use TheCodingMachine\GraphQLite\Security\FailAuthorizationService;
4848
use TheCodingMachine\GraphQLite\Security\SecurityExpressionLanguageProvider;
4949
use TheCodingMachine\GraphQLite\Types\ArgumentResolver;
50+
use TheCodingMachine\GraphQLite\Types\InputTypeValidatorInterface;
5051
use TheCodingMachine\GraphQLite\Types\TypeResolver;
5152
use TheCodingMachine\GraphQLite\Utils\NamespacedCache;
5253
use TheCodingMachine\GraphQLite\Utils\Namespaces\NamespaceFactory;
@@ -63,48 +64,61 @@
6364
*/
6465
class SchemaFactory
6566
{
67+
6668
public const GLOB_CACHE_SECONDS = 2;
6769

70+
6871
/** @var string[] */
69-
private $controllerNamespaces = [];
72+
private array $controllerNamespaces = [];
73+
7074
/** @var string[] */
71-
private $typeNamespaces = [];
75+
private array $typeNamespaces = [];
76+
7277
/** @var QueryProviderInterface[] */
73-
private $queryProviders = [];
78+
private array $queryProviders = [];
79+
7480
/** @var QueryProviderFactoryInterface[] */
75-
private $queryProviderFactories = [];
81+
private array $queryProviderFactories = [];
82+
7683
/** @var RootTypeMapperFactoryInterface[] */
77-
private $rootTypeMapperFactories = [];
84+
private array $rootTypeMapperFactories = [];
85+
7886
/** @var TypeMapperInterface[] */
79-
private $typeMappers = [];
87+
private array $typeMappers = [];
88+
8089
/** @var TypeMapperFactoryInterface[] */
81-
private $typeMapperFactories = [];
90+
private array $typeMapperFactories = [];
91+
8292
/** @var ParameterMiddlewareInterface[] */
83-
private $parameterMiddlewares = [];
84-
/** @var Reader */
85-
private $doctrineAnnotationReader;
86-
/** @var AuthenticationServiceInterface|null */
87-
private $authenticationService;
88-
/** @var AuthorizationServiceInterface|null */
89-
private $authorizationService;
90-
/** @var CacheInterface */
91-
private $cache;
92-
/** @var NamingStrategyInterface|null */
93-
private $namingStrategy;
94-
/** @var ContainerInterface */
95-
private $container;
96-
/** @var ClassNameMapper */
97-
private $classNameMapper;
98-
/** @var SchemaConfig */
99-
private $schemaConfig;
100-
/** @var int|null */
101-
private $globTTL = self::GLOB_CACHE_SECONDS;
93+
private array $parameterMiddlewares = [];
94+
95+
private ?Reader $doctrineAnnotationReader = null;
96+
97+
private ?AuthenticationServiceInterface $authenticationService = null;
98+
99+
private ?AuthorizationServiceInterface $authorizationService = null;
100+
101+
private ?InputTypeValidatorInterface $inputTypeValidator = null;
102+
103+
private CacheInterface $cache;
104+
105+
private ?NamingStrategyInterface $namingStrategy = null;
106+
107+
private ContainerInterface $container;
108+
109+
private ?ClassNameMapper $classNameMapper = null;
110+
111+
private ?SchemaConfig $schemaConfig = null;
112+
113+
private ?int $globTTL = self::GLOB_CACHE_SECONDS;
114+
102115
/** @var array<int, FieldMiddlewareInterface> */
103-
private $fieldMiddlewares = [];
104-
/** @var ExpressionLanguage|null */
105-
private $expressionLanguage;
106-
/** @var string */
107-
private $cacheNamespace;
116+
private array $fieldMiddlewares = [];
117+
118+
private ?ExpressionLanguage $expressionLanguage = null;
119+
120+
private string $cacheNamespace;
121+
108122

109123
public function __construct(CacheInterface $cache, ContainerInterface $container)
110124
{
@@ -229,6 +243,14 @@ public function setAuthorizationService(AuthorizationServiceInterface $authoriza
229243
return $this;
230244
}
231245

246+
public function setInputTypeValidator(?InputTypeValidatorInterface $inputTypeValidator): self
247+
{
248+
$this->inputTypeValidator = $inputTypeValidator;
249+
250+
return $this;
251+
}
252+
253+
232254
public function setNamingStrategy(NamingStrategyInterface $namingStrategy): self
233255
{
234256
$this->namingStrategy = $namingStrategy;
@@ -305,20 +327,21 @@ public function setExpressionLanguage(ExpressionLanguage $expressionLanguage): s
305327

306328
public function createSchema(): Schema
307329
{
308-
$symfonyCache = new Psr16Adapter($this->cache, $this->cacheNamespace);
309-
$annotationReader = new AnnotationReader($this->getDoctrineAnnotationReader($symfonyCache), AnnotationReader::LAX_MODE);
310-
$authenticationService = $this->authenticationService ?: new FailAuthenticationService();
311-
$authorizationService = $this->authorizationService ?: new FailAuthorizationService();
312-
$typeResolver = new TypeResolver();
313-
$namespacedCache = new NamespacedCache($this->cache);
314-
$cachedDocBlockFactory = new CachedDocBlockFactory($namespacedCache);
315-
$namingStrategy = $this->namingStrategy ?: new NamingStrategy();
316-
$typeRegistry = new TypeRegistry();
330+
$symfonyCache = new Psr16Adapter($this->cache, $this->cacheNamespace);
331+
$annotationReader = new AnnotationReader($this->getDoctrineAnnotationReader($symfonyCache), AnnotationReader::LAX_MODE);
332+
$authenticationService = $this->authenticationService ?: new FailAuthenticationService();
333+
$authorizationService = $this->authorizationService ?: new FailAuthorizationService();
334+
$typeResolver = new TypeResolver();
335+
$namespacedCache = new NamespacedCache($this->cache);
336+
$cachedDocBlockFactory = new CachedDocBlockFactory($namespacedCache);
337+
$namingStrategy = $this->namingStrategy ?: new NamingStrategy();
338+
$typeRegistry = new TypeRegistry();
317339

318340
$namespaceFactory = new NamespaceFactory($namespacedCache, $this->classNameMapper, $this->globTTL);
319-
$nsList = array_map(static function (string $namespace) use ($namespaceFactory) {
320-
return $namespaceFactory->createNamespace($namespace);
321-
}, $this->typeNamespaces);
341+
$nsList = array_map(
342+
static fn (string $namespace) => $namespaceFactory->createNamespace($namespace),
343+
$this->typeNamespaces
344+
);
322345

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

390413
$typeGenerator = new TypeGenerator($annotationReader, $namingStrategy, $typeRegistry, $this->container, $recursiveTypeMapper, $fieldsBuilder);
391414
$inputTypeUtils = new InputTypeUtils($annotationReader, $namingStrategy);
392-
$inputTypeGenerator = new InputTypeGenerator($inputTypeUtils, $fieldsBuilder);
415+
$inputTypeGenerator = new InputTypeGenerator($inputTypeUtils, $fieldsBuilder, $this->inputTypeValidator);
393416

394417
foreach ($nsList as $ns) {
395418
$compositeTypeMapper->addTypeMapper(new GlobTypeMapper(
@@ -418,6 +441,7 @@ public function createSchema(): Schema
418441
$recursiveTypeMapper,
419442
$this->container,
420443
$namespacedCache,
444+
$this->inputTypeValidator,
421445
$this->globTTL
422446
);
423447
}

src/Types/InputType.php

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,24 @@
2222
class InputType extends MutableInputObjectType implements ResolvableMutableInputInterface
2323
{
2424
/** @var InputTypeProperty[] */
25-
private $fields;
25+
private array $fields;
2626

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

30+
private ?InputTypeValidatorInterface $inputTypeValidator;
31+
3032
/**
3133
* @param class-string<object> $className
3234
*/
33-
public function __construct(string $className, string $inputName, ?string $description, bool $isUpdate, FieldsBuilder $fieldsBuilder)
34-
{
35+
public function __construct(
36+
string $className,
37+
string $inputName,
38+
?string $description,
39+
bool $isUpdate,
40+
FieldsBuilder $fieldsBuilder,
41+
?InputTypeValidatorInterface $inputTypeValidator = null
42+
) {
3543
$reflection = new ReflectionClass($className);
3644
if (! $reflection->isInstantiable()) {
3745
throw FailedResolvingInputType::createForNotInstantiableClass($className);
@@ -71,6 +79,7 @@ public function __construct(string $className, string $inputName, ?string $descr
7179

7280
parent::__construct($config);
7381
$this->className = $className;
82+
$this->inputTypeValidator = $inputTypeValidator;
7483
}
7584

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

108+
if ($this->inputTypeValidator && $this->inputTypeValidator->isEnabled()) {
109+
$this->inputTypeValidator->validate($instance);
110+
}
111+
99112
return $instance;
100113
}
101114

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TheCodingMachine\GraphQLite\Types;
6+
7+
/**
8+
* Used for validating InputTypes
9+
* An implementation of this interface can be registered with the SchemaFactory.
10+
*
11+
* @author Jacob Thomason <jacob@thomason.xxx>
12+
*/
13+
interface InputTypeValidatorInterface
14+
{
15+
/**
16+
* Checks to see if the Validator is currently enabled.
17+
*/
18+
public function isEnabled(): bool;
19+
20+
/**
21+
* Performs the validation of the InputType.
22+
*
23+
* @param object $input The input type object to validate
24+
*/
25+
public function validate(object $input): void;
26+
}

tests/FactoryContextTest.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22

33
namespace TheCodingMachine\GraphQLite;
44

5-
use PHPUnit\Framework\TestCase;
65
use Symfony\Component\Cache\Adapter\ArrayAdapter;
76
use Symfony\Component\Cache\Psr16Cache;
8-
use Symfony\Component\Cache\Simple\ArrayCache;
97
use TheCodingMachine\GraphQLite\Containers\EmptyContainer;
8+
use TheCodingMachine\GraphQLite\Fixtures\Inputs\Validator;
109

1110
class FactoryContextTest extends AbstractQueryProviderTest
1211
{
@@ -17,6 +16,7 @@ public function testContext(): void
1716
$namingStrategy = new NamingStrategy();
1817
$container = new EmptyContainer();
1918
$arrayCache = new Psr16Cache(new ArrayAdapter());
19+
$validator = new Validator();
2020

2121
$context = new FactoryContext(
2222
$this->getAnnotationReader(),
@@ -29,6 +29,7 @@ public function testContext(): void
2929
$this->getTypeMapper(),
3030
$container,
3131
$arrayCache,
32+
$validator,
3233
self::GLOB_TTL_SECONDS
3334
);
3435

@@ -42,6 +43,7 @@ public function testContext(): void
4243
$this->assertSame($this->getTypeMapper(), $context->getRecursiveTypeMapper());
4344
$this->assertSame($container, $context->getContainer());
4445
$this->assertSame($arrayCache, $context->getCache());
46+
$this->assertSame($validator, $context->getInputTypeValidator());
4547
$this->assertSame(self::GLOB_TTL_SECONDS, $context->getGlobTTL());
4648
$this->assertNull($context->getMapTTL());
4749
}

0 commit comments

Comments
 (0)