4 Maa.
2010

Het doel van een framework is om door hergebruik van code de kwaliteit van de oplossing te verhogen en de ontwikkelingstijd te verminderen. Een goede set van gemakkelijk te gebruiken en handige functies binnen een framework is onontbeerlijk wil de programmeur gelukkig blijven en tevreden kunnen gaan slapen.

Het probleem

Een van de veel voorkomende taken bij het bouwen van een website is het schrijven van allerhande html-tags. Bijvoorbeeld:

<a href="/foo/bar" class="bigLink">Linkje</a>
<img src="afbeelding.png" alt="mooie afbeelding" />
<h1>Hoofdstuktitel<h1>
<div id="mainDiv">Hello Div</div>

Uiteraard zouden we deze tags handmatig kunnen typen maar dat heeft een aantal nadelen:

  • je moet er op letten dat je geen quotes vergeet te zetten
  • je mag niet vergeten je tag waar nodig af te sluiten
  • het is niet netjes om bepaalde attributen leeg te laten
  • toekomstige uitbreidingen (bvb placeholder attribute voor input fields) wil je niet overal handmatig uitvoeren

De oplossing

Om deze probleempjes op te lossen maken we een klasse aan die deze zaken regelt. In z'n simpelste vorm ziet die er zo uit:

class TagGenerator {
    protected $attributes;
    protected $tagName;

    public function  __construct() {
        $this->attributes = array();
    }

    public function setTagName($tagName) {
        $this->tagName = $tagName;
    }

    public function  render() {
        $withClosingTagTags = array('a', 'div', 'span' , 'ul' ,'li', 'h1', 'h2','h3',
                                    'h4', 'h5', 'h6', 'table', 'td', 'tr', 'th', 'p', 'pre',
                                    'strong', 'i', 'b', 'em' , 'iframe', 'block', 'quote');
        $withClosingTag = (Utility::inArray($this->tagName, $withClosingTagTags) ? TRUE : FALSE);

        foreach($this->attributes as $name=>$value) {
            if ($value != '') {
               if (! ($name == 'value' AND $withClosingTag)) {
                   $attributeString .= $name . '="' . $value . '" ';
               }
            }
        }
        if ($withClosingTag) {
            $result = '<' . $this->tagName . ' ' . $attributeString . '>' . $this->attributes['value'] . '</' . $this->tagName . '>';
        }
        else {
            $result = '<' . $this->tagName . ' ' . $attributeString . '/>';
        }

        return $result;
    }

    public function setAttribute($name, $value) {
        $this->attributes[$name] = $value;
    }
}

De TagGenerator houdt de waarden van de attributen bij in array. Je kan items aan de array toevoegen met de setAttribute-functie. De render-functie genereert de uiteindelijke htmlstring. Met behulp van TagGenerator kunnen tags op deze manier gegenereerd worden:

