Миграция Yii2 → Yii3: концептуальный гайд
Обобщение подхода, по которому был выполнен перенос крупного Yii2 Advanced приложения на Yii3 (набор пакетов Yiisoft, а не монолит-фреймворк) с переходом на чистую архитектуру. Гайд не привязан к конкретному проекту — это набор концептов, порядок их применения и грабли, на которые стоит заложиться заранее.
Гайд рассчитан на ситуацию: работающее production-приложение на Yii2, которое нельзя остановить, нельзя переписать «за один заход» и нельзя сломать по поведению.
0. Главный тезис
Миграция проходит три фазы, и в каждой меняется критерий правильности:
| Фаза | Вопрос, на который отвечает код | Основной инструмент |
|---|---|---|
| 1. Перенос | «Совпадает ли поведение с legacy?» | Контрактные тесты (старое vs новое) |
| 2. Владение | «Правильно ли устроен наш собственный код?» | Архитектурные тесты, аудит, CI-гейты |
| 3. Зрелость | «Ловит ли ошибки компилятор/анализатор?» | Типизированные контракты, статический анализ, пороги покрытия |
Ошибка планирования №1 — считать, что миграция заканчивается на фазе 1. Ошибка №2 — начинать с фазы 3 (проектировать идеальные типы для кода, поведение которого ещё не зафиксировано тестами).
1. Модель миграции: параллельный прогон
Концепт. Новое приложение живёт рядом со старым и работает с той же базой данных. Модули переезжают по одному. Оба приложения одновременно в рабочем состоянии на всём протяжении миграции.
Практические следствия:
- Общая БД — это контракт, а не временное неудобство. Схема БД на время миграции неприкосновенна: никаких переименований колонок, никаких новых ORM-миграций из нового приложения. Владение схемой передаётся новому приложению отдельным явным шагом в конце, а не походя.
- Новое приложение на старте — read-only там, где это возможно. Право записи открывается по мере переноса write-сценариев.
- Отдельные точки входа и отдельные порты/vhost’ы. Старое и новое адресуются независимо — это необходимое условие для контрактных тестов.
- URL-пути не меняются. Внешние потребители (клиенты API, интеграции, закладки пользователей) не должны узнать о миграции.
Альтернатива — «большой взрыв» с заморозкой фич — на приложении в развитии не работает: пока идёт переписывание, legacy продолжает меняться, и паритет становится движущейся мишенью.
2. Порядок модулей
Порядок выбирается не по важности, а по стоимости верификации.
Рекомендуемая последовательность:
- API / JSON-эндпоинты. Компактный, явный контракт; ответ автоматически сравнивается побайтово; нет зависимостей от UI. Идеальный первый модуль — на нём отлаживается вся оснастка (DI, слои, тесты, Docker, контрактное сравнение).
- Консольные команды. Нет UI, детерминированный вход/выход, легко гонять в обоих стеках и сравнивать эффект в БД. Здесь же переносятся интеграции с внешними API.
- Публичный фронтенд / личный кабинет. Появляются сессии, CSRF, формы, вьюхи — новый класс сложности, но объём умеренный.
- Админ-панель. Обычно самый большой блок (десятки контроллеров, сотни вьюх) и самый однообразный. Идёт последним, когда все паттерны уже устоялись и тиражируются.
Внутри крупного блока — дробить по сложности, а не по алфавиту: сначала простой CRUD над справочниками (на нём ставится каркас: layout, auth-middleware, грид, пагинация, фильтры), затем сущности со связями, затем экраны со сложной агрегацией и AJAX.
Правило «extend, not rewrite»: каждый следующий модуль дописывает существующие слои, а не заводит параллельные. Если для нового модуля нужен репозиторий, который уже есть — расширяем его, а не создаём второй.
3. Контрактные тесты — оракул миграции
Концепт. Пока идёт перенос, единственный надёжный критерий правильности — «новое отвечает так же, как старое». Это не unit-тесты и не интеграционные: это тесты, которые бьют по HTTP в оба приложения одинаковым запросом и сравнивают результат.
Что сравнивать:
- HTTP-статус;
- для JSON — структурное равенство: тот же порядок ключей, те же типы
(
"id": 1, а не"id": "1"), то же представлениеnull/пустых значений, те же форматы дат; - для HTML — распарсенную таблицу: количество строк, значения ячеек, итоговые строки;
- поведение авторизации (401/302 на невалидных кредах).
Принципы работы с паритетом:
- Паритет — дефолт, отклонение — документируется. Если в legacy обнаружен баг и новое поведение сознательно правильнее, это фиксируется в спеке, а соответствующая колонка/поле исключается из сравнения с комментарием «почему». Молча «починить и не сказать» — худший вариант: расхождение всплывёт через полгода как регресс.
- Нет данных для теста — не выдумывать. Тест помечается как skipped, и отдельно решается, откуда брать данные. Тест на подогнанных данных хуже отсутствующего.
- Фиксировать нестабильные входы. Периоды в отчётах — на «прошлый месяц», а не «сегодня»; для взвешенных средних — допуск на округление.
- Контрактные тесты — расходный материал. Они существуют, пока существует legacy. Когда старый стек выключается, сьют удаляется — и это признак завершённости фазы 1, а не потеря. К этому моменту его роль должны перенять собственные тесты нового приложения (см. §8).
4. Целевая архитектура
Yii3 не навязывает структуру — это набор пакетов. Пустоту нужно заполнить осознанно, иначе получится «Yii2 с другими неймспейсами».
Рабочая схема — чистая (луковичная) архитектура из четырёх слоёв:
Domain чистый PHP: сущности, value objects, интерфейсы репозиториев.
Ноль зависимостей от фреймворка и ORM.
Application сценарии использования: Query+Handler+Result (чтение),
Command+Handler (запись, когда она требует прослойки —
критерий в §7), порты чтения и записи, контракты к
инфраструктуре (почта, HTTP-клиенты, аналитика).
Infrastructure адаптеры: реализации репозиториев, клиенты внешних API,
мейлеры. Единственный слой с вендорскими импортами.
Presentation HTTP- и console-контроллеры, middleware, вьюхи.
Тонкий: разбирает запрос, зовёт handler, рендерит.
Дополнительные решения, которые стоит принять на старте:
- CQRS-lite. Один сценарий — один класс:
Query/Command+Handler+Result, никаких «сервисов на 900 строк». Но handler нужен не каждой операции записи — критерий в §7. - Порт чтения и порт записи над одной таблицей — разные интерфейсы в
одном слое (
ReadModel/WriteModel); одна реализация объявляет оба. Порт записи не уезжает в Domain: если за таблицей нет доменной модели и нет потребителя вне модуля, доменного репозитория над ней быть не должно — это порт, придуманный ради архитектурного правила. - Сущности — обычные PHP-классы с
readonly-свойствами, без ORM-атрибутов и аннотаций. Маппинг задаётся отдельно (PHP-схема или ручной DBAL). Это то, что делает Domain по-настоящему чистым. - Сырой SQL легален — но только в Infrastructure. Отказ от ActiveRecord не означает отказ от SQL. Вынос SQL из handler’а — это не «убрать SQL», а «переложить в правильный слой»: тот же запрос, другое место.
declare(strict_types=1)в каждом файле — с первого дня, потом не добавите.- Точка входа = конфигурация. Каждый вход (панель, фронт, консоль) собирает свой DI поверх общей базы. Общие биндинги — в общем файле, специфичные — в файле входа.
5. Что из Yii2 не мигрирует
Половина работы — понять, что переносить не надо. Типовая карта:
| Yii2 | Что делать в Yii3 |
|---|---|
| ActiveRecord | Сущности + интерфейсы репозиториев (Domain), реализации на DBAL/ORM (Infrastructure) |
TimestampBehavior и прочие behaviors | Убрать: значения ставит БД (NOW()/DEFAULT), сущность только читает |
| Scenarios | Убрать: разные сценарии = разные Command/Query DTO |
| Правила валидации в модели | Переехать в Application/Presentation: валидация входного DTO |
Yii::$app->* (сервис-локатор) | Инъекция зависимостей через конструктор |
Фильтры контроллеров (behaviors()) | PSR-15 middleware в стеке маршрута |
ArrayHelper/Url::to() и прочая статика | Явные зависимости; URL — только через генератор маршрутов |
| GridView с ActiveDataProvider | Read-model, возвращающий строки + total + итоги; виджет получает готовые данные |
Модули (modules/) | Группы маршрутов + неймспейсы; модульность обеспечивают слои |
| Ассеты (AssetBundle) | Статика раскладывается на диск, отдаётся веб-сервером |
Отдельно про legacy-схему БД. Готовьтесь к тому, что готовые Yii3-пакеты (RBAC, миграции, очереди) не сядут на схему, созданную Yii2: типы колонок и соглашения отличаются. Стратегия — гибридная: берём из пакета то, что совместимо, остальное закрываем собственным интерфейсом поверх сырого SQL. Не подгонять legacy-схему под пакет во время миграции: это меняет контракт, общий со старым приложением.
6. Границы, которые проверяются автоматически
Концепт. Слои — это не договорённость в вики, а тест. Иначе через три
месяца в Domain окажется use Cycle\Database\....
Минимальный набор архитектурных правил:
- Domain не зависит ни от чего (ни фреймворк, ни ORM, ни другие слои);
- Application не зависит от Infrastructure и от вендорских пакетов БД;
- Presentation не зависит от Domain напрямую (см. политику ниже);
- сущности Domain —
final; - «опасные» классы (writer’ы к внешним API) используются только через DI.
Два приёма, которые делают это применимым к живому проекту:
Guard rails first — правило раньше соблюдения
Правило добавляется сразу, но со skip и пометкой «TODO». Оно
фиксирует направление и не даёт ситуации ухудшаться незаметно. skip
снимается, когда нарушения устранены. Так правило работает как задача с
чётким критерием готовности, а не как недостижимый идеал.
Whitelist вместо тотального запрета
«Presentation не знает про Domain» в чистом виде нереализуемо: вьюхе нужны перечисления статусов, контроллеру — фильтр периода. Рабочая политика:
Presentation может импортировать из Domain только value objects и enum’ы (выделенный под-неймспейс). Всё остальное — сущности, репозитории, DTO — исключительно через интерфейсы Application.
Побочный эффект, который стоит ожидать: правило заставит переклассифицировать
код. Классы, лежащие в DTO/, но по сути являющиеся иммутабельными
самовалидирующимися value objects, просто переезжают в ValueObject/ — и
импорт становится легальным. Это правильный исход: правило проявило
неверное имя, а не породило обходной путь.
7. Осознанные компромиссы
Концепт. Чистая архитектура в CRUD-админке в полном виде — это оверинжиниринг. Компромиссы неизбежны; ценность в том, чтобы они были записаны и одинаковы во всём проекте, а не приняты втихую в каждом контроллере по-своему.
Компромиссы, которые оправдали себя:
- Анемичные сущности — приемлемо для админки, где 80% операций это «показать список / сохранить форму».
- Асимметрия чтения и записи — и прослойка по критерию. Чтение: контроллер обращается к read-порту напрямую, без handler’а — прослойка ничего не добавляет. Запись: handler заводится там, где ему есть что делать — транзакция, побочный эффект, больше одного порта на границе вызова, инвариант, не выразимый валидацией входного DTO. Где ничего из этого нет, контроллер обращается к write-порту напрямую, симметрично чтению: иначе слой use case перестаёт быть границей и становится обязательной индирекцией. Это та же логика, по которой Command-объект нужен не каждому handler’у, применённая на уровень выше — handler нужен не каждой операции. Условие применимости: критерий записан один раз для всего проекта, а решение по каждому модулю — в его спеке. Два шаблона в одном блоке допустимы только тогда, когда читатель знает, какой перед ним и почему.
- Domain events, агрегаты, event sourcing — YAGNI, пока нет реальной потребности. Вводить «на будущее» — платить сложностью сейчас за гипотезу.
- Command-объект нужен не каждому handler’у. Если у операции два скалярных параметра, отдельный класс-команда — церемония.
Формат записи: в каждой спеке — раздел «Out of scope / осознанные компромиссы» с одной строкой обоснования на пункт. Это то место, куда через год посмотрит новый разработчик, прежде чем «исправить архитектуру».
8. Interface Segregation как главный инструмент развязки
Практика, которая на масштабе даёт больше всего:
- Интерфейс объявляет только те методы, которые реально вызывает потребитель. Не «весь репозиторий», а узкий срез под конкретный сценарий. Внутренние методы репозитория не протекают наружу.
- Один конкретный класс реализует несколько интерфейсов. Не нужно плодить адаптеры: реализация репозитория объявляет и доменный интерфейс, и Application-овский read-model.
- Разные потребители — разные интерфейсы. Панель и публичный фронт читают одну таблицу по-разному; попытка сделать один «универсальный» интерфейс приводит к объединению множеств методов и обратно к богу-репозиторию.
- Read/Write разделение для внешних API. Reader-интерфейс и writer-интерфейс отдельно — тогда на dev-окружении writer подменяется заглушкой одной строкой в DI.
⚠️ Грабля DI. Когда один класс реализует два интерфейса, контейнер должен возвращать один и тот же инстанс для обоих. Регистрируйте второй интерфейс через ссылку на первый, а не повторным описанием класса — иначе получите два объекта с независимым состоянием (соединения, кеши, транзакции).
9. Стадийность рефакторинга
Концепт. Крупное изменение границ разбивается на stage’и, каждый из которых — самостоятельный коммит с зелёными тестами. Никакой stage не требует «сначала всё сломать».
Проверенный шаблон на примере «убрать зависимость Presentation от Domain»:
- Stage 0 — сетап. Линтеры и статический анализ, тестовый раннер,
Docker-окружение,
composer(зависимости, автолоад, скрипты), CI/CD. Кода изменения ещё нет — есть инфраструктура, которая покажет, что следующие stage’и ничего не сломали. Без неё «зелёные тесты» как критерий готовности stage’а не работает. - Stage 1 — только Domain, нулевой риск. Выделить value objects, перенести/переименовать неймспейсы. Старые константы оставить как deprecated-алиасы, указывающие на новые значения. Ничего снаружи не ломается.
- Stage 2 — фасад готов, но не потребляется. Завести интерфейсы Application и command-handler’ы, подключить их в DI. Новый путь есть, старый ещё работает.
- Stage 3 — переключение по областям, от наименьшего blast radius. Сначала один изолированный контроллер, потом группа, потом самое связное. После каждого под-stage’а — полный прогон тестов.
- Stage 4 — финализация. Снять
skipс архитектурного правила, затем (и только затем) удалить deprecated-алиасы. Порядок важен: правило поймает отставших раньше, чем компилятор упадёт на удалённой константе.
10. Качество: сначала найти, потом запретить
Концепт. Любой новый гейт вводится в два такта: сначала в режиме отчёта, потом как блокирующее правило — когда дерево уже чистое. Гейт, включённый на грязном дереве, либо отключают, либо обходят.
Аудит + линт
Пара механизмов на один класс багов:
- Аудит (обнаружение): прогон по всем объявленным маршрутам от лица
правильной персоны (пользователь / администратор) с проверками
«здоровья страницы»: статус 200; не отдана страница логина в
авторизованной зоне; не отдана публичная страница вместо кабинета; в
теле ответа нет утечки PHP-ошибки. Дополнительно — аудит каждой формы
на странице:
actionнепустой, разрешается в объявленный маршрут, метод валиден. - Линт (предотвращение): запрет захардкоженных абсолютных URL в
шаблонах — все ссылки и
actionтолько через генератор маршрутов.
Первый прогон линта заведомо красный — он и есть авторитетный список работ. Конвертация идёт пачками по каталогам, и только после полной зачистки линт становится обязательным в CI.
Общий принцип, который стоит вынести: не чинить единичный баг — чинить класс багов, но так, чтобы ценность подтвердилась до крупного рефакторинга. Аудит строится первым именно потому, что ловит исходный баг сразу, не требуя переписывать ни одного шаблона.
Покрытие и мутации
- Покрытие меряется на unit + integration. Тесты, ходящие по HTTP, покрытия исходников не дают — не включайте их в расчёт и не обманывайтесь цифрой.
- Драйвер — быстрый (PCOV-класса), а не полнофункциональный отладчик: line coverage достаточно, а скорость определяет, будут ли метрику гонять регулярно.
- Шаблоны вьюх исключаются из расчёта.
- Порог — пер-слойный, а не глобальный. Единая цифра по проекту
бессмысленна: у Application требования выше, чем у Infrastructure.
Стандартный
--minэтого не умеет — пишется небольшой скрипт-гейт над отчётом покрытия. - Порог — храповик: зафиксировать текущее значение и не давать падать, а не ставить недостижимую цель на старте.
- Мутационное тестирование — аудит качества тестов, только для Domain и Application (в остальных слоях мутируется в основном фреймворочный клей). Запускать редко: оно медленное. Порог MSI не ставить, escaped-мутанты разбирать глазами, за 100% не гнаться.
Типы вместо магических строк
Зрелая фаза. Типичный антипаттерн, унаследованный от Yii2:
метод принимает строковый параметр, который меняет форму возвращаемого
значения, и возвращает array<string, mixed>. Статический анализ
бессилен в обе стороны.
Лечение:
- метод на каждый случай вместо параметра-переключателя (в PHP нет зависимых возвращаемых типов, поэтому enum на входе проблему не решает);
final readonlyDTO вместо ассоциативных массивов на выходе;- нормализация вендорских имён в доменные прямо на границе — чтобы различия внешних API не протекали в handler’ы.
Критерий приёмки такого рефакторинга: результат байт-в-байт прежний, но опечатка и на входе, и в имени поля теперь падает на статическом анализе.
11. Окружения, конфигурация, деплой
- Окружение выбирается одной переменной (
dev/test/stage/prod), которую задаёт среда исполнения (compose, systemd), а не файл в репозитории. - Слои конфигурации: дефолты, не зависящие от окружения → файл на окружение (в git) → секреты (не в git).
- Строгое чтение переменных без дефолтов. Отсутствующая переменная роняет бутстрап немедленно. Тихий фолбэк на «localhost» или пустую строку — это отложенный инцидент в проде.
- Шаблон env-файла без секретов лежит в git и служит чеклистом ключей; серверные env-файлы ведутся руками. Новый ключ в конфиге = обязательное обновление шаблона и всех серверных файлов.
- Тесты гоняются в собственном окружении с отдельной эфемерной БД и заглушками вместо внешних вендоров. Автотест, способный сходить в реальный внешний API или в dev-базу, рано или поздно туда сходит.
- Тяжёлые тесты — не на каждый PR. Быстрый гейт (статический анализ + архитектурные + unit) на pull request; интеграционные, покрытие и аудит — на merge в интеграционную ветку, до сборки и деплоя. Так падение блокирует деплой, а не рабочий процесс.
- Миграции схемы применяются до перезапуска приложения, в одноразовом контейнере. Иначе неудачная миграция оставляет запущенное приложение против несоответствующей схемы.
- Откат — ручное решение. Автоматический откат при провале деплоя чаще вредит; вместо этого — написанный ранбук и хранение нескольких последних артефактов.
12. Документооборот миграции
Три типа документов, каждый со своей ролью.
Спека (design doc)
Отвечает на «что и почему». Устоявшаяся структура:
Problem — что конкретно болит, с фактами и числами
Goals — нумерованный список проверяемых утверждений
Non-goals — что сознательно не делаем (и почему)
Architecture — компоненты, границы, сигнатуры
Risks — что может пойти не так и как это заметить
Out of scope — осознанные компромиссы, по строке обоснования
Acceptance — команды, которые должны пройти зелёными
Разделы Non-goals и Out of scope — самые ценные и чаще всего
пропускаемые. Без них любой рефакторинг расползается.
План
Отвечает на «как». Пошаговый чеклист с - [ ], точными путями файлов,
командами и фрагментами кода. План пишется так, чтобы по нему мог
работать другой человек (или агент) без контекста обсуждения. Один
модуль = один файл плана.
Мастер-план
Живой индекс: список модулей со статусами, ссылки на спеки и планы, сводная таблица технических решений и — обязательно — раздел «критичные нюансы», куда сливаются все найденные грабли. Мастер-план обновляется при каждом закрытии модуля; если модуль декомпозировался — сначала правится мастер-план.
Что дописывать после реализации
Самое слабое место любого такого корпуса: спека описывает намерение, а не результат. Обязательный ретроспективный раздел из трёх пунктов:
- Что сделано — таблица «пункт спеки → коммит → результат».
- Что осталось — отложенный долг с оценкой объёма. Он должен быть сформулирован так, чтобы стать скоупом следующей спеки.
- Наблюдения — расхождения плана с реальностью: ложные срабатывания («этот файл оказался не при чём»), схлопнутые сущности («два интерфейса оказались идентичны»), обнаруженное вне скоупа.
Цепочки спек. Спека явно ссылается на предшественника и на родительскую. Отложенный долг одной становится скоупом следующей — так «технический долг» перестаёт быть абстракцией и превращается в очередь с оценками.
13. Типовые грабли
Собраны как класс проблем, а не как список версий пакетов.
Экосистема Yii3 подвижна. Пакеты Yiisoft развиваются независимо и ломают API между мажорами. Обновление одного пакета ради нового виджета может потребовать переписывания бутстрапа (сборка маршрутов, диспетчер middleware). Держите точки входа тонкими и одинаковыми — тогда правка будет в двух-трёх файлах.
HTTP-клиент в контрактных тестах перезаписывает заголовки. Если
старое приложение адресуется по IP с Host-заголовком, задавайте
заголовок на каждый запрос, а не в конструкторе клиента: базовый URI
имеет приоритет.
Логин в legacy отличается между приложениями одного проекта. Разные пути, разные имена CSRF-полей, разные имена полей формы, где-то CSRF выключен. Один рабочий пример на каждое приложение — в мастер-план.
Константы статусов из legacy — не «как в документации». Проверяйте по данным, а не по соглашениям фреймворка. Расхождение всплывает не в тестах, а в проде, тихой пустой выборкой.
Иерархия ролей RBAC. При переносе прав легко «раскрыть» иерархию не в ту сторону и выдать ограниченной роли права администратора. Если схема прав досталась от legacy и её семантика неочевидна — проверяйте только прямые назначения и держите список допустимых ролей на маршрут в явном конфиге.
Ошибки PHP не должны попадать в тело ответа. В точках входа — логирование вместо вывода. Иначе аудит страниц будет ловить «утечку стектрейса» вместо реальной проблемы, а прод — отдавать пути файловой системы.
Различия в агрегатах между старым и новым. Итоговые строки, средние и проценты в legacy нередко считаются по всей выборке вместо страницы (или наоборот). Это тот случай, когда паритет ломается сознательно — и фиксируется в спеке.
Соглашение об именовании файлов миграций. Механизмы обнаружения миграций опираются на регулярное выражение. Файл, не подошедший под шаблон, молча пропускается — без ошибки. Проверяйте не «файл создан», а «команда видит миграцию в списке».
Автозагрузка и тени вендора. Если код монтируется в контейнер
томом, composer install, выполненный на этапе сборки образа, будет
перекрыт хостовой директорией. Установка зависимостей — отдельный шаг
после подъёма, а не строчка в Dockerfile.
14. Чеклист готовности модуля
- Спека написана, разделы
Non-goalsиOut of scopeзаполнены - План с чеклистом составлен, шаги атомарны
- Domain-сущности и интерфейсы репозиториев — без зависимостей
- Сценарии оформлены как Query/Command + Handler + Result
- Реализации в Infrastructure, сырой SQL только там
- Контроллеры тонкие; чтение — через read-порт, запись — через write-порт или handler по критерию (§7), и выбор обоснован в спеке
- Маршруты объявлены, стек middleware задан явно
- Все URL во вьюхах — через генератор маршрутов
- Архитектурные тесты зелёные (или новое правило добавлено со
skipи записанным долгом) - Unit-тесты на handler’ы с моками, интеграционные — на репозитории
- Контрактные тесты со старым приложением зелёные; расхождения задокументированы
- Статический анализ зелёный
- Аудит маршрутов и линт шаблонов зелёные
- Покрытие не упало ниже порога слоя
- Мастер-план обновлён: статус, ссылки, новые «критичные нюансы»
- Ретроспективный раздел дописан в спеку: сделано / осталось / наблюдения
15. Признаки, что миграция действительно завершена
- Контрактных тестов больше нет — они удалены вместе с выключением legacy-стека, а их роль переняли собственные тесты нового приложения.
- Ни одно архитектурное правило не стоит со
skip. - Схема БД эволюционирует из нового приложения, и это зафиксировано базовой миграцией.
- Деплой, откат и требования к серверу описаны так, что по ним разворачивается новый инстанс без участия автора.
- Новая фича не требует заглядывать в legacy, чтобы понять, «как там было».
Последний пункт — самый важный: миграция закончена не тогда, когда старый код удалён, а когда новый код перестал быть его отражением.
🔗 Связанные заметки
- Claude флоу — как вести разработку по спеке и плану с агентом
- Перенос дампов ClickHouse
- Хосты (hosts) для админок и dev — окружения и доступ к админкам
Источник:
docs/yii2-to-yii3-migration-guide.mdв репозиторииcj-admin-yii3.