garlic/graphql-php-tools
Garlic GraphQL PHP Tools provides developer-friendly helpers for webonyx/graphql-php, simplifying schema setup, type definitions, and common GraphQL utilities so PHP developers can build and maintain APIs with less boilerplate.
Assess before you install. This is a standalone PHP library whose original release was 2019 with a single star and no dependents. It predates most of the schema-stitching features now bundled in webonyx/graphql-php (GraphQL\Utils\BuildSchema, GraphQL\Utils\ExtendSchema, GraphQL\Utils\SchemaExtender). Version 1.0.7 ("Refactor code and bug fix") is the first release in years and is a patch bump with no stated feature or dependency changes — treat it as a maintenance blip, not a revival. Before wiring it into a Laravel app, check whether native extendSchema / SDL extend type already solves your problem — for most Laravel teams it does.
If you still want it:
composer require garlic/graphql-php-tools
There is no Laravel service provider and no auto-discovery, so nothing registers itself. First thing to do is read src/ and composer.json in the vendor directory — specifically:
webonyx/graphql-php. This is the single most important file. 1.0.7 is a "refactor and bug fix" release; it does not claim to have widened the engine constraint, so assume the pin is unchanged until you verify it. If it requires ^0.13 or ^14 and your app is on 15.x (Lighthouse 6, rebing/graphql-laravel 8+), Composer will either refuse or you will get a broken type system.GraphQL\Schema objects, or DocumentNodes, and returns a combined GraphQL\Schema. Because 1.0.7 was a refactor, diff src/ against 1.0.6 before upgrading an existing install; a "refactor" with no changelog detail is exactly the class of release that renames a method or changes a return type.First use case — split a monolithic schema. The classic motivation is a growing graphql/schema.graphql that every team edits and conflicts on. Take your two domains:
use Garlic\GraphQL\Tools\...; // check src/ for the actual namespace
$merged = $merger
->addSchema(file_get_contents(resource_path('graphql/users.graphql')))
->addSchema(file_get_contents(resource_path('graphql/billing.graphql')))
->build();
Then confirm the result actually resolves:
$result = GraphQL::executeQuery(
$merged,
'{ users { id } billingSummary { total } }',
null,
null,
$rootValue
);
If that executes cleanly against your existing resolvers, the library fits. If type names collide or directives vanish, stop and reconsider.
Wrap whatever it gives you in a small SchemaServiceProvider or a singleton binding so you have one place to swap the implementation later.
Domain-module layout. The pattern that pays off is one directory per domain, each self-contained:
graphql/
users/types.graphql
users/queries.graphql
billing/types.graphql
billing/queries.graphql
Merge them in a service that reads a config array:
// config/graphql.php
'modules' => ['users', 'billing', 'reports'],
class SchemaFactory
{
public function __construct(private array $modules) {}
public function build(): Schema
{
$merger = ...; // instantiate the package merger
foreach ($this->modules as $module) {
foreach (glob(base_path("graphql/{$module}/*.graphql")) as $file) {
$merger->addSchema(file_get_contents($file));
}
}
return $merger->build();
}
}
This gives you a single toggle for feature-flagged domains — a genuinely useful thing the package's raw API doesn't provide on its own.
Extension pattern for plugins. The "extend schema" half of the package is best used for optional add-ons: an audit module that adds fields to an existing User type without the core users module knowing about it. Keep base types in one file, extensions in another, and load extensions only when the feature is enabled. This mirrors GraphQL's own extend type User { ... } and is the most defensible way to use the library.
Merge resolver maps explicitly. Schema merging is only half the job. If you are not on Lighthouse (where resolvers are inferred from SDL), you must merge your resolverMap arrays yourself. Build one array per module and array_merge the per-type maps, being careful that two modules don't both define Query.users — the later one silently wins. A small assertion that detects duplicate root field keys across modules will save you hours.
Cache the compiled schema. Parsing and merging on every request is wasteful. Serialize the merged Schema (or, more portably, the merged SDL string) into the cache and invalidate on deploy:
return Cache::rememberForever('graphql.schema', fn () => $this->buildMergedSdl());
Cache the SDL, not the Schema object — PHP object serialization across webonyx versions is fragile. This matters more, not less, now that 1.0.7 touched internals: a cached serialized Schema built by 1.0.6 can blow up after a one-patch upgrade. Cache invalidation on deploy covers you.
Testing. Expose SchemaFactory as a singleton so tests can build the schema from a fixture module list. Add a test that asserts the merged schema contains every type referenced from every module's queries — this catches the "module forgot to register its types file" failure, which is the most common production break in this style of setup.
Framework integration. With Lighthouse, prefer Lighthouse's native @extends / multi-file schema support; this package mostly duplicates it. With rebing/graphql-laravel, feed the merged schema string into the schemas.<name>.schema config as a file path, or override the schema resolution in a subclass. With raw webonyx, the merger is your composition root.
One patch release does not make it maintained. The 1.0.7 notes are three words long — "Refactor code and bug fix" — with no issue references, no breaking-change callout, and no dependency bump announced. There is still one star, still zero dependents, still no obvious CI story. Treat it as: not dead enough to ignore the version bump, not alive enough to trust for new work. If you adopt it, vendor it: copy src/ into your own namespace (App\GraphQL\SchemaTools) and treat it as first-party code you now own. That is a 200-line decision, not a 2,000-line one, and it removes the Composer version-pin deadlock entirely — as well as making 1.0.7's refactor your problem to review once, not a surprise on composer update.
Read the 1.0.7 diff before upgrading. "Refactor code and bug fix" is the least informative changelog entry possible. If you already run this package, pin 1.0.6, run your test suite, then bump to 1.0.7 and diff vendor/garlic/graphql-php-tools/src between the two tags. A refactor in a merger can change duplicate-type behaviour, directive handling, or method signatures without any of that appearing in the release notes.
Duplicate type definitions fail loudly or silently depending on the path. Two modules each declaring type User will either throw during build or — worse — one wins depending on merge order. Enforce a convention: exactly one module owns each type; everyone else uses extend type. Add a CI check that greps for duplicated type Name declarations across the graphql/ tree.
Directives and descriptions are the usual casualties. Hand-rolled mergers frequently drop @directive applications attached to fields and types, because they merge FieldDefinitionNode lists without carrying directives. Before trusting the output, dump the merged SDL and diff it against your inputs. If @guard, @paginate, @can (Lighthouse) or @deprecated disappear, the merger is not directive-safe and you cannot use it on top of Lighthouse. A bug-fix release is precisely the kind of thing that may have changed this behaviour silently — re-run the diff after upgrading.
Resolver map collisions are silent. Merging schemas does not merge resolvers. Two modules defining a Query.me field will build fine and then resolve to whichever map entry was merged last. Namespace your root fields (billingSummary, not summary) or assert uniqueness at boot.
Enums, unions, and interfaces need explicit handling. Naive mergers copy type definitions but neglect resolveType callbacks for interfaces and unions, and enum value lists can end up concatenated with duplicates. Add an integration test that queries every interface-backed field with a couple of concrete types.
Watch the webonyx constraint at install time. Run composer why-not webonyx/graphql-php after adding the package, and again after upgrading to 1.0.7. If it pins an old minor, you will be stuck on an unsupported GraphQL engine — a much larger risk than the merging convenience is worth.
Debugging trick: print, don't guess. There is no debug mode and no meaningful error messages. When a merge fails, GraphQL\Utils\SchemaPrinter::doPrint($schema) (or your engine's SDL printer) is the fastest way to see what the merger actually produced. Bisect by adding modules one at a time. After a patch release with vague notes, this is also your fastest regression check.
Extension points are limited to what's in src/. There are no events, no middleware hooks, no strategy interface for conflict resolution. If you need "last-wins" vs "first-wins" vs "error on conflict" behaviour, you are subclassing or editing vendor code. Plan for that — and note that subclassing gets more expensive every time upstream refactors its internals.
Prefer the native alternative for new work. For a greenfield Laravel project, the maintenance-adjusted recommendation is unchanged by 1.0.7: Lighthouse ≥ 5 with multi-file schema and @extends, or plain webonyx/graphql-php 15 with BuildSchema::build + ExtendSchema::extend. Both are maintained, both cover the same ground, and both spare you from owning a 2019 library that ships three-word changelogs. Reach for garlic/graphql-php-tools only if you are inheriting an app that already depends on it — and then vendor it on your way out.
How can I help you explore Laravel packages today?