Как предлагается создавать проект на Yii2 сейчас? Выбираете шаблон проекта: basic или advanced, форкаете себе, потом пишете и комитите туда. Бам! Случилась копипаста, ваш проект и шаблон теперь развиваются отдельно. Вам не получить исправлений, внесенных, в шаблон, а в yii2-app-basic
, естественно, не возьмут доработок специфических для вашей задачи. Это проблема номер один.
Как расширяется проект на Yii2? Выбираете подходящие расширения и подключаете их с помощью композера. Находите пример конфига этого расширения в README и копипастите в конфиг своего приложения. Оопс… Опять копипаста. Вылазящяя разными боками, в том числе таким: в большом проекте используется много расширений — конфиг приложения становится огромным и просто нечитаемым. Это проблема номер два.
Как эти проблемы связаны? Первая решается так: выделяем переиспользуемый код и превращаем в расширение. И снова здравствуйте: у расширения есть свой конфиг — получили вторую проблему.
Наиболее остро эти проблемы стоят для повторно используемых решений, когда надо поднимать много/несколько, в принципе одинаковых проектов, но с большими/маленькими изменениями. Плюс избавление от копипасты и переиспользование кода ещё никому не мешало.
Хочу поделиться своим вариантом решения этих проблем.
Система плагинов
Не буду томить душу, изложу суть сразу, чтоб, если-что, было удобненько расплеваться сразу, не дочитывая до конца.
Итак, решение такое: использовать систему плагинов — с самого начала создавать свой проект как плагин (расширение вместе с конфигом), делить проект на плагины и собирать конфиг приложения автоматически из конфигов плагинов.
Тут я должен приостановиться и объяснить что я называю плагином. В yii2 предусмотрены расширения (yii2 extensions) и они дают возможность организовывать переиспользуемый код и подключать его к проекту композером. Но мало-мальски сложное расширение нуждается в конфигурации. И тут фреймворк не помогает. У создателя расширения есть два варианта:
- описать желаемый конфиг в README и предложить програмеру его скопипастить;
- сделать bootstrap в своём расширении, который будет закидывать желаемый конфиг в конфиг приложения.
Первый вариант я уже покритиковал в самом начале, возьмусь за второй:
- bootstrap запускается довольно рано, но всё таки объект
Application
уже создан и что-то уже просто не получится сконфигурировать; - довольно сложно правильно смержиться с конфигом уже созданного приложения, придётся работать не с целым массивом конфига, а по частям: компоненты отдельно (и очень нетривиально), алиасы отдельно, контейнер, модули, параметры,
controllerMap
,… (я пробовал — так счастья не видать); - bootstrap не ленивый, он запускается на каждый запрос к приложению и если таких bootstrap'ов много — они просто будут бить по производительности.
В общем, пройдя несколько итераций, намучавшись с разными вариантами родилось радикальное решение — собирать конфиг за пределами приложения и ещё до его запуска (хм, звучит просто и очевидно, но, как известно, хорошая мысля приходит опосля). Собирать оказалось удобнее всего плагином к композеру, из него есть удобный доступ ко всей иерархии зависимостей проекта. Так получился composer-config-plugin.
Composer Config Plugin
Composer-config-plugin работает довольно просто:
- обходит все зависимости проекта, находит в них описание конфигов плагинов в
extra
секции ихcomposer.json
; - мержит конфиги в соответствии с описанием и иерархией пакетов и записывает результирующие конфиг файлы.
В composer.json
расширения (которое превращается в плагин) добавляются такие строчки:
"extra": {
"config-plugin": {
"web": "src/config/web.php"
}
}
Это значит замержить в конфиг под названием web
содержимое файла src/config/web.php
. А в файле этом будет просто то, что плагин хочет добавить в конфиг приложения, например, конфиг интернационализации:
<?php
return [
'components' => [
'i18n' => [
'translations' => [
'my-category' => [
'class' => \yii\i18n\PhpMessageSource::class,
'basePath' => '@myvendor/myplugin/messages',
],
],
],
],
];
Конфигов может быть сколько угодно, включая специальные: dotenv
, defines
и params
. Конфиги обрабатываются в таком порядке:
- переменные окружения —
dotenv
; - константы —
defines
; - параметры —
params
; - все остальные конфиги, например:
common
,console
,web
, ...
Таким образом, чтобы значения полученные на предыдущих шагах могли быть использованы на всех последующих.
То есть: переменные окружения могут использоваться для назначения констант. Константы и переменные окружения могут использоваться для назначения параметров. И весь набор: параметры, константы и переменные окружения могут использоваться в конфигах.
В общем-то всё! composer-config-plugin
просто мержит все массивы конфигов аналогом функции yii\helpers\ArrayHelper::merge
. Естественно, конфиги мержатся в правильном порядке — с учётом кто кого реквайрит — таким образом, чтобы конфиг каждого пакета мержился после своих зависимостей и мог перезаписать значения заданные ими. Т.е. самый верхний пакет имеет полный контроль над конфигом и управляет всеми значениями, а плагины только задают дефолтные значения. В целом, процесс повторяет сборку конфигов в yii2-app-advanced
, только более масштабно.
Изпользовать в приложении тривиально — добавляем в web/index.php
:
$config = require hiqdev\composer\config\Builder::path('web');
(new yii\web\Application($config))->run();
Найти больше информации и примеров, а также задать вопросы можно на гитхабе: hiqdev/composer-config-plugin.
Очень простой пример плагина hiqdev/yii2-yandex-metrika. Но он наглядно демонстрирует возможности этого подхода. Чтобы получить счётчик Яндекс.Метрики достаточно зареквайрить плагин и задать параметр yandexMetrika.id
. Всё! Не надо ничего копипастить в свой конфиг, не надо добавлять виджет в layout — не надо касаться рабочего кода. Плагин — это цельный кусок функционала, который позволяет расширять систему не внося изменений в существующий код.
— Что? Можно написать новую фичу, не поломав старые?!
— Да.
— Крутяк! Теперь можно не писать тесты?
— Нет… Так не бывает…
Итого, composer-config-plugin
даёт систему плагинов и решает вопрос повторного использования так сказать "малых архитектурных форм". Пора вернуться к главному — организации больших переиспользуемых проектов. Повторю и уточню предлагаемое решение: создавать проект как систему плагинов, организованную в правильную иерархию.
Иерархия пакетов
Самый простой вариант организации проекта такой — наш проект реквайрит композером фреймворк и сторонние расширения ("сторонними" я называю не являющиеся частью нашего проекта), т.е. получается такая простая иерархия пакетов (репозиториев):
- проект (выросший из шаблона приложения)
- расширения
- фреймворк
Пропускаю все промежуточные варианты организации, проверенные и отброшенные по итогам практической эксплуатации, и перехожу сразу к оптимальной иерархии, которой мы придерживаемся сейчас:
- "корень"
- плагины, специфичные для данного варианта проекта;
- основной проект;
- плагины проекта;
- сторонние плагины;
- базовый проект;
- плагины, необходимые для работы базового проекта;
- фреймворк.
Иерархия отображает кто кого реквайрит, т.е. корень реквайрит основной проект, тот в свою очередь — базовый проект, а базовый проект — фреймворк.
— Воу-воу! Полегче! Что за "корень" и "базовый проект"?
Извиняюсь, всё придумал сам, терминологии подходящей не нашёл, пришлось велосипедить, буду признателен за лучшие варианты.
"Корнем" я называю самый внешний пакет, содержащий код, конфиг и другие файлы специфические для данного конкретного варианта реализации вашего проекта — то, чем этот вариант отличается от основного проекта. В идеале содержит буквально несколько файлов, об этом ниже.
"Базовый проект" это то, во что превращается yii2-app-basic
в этой схеме. Т.е. переиспользуемая основа приложения реализующая некоторый базовый функционал и оформленная в виде плагина. Эта запчасть не обязательна, но очень полезна. Вам не надо её делать самому, она может разрабатываться сообществом как сейчас разрабатывается yii2-app-basic
. Мы разрабатываем HiSite, об этом ниже.
Таким образом пакеты образуют иерархию композиции — более внешний пакет использует внутренний, в основном переиспользуя его поведение, но переопределяя свою специфику: "корень" использует и уточняет основной проект, основной проект — базовый, базовый проект — фреймворк.
Необходимо уточнить, что речь идёт только об организации кода, т.е. про разделение кода по пакетам/плагинам. Архитектурное деление проекта на слои, естественно, независимо от деления на пакеты, но они могут дополнять друг друга. Например, доменная логика может быть вынесена в отдельный пакет для переиспользования между разными проектами.
— Аааа! Нужен пример!
Например, Вы делаете на потоке сайты визитки. Базовый функционал везде одинаковый, но есть фичи за дополнительну плату, например каталог и, естественно, сайты отличаются внешним видом (темой) и кучей параметров. Это можно организовать в такую иерархию пакетов:
business-card-no42.com
— "корень";
myvendor/yii2-theme-cool
— плагин, специфичный для данного сайта;myvendor/business-card-catalog
— плагин проекта, подключенный на данном сайте;myvendor/business-card
— основной проект;
myvendor/business-card-contacts
— плагин проекта, используемый на всех сайтах;othervendor/yii2-cool-stuff
— сторонний плагин;hiqdev/hisite
— базовый проект;
yiisoft/yii2-swiftmail
— плагин, необходимый для работы базового проекта;yiisoft/yii2
— фреймворк.
Надеюсь, не открыл Америки, и все более менее так и делят свои проекты на части. Только, наверно, без "корня". Попытаюсь донести его полезность.
"Корень"
В "корне" достаточно всего пару файлов, которые подлежат копированию из шаблона и настройке под данную инсталяцию проекта. Можно и желательно обойтись всего тремя файлами:
.env
— переменные окружения, например,ENV=prod
;composer.json
— тут подключается основной проект и специфичные для него плагины;src/config/params.php
— явки, пароли, параметры проекта и используемых плагинов.
Пароли можно положить в .env
и потом использовать их в params.php
так:
return [
'db.password' => $_ENV['DB_PASSWORD'],
];
Учитывая "легкоусвояемость" .env
лучшими претендентами на вынос в .env
являются параметры используемые другими (не PHP) технологиями.
Конечно, можно и нужно класть в "корень" некоторый конфиг и даже код, специфичный сугубо для данной инсталяции, не подлежащий копипастингу. Как только вижу копипасту, страшно её не люблю — уношу в какой-нибудь плагин.
Остальные файлы и каталоги необходимые для функционирования приложения (web/assets/
, web/index.php
) стандартны, их нужно создавать и назначать права "сборщиком" (build tool, task runner) мы велосипедим свой, но это уже совсем другая история.
По сути, "корень" — это params-local.php
на стероидах. В нём концентрируется отличие конкретной инсталяции проекта от общего переиспользуемого кода. Мы создаём репозиторий под корень и храним его на нашем приватном git-сервере, поэтому комитим туда даже секреты (но это холиварная тема). Все остальные пакеты — в публичном доступе на GitHub. Мы комитим composer.lock
в корне, поэтому перенос проекта на другой сервер делается просто composer create-project
(я знаю — Docker получше будет, но об этом в следующий раз).
— А можно ещё конкретнее? Покажите мне код наконец!
HiSite и Asset Packagist
Одно из "базовых приложений", которые мы развиваем — HiSite hiqdev/hisite — это основа для типичного сайта, как yii2-app-basic,
только сделанная как плагин, что даёт все преимущества переиспользования кода над копипастингом:
- можно основать свой проект на HiSite и получать его обновления;
- можно со временем заменить базовый проект на другой, совместимый, но, например, с большим функционалом.
Шаблон "корня" для проекта на HiSite здесь — hiqdev/hisite-template.
Иерархия зависимостей выглядит так:
- "корень" — hiqdev/hisite-template;
- плагин темы — hiqdev/yii2-theme-flat;
- библиотека тем — hiqdev/yii2-thememanager;
- базовый проект — hiqdev/hisite;
- фреймворк — yiisoft/yii2.
- плагин темы — hiqdev/yii2-theme-flat;
В README корня описано как поднять проект у себя — composer create-project
плюс настройка конфигурации. Благодаря реализации тем как плагинов и библиотеке тем hiqdev/yii2-thememanager в composer.json
корня можно поменять yii2-theme-flat
на yii2-theme-original
запустить composer update
и сайт переоденется в новую тему. Вот так просто.
Ещё один реальный рабочий проект, подходящий в качестве примера, сделанный, используя этот подход и полностью доступный на GitHub — Asset Packagist — packagist-совместимый репозиторий, который позволяет устанавливать Bower и NPM пакеты как нативные composer пакеты.
Иерархия зависимостей выглядит так:
- "корень" — hiqdev/asset-packagist.dev;
- плагин темы — hiqdev/yii2-theme-original;
- проект — hiqdev/asset-packagist;
- базовый проект — hiqdev/hisite;
- фреймворк — yiisoft/yii2.
- базовый проект — hiqdev/hisite;
Подробности как поднять проект у себя описаны в README корня.
Итоги подведём
Тема обширная, множество подробностей пришлось опустить. Надеюсь получилось донести общую идею. Ещё раз, используя введенную терминологию:
- переиспользуем код в виде плагинов, т.е. код вместе с конфигурацией;
- создаём проект как иерархию плагинов;
- отделяем переиспользуемую часть проекта от конкретной инсталяции с помощью "корня".
Мы используем описанный подход около года, впечатления самые положительные — волосы стали мягкие и шелковистые: разделяем и властвуем, клепаем плагины легко и непринуждённо, 100+ и останавливаться не собираемся, нужен новый функционал — делаем новый плагин.
Подход, в той или иной мере, применим для других фреймворков и даже языков… Ой, Остапа понесло… На сегодня хватит! Спасибо за внимание. Продолжение следует.
P.S.
На написание таких объёмов текста сподвигла серия статей Фабьена Потенсьера (автора Symfony) про грядущий Symfony 4. Стыдно сказать, не до конца понял как именно всё работает, но уловил идеи и цели: система бандлов будет доработана в сторону их автоматической конфигурации, что в итоге даёт:
new way to create and evolve your applications with ease
новый способ создавать и развивать ваши приложения с лёгкостью
© Fabien Potencier
В общем, не один я считаю поднятые вопросы очень важными для фреймворка.
Я люблю Yii. Давайте сделаем в Yii лучше!