Conceptual Example
This example illustrates the structure of the Decorator design pattern and focuses on the following questions:
What classes does it consist of?
What roles do these classes play?
In what way the elements of the pattern are related?
After learning about the pattern’s structure it’ll be easier for you to grasp the following example, based on a real-world PHP use case.
index.php: Conceptual example
<?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: Execution result
Client: I've got a simple component:
RESULT: ConcreteComponent
Client: Now I've got a decorated component:
RESULT: ConcreteDecoratorB(ConcreteDecoratorA(ConcreteComponent))
Real World Example
In this example, the Decorator pattern helps you to construct complex text filtering rules to clean up content before rendering it on a web page. Different types of content, such as comments, forum posts or private messages require different sets of filters.
For example, while you’d want to strip out all HTML from the comments, you might still want to keep some basic HTML tags in forum posts. Also, you may want to allow posting in Markdown format, which shall be processed before any HTML filtering takes place. All these filtering rules can be represented as separate decorator classes, which can be stacked differently, depending on the nature of the content you have.
index.php: Real world example
<?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: Execution result
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>