Veröffentlicht am 26. Juni 2008 (vor 1421 Tagen)
I had a chat about test-driven development with PHP recently. It seems that TDD is not widely used in PHP, so I felt that I a little self-experiment was in order. After all, you want to know what you are talking about.
A while back, I had come up with an idea on how to make that small CMS I use on my own web sites more flexible. I wanted to see how I could translate that idea to code using TDD. And I made a very interesting experience.
The CMS I am using is a small engine that puts together (potentially multi-lingual) page content, templates, and a site structure, and creates semi-static pages. It does not have a sleek GUI frontend, because by nature I am not afraid of a text editor, and most of the time get quicker results by just writing HTML than fighting with one of these what-you-see-is-what-you-might-get HTML editors.
However, I always felt that that CMS engine did not give me enough flexibility, so here I came up with a new concept of putting together content blocks. The idea is to define a template that contains markers (nothing new here), I call them slots, and to assign content blocks to these slots. Each a content block can be static content, or custom PHP code, allowing for maximum flexiblity.
The core of that new CMS engine would be one configuration file. My original notes looked something like this:
*: top_menu = top_menu *: body = empty *: foot = copyright index.php: body = welcome some/dir: body = something_else
Each *: defines the basic mapping which can be overridden for individual pages, or whole directories, including their subdirectories. I guess this approach is inspired by the YAML configuration files that Symfony uses.
When I got back to the idea a while later, I realized that I would also want to be able to define a template. Also, I tried to simplify the configuration file syntax. My second notes looked something like this:
template = bla title_slot = page_title copyright_slot = copyright products/ copyright_slot = ...
I started thinking about an implementation. Obviously, I would need a kind
of "best matching pattern" algorithm. I thought of Path
objects that have one Template and many Rule objects
associated with them. My idea was to create a tree of Path objects
and traverse this tree to find the appropriate slot/block mapping and template
for a given URL. I was planning to create a ContentMapper object
that would actually do the work, returning the slot/block mapping and template
for a given URL.
This concept seemed to be an interesting testbed for some test-driven PHP
development. I fired up my Laptop and installed the latest version of
PHPUnit. Now, TDD is about starting with
the tests, and I must admit that I started off by violating this rule.
I started off by creating the ContentMapper, Rule,
and Url class. Since a Template only consists of
one name, I figured that I would not create an object, but make that template
name a member of Url, which I figured would be a more intuitive
class name than Path.
I implemented the basic relationships between the objects (Url
has children and is associated to many Rule objects, one for each
content slot), and started to think about writing tests. My first test was to
set up a site structure, define a template, then assert that the
ContentMapper would actually return the template name
when queried for a template. This was my first nice TDD experience: Instead of
writing a major part of the matching algorithm, then figuring out test cases,
I just implemented the special case when just one default template is defined.
This is, obviously, very simple: just return that one template name for any URL.
Though I had not really accomplished a lot, I found it interesting to actually
start off by a more edgy case rather than the real algorithm.
My second test case was to build a more complex strucure: a default template
is overridden for a subdirectory, so when retrieving the template for an URL
below that subdirectory, you’d expect to get that template's name,
while the default (empty) URL still must return the default template name.
It seemed not too difficult to implement this, once the test was in place:
traverse the tree of Url objects to find the best matching
template for the given URL.
Iterators came to mind.
I had some trouble to get recursive iteration to work, because the documentation
is not too clear about what that getChildren() method should return.
I found some sample code in ezComponents,
but did not get things to work properly.
After a while I decided to just not use Iterators. Thinking along the lines
of "what is the simplest thing that could possibly work", I started
questioning myself wether Rule objects were required at all:
basically, they just contain a key/value pair. This in turn lead me to the
question of how much effort it would actually be to build an ordered tree of
Url objects. The original idea was to traverse the tree until the
first longer URL is found, and then return the last found template
or slot/block mapping. This only works when the tree is sorted. Would I have
to make the tree self-sorting, or rely on the ContentMapper to
insert each Url object at the right place in the tree?
This is the point where I sensed a bad smell in my code. There seemed to be too much effort involved in that whole concept that did not really contribute to the actual solution. I decided to start over, and use real TDD this time.
I reused my test that would define a default template, and assert that this
template be returned for every URL. My new ContentMapper object
just keeps an associative array of URL => TemplateName pairs.
This array I can keep sorted by just calling ksort() after adding
another element. I quickly got the existing test to pass, and started to add
more tests like, for example, this one:
public function testSubTemplateForNonmatchingUrl()
{
$cm = new enoContentMapper();
$cm->setTemplate('template.php');
$cm->setTemplate('sub.php', 'some/');
$this->assertEquals('sub.php', $cm->getTemplate('some/url.php'));
$this->assertEquals('sub.php', $cm->getTemplate('some/sub/url.php'));
}
This test ensures that defining a template for a certain subdirectory works.
The code of my getTemplate() method is just 22 lines long, and I
use a lot of whitespace for formatting:
public function getTemplate($aUrl = '')
{
$best_match = $this->templates[''];
foreach (array_keys($this->templates) as $key) {
if ($key == ''&& $aUrl == '')
{
return $this->templates[$key];
}
if ($key == $aUrl)
{
return $this->templates[$key];
}
if ($key != '' && $key == substr($aUrl, 0, strlen($key)))
{
$best_match = $this->templates[$key];
}
}
return $best_match;
}
This definitely is much easier than putting together Path,
Rule, and Template objects, making sure the tree is
in order, and then iterate over the tree to find the best matching rule or
template. It is still OOP, and can easily be tested.
The TDD approach has taught me that by writing the test first against a simple API, then thinking about how to implement a feature, you can definitely become a much more productive programmer, since you waste less time on building infrastructure that you may not really need.
Now, in real life you want a configuration file. Again, TDD and
"the PHP way" held a nice 15-line solution in store for me.
I had the idea to try and use parse_ini_file() instead of dealing
with XML or YAML files.
So, first of all, I drew up an ini file:
[/] template = template.php copyright_slot = copyright_block footer_slot = footer_block [sub/] template = sub.php copyright_slot = sub_copyright_block footer_slot = sub_footer_block [sub/url.php] template = special.php copyright_slot = special_copyright_block footer_slot = special_footer_block
Everything I need seems to be in the ini file, so I wrote a test that would ensure that the ini file has been correctly parsed:
class ParseTest extends PHPUnit_Framework_TestCase
{
public function testTemplates()
{
$cm = new enoContentMapper('../testdata/default');
$this->assertEquals('template.php', $cm->getTemplate());
$this->assertEquals('special.php', $cm->getTemplate('sub/url.php'));
$this->assertEquals('template.php', $cm->getTemplate('some/sub/url.php'));
$this->assertEquals('sub.php', $cm->getTemplate('sub/sub/url.php'));
}
public function testBlocks()
{
$cm = new enoContentMapper('../testdata/default');
$this->assertEquals('copyright_block', $cm->getBlock('copyright_slot'));
$this->assertEquals('footer_block', $cm->getBlock('footer_slot'));
$this->assertEquals('sub_copyright_block', $cm->getBlock('copyright_slot', 'sub/something.php'));
$this->assertEquals('sub_footer_block', $cm->getBlock('footer_slot', 'sub/something.php'));
$this->assertEquals('special_copyright_block', $cm->getBlock('copyright_slot', 'sub/url.php'));
$this->assertEquals('special_footer_block', $cm->getBlock('footer_slot', 'sub/url.php'));
$this->assertEquals('sub_copyright_block', $cm->getBlock('copyright_slot', 'sub/sub/url.php'));
$this->assertEquals('sub_footer_block', $cm->getBlock('footer_slot', 'sub/sub/url.php'));
}
}
Then, I wrote a parseIniFile() method for my class. This method
loops over the array that parse_ini_file() returns and calls
setTemplate() and setBlock() methods to build the
mapping structure. Here is the method:
protected function parseIniFile($aFilename)
{
foreach (parse_ini_file($aFilename, true) as $url => $array)
{
if (isset($array['template']))
{
$this->setTemplate($array['template'], $url);
unset($array['template']);
}
foreach ($array as $slot => $block)
{
$this->setBlock($slot, $block, $url);
}
}
}
I ran the test, and it passed. Can it get any simpler? My
ContentMapper class is self-contained, has 108 lines (again,
with a lot of whitespace in between). It does contain no error handling
whatsoever at this point, but it will not be a big deal to add that.
It seems that TDD helps you to keep things simple. While I had started off
with a much more complex implementation in mind, I learned that just a few
lines of PHP code are sufficient to solve my problem. The API of my
ContentMapper class is as simple as it can get: it has get and set
accessor methods for templates and blocks, allowing you to define a mapping
rules, and then query the object. I now also have three test classes for this
object, allowing me to test template mapping, block mapping, and ini parsing.
I will definitely give TDD another shot in the future.
I am also curious to hear about your TDD experiences. Write to me at or add a comment to this blog post.
Kommentare bitte per E-Mail.
Diplom-Informatiker Stefan Priebsch ist IT-Consultant, Trainer und Spezialist für die Entwicklung PHP-basierter Software.
Er ist Autor zahlreicher Bücher und Fachartikel über verschiedene Aspekte des Software-Lebenszyklus und spricht regelmäßig auf internationalen IT-Konferenzen.
Stefan Priebsch ist verheiratet und Vater von Zwillingen. Er lebt und arbeitet in Wolfratshausen bei München und ist Mitgründer und Principal Consultant von thePHP.cc.
"I had the pleasure of working with Stefan on a 2 part PHP Webinar series. Stefan is very knowledgable, and presented an excellent Webinar. Very professional, and very pleasant to work with."
--Nili Gafni, Zend Technologies, Inc.
Es wurde schon viel darüber spekuliert, wie groß der Anteil der PHP-Nutzer ist,
die von HipHop profitieren werden. Die Zahlen bewegen sich im Wesentlichen
zwischen 0% und 100%, daher versuche ich mich gar nicht erst an einer eigenen
Schätzung, sondern will stattdessen über die Voraussetzungen schreiben, die
ein Team beziehungsweise eine Firma erfüllen muss, um von HipHop profitieren zu
können.
(vor 826 Tagen)
» Blog-Eintrag lesen
A lot has already be speculated about what percentage of existing PHP users would benefit from using HipHop.
The numbers seem to range somewhere between 0% and 100%.
I will not try a guess myself, but rather try to shed some light on some the most important prerequisites I think there are for a company to benefit from HipHop.
(vor 834 Tagen)
» Blog-Eintrag lesen
Der erste deutsche PHP Summit - powered by thePHP.cc - ist eine neue und
einzigartige Veranstaltung, die alle wichtigen Themen rund um die
Software-Entwicklung mit PHP in kompakter Form vermittelt.
(vor 836 Tagen)
» Blog-Eintrag lesen
CodeWorks09 Derick Rethans Canada CRAP Migration Slideshare tek09 Relaunch Presentation Atlanta Book OOP Conference Magento Slides MVC Workshop Selenium Facebook PHP Quebec 09 Chicago hack Feedback Testing U.S.A. Webcast HipHop Arne Blankerts fail Training thePHP.cc PHP 5.3 C++ MTA Sebastian Bergmann Montreal Blog PHP Lifecycle Wordpress
RT @thePHPcc: 18 PHP Power Workshops in 3 Tagen? Klar geht das! Beim PHP Summit 2010 - http://phpsummit.de - Schnell buchen, bevor es zu ... (vor 837 Tagen)
» Tweet lesen
@s_bergmann Something must be wrong. There is no water ;-) (vor 837 Tagen)
» Tweet lesen
New blog post: http://priebsch.de/blog/how-to-turn-bad-code-into-good-code/ (vor 838 Tagen)
» Tweet lesen