« Back to Index

S.O.L.I.D - (O)pen/Closed Principle

View original Gist on GitHub

1. Bad Example.php

<?php
namespace Library\View;

class HtmlDiv
{
    private $text;
    private $id;
    private $class;

    public function __construct($text, $id = null, $class = null) {
        $this->setText($text);
        if ($id !== null) {
            $this->setId($id);
        }
        if ($class !== null) {
            $this->setClass($class);
        }
    }

    public function setText($text) {
        if (!is_string($text) || empty($text)) {
            throw new \InvalidArgumentException(
                "The text of the element is invalid.");
        }
        $this->text = $text;
        return $this;
    }

    public function setId($id) {
        if (!preg_match('/^[a-z0-9_-]+$/', $id)) {
            throw new \InvalidArgumentException(
                 "The attribute value is invalid.");
        }
        $this->id = $id;
        return $this;     
    }

    public function setClass($class) {
        if (!preg_match('/^[a-z0-9_-]+$/', $id)) {
            throw new \InvalidArgumentException(
                 "The attribute value is invalid.");
        }
        $this->class = $class;
        return $this;     
    }

    public function renderDiv() {
        return '<div' .
            ($this->id ? ' id="' . $this->id . '"' : '') .
            ($this->class ? ' class="' . $this->class . '"' : '') .
            '>' . $this->text . '</div>';
    }
}

/////////////////////////////////////////////////////////////////////

<?php
namespace Library\View;

class HtmlParagraph
{
    private $text;
    private $id;
    private $class;

    public function __construct($text, $id = null, $class = null) {
        $this->setText($text);
        if ($id !== null) {
            $this->setId($id);
        }
        if ($class !== null) {
            $this->setClass($class);
        }
    }

    public function setText($text) {
        if (!is_string($text) || empty($text)) {
            throw new \InvalidArgumentException(
                "The text of the element is invalid.");
        }
        $this->text = $text;
        return $this;
    }

    public function setId($id) {
        if (!preg_match('/^[a-z0-9_-]+$/', $id)) {
            throw new \InvalidArgumentException(
                 "The attribute value is invalid.");
        }
        $this->id = $id;
        return $this;     
    }

    public function setClass($class) {
        if (!preg_match('/^[a-z0-9_-]+$/', $id)) {
            throw new \InvalidArgumentException(
                 "The attribute value is invalid.");
        }
        $this->class = $class;
        return $this;     
    }

    public function renderParagraph() {
        return '<p' .
            ($this->id ? ' id="' . $this->id . '"' : '') .
            ($this->class ? ' class="' . $this->class . '"' : '') .
            '>' . $this->text . '</p>';
    }
}

/////////////////////////////////////////////////////////////////////

<?php
namespace Library\View;

class HtmlRenderer
{
    private $elements = array();

    public function __construct(array $elements = array()) {
        if (!empty($elements)) {
            $this->addElements($elements);
        }
    }

    public function addElement($element) {
        $this->elements[] = $element;
        return $this;
    }

    public function addElements(array $elements) {
        foreach ($elements as $element) {
            $this->addElement($element);
        }
        return $this;
    }

    public function render() {
        $html = "";
        
        // IF WE FORCE OURSELVES TO NOT TOUCH THIS FILE AGAIN (CLOSED FOR MODIFICATION)
        // THEN HOW DO WE MAKE IT "OPEN FOR EXTENSION" (SO WE DON'T HAVE TO ADD MORE 'if' CONDITIONALS)?
        foreach ($this->elements as $element) {
            if ($element instanceof HtmlDiv) {
                $html .= $element->renderDiv();
            }
            else if ($element instanceof HtmlParagraph) {
                $html .= $element->renderParagraph();
            }
        }
        
        return $html;
    }
}

2. Better Example.php

<?php

// THE SOLUTION IS TO USE POLYMORPHISM AND INTERFACES TO BUILD A CONTRACT
// FOR OUR CLASSES TO ABIDE TO AND THEN THE 'HtmlRender' CLASS CAN TRUST LOOPING
// THROUGH ALL ADDED ELEMENTS AS THEY HAVE TO ABIDE BY THE INTERFACE CONTRACT

namespace Library\View;

interface HtmlElementInterface
{
    public function render();
}

/////////////////////////////////////////////////////////////////////

<?php
namespace Library\View;

abstract class AbstractHtmlElement implements HtmlElementInterface
{
    protected $text;
    protected $id;
    protected $class;

    public function __construct($text, $id = null, $class = null) {
        $this->setText($text);
        if ($id !== null) {
            $this->setId($id);
        }
        if ($class !== null) {
            $this->setClass($class);
        }
    }

    public function setText($text) {
        if (!is_string($text) || empty($text)) {
            throw new \InvalidArgumentException(
                "The text of the element is invalid.");
        }
        $this->text = $text;
        return $this;
    }

    public function setId($id) {
        $this->checkAttribute($id);
        $this->id = $id;
        return $this;     
    }

    public function setClass($class) {
        $this->checkAttribute($class);
        $this->class = $class;
        return $this;     
    }

    protected function checkAttribute($value) {
        if (!preg_match('/^[a-z0-9_-]+$/', $value)) {
            throw new \InvalidArgumentException(
                "The attribute value is invalid.");
        }
    }
}

/////////////////////////////////////////////////////////////////////

<?php
namespace Library\View;

class HtmlDiv extends AbstractHtmlElement
{    
    public function render() {
        return '<div' .
            ($this->id ? ' id="' . $this->id . '"' : '') .
            ($this->class ? ' class="' . $this->class . '"' : '') .
            '>' . $this->text . '</div>';
    }
}

/////////////////////////////////////////////////////////////////////

<?php
namespace Library\View;

class HtmlParagraph extends AbstractHtmlElement
{    
    public function render() {
        return '<p' .
            ($this->id ? ' id="' . $this->id . '"' : '') .
            ($this->class ? ' class="' . $this->class . '"' : '') .
            '>' . $this->text . '</p>';
    }
}

/////////////////////////////////////////////////////////////////////

<?php
namespace Library\View;

class HtmlRenderer
{
    private $elements = array();

    public function __construct(array $elements = array()) {
        if (!empty($elements)) {
            $this->addElements($elements);
        }
    }

    public function addElement(HtmlElementInterface $element) {
        $this->elements[] = $element;
        return $this;
    }

    public function addElements(array $elements) {
        foreach ($elements as $element) {
            $this->addElement($element);
        }
        return $this;
    }

    public function render() {
        $html = "";
        
        // NOW THIS IS CLOSED FOR MODIFICATION
        // BUT DEFINITELY OPEN FOR EXTENSION BY ANY CLASS THAT IMPLEMENTS THE 'render' METHOD
        foreach ($this->elements as $element) {
            $html .= $element->render();
        }
        
        return $html;
    }
}

/////////////////////////////////////////////////////////////////////

<?php
use Library\Loader\Autoloader,
    Library\View\HtmlDiv,
    Library\View\HtmlParagraph,
    Library\View\HtmlRenderer;

require_once __DIR__ . "/Library/Loader/Autoloader.php";
$autoloader = new Autoloader();
$autoloader->register();

$div = new HtmlDiv("This is the text of the div.", "dID", "dClass");

$p = new HtmlParagraph("This is the text of the paragraph.", "pID", "pClass");

$renderer = new HtmlRenderer(array($div, $p));
echo $renderer->render();