Когда разработчик планирует архитектуру своего будущего веб-приложения, полезно подумать о его расширяемости заранее. Модульная архитектура приложения может обеспечить хорошую степень расширяемости. Существует довольно много способов, как такую архитектуру реализовать, но все они сходны в своих фундаментальных принципах: разделение понятий, самодостаточность, взаимная сочетаемость всех компонентов.
Однако есть один подход, который именно в PHP можно встретить довольно редко. Он включает использование нативного наследования и позволяет патчить код «более лучше»(с). Мы называем этот способ “Forwarding Decorator”. Нам он представляется достаточно эффективным, и, кстати, эффектным тоже, хотя последнее не так важно в продакшене.
Как автор оригинальной англоязычной статьи "Achieving Modular Architecture with Forwarding Decorators", опубликованной на SitePoint, я представляю вам авторскую версию перевода. В ней я сохранил изначально задуманный смысл и идею, но постарался максимально улучшить подачу.
В данной статье мы рассмотрим реализацию подхода с использованием Forwarding Decorator, а также его плюсы и минусы. Сравним данный подход с другими хорошо известными альтернативами, а именно — с использованием хуков, патчингом кода или DI (dependency injection). Для наглядности есть демо-приложение вот в этом репозитории GitHub.
Основная идея состоит в том, чтобы рассматривать каждый класс как сервис, и модифицировать этот сервис посредством наследования и реверсирования цепочки наследников при компиляции кода.
В системе, основанной на такой идее, в любом модуле можно создать специальный класс-декоратор (отмеченный особым образом). Такой класс получит поля и методы другого класса через механизм наследования, но после компиляции будет везде использоваться вместо оригинального класса.
Собственно, поэтому мы и называем такие классы Forwarding decorators: эти декораторы являются надстройкой над исходной реализацией, однако выдвигаются вперед в местах использования.
Преимущества такого подхода очевидны:
Однако свои недостатки у этого подхода тоже есть:
Подобный способ расширения системы — в некотором смысле промежуточное решение между прямым патчингом кода (low-level, никаких правил игры, god mode, greatest power but with greatest responsibility и т.д.) и архитектурой на основе плагинов, с четким определением того, каким может быть плагин, какие подсистемы и как он может изменять\предоставлять. Система декораторов позволяет хорошо решать некоторый диапазон задач, но это вовсе не серебряная пуля и не идеальный способ организовать модульность.
Вот пример:
Как так вышло? Это уличная магия ) Мы разворачиваем цепочку наследования вспять. Исходный класс — без внутреннего кода. В результате компиляции мы препроцессим код так, что содержимое исходного класса уходит в отдельный класс, который будет новым родителем для цепочки:
Если кратко, то в приложение встраивается компилятор, который строит промежуточные классы, и autoloader, который будет загружать эти промежуточные классы вместо исходных.
А теперь немного подробнее. Компилятор строит список всех классов, используемых в системе, и для каждого класса, который не является декоратором, находит все подклассы, которые будут его декорировать с помощью DecoratorInterface. Он создает дерево декораторов, проверяет, нет ли там циклов, сортирует декораторы по их приоритету об этом подробнее далее) и строит промежуточные классы, где цепочка наследования будет развернута в обратную сторону. Исходный код преобразуется в новый класс, который станет новым родительс��им классом для цепочки наследования.
Звучит сложно. Так оно и есть, это действительно сложная комплексная система. Однако она позволяет очень гибко комбинировать модули, и с помощью этих модулей вы можете модифицировать абсолютно любую часть вашего приложения.
В случае, если в игру вступает несколько декораторов одновременно, они попадают в цепочку декорирования согласно их приоритету. Приоритет можно задать с помощью аннотаций (мы пользуемся Doctrine\Annotations) или конфигов.
Рассмотрим пример:
В данном примере аннотация Decorator\After используется, чтобы поставить декоратор другого модуля Module 1 перед модулем Module 2. Компилятор проанализирует файлы, учтет аннотации и построит промежуточный класс с такой цепочкой наследования:

