Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Graphql Php Laravel Package

webonyx/graphql-php

A PHP implementation of the GraphQL specification, based on the JavaScript reference implementation. It's fully compliant with the October 2021 spec, with September 2025 compliance in progress, and supports schema building, query execution, and validation.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup in Laravel

  1. Install the package:

    composer require webonyx/graphql-php
    
  2. Define a basic schema (e.g., app/GraphQL/Schema.php):

    use GraphQL\Type\Definition\Type;
    use GraphQL\Type\Schema;
    use GraphQL\Type\Definition\ObjectType;
    
    $queryType = new ObjectType([
        'name' => 'Query',
        'fields' => [
            'hello' => [
                'type' => Type::string(),
                'resolve' => fn() => 'World',
            ],
        ],
    ]);
    
    return new Schema(['query' => $queryType]);
    
  3. Create a GraphQL route (e.g., routes/web.php):

    use GraphQL\Server\ServerConfig;
    use GraphQL\Server\StandardServer;
    
    Route::post('/graphql', function () {
        $config = ServerConfig::create()
            ->setSchema(app('graphql.schema'))
            ->setContext(fn() => ['user' => auth()->user()]);
    
        return (new StandardServer($config))->executeRequest();
    });
    
  4. Test with a query:

    query {
      hello
    }
    

First Use Case: Querying Eloquent Models

use App\Models\User;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;

$schema = new Schema([
    'query' => new ObjectType([
        'name' => 'Query',
        'fields' => [
            'user' => [
                'type' => Type::string(),
                'args' => [
                    'id' => ['type' => Type::int()]
                ],
                'resolve' => function ($root, $args) {
                    return User::find($args['id'])->name;
                }
            ]
        ]
    ])
]);

What's New in v15.38.x

