[Из песочницы] Yii: лучшие практики

В статье будут освещены следующие проблемы разработки и поддержки проектов на базе php-фреймворка Yii:

  1. Главные достоинства и недостатки
  2. Тестирование
  3. Нюансы использования ActiveRecord
  4. Сервис-ориентированный подход
  5. Новшества языка
  6. Расширение фреймворка


Yii я использовал в четырёх коммерческих проектах разной степени сложности. Это были, следующие решения:

  1. Проект с нагрузкой порядка сотен тысяч хитов в сутки, с таргетингом, аналитикой по миллионам событий, обработкой мультимедиа и т. п. Рекламный сервис. Основные технологии:
    1. Yii1
    2. MySQL
    3. RabbitMQ
    4. Memcached

  2. Веб-интерфейс для одного из решений по управлению кампаниями на базе готового API аналитического сервиса. По сути простой web-GUI с некоторым кэшированием и минимальной серверной логикой. Основные технологии:
    1. Yii1
    2. SQLite
    3. bootstrap
    4. jQuery

  3. REST API на Yii2 для веб-приложения на AngularJS, и мобильных клиентов Android, iOS. Скидочная тематика, вся логика на сервере. Основные технологии:
    1. Yii2
    2. MySQL
    3. RabbitMQ

  4. Внутренняя система документооборота, узкоспециализированная под конкретную отрасль, но со множеством различных бизнес-процессов разных департаментов. Основные технологии:
    1. Yii1
    2. MySQL
    3. Клиентская часть была переписана на AngularJS 1 и отвязана от php.


Помимо этого я экспериментировал с этим фреймворком, обеих версий, в некоторых своих pet-проектах. В этой статье, я бы хотел осветить проблемы разработки и поддержки проектов разной степени сложности на Yii и дать ряд рекомендаций, основанных на собственном опыте.

Последний из перечисленных выше проектов (№4), разрабатывался около двух лет, к началу моего участия в нём. Ещё на собеседовании, когда я узнал о двухлетней разработке, без релиза, об использовании первой версии фреймворка (в начале 2015 года), я подумал что без проблем там не обойдётся. Но я согласился участвовать, чтобы принять challenge — взяться за то, что принято называть «унаследованный код». Да, тот самый, что вызывает самые разнообразные отрицательные чувства — отвращение, ненависть, отчаяние, страх — и следуя советам Фаулера, вооружившись тестами, сделать из долгостроя качественный, поддерживаемый, рабочий продукт. Год рефакторинга, смена архитектуры за несколько месяцев перед релизом и сам выход в продакшен проекта с подобной историей, заслуживают отдельной статьи — я получил бесценный опыт, которым хотел бы поделиться, если на то будет поля сообщества.

В этом материале я сделаю акцент на практиках способствующих созданию поддерживаемых приложений. Приведенные примеры я применял с Yii 1 и 2, но концептуально все шаблоны проектирования имеют право на жизнь в любом другом фреймворке или технологии.

Yii о двух концах


Yii хорош низким порогом вхождения и тем, что способствует быстрому прототипированию. Эти же качества и являются источниками проблем.

Низкий порог входа способствует привлечению разработчиков с низкой квалификацией, которые склонны быстро увеличивать технический долг. В долгосрочной перспективе, для серьёзного проекта, это существенный риск. Для менеджмента же это, на начальном этапе, соблазн сэкономить бюджет и время при подборе команды.

Быстрое прототипирование, может создавать у заказчика и менеджмента иллюзию, что проект уже почти готов и можно начинать его эксплуатацию, а добавление функционала в будущем будет обходиться так же дёшево, как и на этапе прототипа. Такое развитие событий крайне невыгодно для разработчиков и чревато неоценимыми рисками для менеджмента и проекта в целом. Я склонен считать, что прототип должен выкидываться — это есть суть прототипа.

Тестирование


Модульное тестирование в сообществе Yii-разработчиков не распространено повсеместно. Есть обзор тестирования в официальной документации (1, 2), многие из расширений создаваемых энтузиастами покрыты тестами. Но, оттого что в сообществе большой процент разработчиков с малым опытом, которые ещё не доросли до автоматического тестирования, начинать проект на Yii по TDD в целом, скорее не принято.

