Using php closures to build html
I've mostly been doing Java and Groovy programming over the last years, but when I read that PHP got support for Closures (okay I'm a little late to the party..) I wanted to see if they could be used to create builder patterns similar to those we find everywhere in the Groovy world.
Why? Building HTML by concatenation strings are ugly, error prone and hard to maintain. Especially if you want multiple objects to take part in the construction. Using an approach similar to this you can pass html nodes around for participating objects to add their html nodes before building it.
Disclaimer: I have not read up about performance hits using php closures, this is just a proof of concept put together in 20 minutes.
HTML
/**
*
* @author Kim A. Betti
*/
class HTML {
protected $tagName;
protected $isShortTag;
protected $attr = array();
protected $childTags = array();
/**
*
* @param string $tagName
* @param mixed $body
* @param boolean $isShort
*/
protected function __construct($tagName, $body, $isShort) {
$this->tagName = $tagName;
$this->isShortTag = $isShort;
}
/**
*
* @param string $tagName
*/
public static function shortTag($tagName) {
return new HTML($tagName, null, true);
}
/**
*
* @param string $tagName
* @param mixed $body
*/
public static function tag($tagName, $body = null) {
$html = new HTML($tagName, $body, false);
$html->setBody($body);
return $html;
}
/**
*
* @param string $methodName
* @param array $args
*/
public function __call($methodName, array $args) {
$attrName = strtolower($methodName);
return $this->setAttr($attrName, $args[0]);
}
/**
*
* @param string $attrName
* @param string $attrValue
*/
public function setAttr($attrName, $attrValue) {
$this->attr[$attrName] = $attrValue;
return $this;
}
/**
*
* @param string $attrName
* @return boolean
*/
public function hasAttr($attrName) {
return array_key_exists($attrName, $this->attr);
}
/**
*
* @param string $attrName
* @return string
*/
public function getAttr($attrName) {
return $this->attr[$attrName];
}
/**
*
* @param String $tagName
* @return HTML
*/
public function addShortTag($tagName) {
return $this->childTags[] = HTML::shortTag($tagName);
}
/**
*
* @param string $tagName
* @param mixed $body
* @return HTML
*/
public function addTag($tagName, $body) {
return $this->childTags[] = HTML::tag($tagName, $body);
}
/**
*
* @param mixed $body String or Closure
*/
public function setBody($body) {
if ($body !== null) {
if (is_callable($body))
$body($this);
else
$this->childTags[] = $body;
}
}
/**
*
* @return string
*/
public function build() {
$str = sprintf('<%s', $this->tagName);
foreach ($this->attr as $name => $value)
$str .= sprintf(' %s="%s"', $name, $value);
if (!$this->isShortTag) {
$str .= '>';
foreach ($this->childTags as $idx => $childTag)
$str .= ( $childTag instanceof HTML)
? $childTag->build() : $childTag;
$str .= sprintf('</%s>', $this->tagName);
} else {
$str .= " />";
}
return $str;
}
}
HTMLTest
Simple unit test demonstrating the idea. It might look a bit over the top? But when if you throw in some more conditional attributes, loops and multiple objects participating in building html I think this is favorable over string concatenation.
require_once 'PHPUnit/Framework.php';
require_once dirname(__FILE__) . '/../HTML.php';
/**
* @author Kim A. Betti
*/
class HTMLTest extends PHPUnit_Framework_TestCase {
public function testExample() {
$html = HTML::tag('form', function(HTML $form) {
$form->addTag('fieldset', function(HTML $fieldset) {
$fieldset->addTag('legend', 'Credentials');
$fieldset->addShortTag('input')->type('text')
->name('username')->value('Superman');
$fieldset->addShortTag('input')->type('password')
->name('password')->value('kryptonite');
$fieldset->addShortTag('input')
->type('submit')->value('Login');
});
})->action('/submit.php')->method('post')->build();
$this->assertEquals('<form action="/submit.php" method="post">'
. '<fieldset><legend>Credentials</legend>'
. '<input type="text" name="username" value="Superman" />'
. '<input type="password" name="password" value="kryptonite" />'
. '<input type="submit" value="Login" /></fieldset></form>', $html);
}
public function testAttributes() {
$input = HTML::tag('input');
$this->assertFalse($input->hasAttr('type'));
$input->type('password');
$this->assertTrue($input->hasAttr('type'));
$this->assertEquals('password', $input->getAttr('type'));
}
}


rick - 29. Jun 2010 00:17
Yuck!! Disgusting.