Альтернативная организация проекта на Yii2


Как предлагается создавать проект на 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.


Иерархия зависимостей выглядит так:



В README корня описано как поднять проект у себя — composer create-project плюс настройка конфигурации. Благодаря реализации тем как плагинов и библиотеке тем hiqdev/yii2-thememanager в composer.json корня можно поменять yii2-theme-flat на yii2-theme-original запустить composer update и сайт переоденется в новую тему. Вот так просто.


Ещё один реальный рабочий проект, подходящий в качестве примера, сделанный, используя этот подход и полностью доступный на GitHub — Asset Packagist — packagist-совместимый репозиторий, который позволяет устанавливать Bower и NPM пакеты как нативные composer пакеты.


Иерархия зависимостей выглядит так:



Подробности как поднять проект у себя описаны в README корня.


Итоги подведём


Тема обширная, множество подробностей пришлось опустить. Надеюсь получилось донести общую идею. Ещё раз, используя введенную терминологию:


  • переиспользуем код в виде плагинов, т.е. код вместе с конфигурацией;
  • создаём проект как иерархию плагинов;
  • отделяем переиспользуемую часть проекта от конкретной инсталяции с помощью "корня".

Мы используем описанный подход около года, впечатления самые положительные — волосы стали мягкие и шелковистые: разделяем и властвуем, клепаем плагины легко и непринуждённо, 100+ и останавливаться не собираемся, нужен новый функционал — делаем новый плагин.


Подход, в той или иной мере, применим для других фреймворков и даже языков… Ой, Остапа понесло… На сегодня хватит! Спасибо за внимание. Продолжение следует.


P.S.


На написание таких объёмов текста сподвигла серия статей Фабьена Потенсьера (автора Symfony) про грядущий Symfony 4. Стыдно сказать, не до конца понял как именно всё работает, но уловил идеи и цели: система бандлов будет доработана в сторону их автоматической конфигурации, что в итоге даёт:


new way to create and evolve your applications with ease
новый способ создавать и развивать ваши приложения с лёгкостью

© Fabien Potencier


В общем, не один я считаю поднятые вопросы очень важными для фреймворка.


Я люблю Yii. Давайте сделаем в Yii лучше!

  • +19
  • 6,6k
  • 9
Поделиться публикацией
Похожие публикации
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 9
  • +4
    Извиняюсь, всё придумал сам, терминологии подходящей не нашёл, пришлось велосипедить, буду признателен за лучшие варианты.

    Сюда подходит термин Уровень переопределения из БЭМ методологии.
    image
    • +2
      Выбираете шаблон проекта: basic или advanced, форкаете себе, потом пишете и комитите туда. Бам!

      composer create-project для кого придумали?

      • 0

        Можно и так, это не меняет сути описанного в статье.

      • +1

        Я что-то не понял, а модули чем неугодили?
        И в чем разница между копипастом настроек "плагина" и расширения?
        Я понимаю, каждый намерен запилить свой велосипед, но сама идея плагина она о другом. Расширения — это по сути то, что мы вставляем внутрь программы, а плагин — это то, что мы прикручиваем​ снаружи. Это совсем разные концепции.

        • 0
          Я что-то не понял, а модули чем неугодили?

          Всем угодили. Но это другой уровень организации кода, перпендикулярный расширению.
          Модуль — это единица кода, класс. А расширение — это код выделенный в composer-пакет.
          Расширение может содержать модули, а может не содержать. И т.д.


          И в чем разница между копипастом настроек "плагина" и расширения?

          Плагин — это расширение + конфигурация необходимая для его работы.
          Composer-config-plugin собирает (мержит) конфиги всех плагинов избавляя от необходимости их копировать.

        • 0
          del
          • +1
            У меня есть готовый модуль. Его конфигурация сделана в файлах Yii2 шаблона advanced.
            Можно подробней, как для модуля использовать Ваш composer-config-plugin?
            • +1

              Если у Вас просто один большой репозиторий и Вы не планируете переиспользовать его части, то composer-config-plugin ни к чему, advanced шабон уже предоставляет удобный и привычный способ сборки конфига.
              Но если вы хотите переиспользовать Ваш модуль в разных проектах, план такой:


              • создать расширение содержащее этот модуль, подробнее об этом в документации
              • добавить необходимые конфиги, например: src/config/params.php, scr/config/common.php, src/config/web.php, ...
              • перечислить эти конфиги в composer.json:
                "extra": {
                    "config-plugin": {
                        "params": "src/config/params.php",
                        "common": "src/config/common.php",
                        "web": "src/config/web.php"
                    }
                }

              Всё. Плагин готов. Теперь чтобы использовать его в проекте надо:


              • подключить плагин composer'ом:
                "require": {
                    "me/my-plugin": "*@dev"
                }
              • использовать конфиг собранный composer-config-plugin'ом:
                $config = require hiqdev\composer\config\Builder::path('web');
                (new yii\web\Application($config))->run();

                учитывая что в Вашем случае не весь конфиг будет собран composer-config-plugin'ом Вам может понадобиться смержиться с уже имеющимся конфигом, приблизительно так:

                $main_config = [...];
                $plugin_config = require hiqdev\composer\config\Builder::path('web');
                $config = ArrayHelper::merge($plugin_config, $main_config);
                (new yii\web\Application($config))->run();


              • 0

                Пример такого плагина (с модулем) — yii2-module-pages

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.