21 совет по эффективному использованию Composer +34

- такой же как Forbes, только лучше.


Хотя большинство 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 для вывода обновлений только минорных версий.

Для каждой устаревшей зависимости придерживайтесь плана:

  1. Создайте новую ветку.
  2. Обновите в composer.json версию зависимости на самую свежую.
  3. Запустите composer update phpunit/phpunit --with-dependencies (замените phpunit/phpunit названием обновляемой библиотеки).
  4. Проверьте CHANGELOG в репозитории библиотеки на GitHub, чтобы узнать, нет ли всё ломающих изменений. Если есть, обновите приложение.
  5. Локально протестируйте приложение. Если используете Symfony, то можете найти предупреждения о deprecated в панели отладки.
  6. Закоммитьте изменения (composer.json, composer.lock и всё, что нужно для работы новой версии).
  7. Дождитесь окончания CI-сборки.
  8. Объедините и разверните.

Иногда целесообразно обновлять сразу несколько зависимостей, например когда обновляешь 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):

  1. pbatanov
    /#10613654

    Не понял прикола с установкой 4.0.2 для приложений. Понимаю, что перевод, но все же.

    Вы правда читаете все ченджлоги и дифы всех зависимостей каждый день, чтобы понять, надо вам обновляться или нет? Вы в курсе, что транизитивные зависимости вы все равно так не ограничиваете и composer update таких «зафиксированных» зависимостей все равно вам все может «сломать». А если вы зафиксируете еще и все транзитивки (в чем я сомневаюсь, т.к. они еще и меняться имеют свойство), то вы по сути получите тот же Lock файл

    Гораздо проще и правильней выставлять ограничение с помощью каретки (или суровей — тильты на minor.major.patch), типа "~4.0.2", и запускать composer update попакетно — типа composer update symfony/symfony. В таком случае вы обновляетесь только в пределах одного пакета и только в пределах его новых патч. релизов (которые не должны ничего ломать, а только фиксить). Если нужно притянуть также зависимости пакета — флаг --with-dependencies

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

  2. OnYourLips
    /#10613888

    Если нужно проанализировать YAML-файлы, то определяйте зависимость, например, так: «symfony/yaml»: «4.0.2».
    Неправильно.
    Если вы боитесь нарушения BC в минорных версиях критического пакета, то хотя бы не фиксируйте патч-версию.
    Допустимо будет так: «symfony/yaml»: "^4.0.2".

    А еще лучше нормально покрывать приложение тестами и фиксировать только мажорную версию.

    • pbatanov
      /#10614276

      Допустимо будет так: «symfony/yaml»: "^4.0.2".


      Это кстати не фиксирует минорную версию. Проверить можно здесь

      jubianchi.github.io/semver-check

      4.2.0 satisfies constraint ^4.0.2

      • OnYourLips
        /#10614384

        Спасибо, с тильдой, конечно же: «symfony/yaml»: "~4.0.2".
        А с "^" уже при нормальном покрытии тестами.

    • Corpsee
      /#10614494

      Вы ошибаетесь, "symfony/yaml": "^4.0.2" обновит yaml вплоть до следующей мажорной версии, то есть 5.0.0. Чтобы получать только патчи, нужно фиксировать так: "symfony/yaml": "~4.0.2" или так: "symfony/yaml": "4.0.*".

  3. Elfet
    /#10618046

    Совет № 6: в библиотеках кладите composer.lock в .gitignore

    Тоже не всегда так: например для библиотек распространяемых через compose и phar.

    • pbatanov
      /#10618120

      Библиотека, распространяемая через phar — это инструмент, целостный и законченный, по сути является полноценным проектом. Но и в этом случае смысл сомнителен, если есть хорошее покрытие тестами, то гораздо удобней при сборке очередной версии инструмента забрать как минимум последний патч-релиз из имеющихся, а это уже повод не хранить лок. phpunit, например, при сборке phar делает update (если верить build.xml)

      github.com/sebastianbergmann/phpunit/blob/master/build.xml#L54