Monorepo with PHP: Code Organization and Best Practices
In modern PHP development, the question of effective code organization arises more and more often. The traditional approach with many separate repositories (multirepo) has its drawbacks: complexity of version synchronization, duplication of configurations, and problems with refactoring. An alternative is the monorepo — a strategy where all related projects, packages, and libraries are stored in a single repository. In this article, we will look at how to organize a monorepo with PHP, which tools to use, and what pitfalls to consider.
What is a monorepo and why is it needed for PHP projects?
A monorepo is a single code repository containing several projects or packages. In the PHP world, a classic example is Laravel, which includes the framework, packages for authentication, queues, mail, and other components in one repository.
Main advantages for PHP projects:
- Simplified refactoring — changes in one package are immediately visible to all dependent components, no need to publish and update versions of each package separately.
- Version integrity — all packages use the same version of dependencies, which eliminates type conflicts.
- Unified CI/CD — one build pipeline for all components, simplifying testing of cross-package changes.
- Improved collaboration — developers see the entire project code, making code review and maintaining unified coding standards easier.
However, a monorepo is not suitable for everyone. If your projects are loosely coupled or have different release cycles, a multirepo might be more convenient. For medium and large PHP projects with tight component integration, a monorepo often becomes the optimal choice.
Monorepo structure in PHP
Proper structure is the foundation of a successful monorepo. Let's look at a typical organization using an example project with a core, several modules, and tests.
my-php-monorepo/├── packages/│ ├── core/│ │ ├── src/│ │ │ └── CoreService.php│ │ ├── tests/│ │ └── composer.json│ ├── module-auth/│ │ ├── src/│ │ ├── tests/│ │ └── composer.json│ └── module-payment/│ ├── src/│ ├── tests/│ └── composer.json├── apps/│ ├── api/│ │ ├── public/│ │ ├── src/│ │ └── composer.json│ └── console/│ ├── src/│ └── composer.json├── tests/│ └── IntegrationTest.php├── composer.json (root)├── phpunit.xml.dist├── .gitignore└── README.mdKey organization principles:
- Separation into packages — each package has its own
composer.jsonwith name, autoloading, and dependencies. - Applications (apps) — entry points (API, console commands) that bring packages together.
- Root composer.json — contains meta-configuration for working with the monorepo.
Configuring the root composer.json
To make the monorepo work, you need to configure the root composer.json so that packages are available locally, without publishing to Packagist.
{ "name": "my-company/my-monorepo", "type": "project", "require": { "php": "^8.1", "my-company/core": "*", "my-company/module-auth": "*", "my-company/module-payment": "*" }, "autoload": { "psr-4": { "MyCompany\\\\": "packages/" } }, "repositories": [ { "type": "path", "url": "packages/*", "options": { "symlink": true } } ], "scripts": { "