Также можно использовать такие аннотации:
Данного набора аннотаций (Before, After, Depend) абсолютно достаточно для построения любой комбинации модулей и классов.
Есть! Для наглядности я подготовил демку приложения, она находится вот в этом репозитории GitHub. Это написанное на PHP приложение имеет модульную архитектуру, и модули могут подмешивать код без рекомпиляции. При этом модули можно добавлять и удалять, но в этом случае рекомпиляция уже понадобится. Более детально все это описано в readme файле.
Есть и совсем «боевые» примеры. На рынке уже есть несколько программных продуктов, которые используют такой подход. В частности, нечто очень похожее используется в OXID eShop. Кстати, у них прикольный стиль изложения в блоге. Еще в одной платформе, X-Cart 5, данный подход реализован именно в той форме, в которой я его описал — код X-Cart 5 даже был взят за основу для этой статьи. Это позволило создать очень гибкое решение для электронной коммерции, которое можно расширять настолько, насколько хватит фантазии разработчика (или денег заказчика =)), и при этом не ломать последующие апгрейды ядра.
Как и подход с Forwarding Decorators, использование хуков и патчинг “в лоб” имеют свои плюсы и минусы.
Forwarding-декораторы — это подход, по меньшей мере заслуживающий внимания. Он может использоваться для решения проблемы разработки расширяемой модульной архитектуры приложений на языке PHP. При этом будут использоваться знаком��е конструкции, такие как наследование или область видимости полей/методов/классов.
Реализация такого концепта — задача нетривиальная, возможны сложности с отладкой, но они преодолимы при условии, что вы потратите некоторое время на должную настройку компилятора.
Если будет интерес к данному материалу, в следующей статье я напишу, как сделать оптимальный компилятор с автозагрузчиком и использовать потоковые фильтры (PHP Stream filters), чтобы включить пошаговый дебаггинг исходного кода через XDebug. Интересно? Дайте об этом знать в комментариях. А еще я буду рад вашим вопросам, советам и конструктивной критике.
Однако есть один подход, который именно в PHP можно встретить довольно редко. Он включает использование нативного наследования и позволяет патчить код «более лучше»(с). Мы называем этот способ “Forwarding Decorator”. Нам он представляется достаточно эффективным, и, кстати, эффектным тоже, хотя последнее не так важно в продакшене.
Как автор оригинальной англоязычной статьи "Achieving Modular Architecture with Forwarding Decorators", опубликованной на SitePoint, я представляю вам авторскую версию перевода. В ней я сохранил изначально задуманный смысл и идею, но постарался максимально улучшить подачу.
Введение
В данной статье мы рассмотрим реализацию подхода с использованием Forwarding Decorator, а также его плюсы и минусы. Сравним данный подход с другими хорошо известными альтернативами, а именно — с использованием хуков, патчингом кода или DI (dependency injection). Для наглядности есть демо-приложение вот в этом репозитории GitHub.
Основная идея состоит в том, чтобы рассматривать каждый класс как сервис, и модифицировать этот сервис посредством наследования и реверсирования цепочки наследников при компиляции кода.
В системе, основанной на такой идее, в любом модуле можно создать специальный класс-декоратор (отмеченный особым образом). Такой класс получит поля и методы другого класса через механизм наследования, но после компиляции будет везде использоваться вместо оригинального класса.

