GraphQLite Documentation

repository·master·Indexed 20 days ago

https://github.com/thecodingmachine/graphqlite

A PHP library that simplifies GraphQL API development by using PHP attributes to define schemas, queries, and mutations directly within controllers and models. It features a declarative pattern using attributes such as #[Type], #[Field], #[Mutation], and #[Input] to map PHP classes to GraphQL types, as well as a parameter middleware system for custom argument resolution.

Tokens
47.9K
Snippets
143
Records
191
Agent score
69%

What's inside GraphQLite

  1. Handle real-time streaming for subscriptions

    master

    GraphQLite does not manage the real-time transport layer (like WebSockets or Server-Sent Events) itself. Instead, it treats the subscription request as a way to initialize the subscription logic.

    Recommended Pattern:

    1. Define the subscription method in your controller with #[Subscription] and a void return type.
    2. Use the controller method to perform setup tasks (e.g., storing subscription details or establishing connections in a backend store).
    3. Use HTTP response headers to pass necessary metadata back to the client, such as a subscription ID or the URL of a dedicated streaming server (SSE/WebSocket) that the client should connect to.

    This approach allows you to offload the long-running connection to a technology better suited for streaming, preventing the PHP process from being held open indefinitely.

  2. Implement custom validation for #[Input] types

    master

    For input types defined with the #[Input] attribute, you can implement a custom validation layer that runs automatically after the object is hydrated but before it reaches your controller.

    To use this:

    1. Implement the InputTypeValidatorInterface.
    2. Register your validator with the SchemaFactory using setInputTypeValidator().

    Important: This mechanism only works for #[Input] type objects. It does not validate input types created via a factory or primitive parameters. Those must be validated manually.

    // 1. Implement the interface
    class MyInputValidator implements TheCodingMachine\GraphQLite\Types\InputTypeValidatorInterface
    {
        public function isEnabled(): bool
        {
            return true;
        }
    
        public function validate(object $input): void
        {
            // Perform validation logic
            // Throw TheCodingMachine\GraphQLite\Exceptions\GraphQLException or 
            // TheCodingMachine\GraphQLite\Exceptions\GraphQLAggregateException on failure
        }
    }
    
    // 2. Register with SchemaFactory
    $factory = new SchemaFactory($cache, $this->container);
    $factory->addNamespace('App');
    $factory->setInputTypeValidator($myInputValidator);
    $factory->createSchema();
  3. Understand GraphQLite's Semantic Versioning and Compatibility

    master

    GraphQLite follows Semantic Versioning (SemVer) to ensure smooth upgrades.

    • Major releases (e.g., 4.0, 5.0) may introduce breaking changes.
    • Minor releases (e.g., 4.1, 4.2) introduce new features but are guaranteed to be backward compatible with the existing API of that major version branch.
    • Patch releases are for bug fixes and are backward compatible.

    Important Exceptions:

    • @unstable or @experimental: Classes or methods marked with these tags may break in a minor version release. They are used when the API is still being refined based on feedback.
    • @internal: Classes or methods marked with this annotation are for GraphQLite's internal use only. They may break at any time (including patch releases) and should not be used in your code or third-party libraries.
  4. Initialize Webonyx context for GraphQLite features

    master

    To enable specific GraphQLite features like prefetching, you must initialize the Webonyx execution context with an instance of TheCodingMachine\GraphQLite\Context\Context when executing a query.

    use TheCodingMachine\GraphQLite\Context\Context;
    
    // Pass the GraphQLite Context instance as the 4th argument to executeQuery
    $result = GraphQL::executeQuery($schema, $query, null, new Context(), $variableValues);
  5. How parameter middleware works in GraphQLite

    master

    A parameter middleware allows you to hook into the argument resolution process for fields, queries, mutations, or factories. You should use a parameter middleware if you want to:

    1. Alter how arguments are injected into a method.
    2. Alter how input types are imported (e.g., adding a validation step).

    The Resolution Process

    Parameter resolution happens in two distinct passes:

    1. First Pass (Mapping): GraphQLite traverses the registered middlewares. Each middleware must decide if it can handle the parameter. If it can, it must return an implementation of ParameterInterface (the resolver). If not, it calls the $next handler to pass the responsibility down the chain.
    2. Second Pass (Resolution): The actual resolver returned in the first pass is executed to produce the value that will be fed into the method.

    Middleware Interface

    To create a middleware, implement ParameterMiddlewareInterface and its mapParameter method:

    interface ParameterMiddlewareInterface
    {
        public function mapParameter(
            ReflectionParameter $parameter, 
            DocBlock $docBlock, 
            ?Type $paramTagType, 
            ParameterAnnotations $parameterAnnotations, 
            ParameterHandlerInterface $next
        ): ParameterInterface;
    }
  6. Understand exception handling and ClientAware

    master

    GraphQLite is built on webonyx/graphql-php. It supports standard Webonyx error handling, including throwing GraphQL\Error\Error or any exception implementing the GraphQL\Error\ClientAware interface.

    Important Behavior:

    • Exceptions implementing ClientAware (or GraphQLExceptionInterface): These are caught by GraphQLite and added to the GraphQL errors response.
    • Exceptions NOT implementing ClientAware: These are NOT caught by GraphQLite. They will propagate to your application's framework error handler (e.g., Symfony or Laravel) and trigger a standard HTML error page.

    It is strongly discouraged to change the underlying Webonyx setting to catch all exceptions, as this can leak sensitive internal error details to clients. Only exceptions explicitly designed for GraphQL should appear in the errors section.

  7. Declare multiple input types for the same PHP class

    master

    In scenarios where a single PHP class (like a database entity) needs different input representations depending on the context (e.g., a partial update vs. a full creation), you can declare multiple factories in one class.

    Use #[Factory(name: "...", default: false)] for specialized input types and #[Factory(name: "...", default: true)] for the primary mapping. Then, use #[UseInputType(for: "$paramName", inputType: "SpecificInputName!")] on your Controller methods to select the correct one.

    class ProductFactory
    {
        #[Factory(name: "ProductRefInput", default: true)]
        public function getProduct(string $id): Product
        {
            return $this->productRepository->findById($id);
        }
    
        #[Factory(name: "CreateProductInput", default: false)]
        public function createProduct(string $name, string $type): Product
        {
            return new Product($name, $type);
        }
    }
    
    class ProductController
    {
        #[Mutation]
        #[UseInputType(for: "$product", inputType: "CreateProductInput!")]
        public function saveProduct(Product $product): Product
        {
            // ...
        }
    }
  8. Best practices for autowiring services

    master

    When autowiring services into your domain models, always type-hint against an interface rather than a concrete implementation.

    • Avoid: Type-hinting a concrete class (e.g., MyTranslator). This tightly couples your domain logic to a specific implementation and makes testing difficult.
    • Preferred: Type-hinting an interface (e.g., TranslatorInterface). This keeps your code decoupled and allows you to swap implementations without changing your domain models.
    // DO THIS:
    #[Field]
    public function getName(#[Autowire] TranslatorInterface $translator): string
    {
        // Good. Decoupled and testable.
    }
    
    // DON'T DO THIS:
    #[Field]
    public function getName(#[Autowire] MyTranslator $translator): string
    {
        // Bad. Tightly coupled to MyTranslator.
    }
  9. Alternatives to autowiring in domain objects

    master
    If you prefer not to use the autowiring mechanism—for example, if your container doesn't support FQCN-based autowiring, if you want to avoid injecting services into domain objects, or if you want to avoid the "magic" of method signature injection—you should use the Type Extension pattern instead of autowiring.
  10. Requirements for GraphQLite Universal Service Provider

    master

    To successfully bootstrap GraphQLite using the universal service provider, ensure you meet the following requirements:

    1. PSR-16 Cache: A compliant cache implementation is required.
    2. HTTP Routing: You must route HTTP requests to the underlying GraphQL library. GraphQLite uses webonyx/graphql-php internally, which works with PSR-7 requests. A PSR-15 middleware is provided to facilitate this.
  11. Handling interfaces without explicit #[Type] attributes

    master

    If a class implements a PHP interface but does not have its own #[Type] attribute, GraphQLite will still attempt to resolve it. If a query returns an interface type, and GraphQLite finds a class implementing that interface without a #[Type] attribute, it will automatically create an object type "on the fly" (e.g., UserImpl) to satisfy the schema requirements.

    /**
     * This class has no #[Type] attribute
     */
    class User implements UserInterface
    {
        public function getUserName(): string;
    }
    
    class UserController
    {
        #[Query]
        public function getUser(): UserInterface // Works! GraphQLite creates UserImpl
        {
            // ...
        }
    }
  12. Implement a custom Root type mapper

    master

    To create a custom mapper that can access PHP DocBlocks or annotations, you must implement the RootTypeMapperInterface.

    Root type mappers are organized in a chain. Each mapper in the chain is responsible for either handling the type or passing control to the next mapper in the sequence.

    The default chain order is:

    1. NullableTypeMapperAdapter (handles nullability)
    2. CompoundTypeMapper (handles unions)
    3. IteratorTypeMapper (handles iterables)
    4. YourCustomRootTypeMapper (your implementation)
    5. EnumTypeMapper (handles enums)
    6. BaseTypeMapper (handles scalars and lists)
    7. FinalRootTypeMapper (throws error if no match found)