
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))
真实世界示例
在本例中, 装饰模式将帮助你创建复杂的文本过滤规则来清理内容, 然后再将其渲染在网页上。 不同类型的内容 (例如评论、 论坛帖文或私信) 需要一组不同的过滤器。
例如, 你希望去掉评论中的所有 HTML 标签, 同时仍然希望在论坛帖文中保留一些基本的 HTML 标签。 此外, 你可能希望允许用户使用 Markdown 格式发帖, 而它必须在所有 HTML 过滤器执行前进行处理。 所有这些过滤规则都可以使用单独的装饰类进行表示, 并且能根据内容的特点以不同方式叠加使用。
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>