Преимущества такого подхода очевидны:
- Любая часть системы может быть расширена с помощью модуля — любой класс, любой public/protected метод. Не нужно заранее отмечать точки расширения специальным кодом.
- Одна подсистема может модифицироваться несколькими модулями одновременно.
- Подсистемы слабо связаны между собой, поэтому могут обновляться по-отдельности, независимо друг от друга.
- Вы можете ограничить расширяемость, используя привычные конструкции: приватные (private) методы и закрытые (final) классы.
Однако свои недостатки у этого подхода тоже есть:
- В первую очередь — это отсутствие четких интерфейсов взаимодействия c расширяемой системой. Мы можем расширять все, что не запрещено явно через private, но система может не ожидать, что в нее зашли не с того конца и будет работать неадекватно в случаях, о которых не задумывался разработчик модуля. Нужно тщательно инспектировать код на наличие нежелательных побочных эффектов.
- Вам придется реализовать своего рода компилятор (подробности ниже).
- При разработке модулей нужно четко соблюдать публичный интерфейс подсистем и не нарушать принцип подстановки Лисков, иначе эти модули сломают систему.
- Наличие дополнительного компилятора усложняет отладку кода. Вы не сможете запускать XDebug на исходном коде напрямую, любое изменение кода сначала требует запуска компилятора. Однако эту проблему можно решить, используя хитрые PHP-трюки так, что запускаться будут скомпилированные файлы, но при этом в дебаггере вы будете видеть исходный код.
Подобный способ расширения системы — в некотором смысле промежуточное решение между прямым патчингом кода (low-level, никаких правил игры, god mode, greatest power but with greatest responsibility и т.д.) и архитектурой на основе плагинов, с четким определением того, каким может быть плагин, какие подсистемы и как он может изменять\предоставлять. Система декораторов позволяет хорошо решать некоторый диапазон задач, но это вовсе не серебряная пуля и не идеальный способ организовать модульность.
Как можно использовать такую систему?
Вот пример:
class Foo {
public function bar() {
echo 'baz';
}
}
namespace Module1;
/**
*
Это класс особого декоратора, его отметкой служит DecoratorInterface (примечание переводчика: также можно использовать аннотации, конфиги и проч)
*/
class ModifiedFoo extends \Foo implements \DecoratorInterface {
public function bar() {
parent::bar();
echo ' modified';
}
}
// ... где-то в коде приложения
$object = new Foo();
$object->bar(); // will echo 'baz modified'
Как так вышло? Это уличная магия ) Мы разворачиваем цепочку наследования вспять. Исходный класс — без внутреннего кода. В результате компиляции мы препроцессим код так, что содержимое исходного класса уходит в отдельный класс, который будет новым родителем для цепочки:
// пустой код исходного класса, который будет использоваться, чтобы инстанцировать новые объекты
class Foo extends \Module1\ModifiedFoo {
// move the implementation from here to FooOriginal
}
namespace Module1;
// Здесь мы создаем особый класс, который будет расширять другой класс с исходным кодом
abstract class ModifiedFoo extends \FooOriginal implements \DecoratorInterface {
public function bar() {
parent::bar();
echo ' modified';
}
}
// Новый родительский класс с исходным кодом. Все цепочки наследования будут начинаться с него
class FooOriginal {
public function bar() {
echo 'baz';
}
}
Если кратко, то в приложение встраивается компилятор, который строит промежуточные классы, и autoloader, который будет загружать эти промежуточные классы вместо исходных.
А теперь немного подробнее. Компилятор строит список всех классов, используемых в системе, и для каждого класса, который не является декоратором, находит все подклассы, которые будут его декорировать с помощью DecoratorInterface. Он создает дерево декораторов, проверяет, нет ли там циклов, сортирует декораторы по их приоритету об этом подробнее далее) и строит промежуточные классы, где цепочка наследования будет развернута в обратную сторону. Исходный код преобразуется в новый класс, который станет новым родительс��им классом для цепочки наследования.
Звучит сложно. Так оно и есть, это действительно сложная комплексная система. Однако она позволяет очень гибко комбинировать модули, и с помощью этих модулей вы можете модифицировать абсолютно любую часть вашего приложения.
А если один класс переписывается несколькими модулями?
В случае, если в игру вступает несколько декораторов одновременно, они попадают в цепочку декорирования согласно их приоритету. Приоритет можно задать с помощью аннотаций (мы пользуемся Doctrine\Annotations) или конфигов.
Рассмотрим пример:
class Foo {
public function bar() {
echo 'baz';
}
}
namespace Module1;
class Foo extends \Foo implements \DecoratorInterface {
public function bar() {
parent::bar();
echo ' modified';
}
}
namespace Module2;
/**
* @Decorator\After("Module1")
*/
class Foo extends \Foo implements \DecoratorInterface {
public function bar() {
parent::bar();
echo ' twice';
}
}
// ... где-то в коде приложения
$object = new Foo();
$object->bar(); // вывод 'baz modified twice'
В данном примере аннотация Decorator\After используется, чтобы поставить декоратор другого модуля Module 1 перед модулем Module 2. Компилятор проанализирует файлы, учтет аннотации и построит промежуточный класс с такой цепочкой наследования:

