Do you know PHPStan, ECS, Monorepo Builder, PHPUnit, Config Transformer or Rector?
What do they have in common? They're PHP tools you install with composer and then run in your command line. Hm, what else? They're all scoped with help of php-scoper.
Do you want to make your tool resistant to any conflict with any project dependencies? Today I'll show you how.
Let's say you want to install symplify/monorepo-builder
to any of your projects with composer:
composer require symplify/monorepo-builder --dev
What can happen?
Now you have to go to your composer.json
, figure out what conflicts you can solve. You can also try to go to the project repository and ask for lowering the minimal required version of this or that package. This is typical for unscoped packages and natural results for semver strategy of PHP packages ecosystem.
But do we care about the dependencies of the tool we use? No, our goal is to run the tool:
composer require symplify/monorepo-builder --dev
vendor/bin/monorepo-builder <command>
We'll scope the tool! Only then we're conflict-free! Our project can have symfony/console
3.4 or 5.4-dev. Nobody cares because the only requirement is of the tool is the PHP version.
Wait, how can we have 2 versions of symfony/console
? Let's look at the file structure we'll find in /vendor
if we install the following packages together:
composer require symfony/console:^3.4
composer require symplify/monorepo-builder # new scoped one
Command
classThese steps will produce 2 different symfony/console
directories with 2 different Symfony\Component\Console\Command\Command
classes.
/vendor
/symfony
/console # version 3.4, autoloaded by your project
/Command
Command.php
That contains typical Command
class as you know it
namespace Symfony\Component\Console\Command;
class Command
{
// ...
}
Pretty standard, right?
Command
classHere the scoping magic happens. The symplify/monorepo-builder
package it's own Command in it's own scoped /vendor
. Like this:
/vendor
/symplify
/monorepo-builder
/vendor # this vendor is scoped and loaded only by monorepo-builder
/symfony
/console
/Command
Command.php
Now, this Command
class is a bit different. It's scoped. What does that mean exactly? It has its unique namespace prefix that makes class name unique to other Command
:
namespace Scope1234\Symfony\Component\Console\Command;
class Command
{
// ...
}
So now in the whole project we now have 2 Command
classes:
Symfony\Component\Console\Command
composer.json
Scope1234\Symfony\Component\Console\Command
The second class was scoped by php-scoper
, that makes all classes unique and accessible exclusively in the tool. This process removes dependency from composer.json
and thus avoid conflicts on install.
Now, should we get crazy and scope everything that pops up a conflict on composer require <x>
? No. This process is valid only for PHP tools in the command line. We should not scope classic dependencies that we use directly in our project, e.g. nette/utils
.
Now that we know why and what we scope, we'll look at how to do the scoping in the next post.
Happy coding!
Do you learn from my contents or use open-source packages like Rector every day?
Consider supporting it on GitHub Sponsors.
I'd really appreciate it!