Skip to content

WIP : Laravel Validator Implementation #12

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 9 commits into from
Oct 8, 2019
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 composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
],
"require" : {
"php" : ">=7.2",
"thecodingmachine/graphqlite" : "~4.0.0",
"thecodingmachine/graphqlite" : "^4",
"illuminate/console": "^5.7|^6.0",
"illuminate/container": "^5.7|^6.0",
"illuminate/support": "^5.7|^6.0",
Expand Down
53 changes: 53 additions & 0 deletions src/Annotations/Validate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Laravel\Annotations;

use BadMethodCallException;
use TheCodingMachine\GraphQLite\Annotations\MiddlewareAnnotationInterface;
use TheCodingMachine\GraphQLite\Annotations\ParameterAnnotationInterface;
use function ltrim;

/**
* Use this annotation to validate a parameter for a query or mutation.
*
* @Annotation
* @Target({"METHOD"})
* @Attributes({
* @Attribute("for", type = "string"),
* @Attribute("rule", type = "string")
* })
*/
class Validate implements ParameterAnnotationInterface
{
/** @var string */
private $for;
/** @var string */
private $rule;

/**
* @param array<string, mixed> $values
*/
public function __construct(array $values)
{
if (! isset($values['for'])) {
throw new BadMethodCallException('The @Validate annotation must be passed a target. For instance: "@Validate(for="$email", rule="email")"');
}
if (! isset($values['rule'])) {
throw new BadMethodCallException('The @Validate annotation must be passed a rule. For instance: "@Validate(for="$email", rule="email")"');
}
$this->for = ltrim($values['for'], '$');
$this->rule = $values['rule'] ?? null;
}

public function getTarget(): string
{
return $this->for;
}

public function getRule(): string
{
return $this->rule;
}
}
50 changes: 50 additions & 0 deletions src/Exceptions/ValidateException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace TheCodingMachine\GraphQLite\Laravel\Exceptions;

use GraphQL\Error\ClientAware;
use TheCodingMachine\GraphQLite\Exceptions\GraphQLExceptionInterface;

class ValidateException extends \Exception implements GraphQLExceptionInterface
{
/**
* @var string
*/
private $argumentName;

public static function create(string $message, string $argumentName)
{
$exception = new self($message, 400);
$exception->argumentName = $argumentName;
return $exception;
}

/**
* @return bool
*/
public function isClientSafe()
{
return true;
}

/**
* @return string
*/
public function getCategory()
{
return 'Validate';
}


/**
* Returns the "extensions" object attached to the GraphQL error.
*
* @return array<string, mixed>
*/
public function getExtensions(): array
{
return [
'argument' => $this->argumentName
];
}
}
17 changes: 17 additions & 0 deletions src/Mappers/Parameters/InvalidValidateAnnotationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php


namespace TheCodingMachine\GraphQLite\Laravel\Mappers\Parameters;


use ReflectionParameter;

class InvalidValidateAnnotationException extends \Exception
{
public static function canOnlyValidateInputType(ReflectionParameter $refParameter): self
{
$class = $refParameter->getDeclaringClass();
$method = $refParameter->getDeclaringFunction();
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.');
}
}
88 changes: 88 additions & 0 deletions src/Mappers/Parameters/ParameterValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php


namespace TheCodingMachine\GraphQLite\Laravel\Mappers\Parameters;


use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\ResolveInfo;
use Illuminate\Validation\Factory as ValidationFactory;
use TheCodingMachine\GraphQLite\Exceptions\GraphQLAggregateException;
use TheCodingMachine\GraphQLite\Laravel\Exceptions\ValidateException;
use TheCodingMachine\GraphQLite\Parameters\InputTypeParameterInterface;
use TheCodingMachine\GraphQLite\Parameters\ParameterInterface;
use function array_combine;
use function array_keys;
use function implode;

class ParameterValidator implements InputTypeParameterInterface
{
/**
* @var InputTypeParameterInterface
*/
private $parameter;
/**
* @var string
*/
private $rules;
/**
* @var ValidationFactory
*/
private $validationFactory;
/**
* @var string
*/
private $parameterName;

public function __construct(InputTypeParameterInterface $parameter, string $parameterName, string $rules, ValidationFactory $validationFactory)
{
$this->parameter = $parameter;
$this->rules = $rules;
$this->validationFactory = $validationFactory;
$this->parameterName = $parameterName;
}


/**
* @param array<string, mixed> $args
* @param mixed $context
*
* @return mixed
*/
public function resolve(?object $source, array $args, $context, ResolveInfo $info)
{
$value = $this->parameter->resolve($source, $args, $context, $info);

$validator = $this->validationFactory->make([$this->parameterName => $value], [$this->parameterName => $this->rules]);

if ($validator->fails()) {
$errorMessages = [];
foreach ($validator->errors()->toArray() as $field => $errors) {
foreach ($errors as $error) {
$errorMessages[] = ValidateException::create($error, $field);
}
}
GraphQLAggregateException::throwExceptions($errorMessages);
}

return $value;
}

public function getType(): InputType
{
return $this->parameter->getType();
}

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

/**
* @return mixed
*/
public function getDefaultValue()
{
return $this->parameter->getDefaultValue();
}
}
78 changes: 78 additions & 0 deletions src/Mappers/Parameters/ValidateFieldMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Laravel\Mappers\Parameters;

use Closure;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\OutputType;
use phpDocumentor\Reflection\DocBlock;
use phpDocumentor\Reflection\Type;
use Psr\Log\LoggerInterface;
use ReflectionFunction;
use ReflectionParameter;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use TheCodingMachine\GraphQLite\Annotations\FailWith;
use TheCodingMachine\GraphQLite\Annotations\ParameterAnnotations;
use TheCodingMachine\GraphQLite\Annotations\Security;
use TheCodingMachine\GraphQLite\Laravel\Annotations\Validate;
use TheCodingMachine\GraphQLite\Mappers\Parameters\ParameterHandlerInterface;
use TheCodingMachine\GraphQLite\Mappers\Parameters\ParameterMiddlewareInterface;
use TheCodingMachine\GraphQLite\Parameters\InputTypeParameterInterface;
use TheCodingMachine\GraphQLite\Parameters\ParameterInterface;
use TheCodingMachine\GraphQLite\QueryFieldDescriptor;
use TheCodingMachine\GraphQLite\Security\AuthenticationServiceInterface;
use TheCodingMachine\GraphQLite\Middlewares\FieldMiddlewareInterface;
use TheCodingMachine\GraphQLite\Security\AuthorizationServiceInterface;
use TheCodingMachine\GraphQLite\Middlewares\FieldHandlerInterface;
use TheCodingMachine\GraphQLite\Annotations\ParameterAnnotationInterface;
use TheCodingMachine\GraphQLite\Laravel\Exceptions\ValidateException;
use Throwable;
use Webmozart\Assert\Assert;
use function array_combine;
use function array_keys;
use function array_map;
use function array_merge;
use function implode;
use function is_array;
use function is_object;
use Illuminate\Validation\Factory as ValidationFactory;

/**
* A field middleware that reads "Security" Symfony annotations.
*/
class ValidateFieldMiddleware implements ParameterMiddlewareInterface
{
/**
* @var ValidationFactory
*/
private $validationFactory;

public function __construct(ValidationFactory $validationFactory)
{
$this->validationFactory = $validationFactory;
}

public function mapParameter(ReflectionParameter $refParameter, DocBlock $docBlock, ?Type $paramTagType, ParameterAnnotations $parameterAnnotations, ParameterHandlerInterface $next): ParameterInterface
{
/** @var Validate[] $validateAnnotations */
$validateAnnotations = $parameterAnnotations->getAnnotationsByType(Validate::class);

$parameter = $next->mapParameter($refParameter, $docBlock, $paramTagType, $parameterAnnotations);

if (empty($validateAnnotations)) {
return $parameter;
}

if (!$parameter instanceof InputTypeParameterInterface) {
throw InvalidValidateAnnotationException::canOnlyValidateInputType($refParameter);
}

// Let's wrap the ParameterInterface into a ParameterValidator.
$rules = array_map(static function(Validate $validateAnnotation): string { return $validateAnnotation->getRule(); }, $validateAnnotations);

return new ParameterValidator($parameter, $refParameter->getName(), implode('|', $rules), $this->validationFactory);
}
}
6 changes: 6 additions & 0 deletions src/Providers/GraphQLiteServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use Illuminate\Contracts\Auth\Access\Gate;
use Illuminate\Contracts\Auth\Factory as AuthFactory;
use TheCodingMachine\GraphQLite\Exceptions\WebonyxErrorHandler;
use TheCodingMachine\GraphQLite\Laravel\Mappers\Parameters\ValidateFieldMiddleware;
use TheCodingMachine\GraphQLite\Laravel\Mappers\PaginatorTypeMapper;
use TheCodingMachine\GraphQLite\Laravel\Mappers\PaginatorTypeMapperFactory;
use TheCodingMachine\GraphQLite\Laravel\Security\AuthenticationService;
Expand Down Expand Up @@ -68,6 +70,8 @@ public function register()
$this->app->singleton(ServerConfig::class, function (Application $app) {
$serverConfig = new ServerConfig();
$serverConfig->setSchema($app[Schema::class]);
$serverConfig->setErrorFormatter([WebonyxErrorHandler::class, 'errorFormatter']);
$serverConfig->setErrorsHandler([WebonyxErrorHandler::class, 'errorHandler']);
return $serverConfig;
});

Expand All @@ -93,6 +97,8 @@ public function register()
$service = new SchemaFactory($app->make('graphqliteCache'), new SanePsr11ContainerAdapter($app));
$service->setAuthenticationService($app[AuthenticationService::class]);
$service->setAuthorizationService($app[AuthorizationService::class]);
$service->addParameterMiddleware($app[ValidateFieldMiddleware::class]);

$service->addTypeMapperFactory($app[PaginatorTypeMapperFactory::class]);

$controllers = config('graphqlite.controllers', 'App\\Http\\Controllers');
Expand Down
20 changes: 20 additions & 0 deletions tests/Fixtures/App/Http/Controllers/TestController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Illuminate\Pagination\LengthAwarePaginator;
use TheCodingMachine\GraphQLite\Annotations\Logged;
use TheCodingMachine\GraphQLite\Annotations\Query;
use TheCodingMachine\GraphQLite\Laravel\Annotations\Validate;

class TestController
{
Expand Down Expand Up @@ -35,4 +36,23 @@ public function testPaginator(): LengthAwarePaginator
{
return new LengthAwarePaginator([1,2,3,4], 42, 4, 2);
}

/**
* @Query()
* @Validate(for="foo", rule="email")
* @Validate(for="bar", rule="gt:42")
*/
public function testValidator(string $foo, int $bar): string
{
return 'success';
}

/**
* @Query()
* @Validate(for="foo", rule="starts_with:192|ipv4")
*/
public function testValidatorMultiple(string $foo): string
{
return 'success';
}
}
Loading