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

Graphqlite Laravel Package

thecodingmachine/graphqlite

GraphQLite is a framework-agnostic PHP library that lets you build a complete GraphQL API by annotating your classes. Define types, queries, mutations, and subscriptions with simple attributes and controller methods, with Symfony and Laravel integrations available.

View on GitHub
Deep Wiki
Context7

Getting Started

Install. In a Laravel app you almost never install the core package directly — pull the Laravel integration, which brings thecodingmachine/graphqlite and webonyx/graphql-php with it:

composer require thecodingmachine/graphqlite-laravel
php artisan vendor:publish --tag=graphqlite   # config/graphqlite.php

Requirements are PHP ^8.1 and webonyx/graphql-php ^15 (GraphQLite 6.x). There is no migration and no schema file: your PHP classes are the schema.

Where to look first. In this order:

  1. config/graphqlite.php — the namespaces array is the single most important setting. GraphQLite recursively scans only those namespaces for #[Type], #[Input], #[Query], #[Mutation] classes. A class outside them simply does not exist to the schema builder. Same file holds the route prefix/middleware and security settings.
  2. The docs site (graphqlite.thecodingmachine.io) — read Types, Queries & Mutations, Input Types, Security, Validation, in that order. Skip the Symfony-bundle pages; the Laravel package substitutes its own wiring there.
  3. vendor/thecodingmachine/graphqlite/src/Attributes/ — the fastest way to see the real attribute surface and their constructor arguments.

First use case — expose a read-only list plus one write, which covers 80% of what you'll do day to day:

#[Type]
class ProductDTO
{
    #[Field] public int $id;
    #[Field] public string $name;
    #[Field] public ?string $description = null;
}

class ProductController
{
    #[Query(name: 'products')]
    /** @return ProductDTO[] */
    public function products(): array { /* map from Eloquent */ }

    #[Mutation]
    public function createProduct(CreateProductInput $input): ProductDTO { /* ... */ }
}

Then POST /graphql with {"query":"{ products { id name } }"}. In development, use the GraphiQL endpoint the package registers (check config/graphqlite.php for the path) — it's how you'll iterate for the rest of the project.

Implementation Patterns

Thin controllers, fat DTOs. The pattern that ages best: #[Query]/#[Mutation] classes are one-liners that validate, delegate to an existing service/action, and map the result. Never put business logic in a resolver, and never return raw Eloquent models directly from a resolver — map to a #[Type] DTO whose #[Field]s are explicitly chosen. This decouples your GraphQL contract from your persistence layer, which is exactly the coupling Lighthouse users fight with.

#[Input] for every mutation argument. Rather than a signature of six scalars, define one input type and let GraphQLite do the coercion:

#[Input]
class CreateProductInput
{
    #[Field] public string $name;
    #[Field] public ?float $price = null;

    #[Field]
    public function setTags(array $tags): void { /* normalize, dedupe */ }
}

Setters run during hydration, so this is the right hook for normalization. In v6 the same class can be both #[Type] and #[Input] — one DTO for output and input is fine for small CRUD, but split them as soon as output and input shapes diverge (they always do).

Authorization via attributes, not if statements. Reach for #[Logged] first, then #[Right('update-product')] (Gate/Policy-backed) or #[Security("...")] for expressions. Add #[HideIfUnauthorized] when a field's existence should depend on the viewer (e.g. email on a public profile) rather than returning an error. Keep the checks declarative so they show up in a code review of the controller.

Inject services, don't app() inside methods. Resolvers are built from the container: constructor-inject your services, and use #[Autowire] for per-argument injection. This is what makes resolvers testable outside HTTP.

Validate with #[Assertion]. The Laravel integration wires Laravel's validator into GraphQLite's assertion layer, so #[Assertion('required|email|unique:users,email')] on an input property produces per-field validation errors in the standard extensions payload. Catch the resulting ValidationFailedException at the framework boundary and let the frontend map errors by path.

Return typed collections, not arrays of anything. Use PHPDoc generics (/** @return ProductDTO[] */, iterable<ProductDTO>) so GraphQLite resolves the element type — this is what makes GraphQL's type system worth having. For lists, return a dedicated pagination type (edges/pageInfo) instead of a bare array; you cannot add cursor semantics later without a breaking change.

Enums: use native PHP enums. Backed enums become GraphQL enums in both input and output positions automatically in 6.x. This is one of the strongest reasons to pick this package over hand-rolled webonyx config.

