Graphqlbundle: Any there good recipes for arguments validation using Symfony validator component?

Created on 24 May 2017  ·  5Comments  ·  Source: overblog/GraphQLBundle

| Q | A
| ---------------- | -----
| Bug report? | no
| Feature request? | no
| BC Break report? |no
| RFC? | no
| Version/Branch | 0.8.3

Hi. I want to validate my arguments, e.g. for registration mutation, and I want to validate it via Symfony validation. Didn't find anything about validation in docs and don't really know how to implement this elegant. Can you show some examples please and extend docs with it?

question

Most helpful comment

Sorry the issue came from my example parseLiteral should not throw exception but return null if not a valid email:

<?php

namespace AppBundle\GraphQL;

use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\StringValueNode;
use GraphQL\Type\Definition\CustomScalarType;

use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Validator\ValidatorInterface;

class EmailType extends CustomScalarType
{
    private $validator;

    public function __construct(ValidatorInterface $validator)
    {
        $this->validator = $validator;

        parent::__construct([
            'name' => 'Email',
            'serialize' => [$this, 'serialize'],
            'parseValue' => [$this, 'parseValue'],
            'parseLiteral' => [$this, 'parseLiteral'],
        ]);
    }

    /**
     * @param string $value
     *
     * @return string
     */
    public function serialize($value)
    {
        $this->validate($value);
        return $value;
    }

    /**
     * @param mixed $value
     *
     * @return mixed
     */
    public function parseValue($value)
    {
        $this->validate($value);
        return $value;
    }

    /**
     * @param Node $ast
     *
     * @return string
     */
    public function parseLiteral($ast)
    {
        if ($ast instanceof StringValueNode) {
            $value = $ast->value;
            if ($this->validate($value, false)) {
                return $value;
            }
        }
        return null;
    }

    private function validate($value, $throw = true)
    {
        $violations = $this->validator->validate($value, array(
            new Assert\Email(),
            new Assert\NotBlank(),
        ));
        if (0 !== count($violations)) {
            if (!$throw) {
                return false;
            }
            $message = "There are errors:\n";
            foreach ($violations as $violation) {
                $message .= '- '.$violation->getMessage()."\n";
            }
            throw new InvariantViolation($message);
        }
        return true;
    }
}

All 5 comments

Hi, the best way is to create custom scalar using the service way and inject the validator directly . Here an example to validate emails:

services:
    AppBundle\EmailType:
        # only for sf < 3.3
        #class: AppBundle\EmailType
        arguments:
            - "@validator"
        tags:
            - { name: overblog_graphql.type, alias: Email }

```php

namespace AppBundle;

use GraphQL\ErrorInvariantViolation;
use GraphQL\Language\AST\Node;
use GraphQL\Type\Definition\CustomScalarType;

use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Validator\ValidatorInterface;

class EmailType extends CustomScalarType
{
private $validator;

public function __construct(ValidatorInterface $validator)
{
    $this->validator = $validator;

    parent::__construct([
        'name' => 'Email',
        'serialize' => [$this, 'serialize'],
        'parseValue' => [$this, 'parseValue'],
        'parseLiteral' => [$this, 'parseLiteral'],
    ]);
}

/**
 * @param string $value
 *
 * @return string
 */
public function serialize($value)
{
    return $value;
}

/**
 * @param mixed $value
 *
 * @return mixed
 */
public function parseValue($value)
{
    $this->validate($value);
    return $value;
}

/**
 * @param Node $valueNode
 *
 * @return string
 */
public function parseLiteral($valueNode)
{
    $this->validate($valueNode->value);
    return $valueNode->value;
}

private function validate($value)
{
    $violations = $this->validator->validate($value, array(
        new Assert\Email(),
        new Assert\NotBlank(),
    ));
    if (0 !== count($violations)) {
        $message = "There are errors:\n";
        foreach ($violations as $violation) {
            $message .= '- '.$violation->getMessage()."\n";
        }
        throw new InvariantViolation($message);
    }
}

}

after you can just use your new scalar type:

```yaml
# src/MyBundle/Resources/config/graphql/Human.types.yml
Human:
    type: object
    config:
        description: "A humanoid creature in the Star Wars universe."
        fields:
            email:
                type: "Email!"
                description: "The email of the character :D."

note custom types will be easier to create in next version, thanks again to auto mapping feature.

@mcg-web, thank you for your reply. Seems like throwing InvariantViolation exception inside EmailType class produces ugly 500 error:

json { "error": { "code": 500, "message": "Internal Server Error", "exception": [ { "message": "There are errors:\n- Значение адреса электронной почты недопустимо.\n", "class": "GraphQL\\Error\\InvariantViolation", "trace": [ ... ] } ] } }

