Декоратор на PHP
Декоратор — це структурний патерн, який дозволяє додавати «на льоту» нові поведінки об’єктам, розміщаючи їх в об’єктах-обгортках.
Декоратор дозволяє загортати об’єкти безліч разів завдяки тому, що і обгортки, і реальні об’єкти, що загортаються, мають спільний інтерфейс.
Складність:
Популярність:
Застосування: Патерн можна часто зустріти в PHP-коді, особливо якщо код створено для роботи з потоками даних.
Ознаки застосування патерна: Декоратор можна розпізнати за створенними методами, які приймають в параметрах об’єкти того ж абстрактного типу чи інтерфейсу, що і поточний клас.
Концептуальний приклад
Цей приклад показує структуру патерна Декоратор, а саме — з яких класів він складається, які ролі ці класи виконують і як вони взаємодіють один з одним.
Після ознайомлення зі структурою, вам буде легше сприймати наступний приклад, що розглядає реальний випадок використання патерна в світі PHP.
index.php: Приклад структури патерна
<?php namespace RefactoringGuru\Decorator\Conceptual; /** * The base Component interface defines operations that can be altered by * decorators. */ interface Component { public function operation(): string; } /** * Concrete Components provide default implementations of the operations. There * might be several variations of these classes. */ class ConcreteComponent implements Component { public function operation(): string { return "ConcreteComponent"; } } /** * The base Decorator class follows the same interface as the other components. * The primary purpose of this class is to define the wrapping interface for all * concrete decorators. The default implementation of the wrapping code might * include a field for storing a wrapped component and the means to initialize * it. */ class Decorator implements Component { /** * @var Component */ protected $component; public function __construct(Component $component) { $this->component = $component; } /** * The Decorator delegates all work to the wrapped component. */ public function operation(): string { return $this->component->operation(); } } /** * Concrete Decorators call the wrapped object and alter its result in some way. */ class ConcreteDecoratorA extends Decorator { /** * Decorators may call parent implementation of the operation, instead of * calling the wrapped object directly. This approach simplifies extension * of decorator classes. */ public function operation(): string { return "ConcreteDecoratorA(" . parent::operation() . ")"; } } /** * Decorators can execute their behavior either before or after the call to a * wrapped object. */ class ConcreteDecoratorB extends Decorator { public function operation(): string { return "ConcreteDecoratorB(" . parent::operation() . ")"; } } /** * The client code works with all objects using the Component interface. This * way it can stay independent of the concrete classes of components it works * with. */ function clientCode(Component $component) { // ... echo "RESULT: " . $component->operation(); // ... } /** * This way the client code can support both simple components... */ $simple = new ConcreteComponent(); echo "Client: I've got a simple component:\n"; clientCode($simple); echo "\n\n"; /** * ...as well as decorated ones. * * Note how decorators can wrap not only simple components but the other * decorators as well. */ $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)) Життєвий приклад
index.php: Приклад з реального світу
<?php namespace RefactoringGuru\Decorator\RealWorld; /** * The Component interface declares a filtering method that must be implemented * by all concrete components and decorators. */ interface InputFormat { public function formatText(string $text): string; } /** * The Concrete Component is a core element of decoration. It contains the * original text, as is, without any filtering or formatting. */ class TextInput implements InputFormat { public function formatText(string $text): string { return $text; } } /** * The base Decorator class doesn't contain any real filtering or formatting * logic. Its main purpose is to implement the basic decoration infrastructure: * a field for storing a wrapped component or another decorator and the basic * formatting method that delegates the work to the wrapped object. The real * formatting job is done by subclasses. */ class TextFormat implements InputFormat { /** * @var InputFormat */ protected $inputFormat; public function __construct(InputFormat $inputFormat) { $this->inputFormat = $inputFormat; } /** * Decorator delegates all work to a wrapped component. */ public function formatText(string $text): string { return $this->inputFormat->formatText($text); } } /** * This Concrete Decorator strips out all HTML tags from the given text. */ class PlainTextFilter extends TextFormat { public function formatText(string $text): string { $text = parent::formatText($text); return strip_tags($text); } } /** * This Concrete Decorator strips only dangerous HTML tags and attributes that * may lead to an XSS vulnerability. */ 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; } } /** * This Concrete Decorator provides a rudimentary Markdown → HTML conversion. */ class MarkdownFormat extends TextFormat { public function formatText(string $text): string { $text = parent::formatText($text); // Format block elements. $chunks = preg_split('|\n\n|', $text); foreach ($chunks as &$chunk) { // Format headers. if (preg_match('|^#+|', $chunk)) { $chunk = preg_replace_callback('|^(#+)(.*?)$|', function ($matches) { $h = strlen($matches[1]); return "<h$h>" . trim($matches[2]) . "</h$h>"; }, $chunk); } // Format paragraphs. else { $chunk = "<p>$chunk</p>"; } } $text = implode("\n\n", $chunks); // Format inline elements. $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; } } /** * The client code might be a part of a real website, which renders user- * generated content. Since it works with formatters through the Component * interface, it doesn't care whether it gets a simple component object or a * decorated one. */ function displayCommentAsAWebsite(InputFormat $format, string $text) { // .. echo $format->formatText($text); // .. } /** * Input formatters are very handy when dealing with user-generated content. * Displaying such content "as is" could be very dangerous, especially when * anonymous users can generate it (e.g. comments). Your website is not only * risking getting tons of spammy links but may also be exposed to XSS attacks. */ $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; /** * Naive comment rendering (unsafe). */ $naiveInput = new TextInput(); echo "Website renders comments without filtering (unsafe):\n"; displayCommentAsAWebsite($naiveInput, $dangerousComment); echo "\n\n\n"; /** * Filtered comment rendering (safe). */ $filteredInput = new PlainTextFilter($naiveInput); echo "Website renders comments after stripping all tags (safe):\n"; displayCommentAsAWebsite($filteredInput, $dangerousComment); echo "\n\n\n"; /** * Decorator allows stacking multiple input formats to get fine-grained control * over the rendered content. */ $dangerousForumPost = <<<HERE # Welcome This is my first post on this **gorgeous** forum. <script src="http://www.iwillhackyou.com/script.js"> performXSSAttack(); </script> HERE; /** * Naive post rendering (unsafe, no formatting). */ $naiveInput = new TextInput(); echo "Website renders a forum post without filtering and formatting (unsafe, ugly):\n"; displayCommentAsAWebsite($naiveInput, $dangerousForumPost); echo "\n\n\n"; /** * Markdown formatter + filtering dangerous tags (safe, pretty). */ $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>