Of Tools and Dependencies

Over the last few weeks I had a few discussions with other developers all along the same line of thought: How to install development-tools in a project.

Why? You might ask. Composer is making that very easy after all. When I need phpunit in my project to run unittests I use composer require phpunit/phpunit and composer itself will ask me whether I want to install that as a dev-dependency. How awesome is that! So why do we need to talk about that?

Well. Let’s put it that way: When you do that, and perhaps not only with phpunit but with other tools as well, you tie your production code to your development tools.

Dependencies

Why? Because composer creates one dependency graph that will make it possible to install everything that you have in your dependencies and your dev-dependencies. And when you then want to update a library or a tool you are using, that will only work when they all use the same set of dependencies. And that can mean that you will not be able to use the latest version of tool X because you are also using tool Y or library Z that are both using a different version of a library than you already have or depend on.

So the main question for me is how I can avoid to tie my production code to some libraries that some development tools need. And after some years of thinking and trying different things there is not one single way to do that. It depends on a multitude of things.

But before we go down that rabbit hole, let’s first take a step back and have a look at what my project actually consists of. After all my development tools are dependencies of my project, aren’t they?

You might have realized that I am speaking a lot about tools, dependencies and production code. That is on purpose as I think we have to distinguish between those. They all are tied together somehow, but they are separate things.

First and foremost:

What is actually production code?

For me, production code is that part of my project that makes up it’s value. The part that generates revenue. The part that is used by others in case of a library or SDK.

Usually that is the part that is distributed either in form of a ZIP-file or that contains the code that will make up the binary in case of a compiled application.

Most of the time that is the code that lives in the src folder, along with (in PHP-projects mostly) what’s in the public folder as well as the vendor folder.

Why? Because that is the code that gets shipped and deployed. That is the code that your company makes money of. That is the code that other people are using.

Supporting code

Everything else has a supporting role. And as in every movie, the supporting role is important. But it is (usually) not what people come for.

The supporting code comes in several flavours. One of those are tests

Tests.

They depend on your production code. But your production code (hopefully) does not depend on your test-code. So the production code is a dependency to your tests. But your tests are not a dependency to your production code.

Whether those are unit- or feature- or end-to-end-tests is irrelevant. The main thing is that they depend on your production code but they are not relevant for the production code to execute.

Your tests make sure that your production code can generate revenue but the tests in itself do not generate revenue. And I am not considering the speed-up of development time as generating revenue!

Dependencies

Dependencies on the other hand is stuff that your code is using that you haven’t written yourself. Code that *your* code … depends on.

And that needs to be distinguished in two different kinds. There is stuff that your production code depends on. The UUID-library that your entities use for creating unique identifiers, the Database-Abstraction layer etc. Stuff that your code needs to generate revenue. Those are dependencies.

And then there are those libraries that you use when writing your supporting code like tests.

And this is the part where it becomes really messy!

When writing tests we – at least in PHP – usually extend some classes that a tool (more on that in a bit) provides. So for unit-tests we extend the TestCase class and can then use methods for assertions or mocking etc. These extension points are usually provided by a tool so now our code depends on a tool.

For quite some time now I am thinking about how cool it would be to be able to just write some test-classes without any tool-dependency and then use whatever runner to execute them and generate test-coverage results etc. But that is a dream and should not be part of the discussion here. The reality is that we have a tool like PHPUnit that my test-code depends on. So now the runner has become a development-dependency of my code.

Tools

The alternative would be to treat the PHPUnit runner and all the other executables like Pest or PHPStan or Psalm or Behat or PHP-Codesniffer or whatever else as what they actually are: BInaries. Tools that we use to execute our code but that we do not need to have to think about how they are internally built. I don’t really care whether any of these tools is internally written in PHP, C#, Kotlin, Go or Rust or Whitespace or whatever else you want to write it in. As long as I can run it in my development environment and on CI everything is cool.

What I definitely **not** want is that those tools dictate which version of a library my production code can use or which version of a different tool I am able to use.

But as most of the tools are actually written in PHP and distributed as source-code via composer that is exactly what happens. Now I am not able to update my production code because some tool uses an incompatible version of a library. Or I am not able to update tool A because it uses a library that tool B also uses – but now in an incompatible version.

So how to get rid of that dilema?

Solutions

As I already said, there are different options at hand. But beware: each and every of these options requires a tad more work than just composer require x/y. Which is not saying that composer is not an awesome tool. But composer by now tries to do too much and is heading away from the Unix Philosophy: Do one thing only but do it exceptionally well. Composer does dependency resolution exceptionally well! (Thanks for that!)

But now that we have an exceptionally well designed hammer, we started to treat everything as a nail. We not only treated our production code dependencies as dependencies. But also our tools. And our tools.

Even though there are other – and better – ways to handle our tools. And this is not a complete list. I am pretty sure there are even more ways. Feel free to add them to the comments.

PHAR files

PHAR files are a way built into PHP itself to create “binaries” that contain everything except for PHP for execution. No external dependencies are required. PHAR files have their own challenges and require some special treatment sometimes but that would sidetrack too far here.

There is for example phive, a tool to install PHAR files similar to composer that you can use to manage PHAR files in your project.

The major downside of PHARs is that sometimes a project requires plugins to a tool and those plugins are distributed via composer but when you then want to use the PHAR for the actual tool you will have to jump some hoops. I have blogged about that some time ago, feel free to read up on that. The main thing is: It’s not easy so let’s check what other options there are

Separate composer file

Another option is to use a separate composer file for each tool. It allows you to define specific dependencies for your tool that will not influence other tools. So you can specify all the different plugins you need for PHPStan in your composer.json under tools/phpstan but that will not interfere with the dependencies in your composer.json under tools/phpunit .

It leaves all tools disconnected from each other as well as your production code. But it comes with the disadvantage of having to maintain different composer.json files per tool. On the other hand: That is an automatable process and when you call your tools via a make-file or something similar calling make test is still easy and no one needs to remember where the tool that runs the tests actually is located.

Containers

The next level would then be to use containers that behave almost like real binaries. While the PHAR and separate composer files always require a PHP binary to be available (and also to match the required version), containers allow you to actually bundle the files with a certain PHP-version to something that you can actually reproducably execute on your code.

The downside here is an extra process to actually create the container and possibly a registry to keep the containers available for others to run so that the container does not need to be build every time it is needed but once. Which also makes the results reproducible as all are using the same container.

You can then extend this reproducability over time by using Nix flakes as Marco Pivetta explained in a recent PR (where he also explains why he removed PHAR support and what other options there are – same list as this).

Conclusion

The main thing for me though is that we start to differentiate between dependencies of our production code and our tools (that might also have some dependencies – like plugins).

And to use the right tools for the job.

The added benefit is that by removing our development tools from our “main” composer.json we will make it much easier to not deploy development tools on our production platforms and therefore have less exploitable code in production.

Leave a Reply

Your email address will not be published. Required fields are marked *

I accept that my given data and my IP address is sent to a server in the USA only for the purpose of spam prevention through the Akismet program.More information on Akismet and GDPR.

This site uses Akismet to reduce spam. Learn how your comment data is processed.