bendeckdavid/graphql-client
A lightweight GraphQL client for Laravel and PHP with a fluent interface for building queries and mutations, passing variables and headers, and handling responses from remote GraphQL APIs. It simplifies integrating GraphQL services into your application with clean, readable code.
This package (bendeckdavid/graphql-client) is a lightweight GraphQL client for Laravel that wraps HTTP calls to a GraphQL endpoint and gives you a fluent request builder plus parsed responses. As of v1.3.1 the client can return responses either as associative arrays (the historical default) or as JSON objects/decoded structures, which matters for how you consume nested data payloads.
Minimal setup:
composer require bendeckdavid/graphql-clientconfig/graphql-client.php) — set the base URL, default headers, and auth token there rather than hard-coding in call sites.use BendeckDavid\GraphQLClient\Facades\GraphQL;
$response = GraphQL::query('
query ($id: ID!) {
user(id: $id) { id name email }
}
', ['id' => $userId]);
$user = $response['data']['user'] ?? null;
Where to look first: the facade/service-provider bindings (to see how the client is resolved from the container) and the request-builder class (to see how variables, headers, and the operation name are assembled). That tells you everything about how to integrate it into your app.
First real use case: wrap your queries in a small repository/service class per domain (e.g. UserGraphQLRepository) so your controllers never touch raw query strings. This keeps queries testable and gives you one place to mock.
Repository-per-domain. Don’t scatter query strings through controllers. Create a class per resource that owns its queries/mutations and returns typed DTOs or Eloquent-ish value objects. This is the single highest-leverage pattern with a raw GraphQL client.
class UserGraphQLRepository
{
public function find(string $id): ?UserData
{
$res = GraphQL::query(
'query ($id: ID!) { user(id: $id) { id name email } }',
['id' => $id]
);
return isset($res['data']['user'])
? UserData::fromArray($res['data']['user'])
: null;
}
}
Variables, never string interpolation. Always pass variables as the second argument. It avoids escaping bugs and lets the server cache/plan the query.
JSON-object responses (new in v1.3.1). The release adds a parameter to request the response as a JSON object, and fixes numeric handling in JSON decoding. Use object mode when your downstream code expects stdClass/objects (e.g. mapping through json_decode twice, or when numeric keys in the response would otherwise be coerced oddly by PHP’s array semantics). Use array mode for the common Laravel case where you data_get() into the payload. Be explicit at the call site so a future default change can’t break you.
// Array mode (default, familiar in Laravel)
$res = GraphQL::query($query, $vars);
// Object mode — useful when the schema returns maps keyed by numbers
$res = GraphQL::query($query, $vars, /* asJsonObject */ true);
Centralize auth + headers. If the endpoint needs a bearer token or tenant header, set it once in config or a middleware-like wrapper rather than per call. For multi-tenant apps, resolve the token from the current tenant in a single bound service.
Error handling as a boundary concern. GraphQL returns HTTP 200 even for errors, so wrap calls and inspect the errors key. A dedicated exception (GraphQLRequestException) thrown from your repository layer keeps controllers clean:
$res = GraphQL::query($query, $vars);
if (!empty($res['errors'])) {
throw new GraphQLRequestException($res['errors']);
}
Testing. Point the config at a fake base URL and use Laravel’s HTTP fake (Http::fake()) since the client ultimately issues HTTP requests. Assert on the query/variables sent and stub the JSON body. This is dramatically easier than mocking the client internals.
Pagination / batching. For lists, use cursor-based pagination in the query and loop in the repository until pageInfo.hasNextPage is false. Batch related lookups into a single query using aliases or fragments rather than N round-trips.
Numerics in JSON (fixed in v1.3.1). Earlier behavior could mangle numeric-looking values when decoding responses. If you were on v1.3 or below and saw IDs or counts arrive as strings or with unexpected types, upgrade — but re-check any code that relied on the old coercion (e.g. loose comparisons, == checks) before you ship.
Array vs object mode is a footgun. Mixing modes across a codebase leads to "Cannot use object of type stdClass as array" bugs. Pick one convention per repository and stick to it. If you switch a repository to object mode, update all its consumers at once.
HTTP 200 ≠ success. Always check errors. Validation errors, partial failures, and authorization issues all come back in-band.
Query string formatting. Multi-line heredoc queries are fine, but be careful with $ inside double-quoted strings in PHP — it will interpolate. Use single-quoted heredocs (<<<'GQL') for queries with variables to avoid accidental interpolation.
Config caching. If you publish a config file, remember php artisan config:cache in deploy. Endpoint/token values resolved at runtime (e.g. from a tenant) won’t be re-read after caching — keep dynamic values out of cached config and resolve them in code instead.
Headers and content-type. Some GraphQL servers are strict about Content-Type: application/json and an Accept header. If you get odd 4xx responses, dump the outgoing request with Http::fake()/Http::recorded() to confirm headers before blaming your query.
Extension points. The fluent builder is usually the right place to hook in: add default headers, an operation name, or a persisted-query hash via a wrapper you control. Prefer wrapping the facade in your own service over patching vendor code — the next minor release (as v1.3.1 shows) can change signatures like adding a parameter, so keep your call sites behind a thin abstraction.
Version pinning. This is a small package with infrequent releases; pin to ^1.3 and read changelogs on upgrade. Since v1.3.1 added a parameter rather than changing the default, it should be a drop-in upgrade — but verify your composer.lock in CI if you depend on exact response shapes.
How can I help you explore Laravel packages today?