Updated Rector/ECS YAML to PHP configuration, as current standard.
It seems like PHP companies are opening to the most comfortable way to manage multiple projects and packages at once.
I've heard questions like "how do we make monorepo if have like 15 repositories?" 3 times just last month - from the Czech Republic, Croatia, and the Netherlands.
I'm so happy to see this because lazy dev = happy dev, happy dev = easy code to read. <br> So how to start a monorepo if you already have existing repositories?
Disclaimer: are you're into git history? Read How to Merge 15 Repositories to 1 Monorepo, Keep their Git History and add Project-Base as Well?.
In practice, keeping git history before merging is not worth the invested time. Why?
git
works in merging to different pathsIf you want to bump your company about a massive pile of money for low gain and want it, go for A + B.
I'm an honest and pragmatic developer, and my customers want to deliver features, not to play around technology sandbox, so we always take path B.
Now that's clear, let's dive in to merge practice.
The right time to think about monorepo is usually around 5 repositories. The longer you wait, the more you'll add to your future developers - exponentially. Companies typically get the idea around 15 repositories - we'll work with only 2, but apply the same for whatever count.
First repository: lazy-company/ecoin-payments
With following code:
/src
/test
composer.json
ecs.php
phpstan.neon
phpunit.xml
rector.php
Second repository: lazy-company/drone-delivery
With following code:
/src
/test
composer.json
ecs.php
phpstan.neon
phpunit.xml
rector.php
Go to your Gitlab or Github and create a new repository. Name it lazy-company/lazy-company
(by convention) or lazy-company/lazy-company-monorepo
(in case the previous is taken).
Clone it locally.
/packages
Don't worry, no git harakiri. Just copy paste your other repositories to /packages
directory:
/packages
/ecoin-payments
/src
/test
composer.json
ecs.php
phpstan.neon
phpunit.xml
rector.php
/drone-delivery
/src
/test
composer.json
ecs.php
phpstan.neon
phpunit.xml
rector.php
Not bad, right?
composer.json
to Root OneIn the root directory, we only have the directory with all packages:
/packages
But where is composer.json
? We can ~~create it manually~~ use a CLI tool that does it for us - MonorepoBuilder.
Use prefixed version to avoid dependency conflicts with your packages.
composer require symplify/monorepo-builder-prefixed --dev
Now that we have this power-tool for working with monorepo, we can do:
vendor/bin/monorepo-builder merge
And...
Damn, what is this?
We have to look into composer.json
files to find out what happened:
{
"name": "lazy-company/ecoin-payments",
"require": {
"symfony/http-kernel": "^4.4|^5.0"
}
}
and
{
"name": "lazy-company/drone-delivery",
"require": {
"symfony/http-kernel": "^3.4|^4.4"
}
}
We have 2 packages that require different versions of the same dependency. One allows Symfony 3; the other does not, but can run on Symfony 5.
What version do they share?
^4.4
The number must be identical for all packages. One package cannot have ^4.3
, and the other ^4.4
.
{
"name": "lazy-company/ecoin-payments",
"require": {
- "symfony/http-kernel": "^4.4|^5.0"
+ "symfony/http-kernel": "^4.4"
}
}
and:
{
"name": "lazy-company/drone-delivery",
"require": {
- "symfony/http-kernel": "^3.4|^4.4"
+ "symfony/http-kernel": "^4.4"
}
}
We have to figure out the package version that would be easier to use. Sometimes the new version requires some refactoring.
In current project I migrate 15 packages, that have these requirements:
If we pick ^3.4
, we have to make sure the code of A and C packages will be updated or downgraded to that version. You get the idea.
When we have all versions synced, we can run the merge command:
vendor/bin/monorepo-builder merge
Tadá!
We should see something like this:
{
"require": {
"symfony/http-kernel": "^4.4"
},
"require-dev": {
"symplify/monorepo-builder-prefixed": "^8.0"
},
"replace": {
"lazy-company/drone-delivery": "self.version",
"lazy-company/ecoin-payments": "self.version"
}
}
Do you? Good!
What is the replace
section? We'll use it in step 5 ↓
It's standard that packages depend on each other. Drone delivery is a service a customer pays for - with bitcoins. So we need it here:
{
"name": "lazy-company/drone-delivery",
"require": {
"symfony/http-kernel": "^4.4",
"lazy-company/ecoin-payments": "^2.0"
}
}
What if 2 packages require a different version of the same package?
Do we apply the same approach as in step 4? No. Instead of the most accessible common version, we'll go with the latest version - ^3.0
.
These numbers also tell us what the first monorepo release version will be. It has to be a major version because there will be BC breaks: so ^4.0.
replace?
Here we also use the replace
composer feature.
If we run composer install
in monorepo, it will install all dependencies of lazy-company/drone-delivery
. This package needs lazy-company/ecoin-payments
(the other package). Normally, the composer would go to Packagist and download the package to /vendor
. But that might end-up in collision:
/packages/ecoin-payments/src # some code
/vendor/lazy-company/ecoin-payments/src # same code?
The replace
option tells the composer not to download anything because the lazy-company/ecoin-payments
is already in /packages/ecoin-payments/src
.
/packages/ecoin-payments/src
-/vendor/lazy-company/ecoin-payments/src
All right, we have working composer.json
with united versions. That was the most challenging part, so great job!
Now we need to clean configs of tools that help us with daily development:
Instead of many configs, paths, setups, and rules, there is only 1 source of Truth - root configs.
/packages
/ecoin-payments
/src
/test
composer.json
- ecs.php
- phpstan.neon
phpunit.xml
- rector.php
/drone-delivery
/src
/test
composer.json
- ecs.php
- phpstan.neon
phpunit.xml
- rector.php
+ecs.php
+phpstan.neon
+rector.php
This step is pretty easy... well, it depends.
What is the thing that can happen? One of your packages has PHPStan level 1, but all others have PHPStan 8.
We can either take time and update the PHPStan level 1 to 8 or lower all to 1. I'd go with * drop all to 1* options now, and do this after creating the monorepo. If we mix too many tasks at once, we can prolong build a monorepo tasks for weeks.
Pro-tip: do you want to make sure all versions of all dependencies of all composer.json
files have united version?
vendor/bin/monorepo-builder validate
phpunit.xml
Very similar to step 6, just with unit tests.
/packages
/ecoin-payments
/src
/test
composer.json
- phpunit.xml
/drone-delivery
/src
/test
composer.json
- phpunit.xml
ecs.php
phpstan.neon
rector.php
+phpunit.xml
Update paths in phpunit.xml
and prepare a common environment for all tests.
In the end, we have to be able to run:
vendor/bin/phpunit
And see the result of all tests.
Everything else will be more complicated than it has to, will annoy us, and demotivate us from actually writing the tests. So make it easy and simple.
If you're serious about monorepo testing, read How to Test Monorepo in 3 Layers.
Don't forget to add .gitignore
with /vendor
. Then git push
and we're finished.
Happy coding!
Do you learn from my contents or use open-souce packages like Rector every day?
Consider supporting it on GitHub Sponsors.
I'd really appreciate it!