marcelthole/openapi-merge
marcelthole/openapi-merge is a PHP package for combining multiple OpenAPI specification files into one merged document. Split large API specs into manageable parts and merge them back, simplifying documentation and code generation workflows.
This package is a build-time CLI/utility for combining multiple OpenAPI 3.x files (yaml/json) into a single spec. It is not a Laravel package: there is no service provider, no facade, no publishable config, and nothing to register in config/app.php. Treat it the way you'd treat any dev-only binary such as php-cs-fixer.
composer require --dev marcelthole/openapi-merge
First look here, in order:
vendor/bin/openapi-merge --help — the console entrypoint is where 90% of real usage lives.OpenApiMerge\Merge\Merger, OpenApiMerge\File\FileReader and the writer classes under src/ if you need to call it programmatically.src/Resolver/ — this is what decides how $refs between files are followed, and it's the part that bites people.The canonical first use case for a Laravel app: you keep per-domain spec fragments (docs/openapi/users.yaml, docs/openapi/billing.yaml, …) plus a hand-written docs/openapi/base.yaml holding info, servers and shared components, and you merge them into public/docs/openapi.yaml for Swagger UI / Redoc / L5-Swagger / Scramble-free consumers.
vendor/bin/openapi-merge merge -f docs/openapi/base.yaml -f docs/openapi/users.yaml -f docs/openapi/billing.yaml -o public/docs/openapi.yaml
Get that working by hand before wiring it into Composer scripts or CI.
Wrap it in an Artisan command, not a shell call. Calling the binary from a Composer script is fine locally but brittle in CI (CWD, PATH, exit code swallowing). A thin command that delegates to the package API gives you logging, config, and a --check mode:
// app/Console/Commands/MergeOpenApi.php
protected $signature = 'openapi:merge {--check : Fail if the committed spec is stale}';
Wire the file list from config/openapi.php (a plain config array — you own it, the package doesn't ship one) so module authors add a fragment by adding a line, not by editing a shell script.
Config-file driven merges. For more than three fragments, keep a docs/openapi/merge.yaml listing inputs and the output path and commit it. It makes the merge reproducible across machines and readable in review, and avoids 12-line shell commands drifting between developers' terminals.
Base file first, always. The first input typically supplies info, servers, security and tags. Decide deliberately which file is the base and document it — otherwise info.version in the output is whatever file happened to be listed first in someone's local invocation.
Namespace your components. Name schemas Users.User, Billing.Invoice (or UserResource / InvoiceResource) rather than User. Merging two #/components/schemas/User definitions from different modules is the single most common cause of "the output is wrong but no error was raised."
Run it on a pre-commit or CI gate. Generate into a file you commit, and fail the pipeline if regeneration produces a diff. That keeps spec changes visible in PRs alongside the code that caused them:
php artisan openapi:merge && git diff --exit-code public/docs/openapi.yaml
Combine with vendor specs when needed. Pulling a third-party or legacy spec in alongside your own paths is a legitimate use, but do it with an explicit path prefix strategy so you don't shadow your own routes.
Feed the artifact downstream. Whatever you point at the merged file (L5-Swagger's paths.docs, a static Redoc page, an upload to Stoplight) should read the generated file, never the fragments.
The 2.6.0 change is a constraint widening, not a behaviour change. It only allows symfony/console ^8. Laravel 11/12 currently pin symfony/console ^7, so on a modern Laravel app this release does nothing for you — and if you try to force symfony/console ^8 into a Laravel app to "use" it, Composer will fight you. Let the framework resolve console; upgrade this package for the other fixes and move on. The only practical beneficiaries are standalone CLI consumers and future Laravel versions.
No Laravel integration means no magic. No auto-discovery, no php artisan vendor:publish, no container bindings, no config. If you want it ergonomic, you build that layer yourself — that's the whole "implementation pattern" above.
$ref resolution is CWD-sensitive. Relative references resolve against the file that contains the ref, but if you run the binary from a different directory than your developer expecting (very easy in a Makefile or Docker WORKDIR), external refs into ../common/schemas.yaml break. Prefer paths relative to a fixed root and run the command from the project root consistently.
YAML parsing traps. PHP YAML parsers turn bare yes/no/on/off into booleans, and version: 3.0 into a float. Always quote: openapi: '3.0.3' and version: '1.0'. Symptom is a confusing validation error rather than a parse error.
Silent override on collisions. When two fragments define the same path or component key, one wins. Order in the input list is the tiebreak. Add a post-merge sanity check (assert expected operation count / that a known schema exists) rather than trusting the output blindly.
Non-deterministic output kills the CI diff gate. If key ordering shifts between runs, git diff --exit-code gives false failures. Verify stability by running the merge twice and diffing before you adopt the gate.
Validation happens at read time. Invalid fragments throw when read (the package validates against the OpenAPI schema), so failures point at the input file, not the merge step. Read the exception message's file path first.
Extension points. If you need custom reference behaviour (e.g. resolving refs over HTTP, or from a package), the resolver layer is the place to hook in; a custom writer is where you'd add path-prefixing or post-processing. Both are more maintainable than post-processing the YAML with string replacement.
Keep it out of the request cycle. Merging is a build/CI step. Never call it from a controller or a service provider's boot() — it's filesystem-heavy and will show up in your latency graphs.
How can I help you explore Laravel packages today?