Monorepo with PHP: organizing code for scalable projects
In modern PHP development, code management becomes critically important as a project grows. The traditional approach with many separate repositories (polyrepo) often leads to synchronization issues, dependency duplication, and complexities during refactoring. An alternative is a monorepo, where all code, including libraries, utilities, and applications, is stored in a single Git repository. In this article, we will break down how to organize a monorepo in PHP using Composer, Git, and modern tools.
What is a monorepo and why is it needed in PHP?
A monorepo is a single repository containing several projects, packages, or modules that can be logically related. For a PHP developer, this means the framework core, API service, admin panel, and shared libraries are all in one folder.
Advantages of a monorepo:
- Single versioning: All changes are committed atomically — a fix in a library is immediately accompanied by an update to the code that uses it.
- Simplified refactoring: You can change a package's interface and immediately update all dependencies within a single pull request.
- Tool standardization: One
composer.jsonfile for the root, unified linting and testing rules. - Reduced duplication: Common code (e.g., DTOs, helpers) does not need to be published to Packagist — it is available directly.
However, a monorepo requires discipline: without a clear structure, it turns into a "dump." The key to success is proper organization using Composer autoload and Git submodules or specialized tools like monorepo-builder.
Monorepo structure in PHP
Let's consider a typical structure for a PHP project that has several libraries and one application. The root directory looks like this:
my-monorepo/├── packages/│ ├── core/ # Base library (core)│ │ ├── src/│ │ │ └── CoreClass.php│ │ └── composer.json│ ├── logger/ # Logging│ │ ├── src/│ │ └── composer.json│ └── api/ # API package (can be a separate application)│ ├── src/│ │ └── Controllers/│ └── composer.json├── apps/│ └── web/ # Web application (Laravel/Symfony)│ ├── public/│ ├── src/│ └── composer.json├── composer.json # Root composer.json (meta-package)├── phpunit.xml.dist└── .gitignoreOrganization principles:
- All packages are in
packages/. Each package has its owncomposer.jsonwith a unique name (e.g.,mycompany/core). - Applications (frontend, admin panel, API) are in
apps/. They can use packages frompackages/as local dependencies. - The root
composer.jsonserves for global configuration (autoload, scripts).
Setting up Composer for working with a monorepo
To make PHP autoloading work without publishing packages to Packagist, we use path repositories. Add the following block to the root composer.json:
{ "name": "mycompany/monorepo", "require": { "mycompany/core": "*", "mycompany/logger": "*" }, "repositories": [ { "type": "path", "url": "packages/*" } ], "autoload": { "psr-4": { "MyCompany\\\\": "packages/" } }, "scripts": { "test": "phpunit" }}Now when running composer install, Comp