$tag = New Tag();
$tag->setTagName('a');
$tag->setAttribute('href', '/foo/bar');
$tag->setAttribute('class, 'bigLink');
$tag->setAttribute('value' 'Linkje'); $html .= $tag->render();

$tag = New Tag();
$tag->setTagName('img');
$tag->setAttribute('src','afbeelding.png');
$tag->setAttribute('alt', 'mooie afbeelding'); $html .= $tag->render();

Je merkt dat we geen tags meer zelf moeten sluiten, de render functie weet welke tags zich moeten afsluiten in de vorm van <tag /> en welke in de vorm van <tag></tag>, quotes worden automatisch gesloten en lege attributen worden gefilterd in de render-functie.

De oplossing... revisited

In het gebruiksvoorbeeld hierboven zie je dat we nogal wat instructies nodig hebben om de tag te genereren. We kunnen deze klasse nog uitbreiden met een aantal zaken zodat we met minder regels code (programmeurs zijn lui, iedereen weet dat) toch hetzelfde resultaat bereiken.

Hier is de uitgebreide klasse:

class Tag {
    public static function create($tagName) {
        $instance = TagGenerator::getInstance();
        $instance->setTagName($tagName);
        return $instance;
    }
}

class TagGenerator {
    protected static $instance = null;
    protected $attributes;
    protected $tagName;

    public function  __construct() {
        $this->attributes = array();
    }

    public static function init() {
        return self::$instance = new self();
    }

    public function setTagName($tagName) {
        $this->tagName = $tagName;
    }

    public static function getInstance() {
        self::init();
        return self::$instance;
    }

    public function  __toString() {
        $withClosingTagTags = array('a', 'div', 'span' , 'ul' ,'li', 'h1', 'h2','h3',
                                    'h4', 'h5', 'h6', 'table', 'td', 'tr', 'th', 'p', 'pre',
                                    'strong', 'i', 'b', 'em' , 'iframe', 'block', 'quote');
        $withClosingTag = (Utility::inArray($this->tagName, $withClosingTagTags) ? TRUE : FALSE);

        foreach($this->attributes as $name=>$value) {
            if ($value != '') { 
               if (! ($name == 'value' AND $withClosingTag)) {
                   $attributeString .= $name . '="' . $value . '" ';
               }
            }
        }
        if ($withClosingTag) {
            $result = '<' . $this->tagName . ' ' . $attributeString . '>' . $this->attributes['value'] . '</' . $this->tagName . '>';
        }
        else {
            $result = '<' . $this->tagName . ' ' . $attributeString . '/>';
        }

        return $result;
    }

    public function setId($value) {
        $this->setAttribute('id', $value);
        return $this;
    }

    public function setName($value) {
        $this->setAttribute('name', $value);
        return $this;
    }

    public function setClass($value) {
        $this->setAttribute('class', $value);
        return $this;
    }

    public function setHref($value) {
        $this->setAttribute('href', $value);
        return $this;
    }

    public function setSrc($value) {
        $this->setAttribute('src', $value);
        return $this;
    }

    public function setTitle($value) {
        $this->setAttribute('title', $value);
        return $this;
    }

    public function setRef($value) {
        $this->setAttribute('ref', $value);
        return $this;
    }

    public function setWidth($value) {
        $this->setAttribute('width', $value);
        return $this;
    }

    public function setHeight($value) {
        $this->setAttribute('height', $value);
        return $this;
    }

    public function setBorder($value) {
        $this->setAttribute('border', $value);
        return $this;
    }

    public function setTarget($value) {
        $this->setAttribute('target', $value);
        return $this;
    }

    public function setValue($value) {
        $this->setAttribute('value', $value);
        return $this;
    }

    public function setAttribute($name, $value) {
        $this->attributes[$name] = $value;
        return $this;
    }
}


Een eerste belangrijke verandering is dat er een helperklasse Tag is bijgekomen. De klasse bevat een statische functie die een instatie van de taggenerator-klasse gaat aanmaken en teruggeven.

Een tweede verandering is dat we helperfuncties als setSrc, setTitle, ... hebben toegevoegd die telkens setAttribute aanroepen zodat we dat zelf niet meer hoeven te doen. Als een bepaalde helperfunctie voor een bepaald attribuut niet bestaat, dan kan je toch nog via setAttribute het attribuut zetten. vb. setAttribute('alt', 'alt value');

De derde wijziging zie je op het einde van alle set-Functies: ze geven nu allemaal zichzelf terug. Op deze manier gaan we aan method chaining kunnen doen.

En als laatste is ook de functie naam render() aangepast naar de magic method __tostring(), waardoor de het object zichzelf zal transformeren tot html string zodra het object als string gebruikt wordt.

Nu kunnen we tags op deze manier genereren:

$htmlString = 'Hier volg een link ' . Tag::create('a')->setHref('foo/bar')
                                                      ->setClass('bigLink')
                                                      ->setValue('Linkje');
                                                      
$htmlString = 'Een afbeelding ' .
                   Tag::create('img')->setSrc('afbeelding.png')
                                     ->setAttribute('alt', 'mooie afbeelding');

Doordat je de tag onmiddellijk concateneert met een string zal de tag zich transformeren naar een string. Je zou ook de tag in een variabele kunnen behouden. Dit heeft als voordeel dat het object dan wel een tagobject blijft en je kan er dan nog Tag-functies op loslaten.

Bijvoorbeeld:

$tag = Tag::create('a')->setHref('foo/bar')
                       ->setClass('bigLink')
                       ->setValue('Linkje');
$tag->setTarget('_blank');
$htmlString = 'Link met een target: ' . $tag;

De oplossing re-revisited

Uiteraard kan deze code nog verder uitgebreid worden. Je zou bijvoorbeeld kunnen controleren of bepaalde attributes wel zin hebben bij een bepaalde tag. Een attribute "target" heeft geen zin bij een tag "img". De setTarget-functie zou ook uitgebreid kunnen worden zodat enkel geldige targets als "_blank", "_parent", ... kunnen doorgegeven worden. Om de zaken simpel te houden is dit nu (nog) niet gebeurd.

Einde monoloog, start conversatie

Ons eigen CMS Blender maakt van deze klasse gebruik, maar bovenstaande code mag vrij gebruikt worden. Als je een bug ontdekt of een vraag hebt over de werking of een idee hebt waardoor deze code beter kan worden, dan kan je hieronder reageren.

door freek
 
6 maanden geledenwillem gelooft: Voor een pure Html-designer misschien handig als de syntax zo kort mogelijk is - bvb een afkorting voor 'Tag::create', en als properties de 'set...' altijd weglaten zodat overstap van HTML miniem wordt?

Bvb. T('a')->href('http://www.spatie.be')->value('spatie site')
 
Commentaren worden na 3 maanden gesloten om onnodige spam te vermijden, en de discussie actueel te houden. Heeft u toch iets essentieel te vertellen? Contacteer ons gerust.

Lees hier op regelmatige tijdstippen wat ons boeit, waar we mee bezig zijn of wat we willen uitproberen.

Abonneren kan ook via de oranje RSS-feed hieronder.
Wat is RSS?