Solve N+1 with #[Prefetch] and eager loading. Root queries should ->with([...]) what they know the client needs. For nested #[Field] resolvers that must touch a relation, mark them #[Prefetch('relation')] so GraphQLite batches the load across the result set instead of firing one query per row. Wire Laravel's query log into tests and assert a query count — this is the single most valuable regression test for a GraphQL API.

Cross-cutting concerns via field middleware. Implement FieldMiddlewareInterface and register it on the schema factory for timing, tenant scoping, or audit logging. It beats repeating the same call in 30 resolvers, and it composes with the security attributes.

Test at two levels. HTTP-level: $this->postJson('/graphql', ['query' => ...]) with Sanctum/actingAs — covers middleware and auth. Schema-level: build the SchemaFactory directly and execute with GraphQL::executeQuery() — fast, no HTTP, ideal for asserting that a field is hidden for anonymous users. Also commit an introspection snapshot (or run GraphQL Codegen in CI) so a frontend TypeScript break shows up as a diff in review rather than a runtime error.

Gotchas and Tips

Namespace scanning is the #1 onboarding trap. New type classes in a namespace not listed in config/graphqlite.php produce "Unknown type" errors that mention nothing about config. Put all schema classes under one dedicated root (e.g. App\GraphQL) and register only that.

The schema is cached; stale cache looks like a code bug. After renaming a field, adding an attribute, or changing the namespaces, run php artisan cache:clear (and clear any configured cache directory) before debugging. In CI/deploy, always clear as part of the release step. Scanning reflects every class in the namespace eagerly, which is why caching exists — don't disable it in production.

Nullability is the source of the webbiest bugs. A PHP string return type becomes String!. If the resolver can return null, the whole parent object gets nulled and errors bubble up to the nearest nullable ancestor — the client sees data: null with a populated errors array, and it looks like the API "randomly failed". Mark genuinely optional fields ?string / #[Field(outputType: 'String')].

Scalars don't always map how you expect. int is Int!, not ID!. For identifiers use GraphQLite\Types\ID (or the ID phpdoc type). Float/Int coercion on input is strict — sending "5" where Int is expected fails validation, which surprises JS clients that stringify numbers.

Multiple input variants need explicit default. Since v6, if you declare #[Input(name: 'UpdateProductInput')] alongside a plain #[Input], the unnamed one is the default; an explicitly named one is not default unless you pass default: true. Two variants of the same class is a documented, supported pattern — just be deliberate about which one arguments resolve to.

Optional input fields need nullable types or defaults. An input property typed string with no default makes it required in the schema. For PATCH-style update mutations, give every optional property a ?type ... = null (or a sensible default) and let the setter skip null.

Error masking will hide your validation messages if you're careless. webonyx only surfaces exceptions implementing ClientAware (or GraphQLException). Domain exceptions thrown from a service will reach the client as a generic "Internal server error". Either throw GraphQLException/ValidationFailedException, use #[FailWith(SomeGraphQLException::class)] to map a domain exception, or accept the masking and log the real one server-side. Always keep APP_DEBUG off in production but make sure your logger captures the full trace.

Laravel vs. Symfony docs drift. Some upstream examples use Symfony's is_granted() helper inside #[Security]. On Laravel, prefer the #[Right]/#[Logged] attributes and Gate-backed authorization; if you need custom expression functions, register them through the Laravel package's security configuration rather than copying Symfony snippets.

File uploads are opt-in. Since 4.1 the upload scalar lives in a separate ecodev/graphql-upload package. If you need UploadedFile arguments, install it explicitly and verify multipart handling survives your middleware stack (CSRF, size limits, php.ini upload_max_filesize).

Custom type mappers are version-sensitive. If you write a RootTypeMapperInterface to support a custom scalar or generic, note that its method signatures changed in 6.1 for stricter typing (OutputType|null $subType, union-typed returns). Pin thecodingmachine/graphqlite to a minor version range and re-run your mapper tests on upgrade.

Protect the endpoint before you go public. Combine route middleware (auth:sanctum) with attribute-level checks, and enable query depth/complexity limits plus introspection restrictions in the config. Introspection is how you'll generate frontend types during development, so gate it by environment rather than removing it.

Pick your paradigm consciously. If your team thinks in .graphql SDL files and wants directive-driven Eloquent batching with minimal PHP, Lighthouse is the more idiomatic Laravel choice. GraphQLite's bet — PHP classes and attributes as the single source of truth, with ordinary IDE navigation, static analysis and refactoring support — pays off in larger domains and mixed PHP/frontend teams. Given its maturity (6.x, PHP 8.1+, steady releases) but comparatively small ecosystem, budget for reading the source under vendor/ when an edge case isn't documented; the attribute classes are small and readable, and that's usually faster than searching.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin