webonyx/graphql-php
webonyx/graphql-php is a GraphQL server implementation for PHP, following the official GraphQL specification and modeled after graphql-js. Build schemas, execute queries, validate documents, and extend via types, resolvers, and tooling for production APIs.
Install the package:
composer require webonyx/graphql-php
Define a basic schema (e.g., app/GraphQL/Schema.php):
use GraphQL\Type\Definition\Type;
use GraphQL\Schema\SchemaConfig;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\FieldDefinition;
$queryType = new ObjectType([
'name' => 'Query',
'fields' => [
'hello' => new FieldDefinition([
'type' => Type::string(),
'resolve' => fn() => 'World',
]),
],
]);
return new SchemaConfig([
'query' => $queryType,
]);
Create a GraphQL controller (e.g., app/Http/Controllers/GraphQLController.php):
use GraphQL\GraphQL;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Validator\DocumentValidator;
class GraphQLController extends Controller
{
public function execute(string $query, array $variables = []): array
{
$schema = (new Schema())->getSchema();
$document = GraphQL::parse($query);
$validator = new DocumentValidator();
$validationErrors = $validator->validate($schema, $document);
if (!empty($validationErrors)) {
return ['errors' => $validationErrors];
}
$result = GraphQL::execute(
$schema,
$document,
null,
null,
$variables
);
return $result->toArray();
}
}
Add a route (routes/web.php):
Route::post('/graphql', [GraphQLController::class, 'execute']);
Test with a query:
query {
hello
}
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
Type Composition:
ObjectType, InterfaceType, and UnionType to model domain entities.$userType = new ObjectType([
'name' => 'User',
'fields' => [
'id' => Type::id(),
'name' => Type::string(),
'posts' => new FieldDefinition([
'type' => new ListOfType(Type::string()),
'resolve' => fn($root) => ['Post 1', 'Post 2'],
]),
],
]);
Input Types:
InputObjectType.$createUserInput = new InputObjectType([
'name' => 'CreateUserInput',
'fields' => [
'name' => Type::string(),
'email' => Type::string(),
],
]);
Lazy Loading:
$schemaConfig = new SchemaConfig([
'query' => fn() => $queryType,
'mutation' => fn() => $mutationType,
]);
Field Resolvers:
'resolve' => fn($root, $args, $context, $info) => User::find($args['id']),
Context Injection:
Auth, DB) via the context:
$result = GraphQL::execute($schema, $document, null, [
'auth' => auth(),
'db' => app('db'),
]);
DataLoader Integration:
GraphQL\Executor\Promise\Adapter\DataLoaderAdapter for batch loading:
$promiseAdapter = new DataLoaderAdapter();
$result = GraphQL::execute($schema, $document, null, null, [], $promiseAdapter);
Batch Execution:
GraphQL::batchExecute() (for subscriptions or multi-query endpoints).Validation:
GraphQL\Validator\Rule\RuleSet:
$validator = new DocumentValidator();
$validator->addRule(new QueryComplexityRule(1000));
Error Handling:
try {
$result = GraphQL::execute($schema, $document, null, null, $variables);
} catch (\Exception $e) {
return ['errors' => [$e->getMessage()]];
}
Middleware:
Route::middleware(['auth:sanctum'])->post('/graphql', [GraphQLController::class, 'execute']);
Service Providers:
$this->app->singleton(Schema::class, fn() => new Schema());
Testing:
$response = $this->post('/graphql', [
'query' => '{ hello }',
]);
$response->assertJson(['data' => ['hello' => 'World']]);
Circular Dependencies:
User referencing Post which references User). Use resolveType for unions/interfaces to break cycles.Null Handling:
Type::nonNull(Type::string())) must always return a value. Use null or null checks carefully in resolvers.Performance:
QueryComplexityRule to limit query depth:
$validator->addRule(new QueryComplexityRule(1000));
Type Safety:
@var annotations for IDE support:
/** @return string */
'resolve' => fn() => 'value',
Deprecated Features:
Type::overrideStandardTypes() (deprecated in v15.31.0). Use per-schema scalar overrides instead:
$schemaConfig = new SchemaConfig([
'types' => [
Type::string() => new CustomStringType(),
],
]);
Validation Errors:
DocumentValidator output:
$validationErrors = $validator->validate($schema, $document);
dd($validationErrors); // Debug validation issues
Execution Errors:
GraphQL::execute() with a custom error formatter:
$result = GraphQL::execute($schema, $document, null, null, $variables);
if ($result->hasErrors()) {
dd($result->getErrors()); // Inspect execution errors
}
Query Complexity:
$validator->addRule(new QueryComplexityRule(1000, fn($complexity) => logger()->debug("Query complexity: {$complexity}")));
Introspection:
$schemaConfig = new SchemaConfig([
'query' => $queryType,
'introspection' => false, // Disable introspection
]);
Custom Directives:
GraphQL\Type\Definition\Directive:
$authDirective = new Directive([
'name' => 'auth',
'locations' => [Directive::FIELD_DEFINITION],
'args' => [],
]);
Scalar Extensions:
Type::string()) for custom validation:
class CustomStringType extends ScalarType {
public function serialize($value) { /* ... */ }
public function parseValue($value) { /* ... */ }
public function parseLiteral($valueNode, ?array $variables = null) { /* ... */ }
}
Execution Middleware:
$executor = new ExecutionStrategy();
$executor->addMiddleware(new CustomMiddleware());
Schema Extender:
How can I help you explore Laravel packages today?