When creating a PHP application or library, you usually have three types of dependencies:
How to manage these dependencies?
Hard dependencies:
{ "require": { "acme/foo": "^1.0" } }
Optional dependencies:
{ "suggest": { "monolog/monolog": "Advanced logging library", "ext-xml": "Required to support XML" } }
Optional and development related dependencies:
{ "require-dev": { "monolog/monolog": "^1.0", "phpunit/phpunit": "^6.0" } }
And so on. What can happen bad? It's all about the limitations inherent in require-dev
.
Dependencies with the package manager are great . This is a great mechanism for code reuse and easy updates. But you are responsible for which dependencies and how you turn on. You enter code that may contain errors or vulnerabilities. You start to depend on what is written by someone else and what you can not even manage. Not to mention the fact that you risk becoming a victim of third-party problems. Packagist and GitHub can greatly reduce these risks, but do not eliminate them at all. The left-pad fiasco in the JavaScript community is a good example of a situation where things can go awry, so adding packages sometimes leads to unpleasant consequences.
The second disadvantage of dependencies is that they must be compatible. This is a task for Composer . But no matter how good Composer is , there are dependencies that cannot be used together, and the more dependencies you add, the more likely a conflict will arise.
Summary
Choose dependencies wisely and try to limit their number.
Consider an example:
{ "require-dev": { "phpstan/phpstan": "^1.0@dev", "phpmetrics/phpmetrics": "^2.0@dev" } }
These two packages are static analysis tools ; they sometimes conflict when they are installed together, because they may depend on different and incompatible versions of PHP-Parser .
This is a variant of a “stupid” conflict: it arises only when you try to include a dependency that is incompatible with your application. Packages are not required to be compatible, your application does not use them directly, and besides, they do not execute your code.
Here is another example where bridges are provided to the library for working with symfony and laravel . Perhaps to test bridges, you want to include both symfony and Laravel as dependencies:
{ "require-dev": { "symfony/framework-bundle": "^4.0", "laravel/framework": "~5.5.0" # gentle reminder that Laravel # packages are not semver } }
In some cases it may work fine, but most likely it will break. The example is somewhat farfetched, because it’s very unlikely that a user would use both packages at the same time, and it’s even less likely that you will need to provide for this scenario.
Look at this composer.json:
{ "require": { "symfony/yaml": "^2.8 || ^3.0" }, "require-dev": { "symfony/yaml": "^3.0" } }
Something is going on here ... It will only be possible to install the Symfony YAML component (the symfony/yaml
) of versions [3.0.0, 4.0.0[
.
In the application you probably will not care. But in the library this can lead to a problem, because you will never be able to test your library with symfony/yaml [2.8.0, 3.0.0[
.
Whether this becomes a real problem depends largely on the specific situation. It must be borne in mind that such a restriction may be across, and it will not be so easy to identify. A simple example is shown, but if the requirement for symfony/yaml: ^3.0
hide it deeper into the dependency tree, for example:
{ "require": { "symfony/yaml": "^2.8 || ^3.0" }, "require-dev": { "acme/foo": "^1.0" # requires symfony/yaml ^3.0 } }
you won't know about it, at least for now.
Kiss . Everything is normal, in fact, you do not need this package!
PHARs (PHP archives) - a way to pack an application into a single file. You can read more about this in the official PHP documentation .
An example of using with PhpMetrics , a static analysis tool:
$ wget https://url/to/download/phpmetrics/phar-file -o phpmetrics.phar $ chmod +x phpmetrics.phar $ mv phpmetrics.phar /usr/local/bin/phpmetrics $ phpmetrics --version PhpMetrics, version 1.9.0 # or if you want to keep the PHAR close and do not mind the .phar # extension: $ phpmetrics.phar --version PhpMetrics, version 1.9.0
Note: PHAR-packed code is not isolated, unlike, for example, JARs in Java.
To illustrate the problem. You have made the console application myapp.phar
, relying on Symfony YAML 2.8.0, which executes the PHP script:
$ myapp.phar myscript.php
Your myscript.php
script uses Composer to use symfony YAML 4.0.0.
What can happen if PHAR loads the Symfony YAML class, for example Symfony\Yaml\Yaml
, and then executes your script? He also uses Symfony\Yaml\Yaml
, but the class is already loaded! And it is loaded from the symfony/yaml 2.8.0
package, and not from 4.0.0
, as your script needs. And if the APIs are different, everything breaks down completely.
Summary
PHARs are great for static analysis tools like PhpStan or PhpMetrics , but unreliable (at least for now) because they execute code depending on dependency collisions (for the moment!).
There is something else to remember about PHARs:
tooly-composer-script
plugin or PhiVe , the PHAR installer.self-update
command a la Composer with different stability channels. In other projects, a unique download endpoint is provided with the latest release. In the third projects, the GitHub release function is used with the delivery of each release in the form of a PHAR, etc.One of the most popular methods. Instead of requiring all bridge dependencies in a single composer.json
file, we divide the package into several repositories.
Take the previous library example. acme/foo
call it acme/foo
, then create acme/foo-bundle
packages for symfony and acme/foo-provider
for Laravel.
Please note, we can still put everything in one repository, and for other packages like symfony, use repositories for reading only.
The main advantage of this approach is that it is relatively simple and does not require additional tools, except for the separator for repositories like splitsh , used for Symfony, Laravel and PhpBB. And the disadvantage is that now instead of one package you need to support several.
You can go the other way and choose a more advanced installation and testing script. For our previous example, you can use the following:
#!/usr/bin/env bash # bin/tests.sh # Test the core library vendor/bin/phpunit --exclude-group=laravel,symfony # Test the Symfony bridge composer require symfony/framework-bundle:^4.0 vendor/bin/phpunit --group=symfony composer remove symfony/framework-bundle # Test the Laravel bridge composer require laravel/framework:~5.5.0 vendor/bin/phpunit --group=symfony composer remove laravel/framework
It will work, but, in my experience, test scripts will turn out to be bloated and relatively slow, difficult to maintain and not very easy-to-understand third-party programmers.
This approach is quite fresh (in PHP), mainly because there were no necessary tools before, so I will tell you a little more.
The idea is simple. Instead
{ "autoload": {...}, "autoload-dev": {...}, "require": {...}, "require-dev": { "phpunit/phpunit": "^6.0", "phpstan/phpstan": "^1.0@dev", "phpmetrics/phpmetrics": "^2.0@dev" } }
we will install phpstan/phpstan
and phpmetrics/phpmetrics
in different composer.json
files. But then the first difficulty arises: where to put them? How to create a structure?
Here composer-bin-plugin
will help. This is a very simple plugin for Composer, allowing you to interact with composer.json
in different folders. Suppose there is a root file composer.json
:
{ "autoload": {...}, "autoload-dev": {...}, "require": {...}, "require-dev": { "phpunit/phpunit": "^6.0" } }
Install the plugin:
$ composer require --dev bamarni/composer-bin-plugin
After that, if you execute composer bin acme smth
, then the composer smth
will be executed in the vendor-bin/acme
. Now install PhpStan and PhpMetrics:
$ composer bin phpstan require phpstan/phpstan:^1.0@dev $ composer bin phpmetrics require phpmetrics/phpmetrics:^2.0@dev
This will create a directory structure:
... # projects files/directories composer.json composer.lock vendor/ vendor-bin/ phpstan/ composer.json composer.lock vendor/ phpmetrics/ composer.json composer.lock vendor/
Here vendor-bin/phpstan/composer.json
looks like this:
{ "require": { "phpstan/phpstan": "^1.0" } }
And vendor-bin/phpmetrics/composer.json
looks like this:
{ "require": { "phpmetrics/phpmetrics": "^2.0" } }
You can now use PhpStan and PhpMetrics by simply calling vendor-bin/phpstan/vendor/bin/phpstan
and vendor-bin/phpmetrics/vendor/bin/phpstan
.
Let's go further. Take the example of a library with bridges for different frameworks:
{ "autoload": {...}, "autoload-dev": {...}, "require": {...}, "require-dev": { "phpunit/phpunit": "^6.0", "symfony/framework-bundle": "^4.0", "laravel/framework": "~5.5.0" } }
Apply the same approach and get the file vendor-bin/symfony/composer.json
for the symfony bridge:
{ "autoload": {...}, "autoload-dev": {...}, "require": {...}, "require-dev": { "phpunit/phpunit": "^6.0", "symfony/framework-bundle": "^4.0" } }
And the vendor-bin/laravel/composer.json
file for the Laravel Bridge:
{ "autoload": {...}, "autoload-dev": {...}, "require": {...}, "require-dev": { "phpunit/phpunit": "^6.0", "laravel/framework": "~5.5.0" } }
Our root composer.json
file will look like this:
{ "autoload": {...}, "autoload-dev": {...}, "require": {...}, "require-dev": { "bamarni/composer-bin-plugin": "^1.0" "phpunit/phpunit": "^6.0" } }
To test the main library and bridges, you now need to create three different PHPUnit files, each with a corresponding startup file (for example, vendor-bin/symfony/vendor/autoload.php
for the symfony bridge).
If you try it yourself, you will notice the main drawback of the approach: configuration redundancy. You will have to duplicate the root configuration of composer.json
to the other two vendor-bin/{symfony,laravel/composer.json
, configure autoload sections, because the paths to the files may change, and when you need a new dependency, you will have to register it in other files composer.json
. It turns out inconvenient, but the composer-inheritance-plugin
comes to the rescue.
This is a small wrapper around the composer-merge-plugin
that allows you to merge the content vendor-bin/symfony/composer.json
with the root composer.json
. Instead
{ "autoload": {...}, "autoload-dev": {...}, "require": {...}, "require-dev": { "phpunit/phpunit": "^6.0", "symfony/framework-bundle": "^4.0" } }
will turn out
{ "require-dev": { "symfony/framework-bundle": "^4.0", "theofidry/composer-inheritance-plugin": "^1.0" } }
This will include the rest of the configuration, autoload and dependencies of the root composer.json
. You do not need to configure anything, the composer-inheritance-plugin
is just a thin wrapper around the composer-merge-plugin
for pre-configuration so that you can use it with the composer-bin-plugin
.
If you want, you can examine the installed dependencies with
$ composer bin symfony show
I applied this approach in different projects, for example in alice
, for different tools like PhpStan or PHP-CS-Fixer and bridges for frameworks. Another example is alice-data-fixtures
, where many different ORM bridges are used for the level of data storage (Doctrine ORM, Doctrine ODM, Eloquent ORM, etc.) and framework integration.
I also applied this approach in several private projects as an alternative to PHARs in applications with different tools, and it worked well.
I'm sure someone will find some techniques strange or not recommended. I was not going to give a rating or advise something specific, but I just wanted to describe the possible ways of managing dependencies, their advantages and disadvantages. Choose what suits you best, based on your goals and personal preferences. As someone said, there are no solutions, there are only compromises.
Source: https://habr.com/ru/post/344498/
All Articles