In the last post I shared a trick on how to reduce CLI project /vendor size by 70 %. Today we'll trim off a bit more with the no-so-known composer feature.
"Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away."
Some packages require another set of packages to delegate responsibility to an external source. Like any other dependency in life, that could be helpful and save time, but it also requires attention and care - it depends.
The following applies to any package that is bloated with transitional dependencies we don't use. But I'll use the one I work with daily as an example.
Let's say we install
symfony/console to a brand new empty project:
composer require symfony/console
Have a guess: how many packages do we have in our
Wow, 9 in total.
Briefly looking at the list, 5 packages deal with strings and language. These packages bring value if we use non-standard language operations - but for native English, those are redundant.
We also use
symfony/console just to invoke a PHP method called to render the output. Nothing fancy like console forms or dynamic games in the command line.
That means we don't need the following packages:
How do we know these packages are not needed? Let's verify our thesis.
We check for a "Symfony\Component\String" string in the
symfony/console Github repository in search
We can see 4 cases - primarily to measure the width of the terminal window:
We try to comment out those cases and run code. All good? We can remove it.
If you use this package locally in a single project and have enough space and DevOps take care of PHP extensions, there is little value in removing it.
But if the package:
... maybe it could be helpful for the developers that use it to make it easier to run and install.
I also check the
symfony/string package size to see its download trace:
composer require tomasvotruba/lines --dev
vendor/bin/lines measure vendor/symfony/string/ --short --json
That's 5 561 lines of PHP code with every download and 4 intl/mbstring packages in transition for 4 simple method calls. Those packages can prevent crash runs or installation on some nasty legacy project code, so if we get rid of them, we'll make our product more usable and cheaper to maintain.
Let's remove it. The first solution is using the
remove command in Composer:
composer remove symfony/string
And Composer replies in big red letters:
Removal failed; symfony/string is still present; another package may require it. See `composer why symfony/string`.
Damn... We don't have it in our
composer.json, so we can't really remove it, can we?
What other options do we have? Run
rm -rf /vendor/symfony/string might help, but we'd have to put it in some weird bash script, which seems like a code smell.
How do we use Composer to help us?
The Composer has this special section called "replace":
composer update and see what happens:
Lock file operations: 0 installs, 0 updates, 4 removals
- Removing symfony/polyfill-ctype (v1.27.0)
- Removing symfony/polyfill-intl-grapheme (v1.27.0)
- Removing symfony/polyfill-intl-normalizer (v1.27.0)
- Removing symfony/string (v6.3.2)
The packages we don't use are gone, yay!
Wow, it looks like magic! It is, until we read the "replace" documentation.
In short, it tells the Composer: "This root package has the same features as
The Composer then sees your root package and the
symfony/string package and decides: "Hm, there are 2 packages with the same name. Let's use the root one and remove the dependency from vendor".
Simple but effective solution. It's not for everyone, but if you care about it and having fewer dependencies brings your project more value and less code to worry about, then:
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!