Skip to content

Commit b565915

Browse files
authored
Merge pull request #12 from AurelGit/laravel-validator
WIP : Laravel Validator Implementation
2 parents e4fd6ad + 496d149 commit b565915

File tree

9 files changed

+396
-1
lines changed

9 files changed

+396
-1
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
],
1818
"require" : {
1919
"php" : ">=7.2",
20-
"thecodingmachine/graphqlite" : "~4.0.0",
20+
"thecodingmachine/graphqlite" : "^4",
2121
"illuminate/console": "^5.7|^6.0",
2222
"illuminate/container": "^5.7|^6.0",
2323
"illuminate/support": "^5.7|^6.0",

src/Annotations/Validate.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TheCodingMachine\GraphQLite\Laravel\Annotations;
6+
7+
use BadMethodCallException;
8+
use TheCodingMachine\GraphQLite\Annotations\MiddlewareAnnotationInterface;
9+
use TheCodingMachine\GraphQLite\Annotations\ParameterAnnotationInterface;
10+
use function ltrim;
11+
12+
/**
13+
* Use this annotation to validate a parameter for a query or mutation.
14+
*
15+
* @Annotation
16+
* @Target({"METHOD"})
17+
* @Attributes({
18+
* @Attribute("for", type = "string"),
19+
* @Attribute("rule", type = "string")
20+
* })
21+
*/
22+
class Validate implements ParameterAnnotationInterface
23+
{
24+
/** @var string */
25+
private $for;
26+
/** @var string */
27+
private $rule;
28+
29+
/**
30+
* @param array<string, mixed> $values
31+
*/
32+
public function __construct(array $values)
33+
{
34+
if (! isset($values['for'])) {
35+
throw new BadMethodCallException('The @Validate annotation must be passed a target. For instance: "@Validate(for="$email", rule="email")"');
36+
}
37+
if (! isset($values['rule'])) {
38+
throw new BadMethodCallException('The @Validate annotation must be passed a rule. For instance: "@Validate(for="$email", rule="email")"');
39+
}
40+
$this->for = ltrim($values['for'], '$');
41+
$this->rule = $values['rule'] ?? null;
42+
}
43+
44+
public function getTarget(): string
45+
{
46+
return $this->for;
47+
}
48+
49+
public function getRule(): string
50+
{
51+
return $this->rule;
52+
}
53+
}

src/Exceptions/ValidateException.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace TheCodingMachine\GraphQLite\Laravel\Exceptions;
4+
5+
use GraphQL\Error\ClientAware;
6+
use TheCodingMachine\GraphQLite\Exceptions\GraphQLExceptionInterface;
7+
8+
class ValidateException extends \Exception implements GraphQLExceptionInterface
9+
{
10+
/**
11+
* @var string
12+
*/
13+
private $argumentName;
14+
15+
public static function create(string $message, string $argumentName)
16+
{
17+
$exception = new self($message, 400);
18+
$exception->argumentName = $argumentName;
19+
return $exception;
20+
}
21+
22+
/**
23+
* @return bool
24+
*/
25+
public function isClientSafe()
26+
{
27+
return true;
28+
}
29+
30+
/**
31+
* @return string
32+
*/
33+
public function getCategory()
34+
{
35+
return 'Validate';
36+
}
37+
38+
39+
/**
40+
* Returns the "extensions" object attached to the GraphQL error.
41+
*
42+
* @return array<string, mixed>
43+
*/
44+
public function getExtensions(): array
45+
{
46+
return [
47+
'argument' => $this->argumentName
48+
];
49+
}
50+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
4+
namespace TheCodingMachine\GraphQLite\Laravel\Mappers\Parameters;
5+
6+
7+
use ReflectionParameter;
8+
9+
class InvalidValidateAnnotationException extends \Exception
10+
{
11+
public static function canOnlyValidateInputType(ReflectionParameter $refParameter): self
12+
{
13+
$class = $refParameter->getDeclaringClass();
14+
$method = $refParameter->getDeclaringFunction();
15+
return new self('In method '.$class.'::'.$method.', the @Validate annotation is targeting parameter $'.$refParameter->getName().'. You cannot target this parameter because it is not part of the GraphQL Input type. You can only validate parameters coming from the end user.');
16+
}
17+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
4+
namespace TheCodingMachine\GraphQLite\Laravel\Mappers\Parameters;
5+
6+
7+
use GraphQL\Type\Definition\InputType;
8+
use GraphQL\Type\Definition\ResolveInfo;
9+
use Illuminate\Validation\Factory as ValidationFactory;
10+
use TheCodingMachine\GraphQLite\Exceptions\GraphQLAggregateException;
11+
use TheCodingMachine\GraphQLite\Laravel\Exceptions\ValidateException;
12+
use TheCodingMachine\GraphQLite\Parameters\InputTypeParameterInterface;
13+
use TheCodingMachine\GraphQLite\Parameters\ParameterInterface;
14+
use function array_combine;
15+
use function array_keys;
16+
use function implode;
17+
18+
class ParameterValidator implements InputTypeParameterInterface
19+
{
20+
/**
21+
* @var InputTypeParameterInterface
22+
*/
23+
private $parameter;
24+
/**
25+
* @var string
26+
*/
27+
private $rules;
28+
/**
29+
* @var ValidationFactory
30+
*/
31+
private $validationFactory;
32+
/**
33+
* @var string
34+
*/
35+
private $parameterName;
36+
37+
public function __construct(InputTypeParameterInterface $parameter, string $parameterName, string $rules, ValidationFactory $validationFactory)
38+
{
39+
$this->parameter = $parameter;
40+
$this->rules = $rules;
41+
$this->validationFactory = $validationFactory;
42+
$this->parameterName = $parameterName;
43+
}
44+
45+
46+
/**
47+
* @param array<string, mixed> $args
48+
* @param mixed $context
49+
*
50+
* @return mixed
51+
*/
52+
public function resolve(?object $source, array $args, $context, ResolveInfo $info)
53+
{
54+
$value = $this->parameter->resolve($source, $args, $context, $info);
55+
56+
$validator = $this->validationFactory->make([$this->parameterName => $value], [$this->parameterName => $this->rules]);
57+
58+
if ($validator->fails()) {
59+
$errorMessages = [];
60+
foreach ($validator->errors()->toArray() as $field => $errors) {
61+
foreach ($errors as $error) {
62+
$errorMessages[] = ValidateException::create($error, $field);
63+
}
64+
}
65+
GraphQLAggregateException::throwExceptions($errorMessages);
66+
}
67+
68+
return $value;
69+
}
70+
71+
public function getType(): InputType
72+
{
73+
return $this->parameter->getType();
74+
}
75+
76+
public function hasDefaultValue(): bool
77+
{
78+
return $this->parameter->hasDefaultValue();
79+
}
80+
81+
/**
82+
* @return mixed
83+
*/
84+
public function getDefaultValue()
85+
{
86+
return $this->parameter->getDefaultValue();
87+
}
88+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TheCodingMachine\GraphQLite\Laravel\Mappers\Parameters;
6+
7+
use Closure;
8+
use GraphQL\Type\Definition\FieldDefinition;
9+
use GraphQL\Type\Definition\NonNull;
10+
use GraphQL\Type\Definition\OutputType;
11+
use phpDocumentor\Reflection\DocBlock;
12+
use phpDocumentor\Reflection\Type;
13+
use Psr\Log\LoggerInterface;
14+
use ReflectionFunction;
15+
use ReflectionParameter;
16+
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
17+
use TheCodingMachine\GraphQLite\Annotations\FailWith;
18+
use TheCodingMachine\GraphQLite\Annotations\ParameterAnnotations;
19+
use TheCodingMachine\GraphQLite\Annotations\Security;
20+
use TheCodingMachine\GraphQLite\Laravel\Annotations\Validate;
21+
use TheCodingMachine\GraphQLite\Mappers\Parameters\ParameterHandlerInterface;
22+
use TheCodingMachine\GraphQLite\Mappers\Parameters\ParameterMiddlewareInterface;
23+
use TheCodingMachine\GraphQLite\Parameters\InputTypeParameterInterface;
24+
use TheCodingMachine\GraphQLite\Parameters\ParameterInterface;
25+
use TheCodingMachine\GraphQLite\QueryFieldDescriptor;
26+
use TheCodingMachine\GraphQLite\Security\AuthenticationServiceInterface;
27+
use TheCodingMachine\GraphQLite\Middlewares\FieldMiddlewareInterface;
28+
use TheCodingMachine\GraphQLite\Security\AuthorizationServiceInterface;
29+
use TheCodingMachine\GraphQLite\Middlewares\FieldHandlerInterface;
30+
use TheCodingMachine\GraphQLite\Annotations\ParameterAnnotationInterface;
31+
use TheCodingMachine\GraphQLite\Laravel\Exceptions\ValidateException;
32+
use Throwable;
33+
use Webmozart\Assert\Assert;
34+
use function array_combine;
35+
use function array_keys;
36+
use function array_map;
37+
use function array_merge;
38+
use function implode;
39+
use function is_array;
40+
use function is_object;
41+
use Illuminate\Validation\Factory as ValidationFactory;
42+
43+
/**
44+
* A field middleware that reads "Security" Symfony annotations.
45+
*/
46+
class ValidateFieldMiddleware implements ParameterMiddlewareInterface
47+
{
48+
/**
49+
* @var ValidationFactory
50+
*/
51+
private $validationFactory;
52+
53+
public function __construct(ValidationFactory $validationFactory)
54+
{
55+
$this->validationFactory = $validationFactory;
56+
}
57+
58+
public function mapParameter(ReflectionParameter $refParameter, DocBlock $docBlock, ?Type $paramTagType, ParameterAnnotations $parameterAnnotations, ParameterHandlerInterface $next): ParameterInterface
59+
{
60+
/** @var Validate[] $validateAnnotations */
61+
$validateAnnotations = $parameterAnnotations->getAnnotationsByType(Validate::class);
62+
63+
$parameter = $next->mapParameter($refParameter, $docBlock, $paramTagType, $parameterAnnotations);
64+
65+
if (empty($validateAnnotations)) {
66+
return $parameter;
67+
}
68+
69+
if (!$parameter instanceof InputTypeParameterInterface) {
70+
throw InvalidValidateAnnotationException::canOnlyValidateInputType($refParameter);
71+
}
72+
73+
// Let's wrap the ParameterInterface into a ParameterValidator.
74+
$rules = array_map(static function(Validate $validateAnnotation): string { return $validateAnnotation->getRule(); }, $validateAnnotations);
75+
76+
return new ParameterValidator($parameter, $refParameter->getName(), implode('|', $rules), $this->validationFactory);
77+
}
78+
}

src/Providers/GraphQLiteServiceProvider.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
use Illuminate\Contracts\Auth\Access\Gate;
66
use Illuminate\Contracts\Auth\Factory as AuthFactory;
7+
use TheCodingMachine\GraphQLite\Exceptions\WebonyxErrorHandler;
8+
use TheCodingMachine\GraphQLite\Laravel\Mappers\Parameters\ValidateFieldMiddleware;
79
use TheCodingMachine\GraphQLite\Laravel\Mappers\PaginatorTypeMapper;
810
use TheCodingMachine\GraphQLite\Laravel\Mappers\PaginatorTypeMapperFactory;
911
use TheCodingMachine\GraphQLite\Laravel\Security\AuthenticationService;
@@ -68,6 +70,8 @@ public function register()
6870
$this->app->singleton(ServerConfig::class, function (Application $app) {
6971
$serverConfig = new ServerConfig();
7072
$serverConfig->setSchema($app[Schema::class]);
73+
$serverConfig->setErrorFormatter([WebonyxErrorHandler::class, 'errorFormatter']);
74+
$serverConfig->setErrorsHandler([WebonyxErrorHandler::class, 'errorHandler']);
7175
return $serverConfig;
7276
});
7377

@@ -93,6 +97,8 @@ public function register()
9397
$service = new SchemaFactory($app->make('graphqliteCache'), new SanePsr11ContainerAdapter($app));
9498
$service->setAuthenticationService($app[AuthenticationService::class]);
9599
$service->setAuthorizationService($app[AuthorizationService::class]);
100+
$service->addParameterMiddleware($app[ValidateFieldMiddleware::class]);
101+
96102
$service->addTypeMapperFactory($app[PaginatorTypeMapperFactory::class]);
97103

98104
$controllers = config('graphqlite.controllers', 'App\\Http\\Controllers');

tests/Fixtures/App/Http/Controllers/TestController.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Illuminate\Pagination\LengthAwarePaginator;
88
use TheCodingMachine\GraphQLite\Annotations\Logged;
99
use TheCodingMachine\GraphQLite\Annotations\Query;
10+
use TheCodingMachine\GraphQLite\Laravel\Annotations\Validate;
1011

1112
class TestController
1213
{
@@ -35,4 +36,23 @@ public function testPaginator(): LengthAwarePaginator
3536
{
3637
return new LengthAwarePaginator([1,2,3,4], 42, 4, 2);
3738
}
39+
40+
/**
41+
* @Query()
42+
* @Validate(for="foo", rule="email")
43+
* @Validate(for="bar", rule="gt:42")
44+
*/
45+
public function testValidator(string $foo, int $bar): string
46+
{
47+
return 'success';
48+
}
49+
50+
/**
51+
* @Query()
52+
* @Validate(for="foo", rule="starts_with:192|ipv4")
53+
*/
54+
public function testValidatorMultiple(string $foo): string
55+
{
56+
return 'success';
57+
}
3858
}

0 commit comments

Comments
 (0)