Monorepo Builder turns a pile of PHP packages into one composer.json and one release command
Composer tools to maintain a monorepo
At a glance
- What is it?
- Monorepo Builder is a set of Composer tools for maintaining a PHP monorepo: it merges every package's composer.json into the root, propagates versions back out, validates that shared dependencies agree, and automates tagging and releasing. Configuration is a PHP file at the project root, and the tool runs on PHP 8.2 or newer.
- Who is it for?
- Adopt Monorepo Builder if you maintain a PHP monorepo where the root composer.json is generated from the packages, because the merge, propagate and release commands remove the manual synchronisation between a dozen package manifests and one root manifest. Do not adopt it if you have a single package or a handful of independent ones, because the tool assumes a packages/ layout and the merge is the point.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 84 days ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The command set, and the three-step quick start
Monorepo Builder is described as a set of tools for managing PHP monorepos: merging composer.json files, validating package versions, releasing with automation, and more. The primary language is PHP, the licence is MIT, and the default branch is main.
The quick start is three commands:
# 1. Scaffold a basic monorepo layout (one time)
vendor/bin/monorepo-builder init
# 2. Fold every package's composer.json into the root composer.json
vendor/bin/monorepo-builder merge
# 3. Cut a release when you're ready
vendor/bin/monorepo-builder release v1.0Step one generates a basic monorepo skeleton, a packages/ directory and a starter monorepo-builder.php, so you can start adding packages immediately, and it does not overwrite existing files.
Step two is the merge, and the README describes its behaviour precisely. All sections are merged, including standard ones such as require and autoload, and custom ones such as scripts-aliases and abandoned. If a package appears in both require and require-dev, the require entry takes priority. And the original key order of the root composer.json is preserved, with new sections appended at the end.
Step three is the release, which is where the automation is.
The other commands fill in the lifecycle between those. validate checks that all packages use the same version for shared dependencies, which is the check that stops two packages drifting onto incompatible versions of the same library. bump-interdependency updates mutual dependencies between packages to a given version. propagate runs the merge in reverse, pushing versions from the root composer.json back into each package's manifest. package-alias updates the branch-alias in every package composer.json to match the current version. localize-composer-paths sets mutual package paths to local packages for pre-split testing, which is how you test a package against its siblings without publishing.
That last one deserves a note, because it solves a problem that is otherwise painful. Before you split a monorepo into separate repositories, a package has to resolve its sibling from the local directory rather than from Packagist, and doing that by hand across a dozen manifests is exactly the kind of repetitive work this tool exists to remove.
Releasing: version, semver levels, dry runs and a configurable worker pipeline
The release command is the most configurable part of the tool, and the README gives it the most space.
It automates the release process: bumping dependencies, tagging, pushing and updating changelogs. The version can be given explicitly:
vendor/bin/monorepo-builder release v7.0or by semver level, where the comment gives the arithmetic:
# current v0.7.1 → v0.7.2
vendor/bin/monorepo-builder release patchwith patch, minor and major all accepted. That is the difference between typing the next version and letting the tool derive it from the current one, and in a monorepo where every package shares a version, deriving it is the behaviour you want.
You can preview what would happen without making changes, using a dry run:
vendor/bin/monorepo-builder release v7.0 --dry-runThe pipeline itself is a sequence of workers, and the default runs TagVersionReleaseWorker followed by PushTagReleaseWorker. The README points at a section on customizing the release pipeline, and lists what you can change: add workers, reorder them, disable the defaults, and enable LTS-aware tag resolution.
That worker model is the design decision worth understanding. A release that tags, pushes and updates changelogs is three operations that can each fail, and hard-coding them as a single function means you cannot insert a step, such as running a test suite or checking that the working tree is clean, between the tag and the push. With workers, the pipeline is data, and your own worker becomes another entry in the list.
The LTS-aware tag resolution is worth a further word. In a monorepo that ships long-term support branches alongside the current line, the tag to create depends on which branch you are on, and a hard-coded tag command cannot express that. Making it a worker means the resolution rule lives in your configuration rather than in the tool.
The commands that are not merge or release, and what the release history shows
Four of the commands are about the state of the repository rather than about publishing, and they are the ones a maintainer runs often.
validate is the guard rail. It checks that all packages use the same version for shared dependencies, and in a monorepo where a dozen packages each declare their own requirements on the same libraries, that is the check that would otherwise fail silently. A mismatch here produces a package that installs a different version of a shared dependency from its sibling, which is the class of bug that only appears in production.
bump-interdependency updates mutual dependencies between packages to a given version, and the example passes a constraint rather than an exact version:
vendor/bin/monorepo-builder bump-interdependency "^4.0"So when you bump a library to version 5, you run this once and every package that depends on it gets the new constraint, instead of editing each manifest.
package-alias updates the branch-alias in every package composer.json to match the current version. This is the Composer mechanism that lets a maintainer publish a branch such as 4.x under a stable alias so that composer require picks it up, and in a monorepo where all packages move together, doing that by hand across every manifest is precisely the repetition the tool removes.
localize-composer-paths sets mutual package paths to local packages for pre-split testing, which is the state a package needs to be in before you split the monorepo into separate repositories.
The release history shows the cadence. The recent releases are 12.7.2 from 2026-07-31, 12.7.1 from 2026-05-20 and 12.7.0 from 2026-04-24, with the last push on 2026-07-31. So roughly one release every two months, with patch versions carrying the fixes.
The repository layout is worth a glance too, because it shows that this is a multi-package project that uses the tool it builds. There is a packages/ directory alongside src/, src-deps/, packages-tests/, templates/, config/, build/, bin/, and the project-level tooling files: ecs.php for coding standards, phpstan.neon for static analysis, rector.php for automated refactoring, phpunit.xml for tests, box.json for the phar build, and composer.json. The project dogfoods the monorepo layout it provides tools for, which is the best available evidence that the merge and release commands work on a real repository of this shape.
Configuration is a PHP closure, and merge behaviour is the thing you tune
All configuration lives in monorepo-builder.php at the project root, and every option is set on the MBConfig instance passed into the configurator closure:
use Symplify\MonorepoBuilder\Config\MBConfig;
return static function (MBConfig $mbConfig): void {
// your configuration here
};A PHP file rather than a YAML or JSON file is an unusual choice for a build tool, and it is a deliberate one for a PHP project: the configuration is PHP, so it can use PHP, and it can be type-checked against the MBConfig class.
Package discovery is the first thing you will need to change. By default, packages are discovered from ./packages, and the configuration lets you list several directories and exclude specific packages:
return static function (MBConfig $mbConfig): void {
$mbConfig->packageDirectories([
__DIR__ . '/packages',
__DIR__ . '/projects',
]);
// exclude specific packages
$mbConfig->packageDirectoriesExcludes([__DIR__ . '/packages/secret-package']);
};The exclude list exists because a monorepo often contains something that is not a publishable package, a private application or a vendored experiment, and folding its composer.json into the root would be wrong.
The larger part of the configuration is merge output. The README documents options to append data after the merge, to remove data after the merge, to enforce a section order, and to skip autoload merging for selected packages. The append and remove calls take ComposerJsonSection constants, so you can add a PSR-4 mapping under AUTOLOAD_DEV, pin a dev dependency, drop phpunit from the root require, or remove the repositories section completely.
The section ordering option is what makes the generated root composer.json stable rather than merely correct. By default the original key order of root composer.json is preserved, and composerSectionOrder with JsonSchema::getProperties() lets you enforce a specific order, which matters if you diff the generated file in review and want the diff to be about content rather than about position.
The autoload skipping deserves the most attention, because it is where a monorepo's shape actually bites. By default every internal package's autoload and autoload-dev PSR-4 entries are folded into the root composer.json so that vendor/bin/phpunit and other root-level tooling can resolve every namespace. The README then describes three scenarios. The default monorepo of libraries needs no skip, because the root composer.json aggregates every internal library's mapping. A mixed monorepo with libraries symlinked and apps not required from the root is the case that needs care: with the package replace section disabled, the libraries are real path-repository dependencies and Composer symlinks them into vendor/, registering their autoload automatically, so folding it into the root would duplicate that registration. There the skip applies to libraries only, so that the apps' autoload still merges and root-level scripts can find them.
Where Monorepo Builder is the wrong tool
Three limits, and the first is about the assumption the whole design rests on.
The first is that a monorepo needs a single generated root composer.json. The merge command folds every package's manifest into the root, and the propagate command pushes versions back out, which means the root manifest is a derived artefact rather than a hand-written file. That model is right for a library monorepo where every package shares a version and a dependency set, and it is wrong for a monorepo of applications that each have their own dependencies and their own release cadence, because merging their requires into one root manifest produces a root that no single application can install cleanly.
The second is the release automation, specifically the tag and push behaviour. The default pipeline runs TagVersionReleaseWorker followed by PushTagReleaseWorker, and the release command writes tags and pushes them. That is a convenient default for a maintainer working alone on a linear history, and it is a hazard for a team with a branching workflow, where creating a tag and pushing it as two steps in one command leaves no room for a review between them. The worker pipeline is what fixes it, and the README documents how, but the default is still the aggressive one.
The third is the dependency and version situation. The install command in the README is composer require monorepo-php/monorepo --dev, so the published package name differs from the repository name, which is a small trap for anyone copying the command from a repository URL. The tool requires PHP 8.2 or newer, and for PHP 8.1 the README points at symplify/monorepo-builder:^11.2 with the note that it is no longer maintained. A project on an older PHP that has not yet moved is being told to take an unmaintained branch or to upgrade the runtime.
The alternative approach is to write a small script for the specific repository, using Composer's own JSON manipulation, and to script the release with a handful of git commands. The difference in approach is that the general tool covers the cases you have not thought of yet, since its configuration options for merge output, section order and autoload skipping were written by someone who hit each of those problems, while a script covers exactly the cases you have today in a few dozen lines and grows when the monorepo changes shape. For a two-package repository, the script is clearly less work. For a thirty-package one with a release pipeline, the tool is what stops the merge from being a manual copy of thirty manifests.
Editorial conclusion
Adopt Monorepo Builder if you maintain a PHP monorepo where the root composer.json is generated from the packages, because the merge, propagate and release commands remove the manual synchronisation between a dozen package manifests and one root manifest. Do not adopt it if you have a single package or a handful of independent ones, because the tool assumes a packages/ layout and the merge is the point. Verify first that you are on PHP 8.2 or newer, since the 11.2 line for PHP 8.1 is marked as no longer maintained, that the package name you install is monorepo-php/monorepo even though the repository is symplify/monorepo-builder, that your package directories are configured before the first merge, and that the release command runs against a clean working tree, since it tags and pushes.
Frequently asked questions
What does the Monorepo Builder merge command do?
It merges all sections from the package composer.json files into the root composer.json, including standard ones such as require and autoload and custom ones such as scripts-aliases and abandoned. If a package appears in both require and require-dev the require entry takes priority, and the original key order of the root file is preserved with new sections appended at the end.
How do I install Monorepo Builder and what are the requirements?
Run composer require monorepo-php/monorepo --dev, noting that the published package name differs from the symplify/monorepo-builder repository name. It requires PHP 8.2 or newer, and for PHP 8.1 the README points at symplify/monorepo-builder:^11.2, which is no longer maintained.
How does the Monorepo Builder release command work?
It automates bumping dependencies, tagging, pushing and updating changelogs. The version can be given explicitly, as in release v7.0, or by semver level with patch, minor or major, and a dry run previews the changes without making them. The default pipeline runs TagVersionReleaseWorker followed by PushTagReleaseWorker.
Where does Monorepo Builder configuration live?
In a monorepo-builder.php file at the project root, where every option is set on the MBConfig instance passed into the configurator closure. Package discovery directories and excludes, data to append or remove after a merge, the composer section order and the autoload skipping options are all configured there.
What is the difference between merge and propagate in Monorepo Builder?
Merge folds every package's composer.json sections into the root composer.json, and propagate is the reverse, pushing versions from the root composer.json back to each package's composer.json. There is also a localize-composer-paths command that points mutual package paths at local packages for pre-split testing.
What does the Monorepo Builder validate command check?
It checks that all packages use the same version for shared dependencies, which is what stops packages in the monorepo from drifting onto incompatible versions of the same library.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/symplify-monorepo-builder)Was this useful?