When I replace InvariantViolation with UserError behavior is the same. Seems like throwing UserError exception only from resolver produces beauty GraphQL like error array.

It's the debug behavior. In no debug mode the violation while be catch. I'll add violation in white​ list...

@mcg-web, sorry, I think I didn't understand something. I have disabled displaying debug info in bundle config:

yml overblog_graphql: definitions: show_debug_info: false config_validation: %kernel.debug% schema: query: Query mutation: Mutation templates: graphiql: '%kernel.root_dir%/Resources/views/graphiql/index.html.twig'

Also I run Symfony without debug mode:

php // web/app.php $kernel = new AppKernel('dev', false);

When I throw any exception from the type class (InvariantViolation or UserError), it produces 500 error:

````php
namespace AppBundle\GraphQL\Type;

use GraphQL\Language\AST\Node;
use GraphQL\Type\Definition\CustomScalarType;
use Overblog\GraphQLBundle\ErrorUserError;
use Overblog\GraphQLBundle\ErrorUserErrors;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Validator\ValidatorInterface;

class EmailType extends CustomScalarType
{
private $validator;

public function __construct(ValidatorInterface $validator)
{
    $this->validator = $validator;

    parent::__construct([
        'name' => 'Email',
        'serialize' => [$this, 'serialize'],
        'parseValue' => [$this, 'parseValue'],
        'parseLiteral' => [$this, 'parseLiteral'],
    ]);
}

/**
 * @param string $value
 *
 * @return string
 */
public function serialize($value)
{
    return $value;
}

/**
 * @param mixed $value
 *
 * @return mixed
 */
public function parseValue($value)
{
    $this->validate($value);

    return $value;
}

/**
 * @param Node $valueNode
 *
 * @return string
 */
public function parseLiteral($valueNode)
{
    $this->validate($valueNode->value);

    return $valueNode->value;
}

private function validate($value)
{
    $violations = $this->validator->validate($value, [
        new Assert\NotBlank(),
        new Assert\Email(),
    ]);

    if (count($violations) > 0) {
        $errors = [];

        foreach ($violations as $violation) {
            $errors[] = new UserError($violation->getMessage());
        }

        throw new UserErrors($errors);
    }
}

}
````

JSON response with disabled debugging everywhere:

json { "error": { "code": 500, "message": "Internal Server Error" } }

JSON response with enabled debugging:

json { "error": { "code": 500, "message": "Internal Server Error", "exception": [ { "message": "", "class": "Overblog\\GraphQLBundle\\Error\\UserErrors", "trace": [ ... ] } ] } }

What should I do to get response like this one?

json { "data": { "register": null }, "errors": [ { "message": "Invalid email.", "locations": [ { "line": 2, "column": 3 } ], "path": [ "register" ] } ] }

Sorry the issue came from my example parseLiteral should not throw exception but return null if not a valid email:

<?php

namespace AppBundle\GraphQL;

use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\StringValueNode;
use GraphQL\Type\Definition\CustomScalarType;

use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Validator\ValidatorInterface;

class EmailType extends CustomScalarType
{
    private $validator;

    public function __construct(ValidatorInterface $validator)
    {
        $this->validator = $validator;

        parent::__construct([
            'name' => 'Email',
            'serialize' => [$this, 'serialize'],
            'parseValue' => [$this, 'parseValue'],
            'parseLiteral' => [$this, 'parseLiteral'],
        ]);
    }

    /**
     * @param string $value
     *
     * @return string
     */
    public function serialize($value)
    {
        $this->validate($value);
        return $value;
    }

    /**
     * @param mixed $value
     *
     * @return mixed
     */
    public function parseValue($value)
    {
        $this->validate($value);
        return $value;
    }

    /**
     * @param Node $ast
     *
     * @return string
     */
    public function parseLiteral($ast)
    {
        if ($ast instanceof StringValueNode) {
            $value = $ast->value;
            if ($this->validate($value, false)) {
                return $value;
            }
        }
        return null;
    }

    private function validate($value, $throw = true)
    {
        $violations = $this->validator->validate($value, array(
            new Assert\Email(),
            new Assert\NotBlank(),
        ));
        if (0 !== count($violations)) {
            if (!$throw) {
                return false;
            }
            $message = "There are errors:\n";
            foreach ($violations as $violation) {
                $message .= '- '.$violation->getMessage()."\n";
            }
            throw new InvariantViolation($message);
        }
        return true;
    }
}
Was this page helpful?
0 / 5 - 0 ratings