Декоратор на PHP
Декоратор — это структурный паттерн, который позволяет добавлять объектам новые поведения на лету, помещая их в объекты-обёртки.
Декоратор позволяет оборачивать объекты бесчисленное количество раз благодаря тому, что и обёртки, и реальные оборачиваемые объекты имеют общий интерфейс.
Сложность:
Популярность:
Применимость: Паттерн можно часто встретить в PHP-коде, особенно в коде, работающем с потоками данных.
Признаки применения паттерна: Декоратор можно распознать по создающим методам, которые принимают в параметрах объекты того же абстрактного типа или интерфейса, что и текущий класс.
Концептуальный пример
Этот пример показывает структуру паттерна Декоратор, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире PHP.
index.php: Пример структуры паттерна
<?php namespace RefactoringGuru\Decorator\Conceptual; /** * Базовый интерфейс Компонента определяет поведение, которое изменяется * декораторами. */ interface Component { public function operation(): string; } /** * Конкретные Компоненты предоставляют реализации поведения по умолчанию. Может * быть несколько вариаций этих классов. */ class ConcreteComponent implements Component { public function operation(): string { return "ConcreteComponent"; } } /** * Базовый класс Декоратора следует тому же интерфейсу, что и другие компоненты. * Основная цель этого класса - определить интерфейс обёртки для всех конкретных * декораторов. Реализация кода обёртки по умолчанию может включать в себя поле * для хранения завёрнутого компонента и средства его инициализации. */ class Decorator implements Component { /** * @var Component */ protected $component; public function __construct(Component $component) { $this->component = $component; } /** * Декоратор делегирует всю работу обёрнутому компоненту. */ public function operation(): string { return $this->component->operation(); } } /** * Конкретные Декораторы вызывают обёрнутый объект и изменяют его результат * некоторым образом. */ class ConcreteDecoratorA extends Decorator { /** * Декораторы могут вызывать родительскую реализацию операции, вместо того, * чтобы вызвать обёрнутый объект напрямую. Такой подход упрощает расширение * классов декораторов. */ public function operation(): string { return "ConcreteDecoratorA(" . parent::operation() . ")"; } } /** * Декораторы могут выполнять своё поведение до или после вызова обёрнутого * объекта. */ class ConcreteDecoratorB extends Decorator { public function operation(): string { return "ConcreteDecoratorB(" . parent::operation() . ")"; } } /** * Клиентский код работает со всеми объектами, используя интерфейс Компонента. * Таким образом, он остаётся независимым от конкретных классов компонентов, с * которыми работает. */ function clientCode(Component $component) { // ... echo "RESULT: " . $component->operation(); // ... } /** * Таким образом, клиентский код может поддерживать как простые компоненты... */ $simple = new ConcreteComponent(); echo "Client: I've got a simple component:\n"; clientCode($simple); echo "\n\n"; /** * ...так и декорированные. * * Обратите внимание, что декораторы могут обёртывать не только простые * компоненты, но и другие декораторы. */ $decorator1 = new ConcreteDecoratorA($simple); $decorator2 = new ConcreteDecoratorB($decorator1); echo "Client: Now I've got a decorated component:\n"; clientCode($decorator2); Output.txt: Результат выполнения
Client: I've got a simple component: RESULT: ConcreteComponent Client: Now I've got a decorated component: RESULT: ConcreteDecoratorB(ConcreteDecoratorA(ConcreteComponent)) Пример из реальной жизни
В этом примере паттерн Декоратора помогает создать сложные правила фильтрации текста для приведения информации в порядок перед её отображением на веб-странице. Разные типы информации, такие как комментарии, сообщения на форуме или личные сообщения, требуют разных наборов фильтров.
Например, вы хотели бы удалить весь HTML из комментариев и в тоже время сохранить некоторые основные теги HTML в сообщениях на форуме. Кроме того, вы можете пожелать разрешить публикацию в формате Markdown, который должен быть обработан перед какой-либо фильтрацией HTML. Все эти правила фильтрации могут быть представлены в виде отдельных классов декораторов, которые могут быть сложены в стек по-разному, в зависимости от характера содержимого.
index.php: Пример из реальной жизни
<?php namespace RefactoringGuru\Decorator\RealWorld; /** * Интерфейс Компонента объявляет метод фильтрации, который должен быть * реализован всеми конкретными компонентами и декораторами. */ interface InputFormat { public function formatText(string $text): string; } /** * Конкретный Компонент является основным элементом декорирования. Он содержит * исходный текст как есть, без какой-либо фильтрации или форматирования. */ class TextInput implements InputFormat { public function formatText(string $text): string { return $text; } } /** * Базовый класс Декоратора не содержит реальной логики фильтрации или * форматирования. Его основная цель – реализовать базовую инфраструктуру * декорирования: поле для хранения обёрнутого компонента или другого декоратора * и базовый метод форматирования, который делегирует работу обёрнутому объекту. * Реальная работа по форматированию выполняется подклассами. */ class TextFormat implements InputFormat { /** * @var InputFormat */ protected $inputFormat; public function __construct(InputFormat $inputFormat) { $this->inputFormat = $inputFormat; } /** * Декоратор делегирует всю работу обёрнутому компоненту. */ public function formatText(string $text): string { return $this->inputFormat->formatText($text); } } /** * Этот Конкретный Декоратор удаляет все теги HTML из данного текста. */ class PlainTextFilter extends TextFormat { public function formatText(string $text): string { $text = parent::formatText($text); return strip_tags($text); } } /** * Этот Конкретный Декоратор удаляет только опасные теги и атрибуты HTML, * которые могут привести к XSS-уязвимости. */ class DangerousHTMLTagsFilter extends TextFormat { private $dangerousTagPatterns = [ "|<script.*?>([\s\S]*)?</script>|i", // ... ]; private $dangerousAttributes = [ "onclick", "onkeypress", // ... ]; public function formatText(string $text): string { $text = parent::formatText($text); foreach ($this->dangerousTagPatterns as $pattern) { $text = preg_replace($pattern, '', $text); } foreach ($this->dangerousAttributes as $attribute) { $text = preg_replace_callback('|<(.*?)>|', function ($matches) use ($attribute) { $result = preg_replace("|$attribute=|i", '', $matches[1]); return "<" . $result . ">"; }, $text); } return $text; } } /** * Этот Конкретный Декоратор предоставляет элементарное преобразование Markdown * → HTML. */ class MarkdownFormat extends TextFormat { public function formatText(string $text): string { $text = parent::formatText($text); // Форматирование элементов блока. $chunks = preg_split('|\n\n|', $text); foreach ($chunks as &$chunk) { // Форматирование заголовков. if (preg_match('|^#+|', $chunk)) { $chunk = preg_replace_callback('|^(#+)(.*?)$|', function ($matches) { $h = strlen($matches[1]); return "<h$h>" . trim($matches[2]) . "</h$h>"; }, $chunk); } // Форматирование параграфов. else { $chunk = "<p>$chunk</p>"; } } $text = implode("\n\n", $chunks); // Форматирование встроенных элементов. $text = preg_replace("|__(.*?)__|", '<strong>$1</strong>', $text); $text = preg_replace("|\*\*(.*?)\*\*|", '<strong>$1</strong>', $text); $text = preg_replace("|_(.*?)_|", '<em>$1</em>', $text); $text = preg_replace("|\*(.*?)\*|", '<em>$1</em>', $text); return $text; } } /** * Клиентский код может быть частью реального веб-сайта, который отображает * создаваемый пользователями контент. Поскольку он работает с модулями * форматирования через интерфейс компонента, ему всё равно, получает ли он * простой объект компонента или обёрнутый. */ function displayCommentAsAWebsite(InputFormat $format, string $text) { // .. echo $format->formatText($text); // .. } /** * Модули форматирования пользовательского ввода очень удобны при работе с * контентом, создаваемым пользователями. Отображение такого контента «как есть» * может быть очень опасным, особенно когда его могут создавать анонимные * пользователи (например, комментарии). Ваш сайт не только рискует получить * массу спам-ссылок, но также может быть подвергнут XSS-атакам. */ $dangerousComment = <<<HERE Hello! Nice blog post! Please visit my <a href='http://www.iwillhackyou.com'>homepage</a>. <script src="http://www.iwillhackyou.com/script.js"> performXSSAttack(); </script> HERE; /** * Наивное отображение комментариев (небезопасное). */ $naiveInput = new TextInput(); echo "Website renders comments without filtering (unsafe):\n"; displayCommentAsAWebsite($naiveInput, $dangerousComment); echo "\n\n\n"; /** * Отфильтрованное отображение комментариев (безопасное). */ $filteredInput = new PlainTextFilter($naiveInput); echo "Website renders comments after stripping all tags (safe):\n"; displayCommentAsAWebsite($filteredInput, $dangerousComment); echo "\n\n\n"; /** * Декоратор позволяет складывать несколько входных форматов для получения * точного контроля над отображаемым содержимым. */ $dangerousForumPost = <<<HERE # Welcome This is my first post on this **gorgeous** forum. <script src="http://www.iwillhackyou.com/script.js"> performXSSAttack(); </script> HERE; /** * Наивное отображение сообщений (небезопасное, без форматирования). */ $naiveInput = new TextInput(); echo "Website renders a forum post without filtering and formatting (unsafe, ugly):\n"; displayCommentAsAWebsite($naiveInput, $dangerousForumPost); echo "\n\n\n"; /** * Форматтер Markdown + фильтрация опасных тегов (безопасно, красиво). */ $text = new TextInput(); $markdown = new MarkdownFormat($text); $filteredInput = new DangerousHTMLTagsFilter($markdown); echo "Website renders a forum post after translating markdown markup" . " and filtering some dangerous HTML tags and attributes (safe, pretty):\n"; displayCommentAsAWebsite($filteredInput, $dangerousForumPost); echo "\n\n\n"; Output.txt: Результат выполнения
Website renders comments without filtering (unsafe): Hello! Nice blog post! Please visit my <a href='http://www.iwillhackyou.com'>homepage</a>. <script src="http://www.iwillhackyou.com/script.js"> performXSSAttack(); </script> Website renders comments after stripping all tags (safe): Hello! Nice blog post! Please visit my homepage. performXSSAttack(); Website renders a forum post without filtering and formatting (unsafe, ugly): # Welcome This is my first post on this **gorgeous** forum. <script src="http://www.iwillhackyou.com/script.js"> performXSSAttack(); </script> Website renders a forum post after translating markdown markupand filtering some dangerous HTML tags and attributes (safe, pretty): <h1>Welcome</h1> <p>This is my first post on this <strong>gorgeous</strong> forum.</p> <p></p>