Хотя большинство PHP-разработчиков умеют пользоваться Composer, не все делают это эффективно или лучшим возможным образом. Поэтому я решил собрать советы, которые важны для моей повседневной работы. Большинство из них опираются на принцип «От греха подальше»: если что-то можно сделать несколькими способами, то я выбираю наименее рискованный.
Совет № 1: читайте документацию
Я серьёзно. Документация у него замечательная, и несколько часов чтения сэкономят вам кучу времени в долгосрочной перспективе. Вы удивитесь, как много всего умеет делать Composer.
Совет № 2: различайте проект и библиотеку
Важно знать, что вы создаёте — проект или библиотеку. Каждый из вариантов требует своего набора методик.
Библиотека — это многократно используемый пакет, который нужно добавлять в качестве зависимости. Например,
symfony/symfony
, doctrine/orm
или elasticsearch/elasticsearch
.Проект обычно представляет собой приложение, зависящее от нескольких библиотек. Обычно он не используется несколько раз (никакому другому проекту он не понадобится в качестве зависимости). Характерные примеры: сайт интернет-магазина, система поддержки пользователей и т. д.
Дальше в советах я буду переключаться между библиотекой и проектом.
Совет № 3: используйте для приложения конкретные версии зависимостей
Если вы создаёте приложение, то используйте для определения зависимости как можно более конкретный номер версии. Если нужно проанализировать YAML-файлы, то определяйте зависимость, например, так:
"symfony/yaml": "4.0.2"
.Даже если библиотека следует правилам семантического версионирования (Semantic Versioning), в минорных и патчевых версиях всё же могут возникать нарушения обратной совместимости. Допустим, если вы используете
"symfony/symfony": "^3.1"
, то что-то устаревшее в 3.2 сломает ваши тесты. Или в PHP_CodeSniffer окажется исправленный баг, и в вашем приложении будут обнаружены новые ошибки форматирования, что снова может привести к сломанной сборке.Обновляйте зависимости обдуманно, а не импульсивно. Подробнее об этом мы поговорим в одном из следующих советов.
Возможно, это кажется чрезмерным, но внимание к версиям зависимостей не даст вашим коллегам неосмотрительно обновить все зависимости при добавлении в проект новой библиотеки (которую вы могли пропустить при ревизии кода).
Совет № 4: для зависимостей библиотек используйте диапазоны версий
Если вы делаете библиотеку, то определяйте самый возможный диапазон версий. Если создаёте библиотеку, использующую библиотеку
symfony/yaml
для YAML-разбора, то запрашивайте её так: "symfony/yaml": "^3.0 || ^4.0"
Тогда ваша библиотека сможет использовать
symfony/yaml
из любых версий Symfony с 3.x по 4.x. Это важно, поскольку данное ограничение распространяется и на приложение, которое обращается к вашей библиотеке.Если есть две библиотеки с конфликтующими требованиями (одной, к примеру, нужна ~3.1.0, а другой ~3.2.0), то будет сбой при установке.
Совет № 5: в приложениях нужно коммитить composer.lock в Git
Если вы создаёте проект, то нужно коммитить composer.lock в Git. Тогда все — вы, ваши коллеги, ваш CI-сервер и рабочий сервер — будут использовать приложение с одинаковыми версиями зависимостей.
На первый взгляд этот совет кажется излишним. Вы уже выбрали конкретную версию, как в совете № 3. Но ещё существуют зависимости ваших зависимостей, которые не связаны этими ограничениями (например,
symfony/console
зависит от symfony/polyfill-mbstring
). Так что без коммита composer.lock вы не получите такой же набор зависимостей.Совет № 6: в библиотеках кладите composer.lock в .gitignore
Если вы создаёте библиотеку (назовём её
acme/my-library
), то не нужно коммитить файл composer.lock. Это никак не влияет на проекты, использующие вашу библиотеку.Допустим,
acme/my-library
использует monolog/monolog
в качестве зависимости. Если вы закоммитили composer.lock, то все, кто разрабатывает acme/my-library
, будут использовать более старую версию Monolog. Но когда вы закончите работу над библиотекой и используете её в реальном проекте, может быть установлена более новая версия Monolog, которая окажется несовместимой с вашей библиотекой. Но раньше вы не заметили этого из-за composer.lock!Лучше всего класть composer.lock в .gitignore, чтобы случайно не закоммитить.
Если хотите быть уверены в совместимости библиотеки с разными версиями её зависимостей, читайте следующий совет!
Совет № 7: запускайте сборки Travis CI с разными версиями зависимостей
Совет относится только к библиотекам (потому что для приложений вы используете конкретные версии).
Если вы собираете open-source библиотеку, то, вероятно, запускаете сборки с помощью Travis CI. По умолчанию Composer устанавливает последние возможные версии зависимостей, допускаемые ограничениями в composer.json. Это означает, что для ограничения зависимости
^3.0 || ^4.0
сборка всегда будет использовать последнюю версию релиза v4. И поскольку версия 3.0 никогда не тестировалась, библиотека может оказаться несовместимой с ней, что опечалит пользователей.К счастью, в Composer есть переключатель
--prefer-lowest
для установки самых старших из возможных версий зависимостей (его нужно использовать вместе с --prefer-stable
для предотвращения установок нестабильных версий).Обновлённая конфигурация
.travis.yml
может выглядеть так:language: php
php:
- 7.1
- 7.2
env:
matrix:
- PREFER_LOWEST="--prefer-lowest --prefer-stable"
- PREFER_LOWEST=""
before_script:
- composer update $PREFER_LOWEST
script:
- composer ci
Можете посмотреть её в работе на примере моей библиотеки
mhujer/fio-api-php
и матричной сборки Travis CI.Хотя это решение позволит выловить большинство несовместимостей, помните, что между старшей и младшей версиями существует много комбинаций зависимостей. И они могут быть несовместимы.
Совет № 8: сортируйте пакеты в require и require-dev по имени
Хорошая привычка — держать пакеты в
require
и require-dev
отсортированными по имени. Это поможет пресекать ненужные конфликты слияния при перебазировании ветки. Потому что если вы в двух ветках добавляете пакет в конце списка, то конфликты слияния будут возникать каждый раз.Вручную это делать нудно, так что лучше сконфигурировать в composer.json:
{
...
"config": {
"sort-packages": true
},
…
}
Когда вы в следующий раз затребуете (
require
) новый пакет, он будет добавлен в правильное место (не в конец).Совет № 9: не пытайтесь объединять composer.lock при перебазировании или слиянии
Если вы добавили в composer.json новую зависимость (и composer.lock) и перед слиянием ветки в мастер была добавлена другая зависимость, вам нужно перебазировать ветку. И вы получите в composer.lock конфликт слияния.
Никогда не пытайтесь разрешить его вручную, потому что файл composer.lock содержит хеш зависимостей, определённых в composer.json. Так что, если даже вы разрешите конфликт, получится некорректный lock-файл.
Лучше создавать в корне проекта .gitattributes со следующей строкой, и тогда ваш Git не будет пытаться объединять composer.lock:
/composer.lock -merge
Можете решить проблему с помощью кратковременных веток фич (feature branches), как предлагается в Trunk Based Development (это нужно делать в любом случае). Если у вас есть правильно объединённая краткосрочная ветка, риск конфликта слияния в composer.lock минимален. Даже можете создать ветку только для добавления зависимости и сразу объединить её.
Но что делать, если в composer.lock возник конфликт слияния при перебазировании? Разрешите его с помощью версии из мастера, так у вас будут изменения только в composer.json (недавно добавленный пакет). А потом запустите
composer update --lock
, который захочет обновить файл composer.lock изменениями из composer.json. Теперь можете стейджить обновлённый composer.lock и продолжать перебазирование.Совет № 10: помните о разнице между require и require-dev
Важно помнить о разнице между блоками
require
и require-dev
.Пакеты, необходимые для запуска приложения или библиотеки, должны быть определены в
require
(например, Symfony, Doctrine, Twig, Guzzle…). Если создаёте библиотеку, то будьте осторожны с тем, что кладёте в require
. Каждая зависимость в этой секции тоже является зависимостью приложения, использующего библиотеку.Пакеты, необходимые для разработки приложения или библиотеки, должны быть определены в
require-dev
(например, PHPUnit, PHP_CodeSniffer, PHPStan).Совет № 11: обновляйте зависимости безопасно
Полагаю, вы согласны с утверждением, что зависимости нужно регулярно обновлять. И я советую делать обновление зависимостей прозрачно и продуманно, а не по мере какой-то другой работы. Если вы что-то рефакторите и в то же время обновляете какие-то библиотеки, то не сможете сказать, чем сломано приложение, рефакторингом или обновлением.
Используйте команду
composer outdated
для просмотра, какие зависимости можно обновить. Можно ещё включать --direct
(или -D
) для вывода только зависимостей, заданных в composer.json. Ещё есть переключатель -m
для вывода обновлений только минорных версий.Для каждой устаревшей зависимости придерживайтесь плана:
- Создайте новую ветку.
- Обновите в composer.json версию зависимости на самую свежую.
- Запустите
composer update phpunit/phpunit --with-dependencies
(заменитеphpunit/phpunit
названием обновляемой библиотеки). - Проверьте
CHANGELOG
в репозитории библиотеки на GitHub, чтобы узнать, нет ли всё ломающих изменений. Если есть, обновите приложение. - Локально протестируйте приложение. Если используете Symfony, то можете найти предупреждения о deprecated в панели отладки.
- Закоммитьте изменения (composer.json, composer.lock и всё, что нужно для работы новой версии).
- Дождитесь окончания CI-сборки.
- Объедините и разверните.
Иногда целесообразно обновлять сразу несколько зависимостей, например когда обновляешь Doctrine или Symfony. Тогда лучше перечислить их в команде обновления:
composer update symfony/symfony symfony/monolog-bundle --with-dependencies
Или можете использовать шаблон для обновления всех зависимостей из определённого пространства имён:
composer update symfony/* --with-dependencies
Знаю, всё это выглядит утомительным, но наверняка вы обновляете зависимости по случаю, так что лучше перестраховаться.
Можно лишь в одном облегчить себе работу: разом обновлять все зависимости
require-dev
(если они не требуют изменений в коде, иначе предлагаю использовать отдельные ветки для упрощения ревизии кода).Совет № 12: можете определять в composer.json другие типы зависимостей
Помимо определения библиотек в качестве зависимостей, вы также можете определять там и другие вещи.
Например, какие версии PHP поддерживает приложение/библиотека:
"require": {
"php": "7.1.* || 7.2.*",
},
Или какие расширения необходимы приложению/библиотеке. Это очень полезно, если пытаешься поместить приложение в контейнер или если твой новый коллега впервые настраивает приложение.
"require": {
"ext-mbstring": "*",
"ext-pdo_mysql": "*",
},
Используйте * для версий расширений, потому что они могут быть несогласованными.
Совет № 13: проверяйте composer.json в ходе CI-сборки
composer.json и composer.lock должны быть всегда синхронизированы. Поэтому целесообразно автоматически проверять их синхронизированность. Просто добавьте этот механизм в свой скрипт сборки:
composer validate --no-check-all --strict
Совет № 14: используйте Composer-плагин в PHPStorm
Существует composer.json-плагин для PHPStorm. Он добавляет автокомплит и ряд проверок при ручном изменении composer.json.
Если вы используете другую IDE (или только редактор кода), можете настроить проверку его JSON-схемы.
Совет № 15: определяйте в composer.json рабочие версии PHP
Если вы, как и я, любите иногда локально запускать предварительные релизы версий PHP, то рискуете обновить зависимости до версий, не работающих в продакшене. Сейчас я использую PHP 7.2.0, т. е. могу устанавливать библиотеки, которые не будут работать на 7.1. А поскольку продакшен использует 7.1, установка завершится сбоем.
Но переживать не нужно, есть лёгкое решение. Просто определите рабочие версии PHP в разделе
config
файла composer.json:"config": {
"platform": {
"php": "7.1"
}
}
Пусть вас не смущает раздел
require
, который ведёт себя иначе. Ваше приложение может работать на 7.1 или 7.2, но в то же время 7.1 будет определена как платформенная версия, т. е. зависимости всегда будут обновляться до версии, совместимой с 7.1:"require": {
"php": "7.1.* || 7.2.*"
},
"config": {
"platform": {
"php": "7.1"
}
},
Совет № 16: используйте приватные пакеты из Gitlab
Рекомендую выбирать vcs в качестве типа репозитория, и Composer должен определить правильный способ извлечения пакетов. Например, если вы добавляете форк с GitHub, он будет использовать свой API для скачивания zip-файла вместо клонирования всего репозитория.
Но с приватной установкой с Gitlab несколько сложнее. Если вы используете vcs в качестве типа репозитория, Composer определит его как Gitlab-установку и попытается скачать пакет через API. Для этого потребуется API-ключ. Я не хотел его настраивать, поэтому сделал так (моя система использует SSH для клонирования).
Сначала определил репозиторий типа
git
:"repositories": [
{
"type": "git",
"url": "git@gitlab.mycompany.cz:package-namespace/package-name.git"
}
]
А затем использовал пакет, как это обычно делается:
"require": {
"package-namespace/package-name": "1.0.0"
}
Совет № 17: как временно использовать ветку из форка с исправлением бага
Если вы нашли баг в публичной библиотеке и исправили его в своём форке на GitHub, то вам нужно установить библиотеку из своего репозитория, а не из официального (пока исправление не будет объединено и не появится исправленный релиз).
Это легко можно сделать с помощью inline aliasing:
{
"repositories": [
{
"type": "vcs",
"url": "https://github.com/you/monolog"
}
],
"require": {
"symfony/monolog-bundle": "2.0",
"monolog/monolog": "dev-bugfix as 1.0.x-dev"
}
}
Можете локально протестировать своё исправление, прежде чем загружать его, используя
path
в качестве типа репозитория.Совет № 18: установите prestissimo для ускорения установки пакетов
Composer-плагин
hirak/prestissimo
ускоряет установку зависимостей посредством параллельного скачивания.Достаточно установить его один раз глобально, и он будет автоматически работать для всех проектов:
composer global require hirak/prestissimo
Совет № 19: если не уверены, протестируйте свои версионные ограничения
Написание корректных версионных ограничений иногда становится непростой задачей после прочтения документации.
К счастью, есть Packagist Semver Checker, позволяющий проверять, какие версии соответствуют конкретным ограничениям. Вместо простого анализа версионных ограничений данные скачиваются из Packagist для отображения актуальных выпущенных версий.
См. результат для
symfony/symfony:^3.1
.Совет № 20: используйте в продакшене авторитарную карту классов (class map)
Сгенерируйте в продакшене авторитарную карту классов. Это ускорит загрузку классов благодаря включению в карту всего необходимого и пропуску любых проверок файловой системы.
Можете делать это в рамках вашей рабочей сборки:
composer dump-autoload --classmap-authoritative
Совет № 21: для тестирования сконфигурируйте autoload-dev
Вам не нужно включать тестовые файлы в рабочую карту классов (из-за размера файла и потребления памяти). Это можно сделать с помощью конфигурирования
autoload-dev
(аналогично autoload
):"autoload": {
"psr-4": {
"Acme\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Acme\\": "tests/"
}
},
Комментарии (7)
OnYourLips
13.01.2018 00:06Если нужно проанализировать YAML-файлы, то определяйте зависимость, например, так: «symfony/yaml»: «4.0.2».
Неправильно.
Если вы боитесь нарушения BC в минорных версиях критического пакета, то хотя бы не фиксируйте патч-версию.
Допустимо будет так: «symfony/yaml»: "^4.0.2".
А еще лучше нормально покрывать приложение тестами и фиксировать только мажорную версию.pbatanov
13.01.2018 10:55Допустимо будет так: «symfony/yaml»: "^4.0.2".
Это кстати не фиксирует минорную версию. Проверить можно здесь
jubianchi.github.io/semver-check
4.2.0 satisfies constraint ^4.0.2
OnYourLips
13.01.2018 12:24Спасибо, с тильдой, конечно же: «symfony/yaml»: "~4.0.2".
А с "^" уже при нормальном покрытии тестами.
Corpsee
13.01.2018 13:47Вы ошибаетесь,
"symfony/yaml": "^4.0.2"
обновит yaml вплоть до следующей мажорной версии, то есть 5.0.0. Чтобы получать только патчи, нужно фиксировать так:"symfony/yaml": "~4.0.2"
или так:"symfony/yaml": "4.0.*"
.
Elfet
16.01.2018 08:22Совет № 6: в библиотеках кладите composer.lock в .gitignore
Тоже не всегда так: например для библиотек распространяемых через compose и phar.
pbatanov
16.01.2018 09:18Библиотека, распространяемая через phar — это инструмент, целостный и законченный, по сути является полноценным проектом. Но и в этом случае смысл сомнителен, если есть хорошее покрытие тестами, то гораздо удобней при сборке очередной версии инструмента забрать как минимум последний патч-релиз из имеющихся, а это уже повод не хранить лок. phpunit, например, при сборке phar делает update (если верить build.xml)
github.com/sebastianbergmann/phpunit/blob/master/build.xml#L54
pbatanov
Не понял прикола с установкой 4.0.2 для приложений. Понимаю, что перевод, но все же.
Вы правда читаете все ченджлоги и дифы всех зависимостей каждый день, чтобы понять, надо вам обновляться или нет? Вы в курсе, что транизитивные зависимости вы все равно так не ограничиваете и composer update таких «зафиксированных» зависимостей все равно вам все может «сломать». А если вы зафиксируете еще и все транзитивки (в чем я сомневаюсь, т.к. они еще и меняться имеют свойство), то вы по сути получите тот же Lock файл
Гораздо проще и правильней выставлять ограничение с помощью каретки (или суровей — тильты на minor.major.patch), типа "~4.0.2", и запускать composer update попакетно — типа composer update symfony/symfony. В таком случае вы обновляетесь только в пределах одного пакета и только в пределах его новых патч. релизов (которые не должны ничего ломать, а только фиксить). Если нужно притянуть также зависимости пакета — флаг --with-dependencies
Из полезных советов могу предложить тулзу composer-lock-diff, очень удобно прикладывать к пулл-реквестам внутри команды.