bnnvara/graphql
A Laravel/PHP GraphQL package from bnnvara that helps integrate GraphQL schemas and APIs into applications, offering a practical foundation for building and querying GraphQL services in Laravel projects.
The package is a thin abstraction over webonyx/graphql-php — think of it as opinionated glue for schema definition, type registration, and resolver wiring in Laravel, not a full-featured GraphQL server like Lighthouse. Because it has zero stars and zero dependents, treat it as an internal/utility package with no community safety net.
Install and inspect before you trust it:
composer require bnnvara/graphql
Then immediately read the package source:
ls vendor/bnnvara/graphql/src
cat vendor/bnnvara/graphql/composer.json
cat vendor/bnnvara/graphql/README.md 2>/dev/null
The composer.json tells you the exact webonyx/graphql-php version it pins and whether it registers a service provider or publishes config. That pin is the single most important fact — the webonyx/graphql-php API drifted across major versions (v14 → v15 changed resolver signatures, type loader expectations, and GraphQL\Type\Definition\Type helpers).
Where to look first:
config/graphql.php (if published) — schema path, type namespace, root query/mutation class.Type, Query, Mutation, and Field classes — these are what you extend.First use case — wire up a single query end-to-end before building anything else. Define one Query subclass returning a string, register it as the root query type, and hit it through whatever controller or route the package provides. If you can get { hello } returning {"data":{"hello":"world"}}, you understand the wiring; everything else is repetition.
Schema-as-code with a root type. Most abstractions of this shape expect you to define a root Query type class that composes field definitions. The pattern:
class QueryType extends Type
{
public function fields(): array
{
return [
'user' => [
'type' => $this->type(UserType::class),
'args' => ['id' => Type::nonNull(Type::id())],
'resolve' => fn ($root, array $args) => User::find($args['id']),
],
];
}
}
Keep resolvers thin — delegate to services or query objects. GraphQL resolvers are the worst place to hide business logic because they are invoked per-field, and N+1 problems compound silently.
Type registry via lazy callables. If the package exposes a type() helper that resolves class names lazily, use it religiously. Circular type references (User → Post → User) will blow up at boot if you instantiate types eagerly. If the package does not provide lazy resolution, wrap types in closures yourself:
'type' => fn () => $this->types[UserType::class]
DataLoader for N+1. webonyx/graphql-php ships a Deferred/promise-aware execution path, but you must opt in. If the package doesn't surface a DataLoader, build one on top of the executor:
$loader = new DataLoader(fn (array $ids) => User::whereIn('id', $ids)->get()->keyBy('id'));
Return $loader->load($id) from resolvers on nested fields. Batch by request, never singleton — the loader must be scoped to a single GraphQL execution.
Middleware for auth and error shaping. GraphQL collapses many HTTP concerns into one endpoint. Do auth per-field, not per-route:
'resolve' => fn ($root, $args, $ctx) => $ctx['user']->can('view', $user)
? $user
: throw new AuthorizationException(),
Return structured errors via GraphQL\Error\UserError so clients get extensions rather than raw exceptions. A global formatError callback (check whether the package exposes one) is the cleanest place to strip stack traces in production.
Integration with Laravel validation. Resolve arguments to a DTO, then run a standard Validator::make($args, $rules). Do not throw ValidationException directly — GraphQL expects either a UserError or a field-level error. Wrap and rethrow with a field path.
Unknown maintenance status. Zero stars and zero dependents on Bitbucket means no third-party bug reports, no CVE tracking, and no guarantee it works with the current webonyx/graphql-php. Pin the webonyx/graphql-php version in your own composer.json and run the full test suite after any composer update. If you need long-term support, budget for the possibility of forking this package or replacing it with a few hundred lines of custom bootstrapping.
Bitbucket dependency is a real risk. If the repo goes private or is deleted, composer install breaks for every new teammate and every CI run. Mirror it: fork into your own VCS, tag it, and depend on the fork. Even better, vendor the relevant classes into app/GraphQL if the abstraction is small — you will likely end up modifying it anyway.
Resolver signatures are version-sensitive. webonyx/graphql-php changed resolver invocation between versions (argument count, ResolveInfo shape). If you see Too few arguments to function or resolver $info being undefined, check the pinned version in composer.lock against the resolver docs for that release, not the latest docs.
Eager type instantiation loops. Circular references are the number-one startup crash. If the container resolves all types at boot, you will get infinite recursion. Always verify by booting the schema in a test:
public function test_schema_builds(): void
{
GraphQL::schema()->assertValid();
}
Run that test in CI before every deploy — schema build failures are the GraphQL equivalent of a broken migration.
Error leaking. By default webonyx/graphql-php returns "Internal server error" for uncaught exceptions but includes debug info when debug is on. Confirm the package respects config('app.debug'). In production, set debug => false explicitly and add a formatError that logs the original exception while returning a safe message.
N+1 hides in relationships. A naive PostType resolver calling $post->author per post is the classic trap. Instrument with Laravel Debugbar or a query-count assertion in feature tests. If the package gives you no loader hook, use @defer (if supported) or restructure into batched root queries.
Complexity limits. Public GraphQL endpoints are trivially DoS-able with deeply nested queries. webonyx/graphql-php supports QueryComplexity and DepthLimit validation rules — verify the package wires them. If not, add them yourself in the execution middleware; a missing depth limit is a production incident waiting to happen.
Extension points worth checking. Before extending anything, grep for protected function and protected $ in the base classes — most thin abstractions expose one or two override hooks (type resolution, resolver wrapping, error formatting) that save you from copy-pasting framework code. Document those hooks in your own README so the next developer on the team doesn't rediscover them.
How can I help you explore Laravel packages today?