Дополнительную сложность для юнит-тестирования может представлять использование шаблона Active Record. Об этом я подробнее напишу ниже.

В упомянутом проекте проводилось только ручное приёмочное тестирование. Были не поддерживаемые тесты под Selenium, почему-то написанные на Java, но они выполнялись слишком долго, а запускались слишком сложно, и, как следствие редко, результат давали неоднозначный.

Одно из первых мероприятий, предпринятых мной было подключение PHPUnit и применение TDD при разработке новых фичей и рефекторинге существующего кода, обучение других членов команды методикам автоматического тестирования.

Позже был использован Codeception для приёмочного тестирования API. Позже был использован Codeception для приёмочного тестирования API. Были попытки использовать его и для функционального тестирования веб-интерфейса, но на фронтенде, начавшем к тому моменту активно использовать AngularJS были проблемы с поддержкой таких тестов, пришлось временно отказаться.

О себе я могу сказать — test infected. Чего и Вам желаю. Я не могу разрабатывать без тестов. Я не верю в работоспособность чьего-либо кода, если на него нет тестов. Я верю что любой код, на который нет авто-тестов будет отправлен на помойку. Он просто будет ломаться со временем и в какой-то момент окажется, что дешевле его переписать с нуля и с тестами, чем приводить в работоспособное состояние, путём отладки вручную.

В Yii есть механизм для работы с фикстурами, который во многом компенсирует сложности тестирования Active Record. Поддержка Ваших фикстур — то о чём стоит задуматься с самого начала. Используйте для их организации способы, которые облегчат будущую поддержку:

  • можно использовать require для разбиения больших файлов по бизнес-процессам, ролям, ещё какой-то доменной логике
  • семантически осмысленные имена (aliases), указывающие в каких именно test cases эта запись используется, с какими другими моделями она связана (relation)
  • для меня было несколько неочевидных моментов в работе FixtureManager, пришлось читать исходники. Всегда стоит знать используемый инструмент
  • не забывайте явно очищать таблицы (tearDown / setUp), заполняемые в процессе теста, чтобы избежать побочных эффектов от разделяемого тестами состояния


Active Record


Active Record — это тоже один из плюсов и одновременно минусов фреймворка. Плюс в том, что при прототипировании database first подход, поощряемый кодогенерацией классов моделей по схеме, может существенно ускорить разработку, а также в том, что порой проще работать с моделью, в контексте которой можно делать и запросы и валидацию и вообще всё что с ней связано. Минусы начинаются, когда бизнес-логика слишком велика для того чтобы всю её помещать в модель, и нарушение принципа единственной ответственности даёт о себе знать — Active Record это же и Domain Model, и Repository, и Data Mapper, и Table и Row Gateways одновременно.

Если Вы видите, что предметная область сложнее сайта-визитки или банального блога — используйте альтернативные шаблоны проектирования, чтобы разгрузить ваши модели: в Yii есть разнообразные пути для этого:

  • можно использовать модели форм для валидации пользовательских данных, до того как передавать их в модель
  • валидаторы удобно оформлять отдельными классами
  • существуют поведения (bеhaviors для композиции вместо наследования!), которые помогают в организации различной переиспользуемой служебной функциональности
  • модель легко декомпозировать делегируя некоторые методы стратегиям — например, если вы кастомизируете во многих моделях метод search () — он наверняка становиться достаточно большим чтобы заслуживать подобный рефакторинг.


Такой подход проще в тестировании и позволяет моделям не заплыть «жиром».

Вообще предпочитайте композицию наследованию — старайтесь выносить функциональность из иерархии наследования в делегаты с узко-специализированным интерфейсом.