Three day-to-day relevant changes:

  • Printer::stripIgnoredCharacters(string $document): string (v15.38.0) — returns the document with insignificant whitespace, commas and comments removed. This is exactly what you want before hashing a query for persisted-query lookup, so cosmetic client-side formatting doesn't create cache misses.
  • Invalid UTF-8 in documents is now rejected (v15.38.0, #1987) rather than silently producing mangled output. Any raw request body / query string that isn't valid UTF-8 now surfaces a proper error at parse time — useful, but it can make previously "working" clients start failing loudly.
  • Malformed batch entries now return RequestError, not TypeError (v15.38.1, #1993). If a client posts a batch array containing a scalar instead of an operation object — ["x"], or {"query":"{ a }","0":"x"} — the server now raises a GraphQL\Error\RequestError, which the server converts into a proper 4xx GraphQL error response. Previously this leaked a PHP TypeError (a 500 in most Laravel setups). If you catch (\TypeError) around batch execution, that catch is now dead code.

Implementation Patterns

Schema Design Patterns

  1. Modular Schema Construction Use a Schema Factory (e.g., app/GraphQL/SchemaFactory.php) to build schemas incrementally:

    class SchemaFactory {
        public static function build(): Schema {
            $query = new ObjectType([
                'name' => 'Query',
                'fields' => [
                    'users' => [
                        'type' => Type::listOf(UserType::get()),
                        'resolve' => fn() => User::all()
                    ],
                ],
            ]);
    
            return new Schema(['query' => $query]);
        }
    }
    
  2. Type Registry Centralize type definitions in a Type Registry (e.g., app/GraphQL/Types/):

    // app/GraphQL/Types/UserType.php
    class UserType extends ObjectType {
        public static function get(): ObjectType {
            return new self([
                'name' => 'User',
                'fields' => [
                    'id' => ['type' => Type::int()],
                    'name' => ['type' => Type::string()]
                ]
            ]);
        }
    }
    
  3. Input Types for Mutations Define input types for mutations (e.g., CreateUserInput):

    $createUserInput = new InputObjectType([
        'name' => 'CreateUserInput',
        'fields' => [
            'name' => ['type' => Type::nonNull(Type::string())],
            'email' => ['type' => Type::nonNull(Type::string())]
        ]
    ]);
    
    $mutation = new ObjectType([
        'name' => 'Mutation',
        'fields' => [
            'createUser' => [
                'type' => UserType::get(),
                'args' => ['input' => ['type' => $createUserInput]],
                'resolve' => function ($root, $args) {
                    return User::create($args['input']);
                }
            ]
        ]
    ]);
    

Resolver Patterns

  1. Dependency Injection in Resolvers Use Laravel's service container to inject dependencies:

    'resolve' => function ($root, $args, $context) {
        return $context['userRepository']->find($args['id']);
    }
    
  2. Batch Loading Optimize N+1 queries with a DataLoader (e.g. overblog/dataloader-php or a hand-rolled buffer resolved per request):

    use GraphQL\Deferred;
    
    $buffer = [];
    $loader = function ($ids) use (&$buffer) {
        // Collect ids during the execution tick, resolve once
        return User::whereIn('id', $buffer)->get()->keyBy('id');
    };
    
    'resolve' => function ($root, $args) use ($loader) {
        return new Deferred(fn() => $loader($args['id']));
    }
    
  3. Error Handling Throw GraphQL\Error\Error for client-safe messages; let everything else bubble up and be masked in production:

    'resolve' => function ($root, $args) {
        $user = User::find($args['id']);
    
        if ($user === null) {
            throw new \GraphQL\Error\Error('User not found', extensions: ['code' => 'NOT_FOUND']);
        }
    
        return $user;
    }
    

Integration with Laravel

  1. Service Provider Bind the schema in AppServiceProvider:

    public function register() {
        $this->app->singleton('graphql.schema', fn() => SchemaFactory::build());
    }
    
  2. Middleware for Authentication Attach middleware to the GraphQL route:

    Route::post('/graphql', function () {
        return app('graphql.server')->executeRequest();
    })->middleware('auth:api');
    
  3. Context Injection Pass Laravel's request or authenticated user:

    $config = ServerConfig::create()
        ->setSchema(app('graphql.schema'))
        ->setContext(fn() => ['user' => auth()->user(), 'request' => request()]);
    

Performance Patterns

  1. Persisted Queries — normalize before hashing Printer::stripIgnoredCharacters() removes whitespace, commas and comments, so two clients sending the "same" query with different formatting map to the same cache key:

    use GraphQL\Language\Parser;
    use GraphQL\Language\Printer;
    
    $normalized = Printer::stripIgnoredCharacters($incomingQuery);
    $hash = hash('sha256', $normalized);
    
    $document = Cache::rememberForever("gql:query:{$hash}", function () use ($incomingQuery) {
        return $incomingQuery; // or the AST, or the SDL source
    });
    
    $serverConfig->setPersistedQueryLoader(fn($queryId) => Cache::get("gql:query:{$queryId}"));
    

    Note: strip is purely lexical — it does not reorder fields or rename variables, so variable-bearing documents still need their variables passed separately.

  2. Query Complexity Validation Limit query depth/complexity to prevent expensive queries:

    use GraphQL\Validator\Rules\QueryComplexity;
    use GraphQL\Validator\DocumentValidator;
    
    DocumentValidator::addRule(new QueryComplexity(1000));
    
  3. Lazy Loading Types Defer type loading for large schemas:

    $schema = new Schema([
        'query' => fn() => $queryType,
        'types' => fn() => [$userType, $postType]
    ]);
    

Gotchas and Tips

Common Pitfalls

  1. Circular Dependencies in Resolvers

    • Issue: Resolvers calling each other directly (e.g., User resolver calling Post resolver which calls User again).
    • Fix: Use a DataLoader/Deferred for batch loading, or return a lazy fields closure so types are only linked at execution time.
  2. Type Mismatches in Arguments

    • Issue: Passing arguments of the wrong type (e.g., string instead of int) silently fails or throws cryptic errors.
    • Fix: Use Type::nonNull(Type::int()) for required arguments and validate input:
      'args' => [
          'id' => ['type' => Type::nonNull(Type::int())],
          'name' => ['type' => Type::string()]
      ]
      
  3. Nested Resolver Errors

    • Issue: Errors in nested resolvers can look like a silent failure when a parent resolver throws first.
    • Fix: Return per-field errors via GraphQL\Error\Error and add a custom error formatter so extensions.code survives into the response.
  4. Schema Validation Failures

    • Issue: Schema validation errors (e.g., duplicate field names) only appear at runtime.
    • Fix: Call $schema->assertValid() in a test or in a php artisan graphql:validate-style command:
      $schema = new Schema([...]);
      $schema->assertValid();
      
  5. Performance with Large Datasets

    • Issue: Queries returning large datasets (e.g., all()) cause memory issues.
    • Fix: Use pagination or cursors:
      query {
        users(first: 10, after: "cursor") {
          edges { node { id name } }
          pageInfo { hasNextPage }
        }
      }
      
  6. Invalid UTF-8 now hard-fails (v15.38.0)

    • Issue: Documents containing malformed UTF-8 (odd encodings, truncated gzip bodies, JSON bodies decoded with the wrong charset, copy-pasted control characters) are now rejected at parse time instead of being silently mangled.
    • Fix: Validate/sanitize the raw body before handing it to the parser, and make sure your web server/Content-Type charset is UTF-8:
      if (! mb_check_encoding($query, 'UTF-8')) {
          throw new \GraphQL\Error\Error('Query must be valid UTF-8');
      }
      
    • Practical impact: if you log raw query strings and replay them (persisted queries, fixtures, test snapshots), re-check that the stored copy round-tripped through your DB encoding intact.
  7. stripIgnoredCharacters() is not a sanitizer

    • It only removes insignificant characters; it will not reformat, reorder or semantically normalize. Don't use its output as the stored document unless you're fine losing comments (which is usually what you want for a persisted-query cache key, but not for an SDL artifact you plan to show humans).
  8. Batch requests now fail with RequestError (v15.38.1)

    • Issue: A batch payload is supposed to be an array of operation objects. A scalar entry (["x"]), or a JSON object that got coerced into an array by PHP's decoder ({"query":"{ a }","0":"x"}), used to blow up as a TypeError — a 500 in most Laravel setups, and invisible to your GraphQL error formatter.
    • What changed: The server now throws GraphQL\Error\RequestError, which is handled like any other request-level problem and produces a proper client error response.
    • Action:
      • Drop any catch (\TypeError $e) guards around batch execution — they no longer fire.
      • If your error formatter special-cases RequestError, verify the batch case now lands there too; extensions.code for these is a request error, not a resolver error.
      • Because PHP's json_decode($body, true) turns {"query":"...","0":"x"} into a mixed array, guard the shape yourself before execution if you want a friendlier message than the default:
        $payload = json_decode($request->getContent(), true);
        $isBatch = array_is_list($payload);
        
        if ($isBatch && collect($payload)->contains(fn ($op) => ! is_array($op))) {
            throw new \GraphQL\Error\RequestError('Each batch entry must be an operation object');
        }
        
    • This is only observable when you actually accept batches (ServerConfig does by default) — if you've disabled batching or always send single operations, nothing changes for you.

Debugging Tips

  1. Enable Debug Mode Configure the server to return detailed errors:

    $config = ServerConfig::create()
        ->setSchema(app('graphql.schema'))
        ->setDebugFlag(app()->environment('local'))
        ->setErrorFormatter(fn(\GraphQL\Error\Error $e) => \GraphQL\Error\FormattedError::createFromException($e, app()->environment('local')));
    
  2. Log the raw body when a parse error looks impossible With UTF-8 rejection in place, a "syntax error" is often a transport problem. Dump request()->getContent() with bin2hex() around the failure offset before blaming the query.

  3. Distinguish request errors from execution errors Since v15.38.1, malformed batch entries surface as RequestError rather than a PHP TypeError. If a batch request "used to 500 and now returns 400", that's the fix working — don't chase it as a regression. Log get_class($e) in your formatter when triaging.

  4. Assert schema validity in CI, not at runtime Add $schema->assertValid() to a single test — it catches duplicate types, missing interfaces and bad field definitions long before a user does.

  5. Use the printer for diffing Printer::doPrint() plus stripIgnoredCharacters() gives you a stable, whitespace-insensitive form — handy for snapshot tests and for eyeballing what a client actually persisted.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
unabomber/timezone
delatbabel/contacts
astritzeqiri/g-recaptcha
kevinongko/laravel-genode
developeruz/laravel-analytics
robbiep/afterthedeadline
frankkessler/incontact-laravel-oauth2-rest
jlxh/laravel-userlog
davidmgilo/timetables
qylinfly/role-permission
reflect-digital/conduit
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr