概念的な例
この例は、 Decorator デザインパターンの構造を説明するためのものです。 以下の質問に答えることを目的としています:
どういうクラスからできているか?
それぞれのクラスの役割は?
パターンの要素同士はどう関係しているのか?
ここでパターンの構造を学んだ後だと、 これに続く、 現実世界の 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))
現実的な例
この例では、 Decorator パターンを使用して、 ウェブ・ページにテキストを描画する前に余計なものを取り除くための複雑なテキスト・フィルタリング規則を作成します。 コメント、 フォーラムへの投稿、 私的なメッセージなど、 内容の種類に応じて異なるフィルターの組み合わせを使用する必要があります。
たとえば、 コメントからすべての 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>