Квест → Как хакнуть форму
Прошли: 77
Когда вы создаете PHP-приложение или библиотеку, вы, как правило, имеете 3 вида зависимостей:
Жесткие зависимости:
{ "require": { "acme/foo": "^1.0" } }
Необязательные зависимости:
{ "suggest": { "monolog/monolog": "Advanced logging library", "ext-xml": "Required to support XML" } }
Необязательные зависимости для разработки:
{ "require-dev": { "monolog/monolog": "^1.0", "phpunit/phpunit": "^6.0" } }
Пока вроде всё хорошо. Так что может пойти не так? В основном это связано с require-dev
, который имеет некоторые ограничения.
Зависимости, управляемые менеджером пакетов - это великолепно. Это фантастический механизм для повторного использования существующего кода и возможности легко его обновлять. Однако вы должны следить за тем, насколько много зависимостей вы подключаете. В конце концов вы подключаете обычный код, а раз так, то он может содержать баги или быть небезопасным. Вы становитесь зависимыми от чего-то, что написал кто-то другой, над чем вы, возможно, не имеете никакого контроля, помимо того, что становитесь предметом сторонних проблем. Packagist и GitHub делают фантастическую работу по сокращению некоторых из этих рисков, но риск тем не менее всё ещё остаётся. Как произошло фиаско left-pad`а в JavaScript-сообществе - это хороший пример того, что что-то может пойти не так, и добавление пакета в зависимости может иметь последствия.
Вторая проблема с зависимостями заключается в том, что они должны быть совместимы. Это как раз работа Composer`а. Но каким бы великолепным не был Composer, есть некоторые пакеты, которые нельзя установить вместе. И чем больше зависимостей вы подключаете, тем больше шансов столкнуться с конфликтом.
TL:DR; Следите за тем, какие зависимости вы подключаете и стремитесь к наименьшему их числу.
Давайте рассмотрим следующий пример:
{ "require-dev": { "phpstan/phpstan": "^1.0@dev", "phpmetrics/phpmetrics": "^2.0@dev" } }
Эти два пакета - инструменты статического анализа, и они могут не установиться вместе, т.е. создать конфликт, т.к. они могут зависеть от разных и несовместимых версий PHP-парсера.
Это случай «глупых» конфликтов: конфликт должен произойти только в том случае, если вы пытаетесь подключить зависимость, которая несовместима с вашим приложением. Эти два пакета не обязательно должны быть совместимы, ваше приложение не использует их напрямую, и они не запускают код вашего приложения.
Другой пример в случае с библиотекой, для которой вы предоставляете бридж (адаптер/переходник) для Symfony и Laravel. Вы можете захотеть подключить в качестве зависимостей как Symfony, так и Laravel для тестирования этих бриджей:
{ "require-dev": { "symfony/framework-bundle": "^4.0", "laravel/framework": "~5.5.0" # небольшое напоминание о том, что # пакеты Laravel не следуют semver } }
Это может работать в некоторых случаях, но очень вероятно, что большую часть времени это будет сломано. Это немного глупо, так как очень вряд ли найдётся пользователь, которому понадобятся оба этих пакета одновременно, но даже если это было более менее правоподобно, то вряд ли вы захотите поддерживать этот сценарий.
Если вы посмотрите на следующий composer.json
:
{ "require": { "symfony/yaml": "^2.8 || ^3.0" }, "require-dev": { "symfony/yaml": "^3.0" } }
Давайте взглянем что тут происходит... Единственными устанавливаемыми версиями компонента Symfony YAML (пакет symfony/yaml
) - будут [3.0.0, 4.0.0]
.
В приложении вам, скорее всего, всё равно. Однако, в случае с библиотекой, это может быть проблемой. Действительно, это означает, что вы никогда не сможете протестировать вашу библиотеку с symfony/yaml [2.8.0, 3.0.0]
.
Так или иначе, станет ли это действительно проблемой, во многом зависит от вашей конкретной ситуации. Здесь главное то, что вы знаете, что это может произойти и что нет простого способа идентифицировать такую ситуацию. Описаный выше случай достаточно простой, но если требование symfony/yaml: ^3.0
скрыто глубже в дереве зависимостей, например так:
{ "require": { "symfony/yaml": "^2.8 || ^3.0" }, "require-dev": { "acme/foo": "^1.0" # требует symfony/yaml ^3.0 } }
То у вас нет никакого способа, по крайней мере на данный момент, узнать об этом.
KISS! Это нормально, в конечном счёте, вам действительно не нужен этот пакет!
PHAR (PHp АРхивы) - это способ упаковать программу в один файл. Если вы хотите узнать побольше о них, я рекомендую вам официальную документацию PHP.
Использование на примере инструмента статического анализа PhpMetrics:
$ 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
Предупреждение: упаковка кода в PHAR не изолирует его, в отличие от, скажем, JAR-ов в Java.
Проиллюстрируем эту проблему на примере. Вы создали консольное приложение myapp.phar
, которое опирается на Symfony YAML 2.8.0 и просто выполняетпереданный PHP-скрипт:
$ myapp.phar myscript.php
Ваш скрипт myscript.php
запускает Composer, чтобы использовать Symfony YAML 4.0.0.
В этом случае может произойти так, что PHAR загрузит класс из Symfony YAML, например Symfony\Yaml\Yaml
, а затем выполнит ваш скрипт. Ваш скрипт также использует Symfony\Yaml\Yaml
, но думает, что этот класс еще не загружен! Проблема здесь заключается в том, что загруженный класс, - это класс из пакета symfony/yaml 2.8.0
, а не из 4.0.0
, который требуется вашему скрипту. В результате, если API отличается, это всё поломается.
TL:DR; PHAR`ы отлично подходят для инструментов на подобие статических анализаторов, как PhpStan или PhpMetrics, но ненадежны (пока по крайней мере), если они выполняют код, приводящий к коллизиям зависимостей.
Есть также несколько других вещей, которые стоит иметь в виду при использовании PHAR`ов:
tooly-composer-script
или PhiVe.self-update
с каналами различной стабильности, некоторые предоставляют уникальный урл для скачивания последней версии, некоторые используют возможности GitHub`а и поставляют PHAR для каждого релиза, и т.д.На сегодняшний день одна из самых популярных техник. Вместо того, чтобы подключать все зависимости в одином composer.json
, мы разбиваем пакет на несколько репозиториев.
Если мы возьмем предыдущий пример с библиотекой, которую мы назовём acme/foo
, тогда мы создадим другой пакет acme/foo-bundle
для Symfony и acme/foo-provider
для Laravel.
Обратите внимание, что всё на самом деле может по-прежнему находиться в одном репозитории и иметь репозитории только-для-чтения для других пакетов, как это делает Symfony.
Основным преимуществом такого подхода является то, что он остается довольно простым и не требует никаких дополнительных инструментов, кроме сплиттера(разделителя) репозиториев вроде splitsh, который используется, например, для Symfony, Laravel и PhpBB. Недостатком является то, что теперь у вас есть несколько пакетов обслуживания вместо одного.
Другой способ будет иметь более продвинутые скрипты установки и тестирования. Для предыдущего примера у нас бы получилось что-то вроде этого:
#!/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
Это работает, но по крайней мере на моем опыте, приводит к раздутым тестовым сценариям, которые сравнительно медленны, трудно поддерживаемы и не очень дружелюбны для новых участников проекта.
composer.json
`овЭтот подход является относительно новым (в PHP), в основном потому, что требуемого инструментария ещё не было, поэтому я немного расширю это решение.
Идея относительно проста. Вместо того, чтобы иметь:
{ "autoload": {...}, "autoload-dev": {...}, "require": {...}, "require-dev": { "phpunit/phpunit": "^6.0", "phpstan/phpstan": "^1.0@dev", "phpmetrics/phpmetrics": "^2.0@dev" } }
Мы установим phpstan/phpstan
и phpmetrics/phpmetrics
в разных composer.json
файлах. Но тут появляется первый вопрос: куда я их дену? Какие структуры принять?
Это тот случай, когда на сцену выходит composer-bin-plugin
. Это очень простой плагин для Composer`а, который позволяет взаимодействовать с composer.json
из другого каталога. Итак, допустим, у нас есть наш composer.json
в корне проекта:
{ "autoload": {...}, "autoload-dev": {...}, "require": {...}, "require-dev": { "phpunit/phpunit": "^6.0" } }
Мы можем установить плагин:
$ composer require --dev bamarni/composer-bin-plugin
Теперь, после установки плагина, когда вы делаете composer bin acme smth
он выполнит команду composer smth
в подкаталоге vendor-bin/acme
. Так что теперь мы можем установить PhpStan и PhpMetrics вот так:
$ composer bin phpstan require phpstan/phpstan:^1.0@dev $ composer bin phpmetrics require phpmetrics/phpmetrics:^2.0@dev
Это создаст следующую структуру каталогов:
... # (файлы и директории проекта) composer.json composer.lock vendor/ vendor-bin/ phpstan/ composer.json composer.lock vendor/ phpmetrics/ composer.json composer.lock vendor/
Где vendor-bin/phpstan/composer.json
выглядит так:
{ "require": { "phpstan/phpstan": "^1.0" } }
А vendor-bin/phpmetrics/composer.json
так:
{ "require": { "phpmetrics/phpmetrics": "^2.0" } }
Так что теперь мы можем легко использовать PhpStan и PhpMetrics вызвав vendor-bin/phpstan/vendor/bin/phpstan
и vendor-bin/phpmetrics/vendor/bin/phpstan
соответственно.
Давайте теперь пойдем дальше и возьмем, к примеру, библиотеку с бриджами для различных фрейворков:
{ "autoload": {...}, "autoload-dev": {...}, "require": {...}, "require-dev": { "phpunit/phpunit": "^6.0", "symfony/framework-bundle": "^4.0", "laravel/framework": "~5.5.0" } }
Итак, давайте применим тот же подход, и наш файл vendor-bin/symfony/composer.json
для Symfony-бриджа будет выглядеть так:
{ "autoload": {...}, "autoload-dev": {...}, "require": {...}, "require-dev": { "phpunit/phpunit": "^6.0", "symfony/framework-bundle": "^4.0" } }
А другой - vendor-bin/laravel/composer.json
- для Laravel-бриджа так:
{ "autoload": {...}, "autoload-dev": {...}, "require": {...}, "require-dev": { "phpunit/phpunit": "^6.0", "laravel/framework": "~5.5.0" } }
И наш корневой composer.json
теперь будет выглядеть так:
{ "autoload": {...}, "autoload-dev": {...}, "require": {...}, "require-dev": { "bamarni/composer-bin-plugin": "^1.0" "phpunit/phpunit": "^6.0" } }
Для тестирования основной библиотеки и бриджей сейчас вам нужно создать 3 разных PHPUnit-файла, - по одному для каждого с соответствующим файлом автозагрузки (например, для Symfony-бриджа: vendor-bin/symfony/vendor/autoload.php
).
Если вы на самом деле попробуете это сделать, вы заметите существенный недостаток в таком подходе: избыточная конфигурация. Действительно, нужно дублировать конфиг корневого composer.json
`а на два других (vendor-bin/{symfony,laravel}/composer.json
), настроить разделы автозагрузки, поскольку путь к файлам изменился, и всякий раз, когда вы подключаете новую зависимость, вы также должны её подключить в других composer.json
файлах. Это становится не управляемым. И вот тут приходит на помощь плагин composer-inheritance-plugin
.
Этот плагин - крошечная обёртка над composer-merge-plugin
для объединения содержимого vendor-bin/symfony/composer.json
с корневым composer.json
`ом. Таким образом, вместо:
{ "autoload": {...}, "autoload-dev": {...}, "require": {...}, "require-dev": { "phpunit/phpunit": "^6.0", "symfony/framework-bundle": "^4.0" } }
Теперь вы будете иметь:
{ "require-dev": { "symfony/framework-bundle": "^4.0", "theofidry/composer-inheritance-plugin": "^1.0" } }
Остальные настройки (автозагрузка и зависимости) будут подтянуты из корневого composer.json
. Теперь не осталось ничего, что нужно настраивать.
Если вы хотите, вы можете посмотреть зависимости, установленные для Symfony-бриджа так:
$ composer bin symfony show
Я использовал этот подход в нескольких проектах таких, как alice
для различных инструментов таких, как PhpStan или PHP-CS-Fixer и бриджей для фрейворков. Другой пример - alice-data-fixtures
, для которого существует множество различных ORM-бриджей для слоя персистентности (Doctrine ORM, Doctrine ODM, Eloquent ORM и т. д.) и интеграции в фреймворк.
Я также использую его в нескольких закрытых проектах для приложений для различных утилит в качестве альтернативы PHAR-ам, и он очень хорошо работает.
Я уверен, что кто-то сочтёт некоторые подходы странными или не рекомендованными к использованию. Цель статьи - не судить или рекомендовать какой-нибудь из них, а скорее изложить список возможных способов управления некоторыми зависимостями и рассмотреть, каковы преимущества и недостатки каждого. Поэтому в зависимости от проблем, с котороми вы сталкиваетесь, и ваших личных предпочтений, выбирайте тот, который работает лучше для вас. Как сказал кто-то, нет решений, только компромиссы.
Если вы сталкивались с подобными проблемами, расскажите в комментариях об опыте решения проблем с зависимостями. Какой способ вы использользовали? На какие подводные камни наткнулись и как их обходили? Как вы считаете на сколько применимы такие подходы и стоит ли ими пользоваться или только некоторыми?
Кто-нибудь использовал плагины composer-bin-plugin
/ composer-inheritance-plugin
/ composer-merge-plugin
?