Также можно использовать такие аннотации:
- Decorator\Before (чтобы поместить декоратор перед декораторами другого модуля или выше по весу)
- Decorator\Depend (чтобы включить декоратор, только если указанный модуль включен в системе)
Данного набора аннотаций (Before, After, Depend) абсолютно достаточно для построения любой комбинации модулей и классов.
А есть прямо рабочие примеры?
Есть! Для наглядности я подготовил демку приложения, она находится вот в этом репозитории GitHub. Это написанное на PHP приложение имеет модульную архитектуру, и модули могут подмешивать код без рекомпиляции. При этом модули можно добавлять и удалять, но в этом случае рекомпиляция уже понадобится. Более детально все это описано в readme файле.
Есть и совсем «боевые» примеры. На рынке уже есть несколько программных продуктов, которые используют такой подход. В частности, нечто очень похожее используется в OXID eShop. Кстати, у них прикольный стиль изложения в блоге. Еще в одной платформе, X-Cart 5, данный подход реализован именно в той форме, в которой я его описал — код X-Cart 5 даже был взят за основу для этой статьи. Это позволило создать очень гибкое решение для электронной коммерции, которое можно расширять настолько, насколько хватит фантазии разработчика (или денег заказчика =)), и при этом не ломать последующие апгрейды ядра.
Привычные хуки и патчинг лучше! Или нет?
Как и подход с Forwarding Decorators, использование хуков и патчинг “в лоб” имеют свои плюсы и минусы.
- Хуки (или какая-либо реализация шаблона Observer ) широко используются во многих популярных приложениях, например в Wordpress. Среди плюс��в — четко определенный API, прозрачный способ регистрации Наблюдателя. Самый большой недостаток — ограниченное количество точек входа для встраивания расширений, также неудобством является порядок выполнения (сложно полагаться на результат работы других хуков)
- Патчинг “в лоб” — самый тривиальный и очевидный способ расширения, однако он нам представляется достаточно рисковым. Во-первых, он существенно затрудняет чтение и анализ кода, во-вторых — усложняет откат изменений в случае их неправильности. Также, осложняется и наложение нескольких патчей одновременно так, чтобы они не противоречили друг другу и не ломали функционал. Другими словами, это наименее контролируемый и управляемый способ, и если в простых решениях он себя оправдывает, то с усложнением системы эти минусы растут пропорционально ее комплексности.
- Dependency Injection — код в системе с DI строится вокруг понимания, что необходимые зависимости не получаются вручную, а поставляются откуда-то извне или к ним доступ осуществляется опосредованно — опять же через некоего поставщика (чаще всего это какой-либо IoC-контейнер).
Зависимости удовлетворяют некому интерфейсу и являются законченной реализацией некой функциональности. Через систему расширения можно подменять одну реализацию зависимости на другую исходя из текущей конфигурации системы.
Реализации могут быть наследованными от базовых или же декорированными в классическом смысле декоратора — как в Symfony 2, например, как описано здесь. Проблема такой архитектуры в том, что весь код должен строиться c использованием DI-style получения зависимостей. Отличие от описанной в статье системы в том, что forwarding decorator позволяет подменять классы абсолютно прозрачно во всех точках использования.
Помимо этого, непонятно, как организовать композицию нескольких модулей, расширяющих один и тот же сервис — придется писать отдельную систему, т. к. популярные IoC-контейнеры никак не разрешают данную проблему (это находится вне области ответственности таких библиотек).
Заключение
Forwarding-декораторы — это подход, по меньшей мере заслуживающий внимания. Он может использоваться для решения проблемы разработки расширяемой модульной архитектуры приложений на языке PHP. При этом будут использоваться знаком��е конструкции, такие как наследование или область видимости полей/методов/классов.
Реализация такого концепта — задача нетривиальная, возможны сложности с отладкой, но они преодолимы при условии, что вы потратите некоторое время на должную настройку компилятора.
Если будет интерес к данному материалу, в следующей статье я напишу, как сделать оптимальный компилятор с автозагрузчиком и использовать потоковые фильтры (PHP Stream filters), чтобы включить пошаговый дебаггинг исходного кода через XDebug. Интересно? Дайте об этом знать в комментариях. А еще я буду рад вашим вопросам, советам и конструктивной критике.