«Жирные» модели, один из самых распространенных антипаттернов, которые я видел в проектах на Yii. Я предпочитаю следующие подходы при работе с ActiveRecord:

  • всегда создавайте свой супер-класс уровня для моделей
  • не редактируете генерируемый код — наследуйте от его!
  • кастомизируйте шаблон и логику генератора для своих нужд — Yii позволяет делать это!
  • размещайте в ваших моделях только тот код, который находиться на уровне взаимодействия с базой данных — таблицы, имена колонок, условия запросов. Избегайте знания о структуре базы данных в других слоях приложения, хорошо если ваши классы Active Record будут самым низкоуровневыми в приложении
  • проверяйте результат работы gii: не все виды сложных связей генерируются корректно, иногда приходиться отступать от чистого реляционного проектирования или реализовывать вручную код, которые не удаётся сгенерировать.


Service Locator, services and singletons


По сути, экземпляр приложения, статически доступный как Yii: app () — есть сам себе singleton и сервис-локатор для компонентов приложения. Если без ООП фанатизма, это вполне рабочее решение, если вам не нужно иметь два разных экземпляра приложения в одном процессе.

Не скупитесь создавать сервисы — маленькие сервисы с минимальной ответственностью хорошо в тестировании, они предсказуемы и понятны. Для решения любой бизнес задачи удобно создать специализированный сервис, которые будет решать ровно одну эту задачу. Я предпочитаю в качестве компонентов приложения регистрировать фабрики таких сервисов — это позволяет наделять сервисы состоянием при необходимости. Так же фабрика даёт дополнительную гибкость при инициализации объектов, в сравнении с наиболее распространённым, судя по примерам в сети, подходом напрямую регистрировать сервисы с предопределённым состоянием в конфигурации приложения.

Организованные таким образом сервисы с узкой специализацией легко заменять моками, чтобы тестировать бизнес-логику более высокого уровня. Ну, а соблюдая принцип единой ответственности на уровне каждого из сервисов, не составляет труда протестировать каждый из них в отдельности.

Для автокомплита ваших компонентов воспользуйтесь рецептом от Александра Макарова.

Namespaces и другие новшества языка


Так как первая версия Yii была выпущена в конце 2008 года и поддерживает php 5.1 — в ней не используются пространства имён на уровне ядра. Но это не мешает Вам использовать их. В проектах на первой версии я успешно использовал их.

Вместо использования т. н. Aliases Вы можете использовать статические имена классов — загрузчик их подхватит. То есть, конфигурация может выглядеть следующим образом (на примере модуля из официальной документации):

// «старый» подход
    'modules'=>array(
        'testmodule' => array(
            'class' => 'application.modules.testmodule.TestModuleModule',
        ),
    ),
// рекомендуемый в документации подход, для версий PHP >= 5.3
    'modules'=>array(
        'testmodule' => array(
            'class' => '\mynamespace\modules\testmodule\TestModuleModule',
        ),
    ),
// в 5.5 и выше это можно делать ещё удобнее
    'modules' => [
        'testmodule' => [
            'class' => \mynamespace\modules\testmodule\TestModuleModule::class,
        ],
    ],


И ещё пара улучшение для Вашего кода, о которых Вы возможно не знали, или забыли:

  • CgridView columns может принимать для value выражения не только в виде строки для eval’а, но и анонимную функции.
  • Кастомизировав шаблон генератора можно использовать свои пространства имён, короткий синтаксис массивов, актуальное psr форматирование кода и всё что вашей душе угодно.
  • Yii1 можно подключить через composer.


Расширяйте фреймворк


Одним из недостающих классов в Yii1 для меня был web\Response, при том что существует Request. Написание простейшей реализации класса ответа, и добавление супер-класса для контроллеров, с обработкой оного в afterAction (), позволило модульно тестировать контроллеры, и в итоге избавить из от лишнего «жира».

Вообще хорошим подспорьем будет введение наследников и супер-классов для каждого из используемых компонентов фреймворка: контроллеров, моделей, поведений и т. п.

А ещё, у сообщества есть полезные расширения, не только функциональные, но архитектурные. К примеру мы в проекте используем ObjectWatcher — реализация Identity Map — для того чтобы в разных контекстах работать иметь одни и те же экземпляры моделей, и NestedTransaction.

Не забывайте мудрые слова Стива МакКоннелла:

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


Эта сентенция справедлива и для фреймворков: будьте Разработчиками, а не %framework_name%-программистами!

© Habrahabr.ru