OverblogGraphQLBundle

repository·master·Indexed 21 days ago

https://github.com/overblog/graphqlbundle

A Symfony bundle providing GraphQL integration via the webonyx/graphql-php library. It supports the Relay specification, query batching, and file uploads. Key features include an Arguments Transformer for mapping GraphQL inputs to PHP classes, a comprehensive set of PHP attributes (such as #[Type], #[Field], #[Query], and #[Mutation]) for schema definition, and integrated access control and visibility management.

Tokens
61K
Snippets
190
Records
218
Agent score
73%

What's inside overblog-graphqlbundle

  1. Overview of Validation in OverblogGraphQLBundle

    master

    The bundle provides tight integration with the Symfony Validator Component to validate user input data.

    Key requirements and behaviors:

    • Schema Format: Currently supports only GraphQL schemas defined with YAML.
    • Mechanism: You apply constraints directly in your YAML type definitions within args (for object types) or fields (for input-object types).
    • Automation: The bundle automatically validates data and throws an exception if validation fails. This exception is caught and returned in the GraphQL response to the client.
    • Execution Timing: Validation occurs automatically before the corresponding resolver is called. If validation fails, the resolver is skipped (unless you manually perform validation inside the resolver).
    # Example of applying constraints in a Mutation
    Mutation:
        type: object
        config:
            fields:
                register:
                    type: User
                    args:
                        username:
                            type: String!
                            validation:
                                - Length:
                                    min: 6
                                    max: 32
  2. Overview of OverblogGraphQLBundle

    master

    OverblogGraphQLBundle is a Symfony bundle that integrates GraphQL into Symfony applications. It is built upon webonyx/graphql-php and supports the GraphQL Relay specification.

    Key features include:

    • Relay Support: Full implementation of GraphQL Relay specifications.
    • Batching: Support for query batching compatible with ReactRelayNetworkLayer and Apollo GraphQL.
    • File Uploads: Support for file uploads and batched uploads via apollo-upload-client.
  3. What is a Resolver Map and how does it work?

    master

    A resolver map is a collection of functions used to respond to GraphQL queries. Since resolve functions cannot be defined directly within the GraphQL schema language, they must be provided separately via a resolver map.

    In OverblogGraphQLBundle, a resolver map is an object implementing Overblog\GraphQLBundle\Resolver\ResolverMapInterface. You can implement this interface directly or, more commonly, extend the concrete Overblog\GraphQLBundle\Resolver\ResolverMap class and override its map() method.

    Key Concepts

    • Resolution Logic: The bundle iterates through available resolver maps. If multiple maps are registered for the same schema, the first one where isResolvable() returns true is used.
    • Priority: Resolver maps are executed based on a priority attribute. Higher numbers are executed earlier. A higher priority map will override matching entries in lower priority maps.
    • Fallback: You do not need to provide resolvers for every type; if a resolver is not found in any map, GraphQL falls back to its default behavior.
    <?php
    
    namespace App\Resolver;
    
    use Overblog\GraphQLBundle\Resolver\ResolverMap;
    
    class MyResolverMap extends ResolverMap
    {
        protected function map()
        {
            return [
                // Resolver definitions go here
            ];
        }
    }
  4. Implement Type-wide Resolvers with `resolveField`

    master

    If you want a single class to handle multiple fields for a specific type, you can use an invokable class assigned to the resolveField configuration option. The __invoke method will receive a GraphQL\Type\Definition\ResolveInfo object, allowing you to branch logic based on the $info->fieldName.

    # config/graphql/types/MyType.types.yaml
    MyType:
        type: object
        config:
            resolveField: '@=query("App\\GraphQL\\Resolver\\Greetings", info, args.name)'
            fields:
                hello:
                    type: String
                goodbye:
                    type: String
    // src/GraphQL/Resolver/Greetings.php
    namespace App\GraphQL\Resolver;
    
    use GraphQL\Type\Definition\ResolveInfo;
    use Overblog\GraphQLBundle\Definition\Resolver\QueryInterface;
    
    class Greetings implements QueryInterface
    {
        public function __invoke(ResolveInfo $info, $name)
        {
            if($info->fieldName === 'hello'){
                return sprintf('hello %s!!!', $name);
            } else if($info->fieldName === 'goodbye'){
                return sprintf('goodbye %s!!!', $name);
            } else {
                throw new \DomainException('Unknown greetings');
            }
        }
    }
  5. Use the Paginator for Relay Connections

    master

    The Overblog\GraphQLBundle\Relay\Connection\Paginator is a helper designed to facilitate Relay-style pagination (Connections) when working with data sets provided by a backend.

    To use it, you must provide a callable (callback) to the Paginator constructor. This callback is responsible for fetching the sliced data set from your backend. It receives two arguments:

    • $offset: The starting index for the slice.
    • $limit: The number of items to retrieve.

    Depending on your needs, you can use different methods to execute the pagination:

    • forward(): Used for forward pagination (e.g., using first and after arguments).
    • backward(): Used for backward pagination (e.g., using last and before arguments).
    • auto(): Automatically determines the direction based on the provided Argument object.
    <?php
    
    use Overblog//... (see full example in 'Implement Paginator with first/after/last parameters' record)
    
    $paginator = new Paginator(function ($offset, $limit) use ($backend) {
        return $backend->getData($offset, $limit);
    });
  6. How resolvers are guessed for Attributes

    master

    When using #[GQL\Field], #[GQL\Query], or #[GQL\Mutation], the bundle automatically guesses a resolver if the resolver attribute is not explicitly defined. The logic depends on whether the attribute is on a property or a method, and whether it is a regular type or a root type.

    On a Property

    • If name is defined and differs from the property name:
      • Regular type: @=value.<property name>
      • Root Query/Mutation: @=service(<FQCN>).<property name>
    • If name is not defined or matches the property name:
      • Regular type: Uses the default GraphQL resolver (accessing the property/key directly).
      • Root Query/Mutation: @=service(<FQCN>).<name>

    On a Method

    • Regular type: @=call(value.<method name>, args)
    • Root Query/Mutation: @=call(service(<FQCN>).<method name>, args)
  7. Understand the structure of validation error responses

    master

    When validation fails, the InputValidator throws an ArgumentsValidationException, which is serialized into a standard GraphQL error response. All validation violations are located under the path errors[index].extensions.validation in the response object. Each violation includes a message and a unique code which can be used for client-side translations.

    Example response structure:

    {
      "data": null,
      "errors": [{
        "message": "validation",
        "extensions": {
          "category": "arguments_validation_error",
          "validation": {
            "username": [
              {
                "message": "This value should be equal to 'Lorem Ipsum'.", 
                "code": "478618a7-95ba-473d-9101-cabd45e49115"
              }
            ]
          }
        }
      }]
    }
    {
      "data": null,
      "errors": [{
        "message": "validation",
        "extensions": {
          "category": "arguments_validation_error",
          "validation": {
            "username": [
              {
                "message": "This value should be equal to 'Lorem Ipsum'.", 
                "code": "478618a7-95ba-473d-9101-cabd45e49115"
              }
            ]
          }
        }
      }]
    }
  8. Work with native PHP 8.1 Enums

    master

    The bundle supports native PHP 8.1 enums. You can declare them using PHP Attributes or YAML configuration.

    Using PHP Attributes

    Apply the #[GQL\Enum] attribute to the enum class. You can use #[GQL\Description] on individual cases to add metadata.

    Using YAML

    In your YAML configuration, set the type to enum and provide the enumClass in the config block. The bundle will automatically extract the possible values.

    To add custom metadata (like description) to specific cases in YAML, use the values key under config.

    Serialization and Deserialization Behavior

    When using PHP enums:

    • Serialization: The bundle extracts the name of the enum case.
    • Deserialization: The bundle returns the enum case by its name.
    • Note on Backed Enums: Even if the enum is a Backed enum, the bundle will always use the name (not the value) for both serialization and deserialization.
    #[GQL\Enum]
    enum Color 
    {
        #[GQL\Description("The color red")]
        case RED;
        case GREEN;
        case BLUE;
    }
  9. Understand Context vs RootValue Separation (v0.11)

    master

    In version 0.11, context and rootValue are no longer the same object.

    • context: Now an ArrayObject.
    • rootValue: Has no type hint (defaults to null).

    Impact: Previously, $context === $info->rootValue was true. Now, they are distinct. For example, uploaded files are no longer automatically accessible under $info->rootValue['request_files']; they must be accessed via the context object.

  10. Use Expression Language in definition configurations

    master

    All definition configuration entries in the bundle can use Symfony's Expression Language. To trigger the expression language, you must prefix the value with @=. The bundle provides a set of specialized functions and variables (like value, args, info, and context) to interact with the GraphQL execution lifecycle.

    For detailed syntax rules, refer to the official Symfony Expression Language documentation.

    # Example of triggering expression language in a config entry
    field_name: "@=some_function(args.id)"
  11. How Validation works via ValidationNode

    master

    To leverage the full power of the Symfony Validator, the bundle converts input data into objects of class ValidationNode before validation begins.

    How objects are created:

    • For object types (e.g., Mutation): The bundle creates a ValidationNode object per field (e.g., one for createUser, one for createPost).
    • For input-object types: The bundle creates one object for the entire type.

    Important Notes:

    • Resolver Arguments: The ValidationNode objects are used only for validation. Your resolvers will still receive the original, raw, unaltered arguments.
    • Recursion: Objects are not created recursively by default. To delegate validation to an embedded type, you must use the cascade keyword.
    • Non-cascaded types: If you do not use cascade, embedded types remain as arrays, which can still be validated using constraints like Collection.