<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" media="screen" href="/~d/styles/atom10full.xsl"?><?xml-stylesheet type="text/css" media="screen" href="http://feeds.feedburner.com/~d/styles/itemcontent.css"?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:openSearch="http://a9.com/-/spec/opensearch/1.1/" xmlns:georss="http://www.georss.org/georss" xmlns:gd="http://schemas.google.com/g/2005" gd:etag="W/&quot;DkICQHg8cSp7ImA9WxNUEkk.&quot;"><id>tag:blogger.com,1999:blog-8420118650236071171</id><updated>2009-11-03T10:36:01.679Z</updated><title>&lt;raphael.on.php/&gt;</title><subtitle type="html">PHP web development blog by Raphael Stolt</subtitle><link rel="http://schemas.google.com/g/2005#feed" type="application/atom+xml" href="http://raphaelstolt.blogspot.com/feeds/posts/default" /><link rel="alternate" type="text/html" href="http://raphaelstolt.blogspot.com/" /><link rel="hub" href="http://pubsubhubbub.appspot.com/" /><link rel="next" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default?start-index=26&amp;max-results=25&amp;redirect=false&amp;v=2" /><author><name>Raphael Stolt</name><uri>http://www.blogger.com/profile/07949831701855458792</uri><email>noreply@blogger.com</email></author><generator version="7.00" uri="http://www.blogger.com">Blogger</generator><openSearch:totalResults>51</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><link rel="self" href="http://feeds.feedburner.com/raphaelonphp" type="application/atom+xml" /><atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="hub" href="http://pubsubhubbub.appspot.com" /><entry gd:etag="W/&quot;DUAAQnc-eCp7ImA9WxNWFUg.&quot;"><id>tag:blogger.com,1999:blog-8420118650236071171.post-7619783294518881515</id><published>2009-10-14T21:22:00.005Z</published><updated>2009-10-14T22:02:23.950Z</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-10-14T22:02:23.950Z</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="Zend Framework" /><category scheme="http://www.blogger.com/atom/ns#" term="PHP" /><category scheme="http://www.blogger.com/atom/ns#" term="Book reviews" /><title>Zend Framework 1.8 Web Application Development book review</title><content type="html">&lt;img style="margin: 0pt 10px 10px 0pt; float: left; cursor: pointer; width: 190px; height: 228px;" src="http://farm3.static.flickr.com/2487/4011785234_2348120baa_o.gif" alt="Zend Framework 1.8 Web Application Development" title="Zend Framework 1.8 Web Application Development" border="0" /&gt;As the days are rapidly getting shorter, my reading appetite grows potentially and this evening I finished the 'Zend Framework 1.8 Web Application Development' book written by &lt;a href="http://www.thepopeisdead.com" target="_self"&gt;Keith Pope&lt;/a&gt;. While Keith worked on the book, I peeked several times at it's &lt;a href="http://code.google.com/p/zendframeworkstorefront/" target="_self"&gt;tutorial application&lt;/a&gt;, dubbed the Storefront, to get me going with the new Zend_Application component. Looking at it's code made me feel certain to get another great digest of the new features and components of version 1.8, and also a different practical perspective on web application development with the &lt;a href="http://framework.zend.com/" target="_self"&gt;Zend Framework&lt;/a&gt;, once the book has been published. Therefor I got in touch with the publisher &lt;a href="http://www.packtpub.com" target="_self"&gt;Packt&lt;/a&gt; and fortunately got a &lt;a href="http://www.packtpub.com/zend-framework-1-8-web-application-development/book" target="_self"&gt;copy&lt;/a&gt; of which I'd like to share a personal review in this blog post.&lt;h4 class="custom"&gt;What's in it?&lt;/h4&gt;The book opens with a quick run-through of the Model-View-Controller (MVC) architecture by creating a project structure via &lt;a href="http://framework.zend.com/manual/en/zend.tool.framework.html" target="_self"&gt;Zend_Tool&lt;/a&gt; and building a first very basic web application. While this introduction intentionally skips over a lot of details, the following chapter provides very detailed insights into the Zend Framework's MVC components by explaining the surrounded objects, the Design Patterns they are based upon and their interactions. &lt;br /&gt;&lt;br /&gt;After laying out that hefty block of theory the aforementioned tutorial application is introduced and built incrementally over several chapters; each one going into more detail for the specific application aspect. The highlight content of these chapters reach from introducing the Fat Model Skinny Controller concept, thoughts on Model design strategies which are reflected in a custom Storefront Model design, to developing application specific Front Controller Plugins, Action-Helpers, and View-Helpers. The application walk-through is completed by looking at general techniques to optimize the Storefront application and by building an automated &lt;a href="http://www.phpunit.de" target="self"&gt;PHPUnit&lt;/a&gt; Test Suite of functional tests utilizing &lt;a href="http://framework.zend.com/manual/en/zend.test.html" target="_self"&gt;Zend_Test&lt;/a&gt; to keep the Zend Framework based application self-reliant and refactorable.&lt;h4 class="custom"&gt;Conclusion&lt;/h4&gt;The book by Keith Pope provides any interested PHP developer, who's not already sold on a specific framework, a thorough introduction to the vivid Zend Framework and it's use in a MVC based web application development context. The content of the book is delivered in a fluent, very enthusiastic and 'knowledge-pillowed' writing tone. By implementing or working through the Storefront application seasoned web developers using older versions of the Framework will get a good blue sheet on new components like &lt;a href="http://framework.zend.com/manual/en/zend.application.html" target="_self"&gt;Zend_Application&lt;/a&gt; and it's implication in the bootstrapping process; while new developers tending towards picking up the Zend Framework will get a current and well compiled guide, which might first start off with a steep learning-curve but will turn into profund knowledge once hanging in there.&lt;br /&gt;&lt;br /&gt;The only thing that seemed a bit odd to me, was the utilization of &lt;a href="http://ant.apache.org" target="_self"&gt;Ant&lt;/a&gt; instead of &lt;a href="http://phing.info" target="_self"&gt;Phing&lt;/a&gt; as the build tool for the Storefront application to set the application environment, to remove all require_once statements from the framework library and to run the PHPUnit Test Suite; but this might also be inflicted by my Phing &lt;a href="http://raphaelstolt.blogspot.com/search/label/Phing" target="_self"&gt;nuttiness&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8420118650236071171-7619783294518881515?l=raphaelstolt.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/raphaelonphp/~4/UdIjktpIl9A" height="1" width="1"/&gt;</content><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=8420118650236071171&amp;postID=7619783294518881515" title="5 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/7619783294518881515?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/7619783294518881515?v=2" /><link rel="alternate" type="text/html" href="http://raphaelstolt.blogspot.com/2009/10/zend-framework-18-web-application.html" title="Zend Framework 1.8 Web Application Development book review" /><author><name>Raphael Stolt</name><uri>http://www.blogger.com/profile/07949831701855458792</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="14172620075949142098" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">5</thr:total></entry><entry gd:etag="W/&quot;Dk4AQ3gzfyp7ImA9WxNQGU8.&quot;"><id>tag:blogger.com,1999:blog-8420118650236071171.post-6528440024079434503</id><published>2009-09-19T19:00:00.012Z</published><updated>2009-09-26T00:29:02.687Z</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-09-26T00:29:02.687Z</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="Zend Framework" /><category scheme="http://www.blogger.com/atom/ns#" term="PHP" /><category scheme="http://www.blogger.com/atom/ns#" term="MongoDb" /><title>Logging to MongoDb and accessing log collections with Zend_Tool</title><content type="html">Influenced by a recent &lt;a href="http://taukon.de/2009/zend_log-mit-mongodb/" target="_self" title="Zend_Log mit mongoDB"&gt;blog post&lt;/a&gt; of a colleague of mine and by being kind of broke on a Saturday night; I tinkered with the just recently discovered &lt;a href="http://www.mongodb.org/display/DOCS/Home" target="_self"&gt;MongoDb&lt;/a&gt; and hooked it into the &lt;a href="http://framework.zend.com/manual/en/zend.log.html" target="_self"&gt;Zend_Log&lt;/a&gt; environment by creating a dedicated Zend_Log_Writer. The following post will therefore present a peek at a &lt;em&gt;prototypesque&lt;/em&gt; implementation of this writer and show how the afterwards accumulated log entries can be accessed and filtered with a &lt;a href="http://raphaelstolt.blogspot.com/2009/07/scaffolding-implementing-and-using.html" target="_self" title="Scaffolding, implementing and using project specific Zend_Tool_Project_Providers"&gt;custom Zend_Tool project provider&lt;/a&gt;.&lt;h4 class="custom"&gt;Logging to a MongoDb database&lt;/h4&gt;The following steps assume that an instance of a MongoDb server is running and that the required PHP MongoDb module is also &lt;a href="http://www.mongodb.org/display/DOCS/Installing+the+PHP+Driver" target="_self"&gt;installed&lt;/a&gt; and loaded. To by-pass log entries to a MongoDb database there is a need to craft a proper Zend_Log_Writer. This can be achieved by extending the Zend_Log_Writer_Abstract class, injecting a &lt;a href="http://php.net/manual/en/class.mongo.php" target="_self"&gt;Mongo connection&lt;/a&gt; instance and implementing the actual write functionality as shown in the next listing.&lt;pre class="codeSnippet"&gt;&amp;lt;?php&lt;br /&gt;require_once 'Zend/Log/Writer/Abstract.php';&lt;br /&gt;&lt;br /&gt;class Recordshelf_Log_Writer_MongoDb extends Zend_Log_Writer_Abstract&lt;br /&gt;{&lt;br /&gt;    private $_db;&lt;br /&gt;    private $_connection;&lt;br /&gt;    &lt;br /&gt;    /**&lt;br /&gt;     * @param Mongo $connection The MongoDb database connection&lt;br /&gt;     * @param string $db The MongoDb database name&lt;br /&gt;     * @param string $collection The collection name string the log entries &lt;br /&gt;     */&lt;br /&gt;    public function __construct(Mongo $connection, $db, $collection)&lt;br /&gt;    {&lt;br /&gt;        $this-&gt;_connection = $connection;&lt;br /&gt;        $this-&gt;_db = $this-&gt;_connection-&gt;selectDB($db)-&gt;createCollection(&lt;br /&gt;            $collection);&lt;br /&gt;    }&lt;br /&gt;    public function setFormatter($formatter)&lt;br /&gt;    {&lt;br /&gt;        require_once 'Zend/Log/Exception.php';&lt;br /&gt;        throw new Zend_Log_Exception(get_class() . ' does not support formatting');&lt;br /&gt;    }&lt;br /&gt;    public function shutdown()&lt;br /&gt;    {&lt;br /&gt;        $this-&gt;_db = null;&lt;br /&gt;        $this-&gt;_connection-&gt;close();&lt;br /&gt;    }&lt;br /&gt;    protected function _write($event)&lt;br /&gt;    {&lt;br /&gt;        $this-&gt;_db-&gt;insert($event);&lt;br /&gt;    }&lt;br /&gt;}&lt;/pre&gt;With the MongoDb writer available and added to the library directory of the application it's now possible to utilize this new storage backend as usual with the known Zend_Log component. The Mongo connection injected into the writer is configured via &lt;a href="http://framework.zend.com/manual/en/zend.config.html" target="_self"&gt;Zend_Config&lt;/a&gt; and initialized via the &lt;a href="http://framework.zend.com/manual/en/zend.application.html" target="_self"&gt;Zend_Application&lt;/a&gt; bootstrapping facility as shown in the listings below.&lt;br /&gt;&lt;br /&gt;&lt;div class="refactoringStatus" style="width: 205px;"&gt;application/configs/application.ini&lt;/div&gt;&lt;pre class="codeSnippetRefactoring"&gt;&lt;br /&gt;[production]&lt;br /&gt;app.name = recordshelf&lt;br /&gt;&lt;br /&gt;....&lt;br /&gt;&lt;br /&gt;log.mongodb.db = zf_mongo&lt;br /&gt;log.mongodb.collection = recordshelf.log&lt;br /&gt;log.mongodb.server = localhost&lt;br /&gt;log.priority = Zend_Log::CRIT&lt;br /&gt;&lt;br /&gt;....&lt;/pre&gt;&lt;div class="refactoringStatus" style="width: 154px;"&gt;application/Bootstrap.php&lt;/div&gt;&lt;pre class="codeSnippetRefactoring"&gt;&amp;lt;?php&lt;br /&gt;&lt;br /&gt;class Bootstrap extends Zend_Application_Bootstrap_Bootstrap&lt;br /&gt;{&lt;br /&gt;    protected $_logger;&lt;br /&gt;&lt;br /&gt;    protected function _initConfig()&lt;br /&gt;    {&lt;br /&gt;        Zend_Registry::set('config', new Zend_Config($this-&gt;getOptions()));&lt;br /&gt;    }&lt;br /&gt;&lt;br /&gt;    protected function _initLogger()&lt;br /&gt;    {&lt;br /&gt;        $this-&gt;bootstrap(array('frontController', 'config'));&lt;br /&gt;        $config = Zend_Registry::get('config');     &lt;br /&gt;        &lt;br /&gt;        $applicationName = $config-&gt;app-&gt;get('name', 'recordshelf');&lt;br /&gt;        $mongoDbServer = $config-&gt;log-&gt;mongodb-&gt;get('server', '127.0.0.1');&lt;br /&gt;        $mongoDbName = $config-&gt;log-&gt;mongodb-&gt;get('db', "{$applicationName}.logs");&lt;br /&gt;        $mongoDbCollection = $config-&gt;log-&gt;mongodb-&gt;get('collection', 'entries');&lt;br /&gt;        &lt;br /&gt;        $logger = new Zend_Log();&lt;br /&gt;        $writer = new Recordshelf_Log_Writer_MongoDb(new Mongo($mongoDbServer), &lt;br /&gt;            $mongoDbName, $mongoDbCollection);&lt;br /&gt;        &lt;br /&gt;        if ('production' === $this-&gt;getEnvironment()) {&lt;br /&gt;            $priority = constant($config-&gt;log-&gt;get('priority', Zend_Log::CRIT));&lt;br /&gt;            $filter = new Zend_Log_Filter_Priority($priority);&lt;br /&gt;            $logger-&gt;addFilter($filter);&lt;br /&gt;        }&lt;br /&gt;        $logger-&gt;addWriter($writer);&lt;br /&gt;        $this-&gt;_logger = $logger;&lt;br /&gt;        Zend_Registry::set('log', $logger);&lt;br /&gt;    }&lt;br /&gt;}&lt;/pre&gt;&lt;div class="refactoringStatus" style="width: 205px;"&gt;controllers/ExampleController.php&lt;/div&gt;&lt;pre class="codeSnippetRefactoring"&gt;&amp;lt;?php&lt;br /&gt;&lt;br /&gt;class ExampleController extends Zend_Controller_Action&lt;br /&gt;{&lt;br /&gt;    private $_logger = null;&lt;br /&gt;    &lt;br /&gt;    public function init()&lt;br /&gt;    {&lt;br /&gt;        $this-&gt;_logger = Zend_Registry::get('log');&lt;br /&gt;    }&lt;br /&gt;&lt;br /&gt;    public function fooAction()&lt;br /&gt;    {&lt;br /&gt;        $this-&gt;_logger-&gt;log('A debug log message from within action ' . &lt;br /&gt;            $this-&gt;getRequest()-&gt;getActionName(), Zend_Log::DEBUG);&lt;br /&gt;    }&lt;br /&gt;&lt;br /&gt;    public function barAction()&lt;br /&gt;    {&lt;br /&gt;        $this-&gt;_logger-&gt;log('A debug log message from within ' . &lt;br /&gt;            __METHOD__, Zend_Log::DEBUG);&lt;br /&gt;    }&lt;br /&gt;}&lt;/pre&gt;&lt;h4 class="custom"&gt;Accessing the log database with a Zend_Tool project provider&lt;/h4&gt;After handling the application-wide logging with the MongoDb writer sooner or later the issue to access the gathered log entries will rise. For this mundane and recurring use case the ProjectProvider provider of the Zend_Tool framework is an acceptable candidate to hook a custom action into the Zend_Tool environment of a given project. Therefor a new Zend_Tool_Project Project provider is first scaffolded via the forthcoming command.&lt;pre class="consoleOutput"&gt;sudo zf create project-provider mongodb-logs filter&lt;/pre&gt;Second the generated provider skeleton its filter action is enliven with the logic to query the MongoDb database and the stored log collection. The action to come accepts three arguments to filter the stored log entry results by a specific date in the format of 'YYYY-MM-DD' and a given Zend_Log priority (currently limited to the constants defined in Zend_Log) in a specific application environment. The next listing shows the implementation of the import  action of the MongodbLogsProvider project provider; which is clearly, as it's length indicates, in need for a clean-up task.&lt;br /&gt;&lt;br /&gt;&lt;div class="refactoringStatus" style="width: 215px;"&gt;providers/Mongodb-logsProvider.php&lt;/div&gt;&lt;pre class="codeSnippetRefactoring"&gt;&amp;lt;?php&lt;br /&gt;require_once 'Zend/Tool/Project/Provider/Abstract.php';&lt;br /&gt;require_once 'Zend/Tool/Project/Provider/Exception.php';&lt;br /&gt;require_once 'Zend/Date.php';&lt;br /&gt;require_once 'Zend/Validate/Date.php';&lt;br /&gt;require_once 'Zend/Log.php';&lt;br /&gt;require_once 'Zend/Config/Ini.php';&lt;br /&gt;&lt;br /&gt;class MongodbLogsProvider extends Zend_Tool_Project_Provider_Abstract&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;    public function filter($date = null, $logPriority = null, &lt;br /&gt;        $env = 'development')&lt;br /&gt;    {&lt;br /&gt;        $ref = new Zend_Reflection_Class('Zend_Log');&lt;br /&gt;        $logPriorities = $ref-&gt;getConstants();&lt;br /&gt;        &lt;br /&gt;        if (in_array(strtoupper($date), array_keys($logPriorities)) || &lt;br /&gt;            in_array(strtoupper($date), array_values($logPriorities))) {&lt;br /&gt;            $logPriority = $date;&lt;br /&gt;            $date = null;&lt;br /&gt;        }&lt;br /&gt;        if (!is_null($date)) {&lt;br /&gt;            $validator = new Zend_Validate_Date();&lt;br /&gt;            if (!$validator-&gt;isValid($date)) {&lt;br /&gt;                $exceptionMessage = "Given date '{$date}' is not a valid date.";&lt;br /&gt;                throw new Zend_Tool_Project_Provider_Exception($exceptionMessage);&lt;br /&gt;            }&lt;br /&gt;            $dateArray = array();&lt;br /&gt;            list($dateArray['year'], $dateArray['month'], $dateArray['day']) = &lt;br /&gt;                explode('-', $date);&lt;br /&gt;            $date = new Zend_Date($dateArray);&lt;br /&gt;        } else {&lt;br /&gt;            $date = new Zend_Date();&lt;br /&gt;        }&lt;br /&gt;        $date = $date-&gt;toString('Y-MM-dd');&lt;br /&gt;                &lt;br /&gt;        if (!is_null($logPriority)) {&lt;br /&gt;            &lt;br /&gt;            if (!is_numeric($logPriority)) {&lt;br /&gt;                $logPriority = strtoupper($logPriority);&lt;br /&gt;                if (!in_array($logPriority, array_keys($logPriorities))) {&lt;br /&gt;                    $exceptionMessage = "Given priority '{$logPriority}' is not defined.";&lt;br /&gt;                    throw new Zend_Tool_Project_Provider_Exception($exceptionMessage);&lt;br /&gt;                } else {&lt;br /&gt;                    $logPriority = $logPriorities[$logPriority];&lt;br /&gt;                }&lt;br /&gt;            }&lt;br /&gt;            if (!in_array($logPriority, array_values($logPriorities))) {&lt;br /&gt;                $exceptionMessage = "Given priority '{$logPriority}' is not defined.";&lt;br /&gt;                throw new Zend_Tool_Project_Provider_Exception();&lt;br /&gt;            }&lt;br /&gt;            $priorities = array_flip($logPriorities);&lt;br /&gt;            $priorityName = $priorities[$logPriority];&lt;br /&gt;        }&lt;br /&gt;        &lt;br /&gt;        if ($env !== 'development' &amp;&amp; $env !== 'production') {&lt;br /&gt;            $exceptionMessage = "Unsupported environment '{$env}' provided.";&lt;br /&gt;            throw new Zend_Tool_Project_Provider_Exception();&lt;br /&gt;        }&lt;br /&gt;        $config = new Zend_Config_Ini('./application/configs/application.ini', &lt;br /&gt;            $env);&lt;br /&gt;&lt;br /&gt;        $applicationName = $config-&gt;app-&gt;get('name', 'recordshelf');&lt;br /&gt;        $mongoDbServer = $config-&gt;log-&gt;mongodb-&gt;get('server', '127.0.0.1');&lt;br /&gt;        $mongoDbName = $config-&gt;log-&gt;mongodb-&gt;get('db', "{$applicationName}.logs");&lt;br /&gt;        $mongoDbCollection = $config-&gt;log-&gt;mongodb-&gt;get('collection', 'entries');&lt;br /&gt;    &lt;br /&gt;        try {&lt;br /&gt;            $connection = new Mongo($mongoDbServer);&lt;br /&gt;            $db = $connection-&gt;selectDB($mongoDbName)-&gt;createCollection(&lt;br /&gt;                $mongoDbCollection);&lt;br /&gt;        } catch (MongoConnectionException $e) {&lt;br /&gt;            throw new Zend_Tool_Project_Provider_Exception($e-&gt;getMessage());&lt;br /&gt;        }&lt;br /&gt;        $dateRegex = new MongoRegex("/$date.*/i");&lt;br /&gt;        &lt;br /&gt;        if (is_null($logPriority)) {&lt;br /&gt;            $query = array('timestamp' =&gt; $dateRegex);&lt;br /&gt;            $appendContentForResults = "Found #amountOfEntries# log entrie(s) "&lt;br /&gt;               . "on {$date}";&lt;br /&gt;            $appendContentForNoResults = "Found no log entries on {$date}";&lt;br /&gt;        } else {            &lt;br /&gt;            $query = array('priority' =&gt; (int) $logPriority, &lt;br /&gt;                'timestamp' =&gt; $dateRegex);&lt;br /&gt;            $appendContentForResults = "Found #amountOfEntries# log entrie(s) "&lt;br /&gt;                . "for priority {$priorityName} on {$date}";&lt;br /&gt;            $appendContentForNoResults = "Found no log entries for priority "&lt;br /&gt;                . "{$priorityName} on {$date}";&lt;br /&gt;        }&lt;br /&gt;        &lt;br /&gt;        $cursor = $db-&gt;find($query);&lt;br /&gt;        $amountOfEntries = $cursor-&gt;count();&lt;br /&gt;&lt;br /&gt;        if ($amountOfEntries &gt; 0) {&lt;br /&gt;            $content = str_replace('#amountOfEntries#', $amountOfEntries, &lt;br /&gt;                $appendContentForResults);&lt;br /&gt;            $this-&gt;_registry-&gt;getResponse()-&gt;appendContent($content);&lt;br /&gt;            foreach ($cursor as $id =&gt; $value) {&lt;br /&gt;                $content = "{$id}: {$value['timestamp']} &gt; ";&lt;br /&gt;                if (is_null($logPriority)) {&lt;br /&gt;                    $content.= "[{$value['priorityName']}] ";&lt;br /&gt;                }&lt;br /&gt;                $content.= "{$value['message']}";&lt;br /&gt;                $this-&gt;_registry-&gt;getResponse()-&gt;appendContent($content);&lt;br /&gt;            }&lt;br /&gt;        } else {&lt;br /&gt;            $content = $appendContentForNoResults;&lt;br /&gt;            $this-&gt;_registry-&gt;getResponse()-&gt;appendContent($content);&lt;br /&gt;        }&lt;br /&gt;        $connection-&gt;close();&lt;br /&gt;    }&lt;br /&gt;}&lt;/pre&gt;The coming outro screenshots show two use cases for the filter action of the MongodbLogsProvider issued against the zf command line client. The first screenshot shows the use case where all log entries for the current day are queried, while the second one shows the use case where all log entries for a specific date and log priority are queried and fed back to the user.&lt;br /&gt;&lt;br /&gt;&lt;a href="http://www.flickr.com/photos/raphaelstolt/3934270869/" title="All log entries of the current day by Raphael Stolt, on Flickr"&gt;&lt;img src="http://farm3.static.flickr.com/2548/3934270869_fa329bea00_o.gif" width="800" height="380" border="0" alt="All log entries of the current day" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;a href="http://www.flickr.com/photos/raphaelstolt/3935054512/" title="All CRIT log entries for a specific date by Raphael Stolt, on Flickr"&gt;&lt;img src="http://farm3.static.flickr.com/2658/3935054512_e929053892_o.gif" width="613" height="170" border="0" alt="All CRIT log entries for a specific date" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8420118650236071171-6528440024079434503?l=raphaelstolt.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/raphaelonphp/~4/nXhvw4IJFk4" height="1" width="1"/&gt;</content><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=8420118650236071171&amp;postID=6528440024079434503" title="2 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/6528440024079434503?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/6528440024079434503?v=2" /><link rel="alternate" type="text/html" href="http://raphaelstolt.blogspot.com/2009/09/logging-to-mongodb-and-accessing-log.html" title="Logging to MongoDb and accessing log collections with Zend_Tool" /><author><name>Raphael Stolt</name><uri>http://www.blogger.com/profile/07949831701855458792</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="14172620075949142098" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">2</thr:total></entry><entry gd:etag="W/&quot;D0EBRH87fip7ImA9WxNSEU0.&quot;"><id>tag:blogger.com,1999:blog-8420118650236071171.post-4987593960043747431</id><published>2009-08-22T13:08:00.004Z</published><updated>2009-08-24T09:20:55.106Z</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-08-24T09:20:55.106Z</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="Automation" /><category scheme="http://www.blogger.com/atom/ns#" term="TextMate" /><category scheme="http://www.blogger.com/atom/ns#" term="Phing" /><title>Kicking off custom Phing task development with TextMate</title><content type="html">As a reader of this blog you migth have noticed that from time to time I like to utilize &lt;a href="http://phing.info" target="_self"&gt;Phing&lt;/a&gt;'s ability to write custom tasks. Though that's not an everyday routine for me and therefor I might, depending on my form of the day, end up with some real smelly code where for example the task's properties validation is handled in the task's main worker method. This is actually a bad habit/practice I'm aware of and to improve my future endeavours in custom Phing task development, I bended &lt;a href="http://macromates.com/" target="_self"&gt;TextMate&lt;/a&gt;'s snippet feature to my needs.&lt;br /&gt;&lt;br /&gt;&lt;a href="http://manual.macromates.com/en/snippets#snippets" target="_self"&gt;Snippets&lt;/a&gt; in TextMate are a very powerful feature that can be used to insert code that you do not want to type again and again, or like in my case might have forgotten over a certain time.&lt;br /&gt;&lt;br /&gt;The next code listing shows the snippet providing a basic custom Phing task class skeleton which can be utilized over and over at the beginning of the implementation activities.&lt;pre class="codeSnippet"&gt;&amp;lt;?php&lt;br /&gt;require_once 'phing/Task.php';&lt;br /&gt;&lt;br /&gt;class ${1:CustomName}Task extends Task&lt;br /&gt;{&lt;br /&gt;    private \$_${2:property} = null;&lt;br /&gt;    &lt;br /&gt;    /**&lt;br /&gt;     * @param string \$${2:property} ${3:description}&lt;br /&gt;     */&lt;br /&gt;    public function set${2/./\u$0/}(\$${2:property})&lt;br /&gt;    {&lt;br /&gt;        \$this-&gt;_${2:property} = trim(\$${2:property});&lt;br /&gt;    }&lt;br /&gt;    /**&lt;br /&gt;     * Initializes the task environment if necessary&lt;br /&gt;     */&lt;br /&gt;    public function init()&lt;br /&gt;    {&lt;br /&gt;    }&lt;br /&gt;    /**&lt;br /&gt;     * Does the task main work or delegates it&lt;br /&gt;     * @throws BuildException&lt;br /&gt;     */&lt;br /&gt;    public function main()&lt;br /&gt;    {&lt;br /&gt;        \$this-&gt;_validateProperties();&lt;br /&gt;    }&lt;br /&gt;    /**&lt;br /&gt;     * Validates the task properties&lt;br /&gt;     * @throws BuildException&lt;br /&gt;     */&lt;br /&gt;    private function _validateProperties()&lt;br /&gt;    {&lt;br /&gt;        if (is_null(\$this-&gt;_${2:property})) {&lt;br /&gt;            throw new BuildException('${4:message}.');&lt;br /&gt;        }$0&lt;br /&gt;    }&lt;br /&gt;}&lt;/pre&gt;To apply the snippet, after installing it, on a PHP source file it can either be selected from the &lt;a href="http://manual.macromates.com/en/bundles#bundles" target="_self"&gt;Bundles&lt;/a&gt; menue or more comfortable via the assigned tab trigger i.e. &lt;em&gt;ctask&lt;/em&gt;. After triggering the snippet it's possible to properly name the task under development and dynamically set it's first property, which is also treated as a mandatory property in the extracted &lt;em&gt;_validateProperties&lt;/em&gt; method.&lt;br /&gt;&lt;br /&gt;The outro image shows the above stated snippet in the TextMate Bundle Editor and it's configuration.&lt;br /&gt;&lt;br /&gt;&lt;a href="http://www.flickr.com/photos/raphaelstolt/3844647803/" title="Phing snippet in the TextMate Bundle Editor by Raphael Stolt, on Flickr"&gt;&lt;img src="http://farm4.static.flickr.com/3550/3844647803_3175559fed_o.gif" width="616" height="431" border="0" alt="Phing snippet in the TextMate Bundle Editor" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8420118650236071171-4987593960043747431?l=raphaelstolt.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/raphaelonphp/~4/3x8YY-dRytA" height="1" width="1"/&gt;</content><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=8420118650236071171&amp;postID=4987593960043747431" title="1 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/4987593960043747431?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/4987593960043747431?v=2" /><link rel="alternate" type="text/html" href="http://raphaelstolt.blogspot.com/2009/08/kicking-off-custom-phing-task.html" title="Kicking off custom Phing task development with TextMate" /><author><name>Raphael Stolt</name><uri>http://www.blogger.com/profile/07949831701855458792</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="14172620075949142098" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">1</thr:total></entry><entry gd:etag="W/&quot;CkQAQ3c7cSp7ImA9WxJVGEs.&quot;"><id>tag:blogger.com,1999:blog-8420118650236071171.post-2974053421730973311</id><published>2009-07-04T15:41:00.011+01:00</published><updated>2009-07-06T07:05:42.909+01:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-07-06T07:05:42.909+01:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="Zend Framework" /><category scheme="http://www.blogger.com/atom/ns#" term="PHP" /><title>Scaffolding, implementing and using project specific Zend_Tool_Project_Providers</title><content type="html">Working on a project involving several legacy data migration tasks, I got curious what the &lt;a href="http://framework.zend.com/manual/en/zend.tool.project.html" target="_self"&gt;Zend_Tool_Project&lt;/a&gt; component of the &lt;a href="http://framework.zend.com" target="_self"&gt;Zend Framework&lt;/a&gt; offers to create project specific providers for the above mentioned tasks or ones of similar nature. Therefore the following post will try to show how these providers can be developed in an iterative manner by scaffolding them via the capabilities of the Zend_Tool_Project ProjectProvider provider, enlived with action/task logic, and be used in the project scope.&lt;h4 class="custom"&gt;Scaffolding project specific providers&lt;/h4&gt;All following steps assume there is a project available i.e. &lt;em&gt;recordshelf&lt;/em&gt; initially created with the Zend_Tool_Project Project provider and that the forthcoming commands are issued from the project root directory against the zf command line client. The scaffolding of a project specific provider can be triggered via the &lt;em&gt;create&lt;/em&gt; action of the &lt;em&gt;ProjectProvider&lt;/em&gt; provider by passing in the name of the provider i.e. &lt;em&gt;csv&lt;/em&gt; and it's intended actions. As the next console snippet shows it's &lt;br /&gt;possible to specify several actions as a comma separated list.&lt;pre class="consoleOutput"&gt;sudo zf create project-provider csv importSpecials,importSummersale&lt;/pre&gt;After running the command the project's profile &lt;em&gt;.zfproject.xml&lt;/em&gt; has been modified and a new &lt;em&gt;providers&lt;/em&gt; directory exists in the project root directory containing the scaffolded Csv provider. The next code snippet shows the initial Csv provider class skeleton and its two empty action methods named &lt;em&gt;importSpecials&lt;/em&gt; and &lt;em&gt;importSummersale&lt;/em&gt;. At the point of this writing, using the Zend Framework 1.8.4 and PHP 5.2.10 on a Mac OS X system the generated Csv provider code or the mapping in the &lt;em&gt;.zfproject.xml&lt;/em&gt; is incorrect, but can be fixed by renaming the class from CsvProvider to Csv.&lt;pre class="codeSnippet"&gt;&amp;lt;?php&lt;br /&gt;&lt;br /&gt;require_once 'Zend/Tool/Project/Provider/Abstract.php';&lt;br /&gt;require_once 'Zend/Tool/Project/Provider/Exception.php';&lt;br /&gt;&lt;br /&gt;class Csv&lt;strike style="font-weight: bolder;"&gt;Provider&lt;/strike&gt; extends Zend_Tool_Project_Provider_Abstract&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;    public function importSpecials()&lt;br /&gt;    {&lt;br /&gt;        /** @todo Implementation */&lt;br /&gt;    }&lt;br /&gt;&lt;br /&gt;    public function importSummersale()&lt;br /&gt;    {&lt;br /&gt;        /** @todo Implementation */&lt;br /&gt;    }&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;}&lt;/pre&gt;&lt;h4 class="custom"&gt;Implementing the action logic&lt;/h4&gt;Having the project provider class skeleton ready to get going, it's time to enliven the actions with their intended features by using either other components of the Zend Framework, any suitable third party library or plain-vanilla PHP. For the sake of brevity I decided to implement only the &lt;em&gt;importSpecials&lt;/em&gt; action which transforms the data of a known CSV file structure into a relevant database table. The CSV parsing steps shown next might not be that sophisticated, as their sole purpose is to illustrate an exemplary implementation of a project specific provider action.&lt;pre class="codeSnippet"&gt;&amp;lt;?php&lt;br /&gt;&lt;br /&gt;require_once 'Zend/Tool/Project/Provider/Abstract.php';&lt;br /&gt;require_once 'Zend/Tool/Project/Provider/Exception.php';&lt;br /&gt;&lt;br /&gt;class Csv extends Zend_Tool_Project_Provider_Abstract&lt;br /&gt;{&lt;br /&gt;    private function _isProjectProviderSupportedInProject(Zend_Tool_Project_Profile $profile, &lt;br /&gt;        $projectProviderName)&lt;br /&gt;    {&lt;br /&gt;        $projectProviderResource = $this-&gt;_getProjectProfileResource($profile, &lt;br /&gt;            $projectProviderName);&lt;br /&gt;        return $projectProviderResource instanceof Zend_Tool_Project_Profile_Resource;&lt;br /&gt;    }&lt;br /&gt;    &lt;br /&gt;    private function _isActionSupportedByProjectProvider(Zend_Tool_Project_Profile $profile, &lt;br /&gt;        $projectProviderName, $actionName)&lt;br /&gt;    {&lt;br /&gt;        $projectProviderResource = $this-&gt;_getProjectProfileResource($profile, &lt;br /&gt;            $projectProviderName);&lt;br /&gt;        $projectProviderAttributes = $projectProviderResource-&gt;getContext()&lt;br /&gt;                                                             -&gt;getPersistentAttributes();&lt;br /&gt;        return in_array($actionName, explode(',', $projectProviderAttributes['actionNames']));&lt;br /&gt;    }&lt;br /&gt;    &lt;br /&gt;    private function _getProjectProfileResource(Zend_Tool_Project_Profile $profile, &lt;br /&gt;        $projectProviderName)&lt;br /&gt;    {&lt;br /&gt;        $profileSearchParams[] = 'ProjectProvidersDirectory';&lt;br /&gt;        $profileSearchParams['ProjectProviderFile'] = &lt;br /&gt;            array('projectProviderName' =&gt; strtolower($projectProviderName));&lt;br /&gt;        return $profile-&gt;search($profileSearchParams);  &lt;br /&gt;    }&lt;br /&gt;&lt;br /&gt;    &lt;em style="font-weight: bolder;"&gt;public function importSpecials($csvFile, $env = 'development')&lt;br /&gt;    {&lt;br /&gt;        $relatedTablename = 'specials';&lt;br /&gt;        &lt;br /&gt;        if (!$this-&gt;_isProjectProviderSupportedInProject($profile, __CLASS__)) {&lt;br /&gt;            throw new Exception("ProjectProvider Csv is not supported in this project.");&lt;br /&gt;        }&lt;br /&gt;        if (!$this-&gt;_isActionSupportedByProjectProvider($profile, __CLASS__, __FUNCTION__)) {&lt;br /&gt;            $exceptionMessage = "Action 'importSpecials' is not supported by "&lt;br /&gt;                . "the Csv ProjectProvider in this project.";&lt;br /&gt;            throw new Exception($exceptionMessage);&lt;br /&gt;        }&lt;br /&gt;&lt;br /&gt;        if (!file_exists($csvFile)) {&lt;br /&gt;            throw new Exception("Given csv-file '{$csvFile}' doesn't exist.");&lt;br /&gt;        }&lt;br /&gt;        &lt;br /&gt;        $importEnvironment = trim($env);&lt;br /&gt;        if ($importEnvironment !== 'development' &amp;&amp; $importEnvironment !== 'production') {&lt;br /&gt;            throw new Exception("Unsupported environment '{$importEnvironment}' provided.");&lt;br /&gt;        }&lt;br /&gt;        &lt;br /&gt;        $csvHandle = fopen($csvFile, "r");&lt;br /&gt;        &lt;br /&gt;        if (!$csvHandle) {&lt;br /&gt;            throw new Exception("Unable to open given csv-file '{$csvFile}'.");&lt;br /&gt;        }&lt;br /&gt;        &lt;br /&gt;        $config = new Zend_Config_Ini('./application/configs/application.ini', &lt;br /&gt;            $importEnvironment);&lt;br /&gt;        $db = Zend_Db::factory($config-&gt;database);&lt;br /&gt;&lt;br /&gt;        $db-&gt;query("TRUNCATE TABLE {$relatedTablename}");&lt;br /&gt;        echo "Truncated the project '{$relatedTablename}' database table." . PHP_EOL;&lt;br /&gt;        &lt;br /&gt;        $rowCount = $insertCount = 0;&lt;br /&gt;        &lt;br /&gt;        while (($csvLine = fgetcsv($csvHandle)) !== false) {&lt;br /&gt;            if ($rowCount &gt; 0) {&lt;br /&gt;                $insertRow = array(&lt;br /&gt;                    'product_name' =&gt; $csvLine[0],&lt;br /&gt;                    'product_image_path' =&gt; $csvLine[1],&lt;br /&gt;                    'price' =&gt; $csvLine[2],&lt;br /&gt;                    'special_until' =&gt; $csvLine[3]&lt;br /&gt;                );&lt;br /&gt;                $db-&gt;insert($relatedTablename, $insertRow);&lt;br /&gt;                ++$insertCount;&lt;br /&gt;            }&lt;br /&gt;            ++$rowCount;&lt;br /&gt;        }&lt;br /&gt;        fclose($csvHandle);&lt;br /&gt;        $importMessage = "Imported {$insertCount} rows into the project "&lt;br /&gt;            . "'{$relatedTablename}' database table.";&lt;br /&gt;        echo $importMessage;&lt;br /&gt;    }&lt;/em&gt;&lt;br /&gt;    ...&lt;br /&gt;}&lt;/pre&gt;&lt;h4 class="custom"&gt;Making providers and actions pretendable&lt;/h4&gt;To make project specific providers its actions pretendable and thereby providing some kind of user documentation the provider classes have to implement a marker interface called &lt;em&gt;Zend_Tool_Framework_Provider_Pretendable&lt;/em&gt;. For making a action of a provider pretendable and giving some feedback to the user, the request is checked if the action has been issued in the pretend mode; which is possible by adding &lt;em&gt;-p&lt;/em&gt; option to the issued zf command line client command. The next code snippet shows how the above stated &lt;em&gt;Csv&lt;/em&gt; provider and its &lt;em&gt;importSpecials&lt;/em&gt; action is made pretendable.&lt;pre class="codeSnippet"&gt;&amp;lt;?php&lt;br /&gt;&lt;br /&gt;require_once 'Zend/Tool/Project/Provider/Abstract.php';&lt;br /&gt;require_once 'Zend/Tool/Project/Provider/Exception.php';&lt;br /&gt;&lt;br /&gt;class Csv extends Zend_Tool_Project_Provider_Abstract&lt;em style="font-weight: bolder;"&gt; implements &lt;br /&gt;    Zend_Tool_Framework_Provider_Pretendable&lt;/em&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;    public function importSpecials($csvFile, $env = 'development')&lt;br /&gt;    {&lt;br /&gt;        ...&lt;br /&gt;        &lt;br /&gt;        &lt;em style="font-weight: bolder;"&gt;if ($this-&gt;_registry-&gt;getRequest()-&gt;isPretend()) {&lt;br /&gt;            $pretendMessage = "I would import the specials data provided in {$csvFile} "&lt;br /&gt;                . "into the project '{$relatedTablename}' database table.";&lt;br /&gt;            echo $pretendMessage;&lt;br /&gt;        } else {&lt;br /&gt;            ...   &lt;br /&gt;        }&lt;/em&gt;&lt;br /&gt;    }&lt;br /&gt;    ...&lt;br /&gt;}&lt;/pre&gt;&lt;h4 class="custom"&gt;Using project specific providers&lt;/h4&gt;To use the bundled up capabilities of project specific providers, these have to made accessable to the zf command line client by putting them in the &lt;em&gt;include_path&lt;/em&gt;. Currently I discovered no best practice for doing so only for single project scopes and simply added the path to the project to my php.ini and thereby global include_path; another approach might be to add the project name as a prefix to the Provider. After doing so it's possible to get an overview of all with the Zend_Tool_Project shipped providers plus the project specific providers and their offered actions by issuing the &lt;em&gt;zf --help&lt;/em&gt; command as shown in the next screenshot. To ensure that project specific providers and its actions are only runnable in projects which support them, it is necessary to check if these and the offered action exists as resources in the project its profile .zfproject.xml file as shown in the implementation of the &lt;em&gt;importSpecials&lt;/em&gt; action in one of above code snippets.&lt;br /&gt;&lt;br /&gt;&lt;a href="http://www.flickr.com/photos/raphaelstolt/3687344640/" title="Provider overview by Raphael Stolt, on Flickr"&gt;&lt;img src="http://farm3.static.flickr.com/2630/3687344640_510d00350a_o.gif" width="606" height="282" border="0" alt="Provider overview" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;As shown in the previous screenshot the first character of the project specific providers are omitted, this is another minor bug which might be fixed in one of the forthcoming Zend Framework releases. The current workaround for this issue is simply to type the command exactly as shown in the help. The outro screenshot shows how the import-specials action of the project specific Csv provider is issued against the zf command line client and its provided user feedback after an successfull import against the projects development database.&lt;br /&gt;&lt;br /&gt;&lt;a href="http://www.flickr.com/photos/raphaelstolt/3686545463/" title="Calling the import-specials action by Raphael Stolt, on Flickr"&gt;&lt;img src="http://farm3.static.flickr.com/2584/3686545463_c93b1b18de_o.gif" width="606" height="282" border="0" alt="Calling the import-specials action" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8420118650236071171-2974053421730973311?l=raphaelstolt.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/raphaelonphp/~4/3SupioXwB6Q" height="1" width="1"/&gt;</content><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=8420118650236071171&amp;postID=2974053421730973311" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/2974053421730973311?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/2974053421730973311?v=2" /><link rel="alternate" type="text/html" href="http://raphaelstolt.blogspot.com/2009/07/scaffolding-implementing-and-using.html" title="Scaffolding, implementing and using project specific Zend_Tool_Project_Providers" /><author><name>Raphael Stolt</name><uri>http://www.blogger.com/profile/07949831701855458792</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="14172620075949142098" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total></entry><entry gd:etag="W/&quot;D0IESXc9fCp7ImA9WxJSGUs.&quot;"><id>tag:blogger.com,1999:blog-8420118650236071171.post-5110082920105713427</id><published>2009-05-10T14:15:00.003Z</published><updated>2009-05-10T14:25:08.964Z</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-05-10T14:25:08.964Z</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="PHP" /><category scheme="http://www.blogger.com/atom/ns#" term="PHPUnit" /><category scheme="http://www.blogger.com/atom/ns#" term="Phing" /><title>Testing Phing buildfiles with PHPUnit</title><content type="html">While &lt;a href="http://raphaelstolt.blogspot.com/2008/07/six-valuable-phing-build-file.html" target="_self"&gt;transforming&lt;/a&gt; some of the &lt;a href="http://ant.apache.org/" target="_self"&gt;Ant&lt;/a&gt; buildfile refactorings described in &lt;a href="http://www.build-doctor.com/" target="_self"&gt;Julian Simpson&lt;/a&gt;'s seminal essay into a &lt;a href="http://phing.info/trac/" target="_self"&gt;Phing&lt;/a&gt; context, it felt plainly wrong that I didn't have any tests for the buildfile to back me up on obtaining the pristine behaviour throughout the process. While Ant users can rely on an Apache project called &lt;a href="http://ant.apache.org/antlibs/antunit/"&gt;AntUnit&lt;/a&gt; there are currently no tailor-made tools available for testing or verifying Phing buildfiles. Therefor I took a weekend off, locked myself in the stuffy lab, and explored the abilities to test Phing buildfiles respectively their included properties, targets and tasks with the &lt;a href="http://phpunit.de/" target="_self"&gt;PHPUnit&lt;/a&gt; testing framework. In case you'd like to take a peek at the emerged &lt;strong&gt;lab jottings&lt;/strong&gt;, keep on scanning.&lt;h4 class="custom"&gt;Introducing the buildfile under test&lt;/h4&gt;The buildfile that will be used as an example is kept simple, and contains several targets ranging from common ones like initializing the build environment by creating the necessary directories to more specific ones like pulling an external artifact from &lt;a href="http://github.com/" target="_self"&gt;GitHub&lt;/a&gt;. To get an overview of the buildfile under test have a look at the following listing.&lt;pre class="xmlSnippet"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;br /&gt;&amp;lt;project name="test-example" default="build" basedir="."&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;property name="project.basedir"&lt;br /&gt;          value="." override="true" /&amp;gt;&lt;br /&gt;&amp;lt;property name="github.repos.dir"&lt;br /&gt;          value="${project.basedir}/build/github-repos" override="true" /&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;target name="clean" depends="clean-github-repos"&lt;br /&gt;        description="Removes runtime build artifacts"&amp;gt;&lt;br /&gt;  &amp;lt;delete dir="${project.basedir}/build" includeemptydirs="true"&lt;br /&gt;          verbose="false" failonerror="true" /&amp;gt;&lt;br /&gt;  &amp;lt;delete dir="${project.basedir}/build/reports"&lt;br /&gt;          includeemptydirs="true" verbose="false" failonerror="true" /&amp;gt;&lt;br /&gt;&amp;lt;/target&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;target name="clean-github-repos" description="Removes runtime build artifacts"&amp;gt;&lt;br /&gt;  &amp;lt;delete dir="${github.repos.dir}" includeemptydirs="true" failonerror="true" /&amp;gt;&lt;br /&gt;&amp;lt;/target&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;target name="-log-build" description="A private target which should only be invoked internally"&amp;gt;&lt;br /&gt;  &amp;lt;!-- omitted --&amp;gt;&lt;br /&gt;&amp;lt;/target&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;target name="build" depends="clean" description="Builds the distributable product"&amp;gt;&lt;br /&gt;  &amp;lt;!-- omitted --&amp;gt;&lt;br /&gt;&amp;lt;/target&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;target name="database-setup" description="Sets up the database structure"&amp;gt;&lt;br /&gt;  &amp;lt;!-- omitted --&amp;gt;&lt;br /&gt;&amp;lt;/target&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;target name="init" description="Initalizes the build by creating directories etc"&amp;gt;&lt;br /&gt;  &amp;lt;mkdir dir="${project.basedir}/build/logs/performance/" /&amp;gt;&lt;br /&gt;  &amp;lt;mkdir dir="${project.basedir}/build/doc" /&amp;gt;&lt;br /&gt;  &amp;lt;mkdir dir="${project.basedir}/build/reports/phploc" /&amp;gt;&lt;br /&gt;&amp;lt;/target&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;target name="init-ad-hoc-tasks"&lt;br /&gt;        description="Initalizes the ad hoc tasks for reusability in multiple targets"&amp;gt;      &lt;br /&gt;  &amp;lt;adhoc-task name="github-clone"&amp;gt;&amp;lt;![CDATA[&lt;br /&gt;      class Github_Clone extends Task {&lt;br /&gt;        private $repository = null;&lt;br /&gt;        private $destDirectory = null;&lt;br /&gt;&lt;br /&gt;        function setRepos($repository) {&lt;br /&gt;            $this-&amp;gt;repository = $repository;&lt;br /&gt;        }&lt;br /&gt;        function setDest($destDirectory) {&lt;br /&gt;            $this-&amp;gt;destDirectory = $destDirectory;&lt;br /&gt;        }&lt;br /&gt;        function main() {&lt;br /&gt;            // Get project name from repos Uri&lt;br /&gt;            $projectName = str_replace('.git', '',&lt;br /&gt;                substr(strrchr($this-&amp;gt;repository, '/'), 1));&lt;br /&gt;&lt;br /&gt;            $gitCommand = 'git clone ' . $this-&amp;gt;repository . ' ' .&lt;br /&gt;                $this-&amp;gt;destDirectory . '/' . $projectName;&lt;br /&gt;&lt;br /&gt;            exec(escapeshellcmd($gitCommand), $output, $return);&lt;br /&gt;&lt;br /&gt;            if ($return !== 0) {&lt;br /&gt;                throw new BuildException('Git clone failed');&lt;br /&gt;            }&lt;br /&gt;            $logMessage = 'Cloned Git repository ' . $this-&amp;gt;repository .&lt;br /&gt;                ' into ' . $this-&amp;gt;destDirectory . '/' . $projectName;&lt;br /&gt;            $this-&amp;gt;log($logMessage);&lt;br /&gt;        }&lt;br /&gt;      }&lt;br /&gt;  ]]&amp;gt;&amp;lt;/adhoc-task&amp;gt;&lt;br /&gt;&lt;br /&gt;  &amp;lt;echo message="Intialized github-clone ad hoc task." /&amp;gt;&lt;br /&gt;&amp;lt;/target&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;target name="github" depends="init-ad-hoc-tasks, clean-github-repos"&lt;br /&gt;        description="Clones given repositories from GitHub"&amp;gt;&lt;br /&gt;  &amp;lt;github-clone repos="git://github.com/raphaelstolt/phploc-phing.git"&lt;br /&gt;                dest="${github.repos.dir}" /&amp;gt;&lt;br /&gt;&amp;lt;/target&amp;gt;&lt;br /&gt;&amp;lt;/project&amp;gt;&lt;/pre&gt;&lt;h4 class="custom"&gt;Testing the buildfile&lt;/h4&gt;All tests for the buildfile under test will be bundled, like 'normal' tests, in a class i.e. BuildfileTest extending the PHPUnit_Framework_TestCase class. When testing buildfiles it's possible to build some tests around the actual buildfile XML &lt;strong&gt;structure&lt;/strong&gt;, by utilizing the &lt;em&gt;xpath&lt;/em&gt; method of PHP's SimpleXMLElement class and asserting against the &lt;a href="http://www.w3.org/TR/xpath" target="_self"&gt;XPath&lt;/a&gt; query results, or around the dispatching of specific targets and asserting against the expected build &lt;strong&gt;artifacts&lt;/strong&gt;. Furthermore these two identified groups, structure and artifact, can be used to &lt;a href="http://mikenaberezny.com/2007/09/04/better-phpunit-group-annotations/" target="_self"&gt;organize&lt;/a&gt; the accumulating tests via PHPUnit's @group annotation.&lt;br /&gt;&lt;br /&gt;To be able to dispatch specific build targets and feed them with properties if necessary I additionally developed a &lt;em&gt;very&lt;/em&gt; basic build runner shown in the next code listing.&lt;pre class="codeSnippet"&gt;&amp;lt;?php&lt;br /&gt;class Phing_Buildfile_Runner {&lt;br /&gt;&lt;br /&gt;    private $_buildfilePath = null;&lt;br /&gt;&lt;br /&gt;    public function __construct($buildfilePath) {&lt;br /&gt;        if (!file_exists($buildfilePath)) {&lt;br /&gt;            throw new Exception("Buildfile '{$buildfilePath}' doesn't exist");&lt;br /&gt;        }&lt;br /&gt;        $this-&gt;buildfilePath = realpath($buildfilePath);&lt;br /&gt;    }&lt;br /&gt;    public function runTarget($targets = array(), $properties = array()) {&lt;br /&gt;        $runTargetCommand = "phing "&lt;br /&gt;                          . "-f {$this-&gt;buildfilePath} ";&lt;br /&gt;        if (count($targets) &gt; 0) {&lt;br /&gt;            foreach ($targets as $target) {&lt;br /&gt;                $runTargetCommand.= $target . " ";&lt;br /&gt;            }&lt;br /&gt;        }&lt;br /&gt;        if (count($properties) &gt; 0) {&lt;br /&gt;            foreach ($properties as $property =&gt; $value) {&lt;br /&gt;                $runTargetCommand.= "-D{$property}={$value} ";&lt;br /&gt;            }       &lt;br /&gt;        }&lt;br /&gt;        exec(escapeshellcmd($runTargetCommand), $output, $return);&lt;br /&gt;        return array('output' =&gt; $output, 'return' =&gt; $return);&lt;br /&gt;    }&lt;br /&gt;}&lt;/pre&gt;Out of the box PHPUnit's &lt;a href="http://www.phpunit.de/manual/current/en/api.html#api.assert" target="_self"&gt;assertion pool&lt;/a&gt; provides all the utilities to test buildfiles; although it would be cleaner to &lt;a href="http://raphaelstolt.blogspot.com/2008/07/creating-custom-phpunit-assertions.html" target="_self"&gt;create domain specfic assertions&lt;/a&gt; for this testing domain this technique will be ignored for the sake of brevity.&lt;br /&gt;&lt;br /&gt;After an initial 1000ft view on how to test buildfiles let's jump into the actual testing of a &lt;strong&gt;structural&lt;/strong&gt; aspect of the buildfile under test. The test to come shows how to verify that a clean target is defined for playing along in the build orchestra by querying a XPath expression against the buildfile XML and asserting that a result is available.&lt;pre class="codeSnippet"&gt;/**&lt;br /&gt;* @test&lt;br /&gt;* @group &lt;strong&gt;structure&lt;/strong&gt;&lt;br /&gt;*/&lt;br /&gt;public function buildfileShouldContainACleanTarget() {&lt;br /&gt;    $xml = new SimpleXMLElement($this-&gt;_buildfileXml);&lt;br /&gt;    $cleanElement = $xml-&gt;xpath("//target[@name='clean']");&lt;br /&gt;    $this-&gt;assertTrue(count($cleanElement) &gt; 0,&lt;br /&gt;        "Buildfile doesn't contain a clean target");&lt;br /&gt;}&lt;/pre&gt;The next &lt;strong&gt;artifactual&lt;/strong&gt; test raises the bar an inch, by verifying that the defined init target of the build does initialize the build environment correctly, or to pick up the orchestra metaphor again that the specific instrument plays along and holds the directed tone. Therefor the build runner executes the target and afterwards asserts a list of expected artifacts against the current state of the build process.&lt;pre class="codeSnippet"&gt;/**&lt;br /&gt;* @test&lt;br /&gt;* @group &lt;strong&gt;artifact&lt;/strong&gt;&lt;br /&gt;*/&lt;br /&gt;public function initTargetShouldCreateInitialBuildArtifacts() {&lt;br /&gt;    $this-&gt;_isTearDownNecessary = true;&lt;br /&gt;    $this-&gt;_buildfileRunner-&gt;runTarget(array('init'));&lt;br /&gt;    $expectedInitArtifacts = array(&lt;br /&gt;        "{$this-&gt;_buildfileBasedir}/build",&lt;br /&gt;        "{$this-&gt;_buildfileBasedir}/build/logs/performance/",&lt;br /&gt;        "{$this-&gt;_buildfileBasedir}/build/doc",&lt;br /&gt;        "{$this-&gt;_buildfileBasedir}/build/reports"&lt;br /&gt;    );&lt;br /&gt;&lt;br /&gt;    foreach ($expectedInitArtifacts as $artifact) {&lt;br /&gt;        $this-&gt;assertFileExists($artifact,&lt;br /&gt;            "Expected file '{$artifact}' doesn't exist");&lt;br /&gt;    }&lt;br /&gt;}&lt;/pre&gt;The next code listing shows the whole picture of the BuildfileTest class containing additional test methods verifying different aspects of the buildfile under test and also the innards of the setup and teardown method.&lt;br /&gt;&lt;br /&gt;The main assignment of the setup method is to load the XML of the buildfile under test and to intialize the build runner so an instance is available for an use in artifactual tests. The teardown method its sole responsibility is to reset the build state by running the clean target of the buildfile.&lt;pre class="codeSnippet"&gt;&amp;lt;?php&lt;br /&gt;require_once 'PHPUnit/Framework.php';&lt;br /&gt;require_once 'Phing/Buildfile/Runner.php';&lt;br /&gt;&lt;br /&gt;class ExampleBuildfileTest extends PHPUnit_Framework_TestCase {&lt;br /&gt;    &lt;br /&gt;    protected $_buildfileXml = null;&lt;br /&gt;    protected $_buildfileName = null;&lt;br /&gt;    protected $_buildfileBasedir = null;&lt;br /&gt;    protected $_buildfileRunner = null;&lt;br /&gt;    protected $_isTearDownNecessary = false;&lt;br /&gt;&lt;br /&gt;    protected function setUp() {&lt;br /&gt;        $this-&gt;_buildfileName = realpath('../../build.xml');        &lt;br /&gt;        $this-&gt;_buildfileBasedir = dirname($this-&gt;_buildfileName);&lt;br /&gt;        $this-&gt;_buildfileXml = file_get_contents($this-&gt;_buildfileName);&lt;br /&gt;        $this-&gt;_buildfileRunner = new Phing_Buildfile_Runner(&lt;br /&gt;            $this-&gt;_buildfileName);&lt;br /&gt;    }&lt;br /&gt;    &lt;br /&gt;   protected function tearDown() {&lt;br /&gt;        if ($this-&gt;_isTearDownNecessary) {&lt;br /&gt;            $this-&gt;_buildfileRunner-&gt;runTarget(array('clean'));&lt;br /&gt;        }&lt;br /&gt;    }&lt;br /&gt;    &lt;br /&gt;    /**&lt;br /&gt;     * @test&lt;br /&gt;     * @group structure&lt;br /&gt;     */&lt;br /&gt;    public function targetBuildShouldBeTheDefaultTarget() {&lt;br /&gt;        $xml = new SimpleXMLElement($this-&gt;_buildfileXml);&lt;br /&gt;        $xpath = "//@default";&lt;br /&gt;        $defaultElement = $xml-&gt;xpath($xpath);&lt;br /&gt;        $this-&gt;assertSame('build', trim($defaultElement[0]-&gt;default), &lt;br /&gt;            "Buildfile doesn't have a default target named 'build'");&lt;br /&gt;    }&lt;br /&gt;    /**&lt;br /&gt;     * @test&lt;br /&gt;     * @group structure&lt;br /&gt;     */&lt;br /&gt;    public function propertyGithubReposDirShouldBeSet() {&lt;br /&gt;        $xml = new SimpleXMLElement($this-&gt;_buildfileXml);&lt;br /&gt;        $xpath = "//property[@name='github.repos.dir']/@value";&lt;br /&gt;        $valueElement = $xml-&gt;xpath($xpath);&lt;br /&gt;        $this-&gt;assertTrue($valueElement[0] instanceof SimpleXMLElement, &lt;br /&gt;            "Buildfile doesn't contain a 'github.repos.dir' property");&lt;br /&gt;        $this-&gt;assertGreaterThan(1, strlen($valueElement[0]-&gt;value));&lt;br /&gt;    }&lt;br /&gt;    /**&lt;br /&gt;     * @test&lt;br /&gt;     * @group structure&lt;br /&gt;     */&lt;br /&gt;    public function buildfileShouldContainACleanTarget() {&lt;br /&gt;        $xml = new SimpleXMLElement($this-&gt;_buildfileXml);&lt;br /&gt;        $cleanElement = $xml-&gt;xpath("//target[@name='clean']");&lt;br /&gt;        $this-&gt;assertTrue(count($cleanElement) &gt; 0, &lt;br /&gt;            "Buildfile doesn't contain a clean target");&lt;br /&gt;    }&lt;br /&gt;    /**&lt;br /&gt;     * @test&lt;br /&gt;     * @group structure&lt;br /&gt;     */&lt;br /&gt;    public function targetLogBuildShouldBeAPrivateOne() {&lt;br /&gt;        $xml = new SimpleXMLElement($this-&gt;_buildfileXml);&lt;br /&gt;        $nameElement = $xml-&gt;xpath("//target[@name='-log-build']");&lt;br /&gt;        $this-&gt;assertTrue(count($nameElement) &gt; 0, &lt;br /&gt;            'Log build target is not a private target');&lt;br /&gt;    }&lt;br /&gt;    /**&lt;br /&gt;     * @test&lt;br /&gt;     * @group structure&lt;br /&gt;     */&lt;br /&gt;    public function targetBuildShouldDependOnCleanTarget() {&lt;br /&gt;        $xml = new SimpleXMLElement($this-&gt;_buildfileXml);&lt;br /&gt;        $xpath = "//target[@name='build']/@depends";&lt;br /&gt;        $dependElement = $xml-&gt;xpath($xpath);&lt;br /&gt;        $this-&gt;assertTrue(count($dependElement) &gt; 0, &lt;br /&gt;            'Target build contains no depends attribute');&lt;br /&gt;        $dependantTasks = array_filter(explode(' ', &lt;br /&gt;            trim($dependElement[0]-&gt;depends)&lt;br /&gt;        ));        &lt;br /&gt;        $this-&gt;assertContains('clean', $dependantTasks, &lt;br /&gt;            "Target build doesn't depend on the clean target");&lt;br /&gt;    }&lt;br /&gt;    /**&lt;br /&gt;     * @test&lt;br /&gt;     * @group structure&lt;br /&gt;     */&lt;br /&gt;    public function allDefinedTargetsShouldHaveADescriptionAttribute() {&lt;br /&gt;        $xml = new SimpleXMLElement($this-&gt;_buildfileXml);&lt;br /&gt;        $xpath = "//target";&lt;br /&gt;        $targetElements = $xml-&gt;xpath($xpath);&lt;br /&gt;        $describedTargetElements = array();&lt;br /&gt;        foreach ($targetElements as $index =&gt; $targetElement) {&lt;br /&gt;            $targetDescription = trim($targetElement-&gt;attributes()-&gt;description); &lt;br /&gt;            if ($targetDescription !== '') {&lt;br /&gt;                $describedTargetElements[] = $targetDescription;&lt;br /&gt;            }&lt;br /&gt;        }&lt;br /&gt;        $this-&gt;assertEquals(count($targetElements), &lt;br /&gt;            count($describedTargetElements), &lt;br /&gt;                'Description not for all targets set');&lt;br /&gt;    }&lt;br /&gt;    /**&lt;br /&gt;     * @test&lt;br /&gt;     * @group structure&lt;br /&gt;     */&lt;br /&gt;    public function githubCloneAdhocTaskShouldBeDefined() {&lt;br /&gt;        $xml = new SimpleXMLElement($this-&gt;_buildfileXml);&lt;br /&gt;        $xpath = "//target[@name='init-ad-hoc-tasks']/adhoc-task";&lt;br /&gt;        $adhocElement = $xml-&gt;xpath($xpath);&lt;br /&gt;        $this-&gt;assertSame('github-clone', &lt;br /&gt;            trim($adhocElement[0]-&gt;attributes()-&gt;name), &lt;br /&gt;                "Ad hoc task 'github-clone' isn't defined");&lt;br /&gt;    }&lt;br /&gt;    /**&lt;br /&gt;     * @test &lt;br /&gt;     * @group artifact&lt;br /&gt;     */&lt;br /&gt;    public function initTargetShouldCreateInitialBuildArtifacts() {&lt;br /&gt;        $this-&gt;_isTearDownNecessary = true;&lt;br /&gt;        $this-&gt;_buildfileRunner-&gt;runTarget(array('init'));&lt;br /&gt;&lt;br /&gt;        $expectedInitArtifacts = array(&lt;br /&gt;            "{$this-&gt;_buildfileBasedir}/build", &lt;br /&gt;            "{$this-&gt;_buildfileBasedir}/build/logs/performance/", &lt;br /&gt;            "{$this-&gt;_buildfileBasedir}/build/doc",&lt;br /&gt;            "{$this-&gt;_buildfileBasedir}/build/reports"&lt;br /&gt;        );&lt;br /&gt;&lt;br /&gt;        foreach ($expectedInitArtifacts as $artifact) {&lt;br /&gt;            $this-&gt;assertFileExists($artifact, &lt;br /&gt;                "Expected file '{$artifact}' doesn't exist");&lt;br /&gt;        }&lt;br /&gt;    }&lt;br /&gt;    /**&lt;br /&gt;     * @test&lt;br /&gt;     * @group artifact&lt;br /&gt;     */&lt;br /&gt;    public function sqlFilesForDatabaseSetupTargetShouldBeAvailable() {&lt;br /&gt;        $expectedSqlFiles = array(&lt;br /&gt;            "{$this-&gt;_buildfileBasedir}/sqlfiles", &lt;br /&gt;            "{$this-&gt;_buildfileBasedir}/sqlfiles/session-storage.sql", &lt;br /&gt;            "{$this-&gt;_buildfileBasedir}/sqlfiles/acl.sql", &lt;br /&gt;            "{$this-&gt;_buildfileBasedir}/sqlfiles/log.sql"&lt;br /&gt;        );&lt;br /&gt;&lt;br /&gt;        foreach ($expectedSqlFiles as $sqlFile) {&lt;br /&gt;            $this-&gt;assertFileExists($sqlFile, "SQL file '{$sqlFile}' doesn't exist");&lt;br /&gt;        }&lt;br /&gt;    }&lt;br /&gt;    /**&lt;br /&gt;     * @test&lt;br /&gt;     * @group artifact&lt;br /&gt;     */&lt;br /&gt;    public function githubTargetShouldFetchExpectedRepository() {&lt;br /&gt;        $this-&gt;_isTearDownNecessary = true;&lt;br /&gt;        $this-&gt;_buildfileRunner-&gt;runTarget(array('github'));&lt;br /&gt;        $expectedGitRepository = "{$this-&gt;_buildfileBasedir}/build/"&lt;br /&gt;                               . "github-repos/phploc-phing/.git";&lt;br /&gt;        $this-&gt;assertFileExists($expectedGitRepository, &lt;br /&gt;            "Github target doesn't fetch the expected 'phploc-phing' repository");&lt;br /&gt;    }&lt;br /&gt;}&lt;/pre&gt;The outro screenshot shows the above stated test class run against the example buildfile on a Mac OS X system utilizing the --colors option; which by the way comes in really handy in combination with &lt;a href="http://mikenaberezny.com/2007/09/04/faster-tdd-with-stakeout-rb/" target="_self"&gt;Stakeout.rb&lt;/a&gt; during the process of refactoring or extending/creating buildfiles the test-driven way.&lt;br /&gt;&lt;br /&gt;&lt;a href="http://www.flickr.com/photos/raphaelstolt/3517905691/" title="PHPUnit console output by Raphael Stolt, on Flickr"&gt;&lt;img src="http://farm4.static.flickr.com/3559/3517905691_f044985a04_o.gif" alt="PHPUnit console output" border="0" height="199" width="550" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8420118650236071171-5110082920105713427?l=raphaelstolt.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/raphaelonphp/~4/UvEGicAWztc" height="1" width="1"/&gt;</content><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=8420118650236071171&amp;postID=5110082920105713427" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/5110082920105713427?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/5110082920105713427?v=2" /><link rel="alternate" type="text/html" href="http://raphaelstolt.blogspot.com/2009/05/testing-phing-buildfiles-with-phpunit.html" title="Testing Phing buildfiles with PHPUnit" /><author><name>Raphael Stolt</name><uri>http://www.blogger.com/profile/07949831701855458792</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="14172620075949142098" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total></entry><entry gd:etag="W/&quot;CEMBQH86eSp7ImA9WxJTEEo.&quot;"><id>tag:blogger.com,1999:blog-8420118650236071171.post-4677781334297408181</id><published>2009-04-18T00:48:00.006Z</published><updated>2009-04-18T16:34:11.111Z</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-04-18T16:34:11.111Z</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="PHP" /><category scheme="http://www.blogger.com/atom/ns#" term="Phing" /><title>Creating and using Phing ad hoc tasks</title><content type="html">Sometimes there are build scenarios where you'll badly need a functionality, like adding a &lt;a href="http://en.wikipedia.org/wiki/MD5#Applications" target="_self"&gt;MD5&lt;/a&gt; checksum file to a given project, that isn't provided neither by the available Phing core nor the optional tasks. Phing supports developers with two ways for extending the useable task pool: by writing 'outline' tasks that will end up in a directory of the Phing installation or by utilizing the &lt;a href="http://phing.info/docs/guide/current/chapters/appendixes/AppendixB-CoreTasks.html#AdhocTaskdefTask" target="_self"&gt;AdhocTaskdefTask&lt;/a&gt;, which allows to define custom tasks in the buildfile itself. The following post will try to outline how to define and use these inline tasks, by sketching an ad hoc task that enables the build orchestra to clone Git repositories from GitHub during a hypothetical workbench setup.&lt;br /&gt;&lt;h4 class="custom"&gt;Creating the inline/ad hoc task&lt;/h4&gt;The AdhocTaskdefTask expects a name attribute i.e. &lt;em&gt;github-clone&lt;/em&gt; for the XML element which will later referr to the ad hoc task and a CDATA section hosting the task implementation. Similar to 'outline' tasks the ad hoc task extends Phing's Task class, configures the task via attributes and holds the logic to perform. Unfortunately inline task implementations don't allow to require or include external classes available in the &lt;em&gt;include_path&lt;/em&gt;, like &lt;a href="http://framework.zend.com/manual/en/zend.http.html" target="_self"&gt;Zend_Http_Client&lt;/a&gt; which I initially tried to use for an example task fetching short Urls from is.gd. This limits the available functions and classes to craft the task from to the ones built into PHP. The following buildfile snippet shows the implementation of the github-clone ad hoc task which is wrapped by a &lt;a href="http://raphaelstolt.blogspot.com/2008/03/getting-overview-of-all-targets.html" target="_self"&gt;private target&lt;/a&gt; to encourage reusability and limit it's &lt;a href="http://raphaelstolt.blogspot.com/2008/03/getting-overview-of-all-targets.html" target="_self"&gt;callability&lt;/a&gt;.&lt;pre class="xmlSnippet"&gt;&amp;lt;target name="-init-ad-hoc-tasks" &lt;br /&gt;        description="Initializes the ad hoc task(s)"&amp;gt;&lt;br /&gt;  &amp;lt;adhoc-task name="github-clone"&amp;gt;&amp;lt;![CDATA[&lt;br /&gt;      class Github_Clone extends Task {&lt;br /&gt;&lt;br /&gt;          private $repository = null;&lt;br /&gt;          private $destDirectory = null;&lt;br /&gt;&lt;br /&gt;          function setRepos($repository) {&lt;br /&gt;              $this-&amp;gt;repository = $repository;&lt;br /&gt;          } &lt;br /&gt;          function setDest($destDirectory) {&lt;br /&gt;              $this-&amp;gt;destDirectory = $destDirectory;&lt;br /&gt;          }&lt;br /&gt;          function main() {&lt;br /&gt;              // Get project name from repos Uri&lt;br /&gt;              $projectName = str_replace('.git', '', &lt;br /&gt;                  substr(strrchr($this-&amp;gt;repository, '/'), 1));&lt;br /&gt;&lt;br /&gt;              $gitCommand = 'git clone ' . $this-&amp;gt;repository . ' ' . &lt;br /&gt;                  $this-&amp;gt;destDirectory . '/' . $projectName;&lt;br /&gt;&lt;br /&gt;              exec(escapeshellcmd($gitCommand), $output, $return);&lt;br /&gt;&lt;br /&gt;              if ($return !== 0) {&lt;br /&gt;                  throw new BuildException('Git clone failed');&lt;br /&gt;              }&lt;br /&gt;              $logMessage = 'Cloned Git repository ' . $this-&amp;gt;repository . &lt;br /&gt;                  ' into ' . $this-&amp;gt;destDirectory . '/' . $projectName;&lt;br /&gt;              $this-&amp;gt;log($logMessage);&lt;br /&gt;          }&lt;br /&gt;      }&lt;br /&gt;  ]]&amp;gt;&amp;lt;/adhoc-task&amp;gt;&lt;br /&gt;  &amp;lt;echo message="Initialized github-clone ad hoc task." /&amp;gt;&lt;br /&gt;&amp;lt;/target&amp;gt;&lt;/pre&gt;&lt;h4 class="custom"&gt;Using the ad hoc task&lt;/h4&gt;With the ad hoc task in the place to be, it's provided functionality can now be used from any target using the tasks XML element according to the given name i.e. &lt;em&gt;github-clone&lt;/em&gt; in the AdhocTaskdefTask element earlier and by feeding it with the required attributes i.e. &lt;em&gt;repos&lt;/em&gt; and &lt;em&gt;dest&lt;/em&gt;. The next snippet allows you to take a peek at the complete buildfile with the ad hoc task in action.&lt;pre class="xmlSnippet"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;br /&gt;&amp;lt;project name="recordshelf" default="init-work-bench" basedir="."&amp;gt;&lt;br /&gt;  &lt;br /&gt;  &amp;lt;property name="github.repos.dir" value="./github-repos" override="true" /&amp;gt;&lt;br /&gt;&lt;br /&gt;  &amp;lt;target name="init-work-bench" &lt;br /&gt;          depends="-init-ad-hoc-tasks, -clone-git-repos" &lt;br /&gt;          description="Initializes the hypothetical workbench"&amp;gt;&lt;br /&gt;    &amp;lt;echo message="Initialized workbench." /&amp;gt;&lt;br /&gt;  &amp;lt;/target&amp;gt;&lt;br /&gt;&lt;br /&gt;  &amp;lt;target name="-clean-git-repos" &lt;br /&gt;          description="Removes old repositories before initializing a new workbench"&amp;gt;&lt;br /&gt;    &amp;lt;delete dir="${github.repos.dir}" includeemptydirs="true" failonerror="true" /&amp;gt;&lt;br /&gt;  &amp;lt;/target&amp;gt;&lt;br /&gt;&lt;br /&gt;  &amp;lt;target name="-init-ad-hoc-tasks" &lt;br /&gt;          description="Initializes the ad hoc task(s)"&amp;gt;&lt;br /&gt;    &amp;lt;adhoc-task name="github-clone"&amp;gt;&amp;lt;![CDATA[&lt;br /&gt;        class Github_Clone extends Task {&lt;br /&gt;&lt;br /&gt;            private $repository = null;&lt;br /&gt;            private $destDirectory = null;&lt;br /&gt;&lt;br /&gt;            function setRepos($repository) {&lt;br /&gt;                $this-&amp;gt;repository = $repository;&lt;br /&gt;            } &lt;br /&gt;            function setDest($destDirectory) {&lt;br /&gt;                $this-&amp;gt;destDirectory = $destDirectory;&lt;br /&gt;            }&lt;br /&gt;            function main() {&lt;br /&gt;                // Get project name from repos Uri&lt;br /&gt;                $projectName = str_replace('.git', '', &lt;br /&gt;                    substr(strrchr($this-&amp;gt;repository, '/'), 1));&lt;br /&gt;&lt;br /&gt;                $gitCommand = 'git clone ' . $this-&amp;gt;repository . ' ' . &lt;br /&gt;                    $this-&amp;gt;destDirectory . '/' . $projectName;&lt;br /&gt;&lt;br /&gt;                exec(escapeshellcmd($gitCommand), $output, $return);&lt;br /&gt;&lt;br /&gt;                if ($return !== 0) {&lt;br /&gt;                    throw new BuildException('Git clone failed');&lt;br /&gt;                }&lt;br /&gt;                $logMessage = 'Cloned Git repository ' . $this-&amp;gt;repository . &lt;br /&gt;                    ' into ' . $this-&amp;gt;destDirectory . '/' . $projectName;&lt;br /&gt;                $this-&amp;gt;log($logMessage);&lt;br /&gt;            }&lt;br /&gt;        }&lt;br /&gt;    ]]&amp;gt;&amp;lt;/adhoc-task&amp;gt;&lt;br /&gt;    &amp;lt;echo message="Initialized github-clone ad hoc task." /&amp;gt;&lt;br /&gt;  &amp;lt;/target&amp;gt;&lt;br /&gt;  &lt;br /&gt;  &amp;lt;target name="-clone-git-repos" depends="-clean-git-repos"&lt;br /&gt;          description="Clones the needed Git repositories from GitHub"&amp;gt;&lt;br /&gt;    &amp;lt;github-clone repos="git://github.com/abc/abc.git" &lt;br /&gt;                  dest="${github.repos.dir}" /&amp;gt;&lt;br /&gt;    &amp;lt;github-clone repos="git://github.com/xyz/xyz.git" &lt;br /&gt;                  dest="${github.repos.dir}" /&amp;gt;&lt;br /&gt;  &amp;lt;/target&amp;gt;&lt;br /&gt;    &lt;br /&gt;&amp;lt;/project&amp;gt;&lt;/pre&gt;&lt;h4 class="custom"&gt;Favouring inline over 'outline' tasks?&lt;/h4&gt;The one big advantage of using inline tasks over 'outline' tasks is that they are distributed with the buildfile and are instantly available without the need to modify the Phing installation. Some severe disadvantages of inline tasks are the limitation to use only the core PHP functions and classes for the implementation, the introduction of an additional hurdle to verify the task behaviour via PHPUnit as it's located in a CDATA section of the buildfile and the fact that the use of several inline tasks will blow up the buildfile, and thereby obfuscate the build flow.&lt;br /&gt;&lt;br /&gt;Regrettably Phing doesn't provide an &lt;a href="http://ant.apache.org/manual/CoreTasks/import.html" target="_self"&gt;import&lt;/a&gt; task like Ant which might enable a refactoring to pull the ad hoc task definitions into a seperate XML file and include them at buildtime; in case you might have some expertise or ideas for a suitable workaround hit me with a comment. So far I tried to get it working, with no success, by utilizing Phing's &lt;a href="http://phing.info/docs/guide/current/chapters/appendixes/AppendixB-CoreTasks.html#PhingTask"&gt;PhingTask&lt;/a&gt; and XML's &lt;a href="http://www.w3.org/TR/REC-xml/#sec-external-ent"&gt;external entities&lt;/a&gt; declaration.&lt;br /&gt;&lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8420118650236071171-4677781334297408181?l=raphaelstolt.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/raphaelonphp/~4/YfT5X6hpqo0" height="1" width="1"/&gt;</content><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=8420118650236071171&amp;postID=4677781334297408181" title="3 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/4677781334297408181?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/4677781334297408181?v=2" /><link rel="alternate" type="text/html" href="http://raphaelstolt.blogspot.com/2009/04/creating-and-using-phing-ad-hoc-tasks.html" title="Creating and using Phing ad hoc tasks" /><author><name>Raphael Stolt</name><uri>http://www.blogger.com/profile/07949831701855458792</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="14172620075949142098" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">3</thr:total></entry><entry gd:etag="W/&quot;CEMDQXY6fip7ImA9WxJTEEo.&quot;"><id>tag:blogger.com,1999:blog-8420118650236071171.post-1912712568674590257</id><published>2009-03-31T14:02:00.061Z</published><updated>2009-04-18T16:34:30.816Z</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-04-18T16:34:30.816Z</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="Ruby" /><category scheme="http://www.blogger.com/atom/ns#" term="Rake" /><title>Using Haml &amp; Sass from a Rake task</title><content type="html">&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://farm4.static.flickr.com/3007/3401813196_6d7d035d7b_o.gif"&gt;&lt;img style="float:left; margin:0 10px 10px 0;cursor:pointer; cursor:hand;width: 198px; height: 215px;" src="http://farm4.static.flickr.com/3007/3401813196_6d7d035d7b_o.gif" border="0" alt="Haml logo" title="Haml logo" border="0" /&gt;&lt;/a&gt;Some time ago I had the 'lightning' idea to implement &lt;a href="http://raphaelstolt.blogspot.com/2009/01/broadcasting-blog-post-notifications-to.html" target="_self"&gt;another&lt;/a&gt; Rake automation to support my current blogging workflow, which at the moment consists of finding a sparkling idea to blog about, write it out in &lt;a href="http://www.hogbaysoftware.com/products/writeroom" target="_self"&gt;WriteRoom&lt;/a&gt; and refine the post in &lt;a href="http://macromates.com/" target="_self"&gt;TextMate&lt;/a&gt; before publishing. As this process was a recurring and copy &amp; paste driven event, I strove for an automation supporting this workflow. So unsurprisingly the post will show my current solution to achieve this goal by utilizing &lt;a href="http://rake.rubyforge.org/" target="_self"&gt;Rake&lt;/a&gt;, &lt;a href="http://haml.hamptoncatlin.com" target="_self"&gt;Haml&lt;/a&gt; and &lt;a href="http://haml.hamptoncatlin.com/docs/rdoc/classes/Sass.html" target="_self"&gt;Sass&lt;/a&gt;.&lt;h4 class="custom"&gt;So what's that Haml and Sass thingy?&lt;/h4&gt;Haml (HTML Abstraction Markup Language) is a templating language/engine with the primary goal to make Markup &lt;a href="http://en.wikipedia.org/wiki/DRY" target="_self"&gt;DRY&lt;/a&gt;, beautiful and readable again. It has a very shallow learning curve and therefor is perfectly suited for programmers and designers alike. Haml is primarily targeted at making the views of &lt;a href="http://rubyonrails.org/" target="_self"&gt;Ruby on Rails&lt;/a&gt;, &lt;a href="http://merbivore.com/" target="_self"&gt;Merb&lt;/a&gt; or &lt;a href="http://www.sinatrarb.com/" target="_self"&gt;Sinatra&lt;/a&gt; web applications leaner, but as you will see later the Ruby implementation also can be used framework independently.&lt;br /&gt;&lt;br /&gt;Sass (Syntactically Awesome StyleSheets) is a module which comes bundled with Haml providing a meta-language/abstraction on top of CSS sharing the same goals and advantages as Haml.&lt;h4 class="custom"&gt;Gluing Haml and Sass into a Rake task&lt;/h4&gt;To get going you first have to install Haml and Sass by running the gem command shown next.&lt;pre class="consoleOutput"&gt;sudo gem install haml&lt;/pre&gt;With Haml and Sass available it's about time to identify and outline the parts you want to automate, in my case it's the creation of a WriteRoom and/or a XHTML draft document for initial editings. So the parameters to pass into the task to come are the targeted &lt;em&gt;editor(s)&lt;/em&gt;, the &lt;em&gt;title&lt;/em&gt; of the blog post to draft and a list of associated and whitespace separated &lt;em&gt;category tags&lt;/em&gt;.&lt;br /&gt;&lt;br /&gt;The XHTML document skeleton content and it's inline CSS are defined each in a separate Haml and Sass template file and will be rendered into the outcoming document along with the content passed into the Rake task. While the document skeleton for the WriteRoom draft document, due to it's brevity, is defined inside of the task itself. The following snippets are showing the mentioned Haml and Sass templates for the XHTML draft output file, which are located in the same directory as the Rake file.&lt;br /&gt;&lt;br /&gt;&lt;div class="refactoringStatus"&gt;&amp;nbsp;Haml&lt;/div&gt;&lt;pre class="codeSnippetRefactoring"&gt;!!! 1.1&lt;br /&gt;%html&lt;br /&gt;  %head &lt;br /&gt;    %title= "&lt;em&gt;#{title}&lt;/em&gt; - Draft"&lt;br /&gt;    %style{ :type =&gt; 'text/css' }= &lt;em&gt;inline_css&lt;/em&gt;&lt;br /&gt;  %body&lt;br /&gt;    %h3= &lt;em&gt;title&lt;/em&gt;&lt;br /&gt;    %h4.custom sub headline &lt;br /&gt;    %pre.consoleOutput console command&lt;br /&gt;    %pre.codeSnippet code snippet&lt;br /&gt;    %br/&lt;br /&gt;    = "Tags: &lt;em&gt;#{tags.join ', '}&lt;/em&gt;"&lt;/pre&gt;&lt;div class="refactoringStatus"&gt;&amp;nbsp;Sass&lt;/div&gt;&lt;pre class="codeSnippetRefactoring"&gt;body&lt;br /&gt;  :margin 5&lt;br /&gt;  :line-height 1.5em&lt;br /&gt;  :font small Trebuchet MS, Verdana, Arial, Sans-serif&lt;br /&gt;  :color #000000&lt;br /&gt;h4&lt;br /&gt;  :margin-bottom 0.3em&lt;br /&gt;.consoleOutput&lt;br /&gt;  :padding 6px &lt;br /&gt;  :background-color #000 &lt;br /&gt;  :color rgb(20, 218, 62)&lt;br /&gt;  :font-size 12px&lt;br /&gt;  :font-weight bolder&lt;br /&gt;.codeSnippet&lt;br /&gt;  :padding 3px&lt;br /&gt;  :background-color rgb(243, 243, 243)&lt;br /&gt;  :color rgb(93, 91, 91)&lt;br /&gt;  :font-size small&lt;br /&gt;  :border 1px solid #6A6565&lt;/pre&gt;To inject the dynamic content into the Haml template and have it rendered into the outcoming document, the values i.e. &lt;em&gt;draft_title&lt;/em&gt;, &lt;em&gt;draft_tags&lt;/em&gt; and &lt;em&gt;draft_inline_css&lt;/em&gt; have to be made available to the template engine by passing them in a bundling Hash into the  &lt;em&gt;to_html&lt;/em&gt; alias method of the Haml Engine object like shown in the next Rake task.&lt;pre class="codeSnippet"&gt;task :default do&lt;br /&gt;  Rake::Task['blog_utils:create_draft_doc'].invoke&lt;br /&gt;end&lt;br /&gt;&lt;br /&gt;namespace :blog_utils do&lt;br /&gt;  &lt;br /&gt;  desc 'Create a new draft document for a given title, category tags and editor'&lt;br /&gt;  task :create_draft_doc, [:title, :tags, :editor] do |t, args|&lt;br /&gt;    draft_title = args.title&lt;br /&gt;    draft_tags = args.tags.split(' ')&lt;br /&gt;    draft_target_editor = args.editor&lt;br /&gt;    &lt;br /&gt;    raise_message = 'No title for draft provided'&lt;br /&gt;    raise raise_message if draft_title.nil?&lt;br /&gt;    &lt;br /&gt;    raise_message = 'No tags for draft provided'&lt;br /&gt;    raise raise_message if draft_tags.nil?&lt;br /&gt;    &lt;br /&gt;    draft_target_editor = '*' if draft_target_editor.nil?&lt;br /&gt;    &lt;br /&gt;    raise_message = 'Unsupported target editor provided' &lt;br /&gt;    raise raise_message unless draft_target_editor == 'Textmate' || &lt;br /&gt;      draft_target_editor == 'Writeroom' || draft_target_editor == '*'&lt;br /&gt;    &lt;br /&gt;    if draft_target_editor == 'Writeroom' || draft_target_editor == '*'&lt;br /&gt;      draft_output_file = draft_title.gsub(' ', '_') + '.txt'&lt;br /&gt;      &lt;br /&gt;      File.open(draft_output_file, 'w') do |draft_file_txt|&lt;br /&gt;        draft_file_txt.puts draft_title&lt;br /&gt;        draft_file_txt.puts&lt;br /&gt;        draft_file_txt.puts "Tags: #{draft_tags.join ', '}"&lt;br /&gt;      end&lt;br /&gt;    end&lt;br /&gt;    &lt;br /&gt;    if draft_target_editor == 'Textmate' || draft_target_editor == '*'&lt;br /&gt;      &lt;br /&gt;      template_sass_content, template_haml_content = ''&lt;br /&gt;      &lt;br /&gt;      ['haml', 'sass'].each do |template_type|&lt;br /&gt;        template = File.dirname(__FILE__) + "/draft_template.#{template_type}"&lt;br /&gt;        raise_message = "#{template_type.capitalize} template '#{template}' not found"&lt;br /&gt;        raise raise_message if !File.exists?(template)&lt;br /&gt;        &lt;br /&gt;        template_sass_content = File.read(template) if template_type === 'sass'&lt;br /&gt;        template_haml_content = File.read(template) if template_type === 'haml'&lt;br /&gt;      end&lt;br /&gt;      &lt;em&gt;&lt;br /&gt;      require 'sass'&lt;br /&gt;      require 'haml'&lt;br /&gt;&lt;br /&gt;      draft_inline_css = Sass::Engine.new(template_sass_content).to_css&lt;br /&gt;      draft_document_content = Haml::Engine.new(template_haml_content).to_html(&lt;br /&gt;        Object.new, { :title =&gt; draft_title , :tags =&gt; draft_tags ,&lt;br /&gt;          :inline_css =&gt; draft_inline_css } )&lt;/em&gt;&lt;br /&gt;      &lt;br /&gt;      draft_output_file = draft_title.gsub(' ', '_') + '.html'      &lt;br /&gt;      File.open(draft_output_file, 'w') do |draft_file_html|&lt;br /&gt;        draft_file_html.puts(draft_document_content)&lt;br /&gt;      end&lt;br /&gt;    end&lt;br /&gt;    &lt;br /&gt;  end&lt;br /&gt;end&lt;/pre&gt;&lt;h4 class="custom"&gt;Easing invocation pain with alias&lt;/h4&gt;Now as the Rake task is implemented and waiting for demands it can be invoked by calling the task as shown in the next console snippet. &lt;pre class="consoleOutput"&gt;sudo rake -f $HOME/Automations/Rakefile.rb blog_utils:create_draft_doc['Title','Tag1 TagN','Editor']&lt;/pre&gt;As I'm not even close to being a console ninja and probably will have forgotten the task call structure before initiating the next blog post, I decided to add an easing and more memorizable alias to &lt;em&gt;$HOME/.profile&lt;/em&gt; as shown next.&lt;pre class="codeSnippet"&gt;alias createdraft='sudo rake -f $HOME/Automations/Rakefile.rb blog_utils:create_draft_doc[$title,$tags,$editor]'&lt;/pre&gt;The created alias now allows to invoke the Rake task in a nice and easy way as shown in the next console command.&lt;pre class="consoleOutput"&gt;createdraft title='Using Haml &amp; Sass from a Rake task' tags='Rake Ruby' editor='Textmate'&lt;/pre&gt;&lt;h4 class="custom"&gt;Taking a peek at the generated draft document&lt;/h4&gt;After running the described Rake task I end up with the XHTML document shown in the outro code snippet, which then can be used for the further editing process. Of course I could have setup a &lt;a href="http://raphaelstolt.blogspot.com/2008/02/creating-zend-framework-snippets-for.html" target="_self"&gt;TextMate Snippet&lt;/a&gt; to get me going, but that way I would have missed the opportunity to mess around with another amazing Ruby tool.&lt;pre class="codeSnippet"&gt;&amp;lt;!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"&amp;gt;&lt;br /&gt;&amp;lt;html&amp;gt;&lt;br /&gt;  &amp;lt;head&amp;gt;&lt;br /&gt;    &amp;lt;title&amp;gt;&lt;em&gt;Using Haml &amp; Sass from a Rake task&lt;/em&gt; - Draft&amp;lt;/title&amp;gt;&lt;br /&gt;    &amp;lt;style type='text/css'&amp;gt;&lt;br /&gt;      &lt;em&gt;body {&lt;br /&gt;        margin: 5;&lt;br /&gt;        line-height: 1.5em;&lt;br /&gt;        font: small Trebuchet MS, Verdana, Arial, Sans-serif;&lt;br /&gt;        color: #000000; }&lt;br /&gt;&lt;br /&gt;      h4 {&lt;br /&gt;        margin-bottom: 0.3em; }&lt;br /&gt;&lt;br /&gt;      .consoleOutput {&lt;br /&gt;        padding: 6px;&lt;br /&gt;        background-color: #000;&lt;br /&gt;        color: rgb(20, 218, 62);&lt;br /&gt;        font-size: 12px;&lt;br /&gt;        font-weight: bolder; }&lt;br /&gt;      &lt;br /&gt;      .codeSnippet {&lt;br /&gt;        padding: 3px;&lt;br /&gt;        background-color: rgb(243, 243, 243);&lt;br /&gt;        color: rgb(93, 91, 91);&lt;br /&gt;        font-size: small;&lt;br /&gt;        border: 1px solid #6A6565; }&lt;/em&gt;&lt;br /&gt;    &amp;lt;/style&amp;gt;&lt;br /&gt;  &amp;lt;/head&amp;gt;&lt;br /&gt;  &amp;lt;body&amp;gt;&lt;br /&gt;    &amp;lt;h3&amp;gt;&lt;em&gt;Using Haml &amp; Sass from a Rake task&lt;/em&gt;&amp;lt;/h3&amp;gt;&lt;br /&gt;    &amp;lt;h4&amp;gt;sub headline&amp;lt;/h4&amp;gt;&lt;br /&gt;    &amp;lt;pre class='consoleOutput'&amp;gt;console command&amp;lt;/pre&amp;gt;&lt;br /&gt;    &amp;lt;pre class='codeSnippet'&amp;gt;code snippet&amp;lt;/pre&amp;gt;&lt;br /&gt;    &amp;lt;br /&amp;gt;&lt;br /&gt;    Tags: &lt;em&gt;Rake, Ruby&lt;/em&gt;&lt;br /&gt;  &amp;lt;/body&amp;gt;&lt;br /&gt;&amp;lt;/html&amp;gt;&lt;/pre&gt;&lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8420118650236071171-1912712568674590257?l=raphaelstolt.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/raphaelonphp/~4/xxIO7herpB8" height="1" width="1"/&gt;</content><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=8420118650236071171&amp;postID=1912712568674590257" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/1912712568674590257?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/1912712568674590257?v=2" /><link rel="alternate" type="text/html" href="http://raphaelstolt.blogspot.com/2009/03/using-haml-sass-from-rake-task.html" title="Using Haml &amp;amp; Sass from a Rake task" /><author><name>Raphael Stolt</name><uri>http://www.blogger.com/profile/07949831701855458792</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="14172620075949142098" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total></entry><entry gd:etag="W/&quot;CEMNQHY4cCp7ImA9WxJTEEo.&quot;"><id>tag:blogger.com,1999:blog-8420118650236071171.post-6206018638608847732</id><published>2009-02-22T13:35:00.007Z</published><updated>2009-04-18T16:34:51.838Z</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-04-18T16:34:51.838Z</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="PHP" /><category scheme="http://www.blogger.com/atom/ns#" term="Phing" /><title>Phplocing your projects with Phing</title><content type="html">When I started to play around with &lt;a href="http://rubyonrails.org/" target="_self"&gt;Ruby on Rails&lt;/a&gt;, my attention got somehow soon drawn to it's &lt;a href="http://rake.rubyforge.org/" target="_self"&gt;Rake&lt;/a&gt; stats task, which provides developers or more likely project managers with an overview of the actual project size. Exactly one month ago Sebastian Bergmann, of PHPUnit fame, started to implement a similar tool dubbed &lt;a href="http://github.com/sebastianbergmann/phploc/tree/master" target="_self"&gt;phploc&lt;/a&gt; which can give you an overview of the size for any given PHP project. As I wanted to automate the invocation of this handy tool and collect it's report output out of a &lt;a href="http://phing.info/trac/" target="_self"&gt;Phing&lt;/a&gt; buildfile, I invested some time to develop a custom Phing task doing so. Thereby the following post will show you a possible implementation of this task and it's use in a buildfile.&lt;h4 class="custom"&gt;Installing phploc&lt;/h4&gt;To setup phploc on your system simply install the phploc PEAR package available from the &lt;em&gt;pear.phpunit.de&lt;/em&gt; channel as shown in the next commands. In case you already have installed PHPUnit via PEAR you can omit the channel-discover command.&lt;pre class="consoleOutput"&gt;sudo pear channel-discover pear.phpunit.de&lt;br /&gt;sudo pear install phpunit/phploc&lt;/pre&gt;&lt;h4 class="custom"&gt;Implementing the phploc task&lt;/h4&gt;As I already blogged about &lt;a href="http://raphaelstolt.blogspot.com/2007/03/rolling-your-own-phing-task.html" target="_self"&gt;developing custom Phing task&lt;/a&gt; I'm only going to show the actual implementation and not dive into any details; alternatively you can also grab it from &lt;a href="http://github.com/raphaelstolt/phploc-phing/tree/master" target="_self"&gt;this&lt;/a&gt; public GitHub repository.&lt;pre class="codeSnippet"&gt;&amp;lt;?php&lt;br /&gt;require_once 'phing/Task.php';&lt;br /&gt;require_once 'phing/BuildException.php';&lt;br /&gt;require_once 'PHPLOC/Analyser.php';&lt;br /&gt;require_once 'PHPLOC/Util/FilterIterator.php';&lt;br /&gt;require_once 'PHPLOC/TextUI/ResultPrinter.php';&lt;br /&gt;&lt;br /&gt;class PHPLocTask extends Task&lt;br /&gt;{&lt;br /&gt;    protected $suffixesToCheck = null;&lt;br /&gt;    protected $acceptedReportTypes = null;&lt;br /&gt;    protected $reportDirectory = null;&lt;br /&gt;    protected $reportType = null;&lt;br /&gt;    protected $fileToCheck = null;&lt;br /&gt;    protected $filesToCheck = null;&lt;br /&gt;    protected $reportFileName = null;&lt;br /&gt;    protected $fileSets = null;&lt;br /&gt;    &lt;br /&gt;    public function init() {&lt;br /&gt;        $this-&amp;gt;suffixesToCheck = array('php');&lt;br /&gt;        $this-&amp;gt;acceptedReportTypes = array('cli', 'txt', 'xml');&lt;br /&gt;        $this-&amp;gt;reportType = 'cli';&lt;br /&gt;        $this-&amp;gt;reportFileName = 'phploc-report';&lt;br /&gt;        $this-&amp;gt;fileSets = array();&lt;br /&gt;        $this-&amp;gt;filesToCheck = array();&lt;br /&gt;    }&lt;br /&gt;    public function setSuffixes($suffixListOrSingleSuffix) {&lt;br /&gt;        if (stripos($suffixListOrSingleSuffix, ',')) {&lt;br /&gt;            $suffixes = explode(',', $suffixListOrSingleSuffix);&lt;br /&gt;            $this-&amp;gt;suffixesToCheck = array_map('trim', $suffixes);&lt;br /&gt;        } else {&lt;br /&gt;            array_push($this-&amp;gt;suffixesToCheck, trim($suffixListOrSingleSuffix));&lt;br /&gt;        }&lt;br /&gt;    }&lt;br /&gt;    public function setFile(PhingFile $file) {&lt;br /&gt;        $this-&amp;gt;fileToCheck = trim($file);&lt;br /&gt;    }&lt;br /&gt;    public function createFileSet() {&lt;br /&gt;        $num = array_push($this-&amp;gt;fileSets, new FileSet());&lt;br /&gt;        return $this-&amp;gt;fileSets[$num - 1];&lt;br /&gt;    }&lt;br /&gt;    public function setReportType($type) {&lt;br /&gt;        $this-&amp;gt;reportType = trim($type);&lt;br /&gt;    }&lt;br /&gt;    public function setReportName($name) {&lt;br /&gt;        $this-&amp;gt;reportFileName = trim($name);&lt;br /&gt;    }&lt;br /&gt;    public function setReportDirectory($directory) {&lt;br /&gt;        $this-&amp;gt;reportDirectory = trim($directory);&lt;br /&gt;    }&lt;br /&gt;    public function main() {       &lt;br /&gt;        if (!isset($this-&amp;gt;fileToCheck) &amp;&amp; count($this-&amp;gt;fileSets) === 0) {&lt;br /&gt;            $exceptionMessage = "Missing either a nested fileset or the "&lt;br /&gt;                . "attribute 'file' set.";&lt;br /&gt;            throw new BuildException($exceptionMessage);&lt;br /&gt;        }&lt;br /&gt;        if (count($this-&amp;gt;suffixesToCheck) === 0) {&lt;br /&gt;            throw new BuildException("No file suffix defined.");&lt;br /&gt;        }&lt;br /&gt;        if (is_null($this-&amp;gt;reportType)) {&lt;br /&gt;            throw new BuildException("No report type defined.");&lt;br /&gt;        }&lt;br /&gt;        if (!is_null($this-&amp;gt;reportType) &amp;&amp; &lt;br /&gt;            !in_array($this-&amp;gt;reportType, $this-&amp;gt;acceptedReportTypes)) {&lt;br /&gt;            throw new BuildException("Unaccepted report type defined.");&lt;br /&gt;        }&lt;br /&gt;        if (!is_null($this-&amp;gt;fileToCheck) &amp;&amp; !file_exists($this-&amp;gt;fileToCheck)) {&lt;br /&gt;            throw new BuildException("File to check doesn't exist.");&lt;br /&gt;        }&lt;br /&gt;        if ($this-&amp;gt;reportType !== 'cli' &amp;&amp; is_null($this-&amp;gt;reportDirectory)) {&lt;br /&gt;            throw new BuildException("No report output directory defined.");&lt;br /&gt;        }&lt;br /&gt;        if (count($this-&amp;gt;fileSets) &amp;gt; 0 &amp;&amp; !is_null($this-&amp;gt;fileToCheck)) {&lt;br /&gt;            $exceptionMessage = "Either use a nested fileset or 'file' " &lt;br /&gt;                . "attribute; not both.";&lt;br /&gt;            throw new BuildException($exceptionMessage);&lt;br /&gt;        }&lt;br /&gt;        if (!is_null($this-&amp;gt;reportDirectory) &amp;&amp; !is_dir($this-&amp;gt;reportDirectory)) {&lt;br /&gt;            $reportOutputDir = new PhingFile($this-&amp;gt;reportDirectory);&lt;br /&gt;            $logMessage = "Report output directory does't exist, creating: " &lt;br /&gt;                . $reportOutputDir-&amp;gt;getAbsolutePath() . '.';&lt;br /&gt;            $this-&amp;gt;log($logMessage);&lt;br /&gt;            $reportOutputDir-&amp;gt;mkdirs();&lt;br /&gt;        }&lt;br /&gt;        if ($this-&amp;gt;reportType !== 'cli') {&lt;br /&gt;            $this-&amp;gt;reportFileName.= '.' . trim($this-&amp;gt;reportType);&lt;br /&gt;        }&lt;br /&gt;        if (count($this-&amp;gt;fileSets) &amp;gt; 0) {&lt;br /&gt;            $project = $this-&amp;gt;getProject();&lt;br /&gt;            foreach ($this-&amp;gt;fileSets as $fileSet) {&lt;br /&gt;                $directoryScanner = $fileSet-&amp;gt;getDirectoryScanner($project);&lt;br /&gt;                $files = $directoryScanner-&amp;gt;getIncludedFiles();&lt;br /&gt;                $directory = $fileSet-&amp;gt;getDir($this-&amp;gt;project)-&amp;gt;getPath();&lt;br /&gt;                foreach ($files as $file) {&lt;br /&gt;                    if ($this-&amp;gt;isFileSuffixSet($file)) {&lt;br /&gt;                        $this-&amp;gt;filesToCheck[] = $directory . DIRECTORY_SEPARATOR &lt;br /&gt;                            . $file;&lt;br /&gt;                    }&lt;br /&gt;                }&lt;br /&gt;            }&lt;br /&gt;            $this-&amp;gt;filesToCheck = array_unique($this-&amp;gt;filesToCheck);&lt;br /&gt;        }&lt;br /&gt;        if (!is_null($this-&amp;gt;fileToCheck)) {&lt;br /&gt;            if (!$this-&amp;gt;isFileSuffixSet($file)) {&lt;br /&gt;                $exceptionMessage = "Suffix of file to check is not defined in"&lt;br /&gt;                    . " 'suffixes' attribute.";&lt;br /&gt;                throw new BuildException($exceptionMessage);&lt;br /&gt;            }&lt;br /&gt;        }&lt;br /&gt;        $this-&amp;gt;runPhpLocCheck();&lt;br /&gt;    }&lt;br /&gt;    protected function isFileSuffixSet($filename) {&lt;br /&gt;        $pathinfo = pathinfo($filename);&lt;br /&gt;        $fileSuffix = $pathinfo['extension'];&lt;br /&gt;        return in_array($fileSuffix, $this-&amp;gt;suffixesToCheck);&lt;br /&gt;    }&lt;br /&gt;    protected function runPhpLocCheck() {&lt;br /&gt;        $files = $this-&amp;gt;getFilesToCheck();&lt;br /&gt;        $result = $this-&amp;gt;getCountForFiles($files); &lt;br /&gt;        &lt;br /&gt;        if ($this-&amp;gt;reportType === 'cli' || $this-&amp;gt;reportType === 'txt') {&lt;br /&gt;            $printer = new PHPLOC_TextUI_ResultPrinter;&lt;br /&gt;            if ($this-&amp;gt;reportType === 'txt') {&lt;br /&gt;                ob_start();&lt;br /&gt;                $printer-&amp;gt;printResult($result);&lt;br /&gt;                file_put_contents($this-&amp;gt;reportDirectory &lt;br /&gt;                    . DIRECTORY_SEPARATOR . $this-&amp;gt;reportFileName, &lt;br /&gt;                        ob_get_contents());&lt;br /&gt;                ob_end_clean();&lt;br /&gt;                $reportDir = new PhingFile($this-&amp;gt;reportDirectory);&lt;br /&gt;                $logMessage = "Writing report to: " &lt;br /&gt;                    . $reportDir-&amp;gt;getAbsolutePath() . DIRECTORY_SEPARATOR &lt;br /&gt;                        . $this-&amp;gt;reportFileName;&lt;br /&gt;                $this-&amp;gt;log($logMessage);&lt;br /&gt;            } else {&lt;br /&gt;                $printer-&amp;gt;printResult($result);&lt;br /&gt;            }&lt;br /&gt;        } elseif ($this-&amp;gt;reportType === 'xml') {&lt;br /&gt;            $xml = $this-&amp;gt;getResultAsXml($result);&lt;br /&gt;            $reportDir = new PhingFile($this-&amp;gt;reportDirectory);&lt;br /&gt;            $logMessage = "Writing report to: " . $reportDir-&amp;gt;getAbsolutePath()&lt;br /&gt;                . DIRECTORY_SEPARATOR . $this-&amp;gt;reportFileName;&lt;br /&gt;            $this-&amp;gt;log($logMessage);&lt;br /&gt;            file_put_contents($this-&amp;gt;reportDirectory . DIRECTORY_SEPARATOR&lt;br /&gt;                . $this-&amp;gt;reportFileName, $xml);&lt;br /&gt;        }&lt;br /&gt;    }&lt;br /&gt;    protected function getFilesToCheck() {&lt;br /&gt;        if (count($this-&amp;gt;filesToCheck) &amp;gt; 0) {&lt;br /&gt;            $files = array();&lt;br /&gt;            foreach ($this-&amp;gt;filesToCheck as $file) {&lt;br /&gt;                $files[] = new SPLFileInfo($file);&lt;br /&gt;            }&lt;br /&gt;        } elseif (!is_null($this-&amp;gt;fileToCheck)) {&lt;br /&gt;            $files = array(new SPLFileInfo($this-&amp;gt;fileToCheck));&lt;br /&gt;        }&lt;br /&gt;        return $files;&lt;br /&gt;    }&lt;br /&gt;    protected function getCountForFiles($files) {&lt;br /&gt;        $count = array('files' =&amp;gt; 0, 'loc' =&amp;gt; 0, 'cloc' =&amp;gt; 0, 'ncloc' =&amp;gt; 0,&lt;br /&gt;            'eloc' =&amp;gt; 0, 'interfaces' =&amp;gt; 0, 'classes' =&amp;gt; 0, 'functions' =&amp;gt; 0);&lt;br /&gt;        $directories = array();&lt;br /&gt;&lt;br /&gt;        foreach ($files as $file) {&lt;br /&gt;            $directory = $file-&amp;gt;getPath();&lt;br /&gt;            if (!isset($directories[$directory])) {&lt;br /&gt;                $directories[$directory] = TRUE;&lt;br /&gt;            }          &lt;br /&gt;            PHPLOC_Analyser::countFile($file-&amp;gt;getPathName(), $count);&lt;br /&gt;        }&lt;br /&gt;        &lt;br /&gt;        if (!function_exists('parsekit_compile_file')) {&lt;br /&gt;            unset($count['eloc']);&lt;br /&gt;        }&lt;br /&gt;        $count['directories'] = count($directories) - 1;&lt;br /&gt;        return $count;&lt;br /&gt;    }&lt;br /&gt;    protected function getResultAsXml($result) {        &lt;br /&gt;        $newline = "\n";&lt;br /&gt;        $newlineWithSpaces = sprintf("\n%4s",'');&lt;br /&gt;        $xml = '&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;';&lt;br /&gt;        $xml.= $newline . '&amp;lt;phploc&amp;gt;'; &lt;br /&gt;        &lt;br /&gt;        if ($result['directories'] &amp;gt; 0) {&lt;br /&gt;            $xml.= $newlineWithSpaces . '&amp;lt;directories&amp;gt;' . $result['directories'] . '&amp;lt;/directories&amp;gt;';&lt;br /&gt;            $xml.= $newlineWithSpaces . '&amp;lt;files&amp;gt;' . $result['files'] . '&amp;lt;/files&amp;gt;';&lt;br /&gt;        }&lt;br /&gt;        $xml.= $newlineWithSpaces . '&amp;lt;loc&amp;gt;' . $result['loc'] . '&amp;lt;/loc&amp;gt;';&lt;br /&gt;        &lt;br /&gt;        if (isset($result['eloc'])) {&lt;br /&gt;            $xml.= $newlineWithSpaces . '&amp;lt;eloc&amp;gt;' . $result['eloc'] . '&amp;lt;/eloc&amp;gt;';&lt;br /&gt;        }&lt;br /&gt;        $xml.= $newlineWithSpaces . '&amp;lt;cloc&amp;gt;' . $result['cloc'] . '&amp;lt;/cloc&amp;gt;';&lt;br /&gt;        $xml.= $newlineWithSpaces . '&amp;lt;ncloc&amp;gt;' . $result['ncloc'] . '&amp;lt;/ncloc&amp;gt;';&lt;br /&gt;        $xml.= $newlineWithSpaces . '&amp;lt;interfaces&amp;gt;' . $result['interfaces'] . '&amp;lt;/interfaces&amp;gt;';&lt;br /&gt;        $xml.= $newlineWithSpaces . '&amp;lt;classes&amp;gt;' . $result['classes'] . '&amp;lt;/classes&amp;gt;';&lt;br /&gt;        $xml.= $newlineWithSpaces . '&amp;lt;methods&amp;gt;' . $result['functions'] . '&amp;lt;/methods&amp;gt;' . $newline;&lt;br /&gt;        $xml.= '&amp;lt;/phploc&amp;gt;';&lt;br /&gt;        return $xml;&lt;br /&gt;    }&lt;br /&gt;}&lt;/pre&gt;&lt;h4 class="custom"&gt;Hooking the phploc task into Phing&lt;/h4&gt;To use the task in your Phing builds simply copy it into the &lt;em&gt;phing/tasks/my&lt;/em&gt; directory and make it available via the &lt;a href="http://phing.info/docs/guide/current/chapters/appendixes/AppendixB-CoreTasks.html#TaskdefTask" target="_self"&gt;taskdef task&lt;/a&gt;. The next table shows the available task attributes and the values they can take to configure it's behaviour and output. As you will see it also provides the ability to generate reports in a XML format; I chose to implement this feature to have the possibilty to transform the report results into HTML documents by applying for example a XSLT stylesheet. This way they can provide more value to non-technical project members or can be made accessible in a CI system dashboard if desired.&lt;br /&gt;&lt;br /&gt;&lt;table class="listing" cellpadding="1"cellspacing="1"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th&gt;Name&lt;/th&gt;&lt;th&gt;Type&lt;/th&gt;&lt;th&gt;Description&lt;/th&gt;&lt;th&gt;Default&lt;/th&gt;&lt;th&gt;Required&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="content" valign="top"&gt;reportType&lt;/td&gt;&lt;td class="content" valign="top"&gt;string&lt;/td&gt;&lt;td class="content" valign="top"&gt;The type of the report. Available types are cli|txt|xml.&lt;/td&gt;&lt;td class="content" valign="top"&gt;cli&lt;/td&gt;&lt;td class="content" valign="top"&gt;No&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="content" valign="top"&gt;reportName&lt;/td&gt;&lt;td class="content" valign="top"&gt;string&lt;/td&gt;&lt;td class="content" valign="top"&gt;The name of the report type without a file extension.&lt;/td&gt;&lt;td class="content" valign="top"&gt;phploc-report&lt;/td&gt;&lt;td class="content" valign="top"&gt;No&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="content" valign="top"&gt;reportDirectory&lt;/td&gt;&lt;td class="content" valign="top"&gt;string&lt;/td&gt;&lt;td class="content" valign="top"&gt;The directory to write the report file to.&lt;/td&gt;&lt;td class="content" valign="top"&gt;false&lt;/td&gt;&lt;td class="content" valign="top"&gt;Yes, when report type txt or xml is defined.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="content" valign="top"&gt;file&lt;/td&gt;&lt;td class="content" valign="top"&gt;string&lt;/td&gt;&lt;td class="content" valign="top"&gt;The name of the file to check.&lt;/td&gt;&lt;td class="content" valign="top"&gt;n/a&lt;/td&gt;&lt;td class="content" valign="top"&gt;Yes, when no nested fileset is defined.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="content" valign="top"&gt;suffixes&lt;/td&gt;&lt;td class="content" valign="top"&gt;string&lt;/td&gt;&lt;td class="content" valign="top"&gt;A comma-separated list of file suffixes to check.&lt;/td&gt;&lt;td class="content" valign="top"&gt;php&lt;/td&gt;&lt;td class="content" valign="top"&gt;No&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;span style="font-weight:bold;"&gt;Supported Nested Tags:&lt;/span&gt;&lt;br /&gt;&lt;ul type="square" style="padding-left: 25px; line-height: 15px;"&gt;&lt;li&gt;fileset&lt;/li&gt;&lt;/ul&gt;The closing buildfile extract shows an example phploc task configuration and is also available at the &lt;a href="http://github.com/raphaelstolt/phploc-phing/tree/master" target="_self"&gt;public GitHub repository&lt;/a&gt;. Happy phplocing!&lt;pre class="xmlSnippet"&gt;&amp;lt;?xml version="1.0"?&amp;gt;&lt;br /&gt;&amp;lt;project name="example" default="phploc" basedir="."&amp;gt;&lt;br /&gt;    &amp;lt;taskdef name="phploc" classname="phing.tasks.my.PHPLocTask" /&amp;gt;&lt;br /&gt;    &amp;lt;target name="phploc"&amp;gt;&lt;br /&gt;      &amp;lt;tstamp&amp;gt;&lt;br /&gt;        &amp;lt;format property="check.date.time" pattern="%Y%m%d-%H%M%S" locale="en_US"/&amp;gt;&lt;br /&gt;      &amp;lt;/tstamp&amp;gt;&lt;br /&gt;      &amp;lt;phploc reportType="txt" reportName="${check.date.time}-report"&lt;br /&gt;              reportDirectory="phploc-reports"&amp;gt;&lt;br /&gt;        &amp;lt;fileset dir="."&amp;gt;&lt;br /&gt;          &amp;lt;include name="**/*.php" /&amp;gt;&lt;br /&gt;          &amp;lt;include name="*.php" /&amp;gt;&lt;br /&gt;        &amp;lt;/fileset&amp;gt;&lt;br /&gt;      &amp;lt;/phploc&amp;gt;&lt;br /&gt;    &amp;lt;/target&amp;gt;&lt;br /&gt;&amp;lt;/project&amp;gt;&lt;/pre&gt;&lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8420118650236071171-6206018638608847732?l=raphaelstolt.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/raphaelonphp/~4/qjZVsS94wcg" height="1" width="1"/&gt;</content><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=8420118650236071171&amp;postID=6206018638608847732" title="2 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/6206018638608847732?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/6206018638608847732?v=2" /><link rel="alternate" type="text/html" href="http://raphaelstolt.blogspot.com/2009/02/phplocing-your-projects-with-phing.html" title="Phplocing your projects with Phing" /><author><name>Raphael Stolt</name><uri>http://www.blogger.com/profile/07949831701855458792</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="14172620075949142098" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">2</thr:total></entry><entry gd:etag="W/&quot;DEYCRng_eip7ImA9WxJSGUs.&quot;"><id>tag:blogger.com,1999:blog-8420118650236071171.post-8847983296330423769</id><published>2009-01-24T05:08:00.032Z</published><updated>2009-05-10T14:36:07.642Z</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-05-10T14:36:07.642Z</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="Ruby" /><category scheme="http://www.blogger.com/atom/ns#" term="Rake" /><title>Broadcasting blog post notifications to Twitter with Ruby and Rake</title><content type="html">&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://www.flickr.com/photos/raphaelstolt/3228209607/" title="Blogger to Twitter Logo by Raphael Stolt, on Flickr"&gt;&lt;img src="http://farm4.static.flickr.com/3421/3228209607_f9e31cfb5b_m.jpg" style="float:left; margin:0 10px 10px 0;cursor:pointer; cursor:hand;width: 240px; height: 103px;" width="240" height="103" border="0" alt="Blogger to Twitter Logo" /&gt;&lt;/a&gt;During my latest blogging absence I had some time to tinker around with Ruby. For an introductory challenge I chose to implement a real life feature which currently isn't supported by Blogger.com and screams siren-like for an one-button automation: Broadcasting the latest blog entry to my Twitter account. As I didn't want to sign up for a &lt;a href="http://twitterfeed.com/" target="_self"&gt;Twitterfeed&lt;/a&gt; account and couldn't resort to the &lt;a href="http://alexking.org/projects/wordpress" target="_self"&gt;Twitter Tools&lt;/a&gt; plugin like WordPress users, I had to perform these broadcasting steps manually, until now. To see how this repetitive and time-stealing process was transformed into a semi-automated one by utilizing Ruby, a splash of &lt;a href="http://code.whytheluckystiff.net/hpricot/" target="_self"&gt;Hpricot&lt;/a&gt;, Ruby's excellent &lt;a href="http://twitter.rubyforge.org/rdoc/" target="_self"&gt;Twitter Api wrapper&lt;/a&gt; and &lt;a href="http://rake.rubyforge.org/" target="_self"&gt;Rake&lt;/a&gt;, read on my dear.&lt;h4 class="custom"&gt;Installing the required RubyGems&lt;/h4&gt;Prior to diving into the implementation details of the given scenario I had to install the required &lt;a href="http://www.rubygems.org/" target="_self"&gt;RubyGems&lt;/a&gt; like shown in the next console snippet. The installation of the twitter gem might take a while due to it's dependency on several other gems.&lt;pre class="consoleOutput"&gt;sudo gem install hpricot rake twitter&lt;/pre&gt;&lt;h4 class="custom"&gt;Scraping the latest blog post details with Hpricot&lt;/h4&gt;The initial implementation step was to gather relevant metadata (Url, title and used tags) of the latest blog post. I first took the route to get it by grabbing the blog's RSS feed and extracting the metadata from there, but soon stumbled into problems getting an outdated feed from Feedburner. The next alternative was to scrape the needed metadata directly from the blog landing page. As I went this route &lt;a href="http://raphaelstolt.blogspot.com/2008/10/scraping-websites-with-zenddomquery.html" target="_self"&gt;before&lt;/a&gt; with the Zend_Dom_Query component of the Zend Framework I decided to use something similar from the Ruby toolbox. Some Google hops later I was sold to Hpricot, a HTML Parser for Ruby and as you can see in the first code snippet, showing an extract of the Rake file to come, this is done in just 13 lines of code.&lt;pre class="codeSnippet"&gt;doc = Hpricot(open(blog_landing_page, scrape_options))&lt;br /&gt;latest_post_url = doc.at('h3.post-title &gt; a')['href']&lt;br /&gt;latest_post_title = doc.at('h3.post-title &gt; a').inner_html&lt;br /&gt;label_doc = Hpricot(doc.search('span.post-labels').first.to_s)&lt;br /&gt;label_links = label_doc.search('span.post-labels &gt; a').each do |label_link|&lt;br /&gt;  label = label_link.inner_html.gsub(' ', '').downcase&lt;br /&gt;  if label.include?('/')&lt;br /&gt;    labels = label.split('/')&lt;br /&gt;    labels.each { |label| last_post_labels.push(label) }&lt;br /&gt;  else&lt;br /&gt;    last_post_labels.push(label)&lt;br /&gt;  end&lt;br /&gt;end&lt;/pre&gt;&lt;h4 class="custom"&gt;Outstanding tasks&lt;/h4&gt;With the metadata available the oustanding tasks to implement were:&lt;ul type="square" style="padding-left: 25px; line-height: 15px;"&gt;&lt;li&gt;to get a short Url for the actual blog post by utilzing a public API of an Url shortening service i.e. &lt;a href="http://is.gd/" target="_self"&gt;is.gd&lt;/a&gt;&lt;/li&gt;&lt;li&gt;to build the tweet to broadcast by injecting the available metadata into a tweet template&lt;/li&gt;&lt;li&gt;to broadcast the notification tweet to the given Twitter account&lt;/li&gt;&lt;li&gt;to log the broadcasted blog title to prevent spamming or duplication scenarios&lt;/li&gt;&lt;/ul&gt;As a guy sold to build tools and eager to learn something new I subverted Rake, Ruby's number one build language, to glue the above mentioned tasks and their implementation together, to manage their sequential dependencies and to have a comfortable invocation interface. The nice thing about Rake is that it allows you to implement each tasks unit of work by using the Ruby language; and there is no need to follow a given structure to implement custom tasks like it's the case for &lt;a href="http://raphaelstolt.blogspot.com/2007/03/rolling-your-own-phing-task.html" target="_self"&gt;custom Phing tasks&lt;/a&gt;. As you will see in the forthcoming complete Rakefile some of the tasks are getting quite long and complex; therefor some of them are pending candidates for Refactoring activities like for example extract task units of work into helper/worker classes.&lt;pre class="codeSnippet"&gt;  require 'rubygems'&lt;br /&gt;  require 'hpricot'&lt;br /&gt;  require 'open-uri'&lt;br /&gt;  require 'twitter'&lt;br /&gt;&lt;br /&gt;  task :default do&lt;br /&gt;    Rake::Task['blog_utils:broadcast_notification'].invoke&lt;br /&gt;  end&lt;br /&gt;&lt;br /&gt;  namespace :blog_utils do&lt;br /&gt;&lt;br /&gt;    scrape_options = { 'UserAgent' =&gt; "Ruby/#{RUBY_VERSION}" }&lt;br /&gt;    blog_landing_page = 'http://raphaelstolt.blogspot.com'&lt;br /&gt;    latest_post_short_url, latest_post_url, latest_post_title = nil&lt;br /&gt;    notification_tweet = nil&lt;br /&gt;    last_post_labels = []&lt;br /&gt;    broadcast_log_file = File.dirname(__FILE__) + '/broadcasted_posts.log'&lt;br /&gt;    twitter_credentials = { :user =&gt; 'raphaelstolt', :pwd =&gt; 'thatsasecret'}&lt;br /&gt;&lt;br /&gt;    desc 'Scrape metadata of latest blog post from landing page'&lt;br /&gt;    task :scrape_actual_post_metadata do&lt;br /&gt;      doc = Hpricot(open(blog_landing_page, scrape_options))&lt;br /&gt;      latest_post_url = doc.at('h3.post-title &gt; a')['href']&lt;br /&gt;      latest_post_title = doc.at('h3.post-title &gt; a').inner_html&lt;br /&gt;      label_doc = Hpricot(doc.search('span.post-labels').first.to_s)&lt;br /&gt;      label_links = label_doc.search('span.post-labels &gt; a').each do |label_link|&lt;br /&gt;        label = label_link.inner_html.gsub(' ', '').downcase&lt;br /&gt;        if label.include?('/')&lt;br /&gt;          labels = label.split('/')&lt;br /&gt;          labels.each { |label| last_post_labels.push(label) }&lt;br /&gt;        else&lt;br /&gt;          last_post_labels.push(label)&lt;br /&gt;        end&lt;br /&gt;      end&lt;br /&gt;    end&lt;br /&gt;&lt;br /&gt;    desc 'Shorten the Url of the latest blog post'&lt;br /&gt;    task :shorten_post_url =&gt; [:scrape_actual_post_metadata] do&lt;br /&gt;      raise_message = 'No Url for latest blog post available'&lt;br /&gt;      raise raise_message if latest_post_url.nil?&lt;br /&gt;      url_shorten_service_call = "http://is.gd/api.php?longurl=#{latest_post_url}"&lt;br /&gt;      latest_post_short_url = open(url_shorten_service_call, scrape_options).read&lt;br /&gt;    end&lt;br /&gt;&lt;br /&gt;    desc 'Check if generate shorten Url references the latest blog post url'&lt;br /&gt;    task :check_shorten_url_references_latest do&lt;br /&gt;      url_referenced_by_short_url = nil&lt;br /&gt;      open(latest_post_short_url, scrape_options) do |f|&lt;br /&gt;        url_referenced_by_short_url = f.base_uri.to_s&lt;br /&gt;      end &lt;br /&gt;      raise_message = "Generated short Url '#{latest_post_short_url}' does not"&lt;br /&gt;      raise_message &lt;&lt; " reference actual blog post url '#{latest_post_url}'"&lt;br /&gt;      raise raise_message unless url_referenced_by_short_url.eql?(latest_post_url)&lt;br /&gt;    end&lt;br /&gt;&lt;br /&gt;    desc 'Check if latest blog post has already been broadcasted'&lt;br /&gt;    task :check_logged_broadcasts do&lt;br /&gt;      logged_broadcasts = []&lt;br /&gt;      if  File.exist?(broadcast_log_file)&lt;br /&gt;        File.open(broadcast_log_file, 'r') do |f|&lt;br /&gt;          logged_broadcasts = f.readlines.collect { |line| line.chomp }&lt;br /&gt;        end&lt;br /&gt;      end&lt;br /&gt;      raise_message = "Blog post '#{latest_post_title}' has already been "&lt;br /&gt;      raise_message &lt;&lt; "broadcasted"&lt;br /&gt;      raise raise_message if logged_broadcasts.include?(latest_post_title)&lt;br /&gt;    end&lt;br /&gt;&lt;br /&gt;    desc 'Build notification tweet by injecting scraped metadata into template'&lt;br /&gt;    task :build_notification_tweet =&gt; [:shorten_post_url, &lt;br /&gt;      :check_shorten_url_references_latest] do&lt;br /&gt;      raise_message = 'Required metadata to build tweet is not available'&lt;br /&gt;      raise raise_message if latest_post_title.nil? || latest_post_short_url.nil? &lt;br /&gt;      raise raise_message if last_post_labels.nil?&lt;br /&gt;&lt;br /&gt;      notification_tweet = "Published a new blog post '#{latest_post_title}' "&lt;br /&gt;      notification_tweet &lt;&lt; "available at #{latest_post_short_url}."&lt;br /&gt;&lt;br /&gt;      raise_message = 'Broadcast for latest blog post exceeds 140 characters'&lt;br /&gt;      raise raise_message if notification_tweet.length &gt; 140&lt;br /&gt;&lt;br /&gt;      last_post_labels.each do |tag|&lt;br /&gt;        notification_tweet &lt;&lt; " ##{tag}" unless notification_tweet.length + &lt;br /&gt;          " ##{tag}".length &gt; 140&lt;br /&gt;      end&lt;br /&gt;    end&lt;br /&gt;&lt;br /&gt;    desc 'Broadcast latest blog post notification to twitter'&lt;br /&gt;    task :broadcast_notification_to_twitter =&gt; [:build_notification_tweet, &lt;br /&gt;      :check_logged_broadcasts] do&lt;br /&gt;      raise_message = "Notification tweet to broadcast is not available"&lt;br /&gt;      raise raise_message if notification_tweet.nil?&lt;br /&gt;      puts "Broadcasting '#{notification_tweet}'"&lt;br /&gt;      http_auth = Twitter::HTTPAuth.new(twitter_credentials[:user], twitter_credentials[:pwd])&lt;br /&gt;      Twitter::Base.new(http_auth).update(notification_tweet)&lt;br /&gt;      #Twitter::Base.new(twitter_credentials[:user], twitter_credentials[:pwd]).post(notification_tweet)&lt;br /&gt;      Rake::Task['blog_utils:log_broadcast_title'].invoke&lt;br /&gt;    end&lt;br /&gt;&lt;br /&gt;    desc 'Log broadcasted blog post title'&lt;br /&gt;    task :log_broadcast_title do&lt;br /&gt;      puts "Logging latest post title to #{broadcast_log_file}"&lt;br /&gt;      File.open(broadcast_log_file, 'a') do |f|&lt;br /&gt;        f.puts latest_post_title&lt;br /&gt;      end&lt;br /&gt;    end&lt;br /&gt;&lt;br /&gt;  end&lt;/pre&gt;&lt;h4 class="custom"&gt;Putting the Rake task(s) to work&lt;/h4&gt;The next step was to put the Rakefile into my $HOME directory; and after publishing a new blog post I'm now able to broadcast an automated notification by firing up the console and calling the Rake task like shown next.&lt;pre class="consoleOutput"&gt;sudo rake -f $HOME/Automations/Rakefile.rb blog_utils:broadcast_notification&lt;/pre&gt;And as I'm too lazy to type this lengthy command everytime I further added an alias to the $HOME/.profile file which allows me to call the task via the associated alias i.e. blogger2twitter shown in the .profile excerpt.&lt;pre class="codeSnippet"&gt;alias blogger2twitter='sudo rake -f $HOME/Automations/Rakefile.rb blog_utils:broadcast_notification'&lt;/pre&gt;After running the Rake task against this blog post the notification gets added to the given Twitter timeline like shown in the outro image.&lt;br /&gt;&lt;br /&gt;&lt;a href="http://www.flickr.com/photos/raphaelstolt/3261723626/" title="Notification tweet by Raphael Stolt, on Flickr"&gt;&lt;img src="http://farm4.static.flickr.com/3434/3261723626_56516e74f4_o.gif" width="523" height="89" border="0" alt="Notification tweet screenshot" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8420118650236071171-8847983296330423769?l=raphaelstolt.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/raphaelonphp/~4/dtGq_oxLh20" height="1" width="1"/&gt;</content><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=8420118650236071171&amp;postID=8847983296330423769" title="2 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/8847983296330423769?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/8847983296330423769?v=2" /><link rel="alternate" type="text/html" href="http://raphaelstolt.blogspot.com/2009/01/broadcasting-blog-post-notifications-to.html" title="Broadcasting blog post notifications to Twitter with Ruby and Rake" /><author><name>Raphael Stolt</name><uri>http://www.blogger.com/profile/07949831701855458792</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="14172620075949142098" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">2</thr:total></entry><entry gd:etag="W/&quot;CE4FRH4_fyp7ImA9WxVRF0s.&quot;"><id>tag:blogger.com,1999:blog-8420118650236071171.post-8224899344727821893</id><published>2009-01-23T13:19:00.032Z</published><updated>2009-01-24T02:08:35.047Z</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-01-24T02:08:35.047Z</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="Zend Framework" /><title>Installing Zend_Tool on Mac OS X</title><content type="html">Yesterday I decided to tiptoe into the development of custom Zend_Tool Providers as the introductional &lt;a href="http://devzone.zend.com/article/4124-Zend_Tool-for-the-Developer" target="_self"&gt;article&lt;/a&gt; series by Ralph Schindler motivated me to learn more about it and I already have some useful use cases on my mind. Therefor I prior had to install the Zend_Tool component and it's driving CLI scripts on my MacBook. The following brief instruction describes a possible approach that got me running in no time on a Mac OS X system. Once the Zend Framework has an official PEAR channel most of the forthcoming steps should be obsolete and entirely performed by the PEAR package installer command.&lt;h4 class="custom"&gt;Fetching and installing the Zend_Tool component&lt;/h4&gt;First I tried to install the 1.8.0(devel) version of the Zend Framework via the &lt;a href="http://pear.zfcampus.org/" target="_self"&gt;pear.zfcampus.org&lt;/a&gt; PEAR channel but it currently only delivers the 1.7.3PL1(stable) package; even after switching the stability state of the PEAR config. To dodge the include_path setting hassle and for a further use when customizing other tools like Phing tasks I decided to keep the installed package.&lt;pre class="consoleOutput"&gt;sudo pear channel-discover pear.zfcampus.org&lt;br /&gt;sudo pear install zfcampus/zf-devel&lt;/pre&gt;The next commands are showing the footwork I had to do to get the Zend_Tool component into the PEAR Zend Framework package installed in &lt;em&gt;/opt/local/lib/php/Zend&lt;/em&gt;.&lt;pre class="consoleOutput"&gt;sudo svn co http://framework.zend.com/svn/framework/standard/incubator/library/Zend/Tool/ $HOME/Cos/Zend/Tool&lt;br /&gt;sudo rsync -r --exclude=.svn $HOME/Cos/Zend/Tool /opt/local/lib/php/Zend&lt;/pre&gt;&lt;h4 class="custom"&gt;Putting the Zend_Tool CLI scripts to work&lt;/h4&gt;The next steps were to fetch the CLI scripts from the public Subversion repository and to link them into the system path &lt;em&gt;/opt/local/bin&lt;/em&gt; as shown in the next commands. &lt;pre class="consoleOutput"&gt;sudo svn co http://framework.zend.com/svn/framework/standard/incubator/bin $HOME/Cos/Zend/bin&lt;br /&gt;sudo ln $HOME/Cos/Zend/bin/zf.sh /opt/local/bin/zf&lt;br /&gt;sudo ln $HOME/Cos/Zend/bin/zf.php /opt/local/bin/zf.php&lt;/pre&gt;&lt;h4 class="custom"&gt;Checking the installation&lt;/h4&gt;With everything hopefully in place it was time to verify the success of the installation via the below stated provider action call; and as I got the version of the installed Zend Framework as a response of the executed action/command I'm good to go.&lt;pre class="consoleOutput"&gt;zf show version&lt;/pre&gt;&lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8420118650236071171-8224899344727821893?l=raphaelstolt.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/raphaelonphp/~4/Ev8OQ2xeZPM" height="1" width="1"/&gt;</content><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=8420118650236071171&amp;postID=8224899344727821893" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/8224899344727821893?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/8224899344727821893?v=2" /><link rel="alternate" type="text/html" href="http://raphaelstolt.blogspot.com/2009/01/installing-zendtool-on-mac-os-x.html" title="Installing Zend_Tool on Mac OS X" /><author><name>Raphael Stolt</name><uri>http://www.blogger.com/profile/07949831701855458792</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="14172620075949142098" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total></entry><entry gd:etag="W/&quot;CUACQX4zcCp7ImA9WxRbF0g.&quot;"><id>tag:blogger.com,1999:blog-8420118650236071171.post-4175798713421271826</id><published>2008-11-13T15:21:00.007Z</published><updated>2008-12-08T16:29:20.088Z</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2008-12-08T16:29:20.088Z</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="PHP" /><category scheme="http://www.blogger.com/atom/ns#" term="Book reviews" /><category scheme="http://www.blogger.com/atom/ns#" term="Ruby/Rails" /><title>Rails for PHP Developers book review</title><content type="html">&lt;img style="margin: 0pt 10px 10px 0pt; float: left; cursor: pointer; width: 190px; height: 228px;" src="http://farm3.static.flickr.com/2151/2274548280_0d6516d8a4_o.jpg" alt="Rails for PHP Developers" title="Rails for PHP Developers" border="0" /&gt;The e-book version of the Pragmatic Programmers release &lt;a href="http://www.pragprog.com/titles/ndphpr/rails-for-php-developers" target="_self"&gt;Rails for PHP Developers&lt;/a&gt; written by Derek DeVries and Mike Naberezny occupies now some of my scarce hard drive space for several months, and today I managed to hit the last page of it. In case you're interested in knowing if it's worthy to sacrifice some rare hard drive or bookshelf space for this book read on.&lt;h4 class="custom"&gt;What's in it?&lt;/h4&gt;The book consists of three main parts which are addressing open-minded developers with a PHP background tempted to add the Ruby language and the thereupon built Rails 2.0 framework to their toolset. &lt;br /&gt;&lt;br /&gt;The first part introduces the classic and nowadays omnipresent MVC pattern, the concepts and conventions of Rails by converting a simple PHP newsletter application into a Rails based one. The follow-up chapters of the first part are covering the basics of the Ruby language by looking at known PHP language features and constructs, and how they translate to their Ruby counterparts. Reading these chapters you will get a thorough understanding of the Ruby language and be able to apply unique features like blocks or the reopening of existing classes. The communicated knowledge builds the foundation to accelerate the use and understanding of the Rails framework which is covered in-depth through-out the book's second part. &lt;br /&gt;&lt;br /&gt;While teaming up with their imaginary buddy Joe the authors walk you through building a Rails user group application. The chapters of the second part are covering a lot of ground reaching from domain modeling, putting the particular MVC parts to work, ensuring quality by utilizing the Test::Unit library to finally deploying the application into a &lt;strike&gt;productive&lt;/strike&gt; production environment. &lt;br /&gt;&lt;br /&gt;The first two chapters of the final and reference part cover the differences and similarities between PHP and Ruby data structures, operations and language constructs. The final chapter of the book closes with a web development specific comparision of PHP constructs and approaches to the ones used by the Rails framework. The book is accompanied by a dedicated &lt;a href="http://railsforphp.com/"&gt;blog&lt;/a&gt; and a &lt;a href="http://railsforphp.com/reference" target="_self"&gt;PHP to Rails online reference&lt;/a&gt; to satisfy severe thirst for more knowledge.&lt;h4 class="custom"&gt;Conclusion&lt;/h4&gt;The book provides interested PHP developers a thorough introduction to the Ruby language and the Rails framework in a fluent and enjoyable writing tone. By implementing the example application of the second book part any decent PHP developer will derive a solid understanding of the Rails framework, he can build upon and that puts him in the position to make reasonable judgments for using/flaming it or not. IMHO this book is so far one of the best PHP related book releases of the out fading year 2008, and can be a real motivator to extend the just gained knowledge by diving deeper into the Ruby/Rails ocean. &lt;br /&gt;&lt;br /&gt;So be prepared to see one or another Ruby related post popping up in the future timeline of this blog; I just added another costly addiction to the medicine cupboard. Word!&lt;br /&gt;&lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8420118650236071171-4175798713421271826?l=raphaelstolt.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/raphaelonphp/~4/66LMbdRejE4" height="1" width="1"/&gt;</content><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=8420118650236071171&amp;postID=4175798713421271826" title="2 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/4175798713421271826?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/4175798713421271826?v=2" /><link rel="alternate" type="text/html" href="http://raphaelstolt.blogspot.com/2008/11/rails-for-php-developers-book-review.html" title="Rails for PHP Developers book review" /><author><name>Raphael Stolt</name><uri>http://www.blogger.com/profile/07949831701855458792</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="14172620075949142098" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">2</thr:total></entry><entry gd:etag="W/&quot;CkMBRXw8cSp7ImA9WxRWFEQ.&quot;"><id>tag:blogger.com,1999:blog-8420118650236071171.post-3117637992915026495</id><published>2008-10-31T21:44:00.002Z</published><updated>2008-10-31T22:00:54.279Z</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2008-10-31T22:00:54.279Z</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="Zend Framework" /><category scheme="http://www.blogger.com/atom/ns#" term="PHP" /><title>Tinyizing URLs with Zend_Http_Client</title><content type="html">While doing some initial research for a blog related automation task to implement I learned some more about services which transform long URLs into short ones. The well-knownst of these services, due to the Twitter hype, is probably &lt;a href="http://tinyurl.com/" target="_self"&gt;TinyURL&lt;/a&gt; which can be accessed via a classic webinterface or by calling a public API. In a recent &lt;a href="http://www.davedevelopment.co.uk/2008/10/13/zend-framework-and-the-twitter-api/" target="_self"&gt;blog post&lt;/a&gt; Dave Marshall outlined a quick workaround for tweeting via the &lt;a href="http://framework.zend.com/manual/en/zend.http.html#zend.http.client" target="_self"&gt;Zend_Http_Client&lt;/a&gt; component which is a reasonable approach for calling services that aren't in the Zend Framework core yet like &lt;a href="http://framework.zend.com/wiki/display/ZFPROP/Zend_Service_Twitter" target="_self"&gt;Zend_Service_Twitter&lt;/a&gt; or are not supported out of the box. Therefore this post will try to describe a Zend Framework way of creating tinyized URLs.&lt;h4 class="custom"&gt;Getting tiny tiny y'all&lt;/h4&gt;According to Wikipedia there are numerous services available e.g. &lt;a href="http://www.rubyurl.com/home" target="_self"&gt;RubyUrl&lt;/a&gt; providing the same feature as TinyURL, so to be prepared for the future and thereby maybe violating the &lt;a href="http://en.wikipedia.org/wiki/YAGNI" target="_self"&gt;YAGNI principle&lt;/a&gt; I decided to declare a very basic interface first in case of switching the service provider someday.&lt;pre class="codeSnippet"&gt;&amp;lt;?php&lt;br /&gt;/** &lt;br /&gt; * 'Interface-level' PHPDoc Block   &lt;br /&gt; */&lt;br /&gt;interface Recordshelf_Service_UrlShortener_Interface&lt;br /&gt;{&lt;br /&gt;    public function __construct($serviceEndpoint = '');&lt;br /&gt;    public function shortenize($url);&lt;br /&gt;}&lt;/pre&gt;The next code snippet shows the implementation for the TinyURL service programmed against the interface and hosting an additional alias method called &lt;em&gt;tinyize&lt;/em&gt; which is simply wrapping the actual worker method. The service utilizes Zend_Http_Client by setting the endpoint of the service, transmitting a GET request parameterized with the URL to shorten against it and returning the response containing the tinyized URL.&lt;pre class="codeSnippet"&gt;&amp;lt;?php&lt;br /&gt;require_once('Zend/Http/Client.php');&lt;br /&gt;require_once('Recordshelf/Service/UrlShortener/Interface.php');&lt;br /&gt;/** &lt;br /&gt; * 'Class-level' PHPDoc Block   &lt;br /&gt; */&lt;br /&gt;class Recordshelf_Service_TinyUrl implements&lt;br /&gt;    Recordshelf_Service_UrlShortener_Interface&lt;br /&gt;{&lt;br /&gt;    /**&lt;br /&gt;     * The service endpoint&lt;br /&gt;     *&lt;br /&gt;     * @var string&lt;br /&gt;     */&lt;br /&gt;    private $_serviceEndpoint = null;&lt;br /&gt;    /**&lt;br /&gt;     * Recordshelf service tinyURL constructor&lt;br /&gt;     *&lt;br /&gt;     * @param string $serviceEndpoint&lt;br /&gt;     */&lt;br /&gt;    public function __construct(&lt;br /&gt;        $serviceEndpoint = 'http://tinyurl.com/api-create.php')&lt;br /&gt;    {&lt;br /&gt;        $this-&gt;_serviceEndpoint = $serviceEndpoint;&lt;br /&gt;    }&lt;br /&gt;    /**&lt;br /&gt;     * Shortenizes a given Url&lt;br /&gt;     *&lt;br /&gt;     * @param string $url&lt;br /&gt;     * @return string&lt;br /&gt;     * @throws Exception&lt;br /&gt;     */&lt;br /&gt;    public function shortenize($url) {&lt;br /&gt;        if (is_null($this-&gt;_serviceEndpoint)) {&lt;br /&gt;            throw new Exception('No service endpoint set');&lt;br /&gt;        }&lt;br /&gt;        $client = new Zend_Http_Client($this-&gt;_serviceEndpoint);&lt;br /&gt;        $client-&gt;setParameterGet('url', $url)&lt;br /&gt;               -&gt;setMethod(Zend_Http_Client::GET);&lt;br /&gt;        try {&lt;br /&gt;            $response = $client-&gt;request();&lt;br /&gt;        } catch (Exception $e) {&lt;br /&gt;            throw $e;&lt;br /&gt;        }&lt;br /&gt;&lt;br /&gt;        if (200 === $response-&gt;getStatus()) {&lt;br /&gt;            return $response-&gt;getBody();&lt;br /&gt;        } else {&lt;br /&gt;            throw new Exception($response-&gt;getStatus() . ": " .&lt;br /&gt;                $response-&gt;getMessage());&lt;br /&gt;        }&lt;br /&gt;    }&lt;br /&gt;    /**&lt;br /&gt;     * Alias method for the shortenize method&lt;br /&gt;     *&lt;br /&gt;     * @param string $url&lt;br /&gt;     * @throws Exception&lt;br /&gt;     * @see shortenize&lt;br /&gt;     */&lt;br /&gt;    public function tinyize($url)&lt;br /&gt;    {&lt;br /&gt;        return $this-&gt;shortenize($url);&lt;br /&gt;    }&lt;br /&gt;}&lt;/pre&gt;Now with everything hopefully operating smoothly it's time for a test-drive, yeah I'm lazy and cut that development approach called TDD, by creating a service instance and requesting a TinyURL for the Zend Framework website as shown in the outro listing.&lt;pre class="codeSnippet"&gt;&amp;lt;?php&lt;br /&gt;$service = new Recordshelf_Service_TinyUrl();&lt;br /&gt;$service-&gt;tinyize('http://framework.zend.com');&lt;br /&gt;// =&gt; http://tinyurl.com/nf8kf&lt;/pre&gt;In case off considering or favouring a more framework independent approach there are also other blends available like one via &lt;a href="http://snippets.dzone.com/posts/show/4720" target="_self"&gt;file_get_contents&lt;/a&gt; or via &lt;a href="http://strategicv.com/2008/09/29/code-snippet-tinyurl-link-creation-api/" target="_self"&gt;curl&lt;/a&gt;. Happy tinyizing!&lt;br /&gt;&lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8420118650236071171-3117637992915026495?l=raphaelstolt.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/raphaelonphp/~4/IoX7BeWomBY" height="1" width="1"/&gt;</content><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=8420118650236071171&amp;postID=3117637992915026495" title="6 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/3117637992915026495?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/3117637992915026495?v=2" /><link rel="alternate" type="text/html" href="http://raphaelstolt.blogspot.com/2008/10/tinyizing-urls-with-zendhttpclient.html" title="Tinyizing URLs with Zend_Http_Client" /><author><name>Raphael Stolt</name><uri>http://www.blogger.com/profile/07949831701855458792</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="14172620075949142098" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">6</thr:total></entry><entry gd:etag="W/&quot;CkQHSH45eSp7ImA9WxRWEEg.&quot;"><id>tag:blogger.com,1999:blog-8420118650236071171.post-5045155133811560111</id><published>2008-10-26T19:28:00.004Z</published><updated>2008-10-26T19:45:39.021Z</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2008-10-26T19:45:39.021Z</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="PHP" /><category scheme="http://www.blogger.com/atom/ns#" term="Phing" /><title>Getting a visualization of a Phing buildfile</title><content type="html">Today I spent some time to get a tool running to visualize Phing buildfiles as this can come in handy for maintaing, &lt;a href="http://raphaelstolt.blogspot.com/2008/07/six-valuable-phing-build-file.html" target="_self"&gt;refactoring&lt;/a&gt; or extending large buildfiles. Out of the box the Phing &lt;em&gt;-l&lt;/em&gt; option can be used to &lt;a href="http://raphaelstolt.blogspot.com/2008/03/getting-overview-of-all-targets.html" target="_self"&gt;get a first overview of all available targets in a given buildfile&lt;/a&gt; but it doesn't untangle the target dependencies and sometimes a picture is still worth a thousand words. Luckily the Ant community already provides several tools to accomplish the visualization of Ant buildfiles, reaching from solutions that apply a Xslt stylesheet upon a given buildfile e.g. &lt;a href="http://ant2dot.sourceforge.net/" target="_self"&gt;ant2dot&lt;/a&gt; to those ones that take a programmatically approach e.g. &lt;a href="http://www.ggtools.net/grand/" target="_self"&gt;Grand&lt;/a&gt;. All these solutions utilize &lt;a href="http://www.graphviz.org/" target="_self"&gt;Graphiz&lt;/a&gt; to generate a graphic from a DOT file representing the buildfile structure, it's targets and their dependencies. As Phing is a very close descendant of Ant the Xslt approach was best suited and the one with the least effort because their buildfile markup is very similar. The following post will walk you through on how to get a simple Phing buildfile visualization tool running in just a few minutes.&lt;h4 class="custom"&gt;Grabbing the Xslt file&lt;/h4&gt;The first step is to get the ant2dot Xslt &lt;a href="http://ant2dot.sourceforge.net/xsl/ant2dot.xsl" target="_self"&gt;stylesheet&lt;/a&gt; and put it into the same directory as the visualization buildfile and target to come. Due to the aforementioned Phing and Ant buildfile markup similarities it can be used without any modfications.&lt;h4 class="custom"&gt;Setting up the buildfile visualization target&lt;/h4&gt;The next step is to create a Phing target that utilizes the &lt;a href="http://phing.info/docs/guide/current/chapters/appendixes/AppendixB-CoreTasks.html#XsltTask" target="_self"&gt;Xslt task&lt;/a&gt; to transfrom the fed buildfile into a DOT file which gets passed further to a platform dependent &lt;a href="http://phing.info/docs/guide/current/chapters/appendixes/AppendixB-CoreTasks.html#ExecTask" target="_self"&gt;Exec task&lt;/a&gt; handling the final transformation into a PNG image. To make the visualization target independent from the buildfile to visualize it's hosted in an own buildfile and the target accepts the buildfile to be transformed as a property passed to the Phing Cli or if none given uses the default build.xml. Further the Xslt stylesheet accepts several parameters to add extended data to the resulting DOT file/PNG image which can be set in the &amp;lt;param&amp;gt; tags of the Xslt task. For a list of possible parameters have a look at the &lt;a href="http://ant2dot.sourceforge.net/#options" target="_self"&gt;options&lt;/a&gt; section of ant2dot. The following codesnippet shows the visualization buildfile and the &lt;em&gt;visualize&lt;/em&gt; target doing the Whodini like magic.&lt;pre class="xmlSnippet"&gt;&amp;lt;?xml version="1.0"?&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;project name="buildfile-visualizer" default="visualize" basedir="."&amp;gt;    &lt;br /&gt;&lt;br /&gt;  &amp;lt;target name="visualize" &lt;br /&gt;          description="Generates a visualization(PNG image) of a given buildfile"&amp;gt;&lt;br /&gt;    &amp;lt;property name="buildfile" value="build.xml" /&amp;gt;&lt;br /&gt;    &amp;lt;property name="phing2dot.xsl" value="${project.basedir}/ant2dot.xsl" /&amp;gt;&lt;br /&gt;    &amp;lt;property name="dot.file" value="${buildfile}.dot" /&amp;gt;&lt;br /&gt;    &amp;lt;property name="png.file" value="${buildfile}.png" /&amp;gt;&lt;br /&gt;    &amp;lt;property name="dot.command.win" value="dot.exe -Tpng ${dot.file} -o ${png.file}" /&amp;gt;&lt;br /&gt;    &amp;lt;property name="dot.command.mac" value="dot -Tpng ${dot.file} -o ${png.file}" /&amp;gt;&lt;br /&gt;    &amp;lt;!-- Transform buildfile into DOT file --&amp;gt;&lt;br /&gt;    &amp;lt;xslt file="${buildfile}" tofile="${project.basedir}/${dot.file}" &lt;br /&gt;          style="${phing2dot.xsl}" overwrite="true"&amp;gt; &lt;br /&gt;      &amp;lt;param name="graph.label" expression="${buildfile}" /&amp;gt;&lt;br /&gt;      &amp;lt;param name="use.target.description" expression="true" /&amp;gt;&lt;br /&gt;    &amp;lt;/xslt&amp;gt;&lt;br /&gt;    &amp;lt;!-- Generate image from DOT file --&amp;gt;&lt;br /&gt;    &amp;lt;exec command="${dot.command.win}" &lt;br /&gt;          dir="${project.basedir}" os="WINNT" /&amp;gt;&lt;br /&gt;    &amp;lt;exec command="${dot.command.mac}" &lt;br /&gt;          dir="${project.basedir}" os="Darwin" /&amp;gt;&lt;br /&gt;    &lt;br /&gt;    &amp;lt;delete file="${project.basedir}/${dot.file}" /&amp;gt;&lt;br /&gt;  &amp;lt;/target&amp;gt;&lt;br /&gt;    &lt;br /&gt;&amp;lt;/project&amp;gt;&lt;/pre&gt;&lt;h4 class="custom"&gt;Running the buildfile visualization target&lt;/h4&gt;Now as mostly all necessary pieces are available it's time to check if the DOT command is available on the targeted platform by running a &lt;em&gt;dot(.exe) -V&lt;/em&gt; on the console. If it isn't available it has to be installed, this might take several minutes depending on the given platform. Finally with everything in place the visualization process/target can be kicked off by calling Phing the&lt;br /&gt;following way.&lt;pre class="consoleOutput"&gt;triton:tmp stolt$ phing -f buildfile-visualizer.xml [-Dbuildfile=&amp;lt;targeted-buildfile.xml&amp;gt;]&lt;/pre&gt;The last picture shows the visualization of the simple buildfile described in the &lt;a href="http://phing.info/docs/guide/current/" target="_self"&gt;Phing Userguide&lt;/a&gt; but it's also possible to get a meaningful &lt;a href="http://www.flickr.com/photos/raphaelstolt/2974505537/" target="_self"&gt;visualization&lt;/a&gt; of larger buildfiles like the one I currently use for &lt;a href="http://raphaelstolt.blogspot.com/2007/08/setting-up-zend-framework applications.html" target="_self"&gt;setting up Zend Framework based projects&lt;/a&gt;.&lt;br /&gt;&lt;a href="http://www.flickr.com/photos/raphaelstolt/2974636277/" title="Visualization of a simple buildfile by Raphael Stolt, on Flickr"&gt;&lt;br /&gt;&lt;img src="http://farm4.static.flickr.com/3030/2974636277_e31efcd536_o.png" width="350" height="133" alt="Visualization of a simple buildfile" border="0" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8420118650236071171-5045155133811560111?l=raphaelstolt.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/raphaelonphp/~4/8UYp3_B9qRw" height="1" width="1"/&gt;</content><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=8420118650236071171&amp;postID=5045155133811560111" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/5045155133811560111?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/5045155133811560111?v=2" /><link rel="alternate" type="text/html" href="http://raphaelstolt.blogspot.com/2008/10/getting-visualization-of-phing.html" title="Getting a visualization of a Phing buildfile" /><author><name>Raphael Stolt</name><uri>http://www.blogger.com/profile/07949831701855458792</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="14172620075949142098" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total></entry><entry gd:etag="W/&quot;CUMBQXoyeCp7ImA9WxVWFEs.&quot;"><id>tag:blogger.com,1999:blog-8420118650236071171.post-383329830967405324</id><published>2008-10-16T18:49:00.007Z</published><updated>2009-02-24T08:17:30.490Z</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-02-24T08:17:30.490Z</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="Zend Framework" /><category scheme="http://www.blogger.com/atom/ns#" term="PHP" /><title>Scraping websites with Zend_Dom_Query</title><content type="html">Today I stumbled upon an interesting and reportable scenario where I had to extract information of the weekly published Drum and Bass &lt;a href="http://www.bbc.co.uk/1xtra/drumbass/chart/" target="_self"&gt;charts&lt;/a&gt; provided by BBC 1Xtra. As this information currently isn't available in any consumer friendly format like for example a RSS feed, I had to go that scraping route but didn't want to hustle with a regex approach. Since version 1.6.0 the &lt;a href="http://framework.zend.com/manual/en/zend.dom.query.html" target="_self"&gt;Zend_Dom_Query&lt;/a&gt; component has been added to the framework mainly to support functional testing of MVC applications, but it also can be used for rolling custom website scrapers in a snap. Woot, perfect match!&lt;br /&gt;&lt;br /&gt;The following code snippets are showing the Bbc_DnbCharts_Scraper class I came up with and an example of its usage. The class utilizes curl to read the website holding the desired data, which will be passed to Zend_Dom_Query to execute queries upon it. For querying the former loaded XHTML &lt;a href="http://en.wikipedia.org/wiki/Document_Object_Model" target="_self"&gt;Document Object Model&lt;/a&gt; it's possible to either utilize XPath or CSS selectors. So I had to pick my poison, and decided to go with the CSS selectors as them were best suited for the document to query and will be more familiar to most jQuery or Prototype users. The query returns a result set of all matching &lt;a href="http://de3.php.net/manual/de/class.domelement.php" target="_self"&gt;DOMElement&lt;/a&gt;s which are further unpuzzled via a private helper method returning just the desired charts data as shown in the closing listing. As you can see the implementation of the scraping can be done with a minimum of effort and these are exactly the moments I love the Zend Framework for.&lt;pre class="codeSnippet"&gt;&amp;lt;?php&lt;br /&gt;require_once('Zend/Dom/Query.php');&lt;br /&gt;/** &lt;br /&gt; * 'Class-level' PHPDoc Block   &lt;br /&gt; */&lt;br /&gt;class Bbc_DnbCharts_Scraper&lt;br /&gt;{&lt;br /&gt;    private $_url = null;&lt;br /&gt;    private $_xhtml = null;&lt;br /&gt;&lt;br /&gt;    /**&lt;br /&gt;     * @param string $url&lt;br /&gt;     */&lt;br /&gt;    public function __construct($url)&lt;br /&gt;    {&lt;br /&gt;        $this-&gt;_url = $url;&lt;br /&gt;    }&lt;br /&gt;    /**&lt;br /&gt;     * Scrapes off the drum and bass charts content from the BBC 1Xtra website.     &lt;br /&gt;     *&lt;br /&gt;     * @return array&lt;br /&gt;     * @throws Exception&lt;br /&gt;     */&lt;br /&gt;    public function scrape()&lt;br /&gt;    {&lt;br /&gt;        try {&lt;br /&gt;            $dom = new Zend_Dom_Query($this-&gt;_getXhtml());&lt;br /&gt;        } catch (Exception $e) {&lt;br /&gt;            throw $e;        &lt;br /&gt;        }&lt;br /&gt;        $results = $dom-&gt;query('div.chart div');&lt;br /&gt;        $chartDetails = array();&lt;br /&gt;        foreach ($results as $index =&gt; $result) {&lt;br /&gt;            /* @var $result DOMElement */&lt;br /&gt;            if ($result-&gt;nodeValue !== '') { //filter out &amp;lt;br /&amp;gt; element&lt;br /&gt;                $chartDetails[] = $result-&gt;nodeValue;&lt;br /&gt;            }&lt;br /&gt;        }&lt;br /&gt;        return $this-&gt;_unpuzzleChartDetails($chartDetails, true);&lt;br /&gt;    }&lt;br /&gt;    /**&lt;br /&gt;     * Unpuzzles the chart details and groups them by their chart position, &lt;br /&gt;     * if desired with associative keys.&lt;br /&gt;     *  &lt;br /&gt;     * @param array $details&lt;br /&gt;     * @param boolean $associative&lt;br /&gt;     * @return array&lt;br /&gt;     */&lt;br /&gt;    private function _unpuzzleChartDetails(array $details, $associative = false)&lt;br /&gt;    {&lt;br /&gt;        if (0 === count($details)) {&lt;br /&gt;            return array();&lt;br /&gt;        } else {  &lt;br /&gt;            $nextChartRank = 2;&lt;br /&gt;            $charts = array();&lt;br /&gt;            $groupedChartDetails = array();&lt;br /&gt;            &lt;br /&gt;            foreach ($details as $index =&gt; $chartDetail)&lt;br /&gt;            { &lt;br /&gt;                if ($index &lt;= $nextChartRank) {&lt;br /&gt;                    $groupedChartDetails[] = $chartDetail;&lt;br /&gt;                }&lt;br /&gt;                if ($index == $nextChartRank) {&lt;br /&gt;                    $nextChartRank+=3;&lt;br /&gt;                    $charts[] = $groupedChartDetails;&lt;br /&gt;                    unset($groupedChartDetails);&lt;br /&gt;                }&lt;br /&gt;            }&lt;br /&gt;            if ($associative) {&lt;br /&gt;                $associatives = array('artist', 'tune', 'label');&lt;br /&gt;                foreach ($charts as $chartsIndex =&gt; $chart) {&lt;br /&gt;                    unset($charts[$chartsIndex]);&lt;br /&gt;                    foreach ($chart as $chartIndex =&gt; $chartDetails) {&lt;br /&gt;                        $charts[$chartsIndex][$associatives[$chartIndex]] = &lt;br /&gt;                            $chartDetails;&lt;br /&gt;                    }&lt;br /&gt;                }&lt;br /&gt;            }&lt;br /&gt;            return $charts;&lt;br /&gt;        }&lt;br /&gt;    }&lt;br /&gt;    /**&lt;br /&gt;     * Gets the XHTML document via curl &lt;br /&gt;     * &lt;br /&gt;     * @return string&lt;br /&gt;     * @throws Exception&lt;br /&gt;     */&lt;br /&gt;    private function _getXhtml()&lt;br /&gt;    {&lt;br /&gt;        $curl = curl_init();&lt;br /&gt;        if (!$curl) {&lt;br /&gt;            throw new Exception('Unable to init curl. ' . curl_error($curl));&lt;br /&gt;        }&lt;br /&gt;        curl_setopt($curl, CURLOPT_URL, $this-&gt;_url);&lt;br /&gt;        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);&lt;br /&gt;        // Faking user agent&lt;br /&gt;        curl_setopt($curl, CURLOPT_USERAGENT, &lt;br /&gt;            'Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)');&lt;br /&gt;        $xhtml = curl_exec($curl);&lt;br /&gt;        if (!$xhtml) {&lt;br /&gt;            throw new Exception('Unable to read XHTML. ' . curl_error($curl));&lt;br /&gt;        }&lt;br /&gt;        curl_close($curl);&lt;br /&gt;        return $xhtml; &lt;br /&gt;    }&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;// Usage demo&lt;br /&gt;$scraper = new Bbc_DnbCharts_Scraper('http://www.bbc.co.uk/1xtra/drumbass/chart/');&lt;br /&gt;$charts = $scraper-&gt;scrape();&lt;/pre&gt;The closing code snippet shows an extract of the Drum and Bass charts from &lt;a href="http://www.bbc.co.uk/1xtra/" target="_self"&gt;BBC 1Xtra&lt;/a&gt; scraped off around the 16th October 2008.&lt;pre class="codeSnippet"&gt;Array&lt;br /&gt;(&lt;br /&gt;    [0] =&gt; Array&lt;br /&gt;        (&lt;br /&gt;            [artist] =&gt; Chase &amp; Status Ft Plan B&lt;br /&gt;            [tune] =&gt; Pieces&lt;br /&gt;            [label] =&gt; Ram Records&lt;br /&gt;        )&lt;br /&gt;&lt;br /&gt;    ...&lt;br /&gt;    &lt;br /&gt;    [9] =&gt; Array&lt;br /&gt;        (&lt;br /&gt;            [artist] =&gt; Zen&lt;br /&gt;            [tune] =&gt; Full Effect&lt;br /&gt;            [label] =&gt; Flipmode Audio&lt;br /&gt;        )&lt;br /&gt;&lt;br /&gt;)&lt;/pre&gt;&lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8420118650236071171-383329830967405324?l=raphaelstolt.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/raphaelonphp/~4/fQQ9MPD5B9s" height="1" width="1"/&gt;</content><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=8420118650236071171&amp;postID=383329830967405324" title="13 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/383329830967405324?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/383329830967405324?v=2" /><link rel="alternate" type="text/html" href="http://raphaelstolt.blogspot.com/2008/10/scraping-websites-with-zenddomquery.html" title="Scraping websites with Zend_Dom_Query" /><author><name>Raphael Stolt</name><uri>http://www.blogger.com/profile/07949831701855458792</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="14172620075949142098" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">13</thr:total></entry><entry gd:etag="W/&quot;DkIHQnY6eCp7ImA9WxdUEks.&quot;"><id>tag:blogger.com,1999:blog-8420118650236071171.post-472328189184808571</id><published>2008-07-28T17:38:00.006+01:00</published><updated>2008-07-28T18:02:13.810+01:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2008-07-28T18:02:13.810+01:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="Refactoring" /><category scheme="http://www.blogger.com/atom/ns#" term="PHP" /><category scheme="http://www.blogger.com/atom/ns#" term="PHPUnit" /><category scheme="http://www.blogger.com/atom/ns#" term="Best Practice" /><title>Creating custom PHPUnit assertions</title><content type="html">While developing PHP applications and applying developer testing the applications safety net will grow along the timeline, and as normal code, test code should be in a fresh, odour free state too. A common test code smell, amongst others, is the duplication of assertion logic which can reduce reusability, readability and thereby obscure the specific verification intention of tests. To subdue this special smell several &lt;a href="http://xunitpatterns.com/"&gt;patterns and refactorings&lt;/a&gt; are available to acquaint the test code with the DRY principle. So in this blog post I'd like to set the focus on some of the aspects of the &lt;a href="http://xunitpatterns.com/Custom%20Assertion.html"&gt;Custom Assertion&lt;/a&gt; pattern, by showing how to create custom &lt;a href="http://www.phpunit.de/"&gt;PHPUnit&lt;/a&gt; assertions, which attacks the above mentioned smell and its retroactive effects with a huge antiperspirant flagon, while also providing the chance to build a customer friendly and domain related test vocabulary.&lt;br /&gt;&lt;br /&gt;The first introductive code snippet shows an example of &lt;em&gt;unwanted&lt;/em&gt; code duplications and smells in a PHPUnit(i.e. version: 3.2.21) test case class spreading over several test methods. The first test code duplications smell is present when verifying that a given bag is having an expected item count and an explicit 'intent obscuration' smell can be spotted when verifying the bags stock id against an assumed convention via a 'distracting' regular expression.&lt;pre class="codeSnippet"&gt;&amp;lt;?php&lt;br /&gt;require_once 'PHPUnit/Framework.php';&lt;br /&gt;require_once 'Record/Bag.php';&lt;br /&gt;require_once 'Record/Item.php';&lt;br /&gt;&lt;br /&gt;class Record_Bag_Test extends PHPUnit_Framework_TestCase &lt;br /&gt;{&lt;br /&gt;    private $_bag = null;&lt;br /&gt;    &lt;br /&gt;    protected function setUp() &lt;br /&gt;    {&lt;br /&gt;        /**&lt;br /&gt;         * Creates a new named bag with an unique stock id&lt;br /&gt;         * in a format of AAA-NN-AA-NNNNNNNN e.g. XDS-76-YS-00000124, &lt;br /&gt;         * where A stands for an alphanumeric and N for a numeric character.&lt;br /&gt;         */&lt;br /&gt;        $this-&gt;_bag = new Record_Bag('Test_Bag');&lt;br /&gt;    }&lt;br /&gt;    &lt;br /&gt;    /**&lt;br /&gt;     * @test&lt;br /&gt;     */&lt;br /&gt;    public function bagShouldNotContainDuplicateItems() &lt;br /&gt;    {&lt;br /&gt;        &lt;em&gt;$this-&gt;assertTrue(0 === $this-&gt;_bag-&gt;getItemQuantity(),&lt;/em&gt; &lt;br /&gt;            &lt;em&gt;'New bag not empty on creation.');&lt;/em&gt;&lt;br /&gt;            &lt;br /&gt;        $this-&gt;_bag-&gt;addItem(new Record_Item('Dubplate 1'));&lt;br /&gt;        $this-&gt;_bag-&gt;addItem(new Record_Item('Dubplate 2'));&lt;br /&gt;        $this-&gt;_bag-&gt;addItem(new Record_Item('Dubplate 2'));&lt;br /&gt;        &lt;br /&gt;        &lt;em&gt;$this-&gt;assertTrue(2 === $this-&gt;_bag-&gt;getItemQuantity(),&lt;/em&gt; &lt;br /&gt;            &lt;em&gt;'Bag does contain duplicated items.');&lt;/em&gt;&lt;br /&gt;    }&lt;br /&gt;    &lt;br /&gt;    /**&lt;br /&gt;     * @test&lt;br /&gt;     */&lt;br /&gt;    public function bagShouldBeReducedByOneItemAfterRemoval()&lt;br /&gt;    {&lt;br /&gt;        &lt;em&gt;$this-&gt;assertTrue(0 === $this-&gt;_bag-&gt;getItemQuantity(),&lt;/em&gt; &lt;br /&gt;            &lt;em&gt;'New bag not empty on creation.');&lt;/em&gt;&lt;br /&gt;&lt;br /&gt;        $this-&gt;_bag-&gt;addItem(new Record_Item('Dubplate 1'));&lt;br /&gt;        $this-&gt;_bag-&gt;addItem(new Record_Item('Dubplate 2'));&lt;br /&gt;        $this-&gt;_bag-&gt;addItem(new Record_Item('Dubplate 3'));&lt;br /&gt;&lt;br /&gt;        $this-&gt;_bag-&gt;removeItem('Dubplate 2');&lt;br /&gt;&lt;br /&gt;        &lt;em&gt;$this-&gt;assertTrue(2 === $this-&gt;_bag-&gt;getItemQuantity(),&lt;/em&gt; &lt;br /&gt;            &lt;em&gt;'Former three items bag does not contain two items after removal of one.');&lt;/em&gt;  &lt;br /&gt;    }&lt;br /&gt;    &lt;br /&gt;    /**&lt;br /&gt;     * @test &lt;br /&gt;     */&lt;br /&gt;    public function bagStockIdShouldFollowAgreedConvention()     &lt;br /&gt;    {    &lt;br /&gt;        &lt;em&gt;$stockIdPattern = '/^[A-Z]{3}-\d{2}-[A-Z]{2}-\d{8}/U';&lt;/em&gt;&lt;br /&gt;        $this-&gt;assertRegExp($stockIdPattern, $this-&gt;_bag-&gt;getStockId(), &lt;br /&gt;            'Stock id &amp;lt;string:' . $this-&gt;_bag-&gt;getStockId() &lt;br /&gt;                . '&amp;gt; does not follow the agreed convention.'); &lt;br /&gt;    }&lt;br /&gt;    &lt;br /&gt;    ....&lt;br /&gt;    &lt;br /&gt;    protected function tearDown() &lt;br /&gt;    {&lt;br /&gt;        unset($this-&gt;_bag);&lt;br /&gt;    }&lt;br /&gt;}&lt;/pre&gt;&lt;h4 class="custom"&gt;Mechanics of rolling custom assertions&lt;/h4&gt;The 'Custom Assertion' pattern can be applied from the very first test code creation activities, in sense of a &lt;a href="http://en.wikipedia.org/wiki/Prefactoring"&gt;prefactoring&lt;/a&gt;, or refactored towards by extracting the assert duplications and verification intention obscurations into tailormade and intent revealing named assertions.&lt;br /&gt;&lt;br /&gt;To define custom assertions are several approaches available, the first and easiest one is to define custom assertions merely for a single test class and make them private (inline) methods of this specific class by facading/wrapping the &lt;a href="http://www.phpunit.de/pocket_guide/3.3/en/api.html#api.assert.tables.assertions"&gt;standard&lt;/a&gt; PHPUnit assertion. This approach is outlined in code snippet &lt;em&gt;a)&lt;/em&gt;. &lt;br /&gt;&lt;br /&gt;Another approach is to create/introduce an &lt;a href="http://www.phpunit.de/pocket_guide/3.3/en/extending-phpunit.html#extending-phpunit.Assert"&gt; Assert Class&lt;/a&gt; to promote a cleaner reusability in other test case classes or scenarios, this approach might get chosen to collect and organize the evolving domain specific assertions. You will see a code sketch of this approach in code listing b). &lt;br /&gt;&lt;br /&gt;Other and more complex approaches would be the utilisation of PHPUnits' Constraint feature which is available since release 3.0.0 or in the near future the use of the &lt;a href="http://sebastian-bergmann.de/archives/735-Getting-Started-with-Hamcrest.html"&gt;Hamcrest&lt;/a&gt; test matcher features which might be available from PHPUnit 4.0.0.&lt;br /&gt;&lt;br /&gt;Either way the purpose specific assertions should further provide a default and convention conform assertion message, which will be raised on a verification failure, and also the feature to feed in a custom one to avoid gambling annoying &lt;a href="http://xunitpatterns.com/Assertion%20Roulette.html"&gt;Assertion Roulette&lt;/a&gt; rounds.&lt;br /&gt;&lt;br /&gt;&lt;div class="refactoringStatus" style="width: 170pt;"&gt;a) Assert definition inside Test Case:&lt;/div&gt;&lt;pre class="codeSnippetRefactoring"&gt;&amp;lt;?php&lt;br /&gt;require_once 'PHPUnit/Framework.php';&lt;br /&gt;require_once 'Record/Bag.php';&lt;br /&gt;require_once 'Record/Item.php';&lt;br /&gt;&lt;br /&gt;class Record_Bag_Test extends PHPUnit_Framework_TestCase &lt;br /&gt;{&lt;br /&gt;    private $_bag = null;&lt;br /&gt;    &lt;br /&gt;    protected function setUp() &lt;br /&gt;    {&lt;br /&gt;        $this-&gt;_bag = new Record_Bag('Test_Bag');&lt;br /&gt;    }&lt;br /&gt;    &lt;br /&gt;    ....&lt;br /&gt;    &lt;br /&gt;    private function assertBagItemCount($expectedCount, $bag, &lt;br /&gt;        $message = 'Expected bag item count &amp;lt;integer:#1&amp;gt; does not match actual count of &amp;lt;integer:#2&amp;gt; items.')&lt;br /&gt;    {&lt;br /&gt;        if (strpos($message, '#1')) {&lt;br /&gt;            $message = str_replace('#1', $expectedCount, $message);        &lt;br /&gt;        }&lt;br /&gt;        if (strpos($message, '#2')) {&lt;br /&gt;            $message = str_replace('#2', $bag-&gt;getItemQuantity(), $message);        &lt;br /&gt;        }&lt;br /&gt;        $this-&gt;assertTrue($expectedCount === $bag-&gt;getItemQuantity(), $message);&lt;br /&gt;    }    &lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;&lt;div class="refactoringStatus" style="width: 170pt;"&gt;b) Assert definition in Assert Class:&lt;/div&gt;&lt;pre class="codeSnippetRefactoring"&gt;&amp;lt;?php&lt;br /&gt;require_once 'PHPUnit/Framework/Assert.php';&lt;br /&gt;&lt;br /&gt;class Record_Bag_Assert extends PHPUnit_Framework_Assert &lt;br /&gt;{&lt;br /&gt;    /**&lt;br /&gt;     * Verifies that a given bag stock id follows the agreed convention.&lt;br /&gt;     *&lt;br /&gt;     * @param string $stockId&lt;br /&gt;     * @param string $message&lt;br /&gt;     * @see Record_Bag::createUniqueStockId()&lt;br /&gt;     */&lt;br /&gt;    public function assertStockIdFollowsConvention($stockId, &lt;br /&gt;        $message = 'Stock id &amp;lt;string:##&amp;gt; does not follow the agreed convention.') &lt;br /&gt;    {&lt;br /&gt;        if (strpos($message, '##')) {&lt;br /&gt;            $message = str_replace('##', $stockId, $message);        &lt;br /&gt;        }&lt;br /&gt;        $stockIdPattern = '/^[A-Z]{3}-\d{2}-[A-Z]{2}-\d{8}/U';&lt;br /&gt;        $this-&gt;assertRegExp($stockIdPattern, $stockId, $message);        &lt;br /&gt;    }&lt;br /&gt;    &lt;br /&gt;    ....&lt;br /&gt;    &lt;br /&gt;}&lt;/pre&gt;&lt;h4 class="custom"&gt;Putting the custom assertions to work&lt;/h4&gt;Now as the tailormade assertions are available all verification work can be delegated to them the same way it would be done with any of the standard PHPUnit assertions. In case the custom assertions are hosted in an 'Assert Class' they can be &lt;em&gt;required_once&lt;/em&gt; or loaded via &lt;em&gt;__autoload()&lt;/em&gt; in the test setup, otherwise if they are defined inside the test case class itself they can be used like regular private methods of that class. The last code extract illustrates the use of the prior outlined 'Assert Class' assertion for verifing the stock id format alongside the use of the 'inline' custom assertion for verifying the amount of items in a given bag.&lt;pre class="codeSnippet"&gt;&amp;lt;?php&lt;br /&gt;require_once 'PHPUnit/Framework.php';&lt;br /&gt;require_once 'Record/Bag/Assert.php';&lt;br /&gt;&lt;br /&gt;class Record_Bag_Test extends PHPUnit_Framework_TestCase &lt;br /&gt;{&lt;br /&gt;    &lt;br /&gt;    ....&lt;br /&gt;    &lt;br /&gt;    /**&lt;br /&gt;     * @test&lt;br /&gt;     */&lt;br /&gt;    public function bagShouldNotContainDuplicateItems() &lt;br /&gt;    {&lt;br /&gt;        $this-&gt;assertBagItemCount(0, $bag,  'New bag not empty on creation.');&lt;br /&gt;            &lt;br /&gt;        $this-&gt;_bag-&gt;addItem(new Record_Item('Dubplate 1'));&lt;br /&gt;        $this-&gt;_bag-&gt;addItem(new Record_Item('Dubplate 2'));&lt;br /&gt;        $this-&gt;_bag-&gt;addItem(new Record_Item('Dubplate 2'));&lt;br /&gt;        &lt;br /&gt;        $this-&gt;assertBagItemCount(2, $bag, 'Bag does contain duplicate items.');&lt;br /&gt;    }&lt;br /&gt;    &lt;br /&gt;    /**&lt;br /&gt;     * @test&lt;br /&gt;     */&lt;br /&gt;    public function bagShouldBeReducedByOneItemAfterRemoval()&lt;br /&gt;    {&lt;br /&gt;        $this-&gt;assertBagItemCount(0, $bag, 'New bag not empty on creation.');&lt;br /&gt;&lt;br /&gt;        $this-&gt;_bag-&gt;addItem(new Record_Item('Dubplate 1'));&lt;br /&gt;        $this-&gt;_bag-&gt;addItem(new Record_Item('Dubplate 2'));&lt;br /&gt;        $this-&gt;_bag-&gt;addItem(new Record_Item('Dubplate 3'));&lt;br /&gt;&lt;br /&gt;        $this-&gt;_bag-&gt;removeItem('Dubplate 2');&lt;br /&gt;&lt;br /&gt;        $this-&gt;assertBagItemCount(2, $bag, &lt;br /&gt;            'Former three items bag does not contain two items after removal of one.');   &lt;br /&gt;    }&lt;br /&gt;    &lt;br /&gt;    /**&lt;br /&gt;     * @test &lt;br /&gt;     */&lt;br /&gt;    public function bagStockIdShouldFollowAgreedConvention()     &lt;br /&gt;    {    &lt;br /&gt;        Record_Bag_Assert::assertStockIdFollowsConvention(&lt;br /&gt;            $this-&gt;_bag-&gt;getStockId());&lt;br /&gt;    }&lt;br /&gt;    &lt;br /&gt;    ....&lt;br /&gt;    &lt;br /&gt;}&lt;/pre&gt;&lt;h4 class="custom"&gt;Adding value through a domain specific test vocabulary&lt;/h4&gt;As you might have noticed the readability and intention communication of the test code has been improved significantly from the introductive code snippet towards the last one. Furthermore by distilling a ubiquitous(see &lt;a href="http://domaindrivendesign.org/books/index.html"&gt;Domain Driven Design book&lt;/a&gt; by Eric Evans) test language domain experts, which are mostly not fluent in the targeted programming language i.e. PHP, are enabled to read test code and provide valuable feedback or contribute changes to test scenarios affecting their domain. &lt;br /&gt;&lt;br /&gt;Another common area where domain specific test vocabularies are used, by providing their own assertion and constraint sets, test case classes and additional &lt;a href="http://xunitpatterns.com/Test%20Helper.html"&gt;test helpers&lt;/a&gt;, are extensions to xUnit frameworks e.g. DBUnit for PHPUnit or the Zend Framework &lt;a href="http://framework.zend.com/manual/en/zend.test.html"&gt;MVC testing scaffold&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8420118650236071171-472328189184808571?l=raphaelstolt.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/raphaelonphp/~4/-fGnjqR7BBs" height="1" width="1"/&gt;</content><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=8420118650236071171&amp;postID=472328189184808571" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/472328189184808571?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/472328189184808571?v=2" /><link rel="alternate" type="text/html" href="http://raphaelstolt.blogspot.com/2008/07/creating-custom-phpunit-assertions.html" title="Creating custom PHPUnit assertions" /><author><name>Raphael Stolt</name><uri>http://www.blogger.com/profile/07949831701855458792</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="14172620075949142098" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total></entry><entry gd:etag="W/&quot;C0IFRns5fCp7ImA9WxVXGEo.&quot;"><id>tag:blogger.com,1999:blog-8420118650236071171.post-6867408109374725659</id><published>2008-07-04T00:16:00.008+01:00</published><updated>2009-02-17T11:51:57.524Z</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-02-17T11:51:57.524Z</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="Refactoring" /><category scheme="http://www.blogger.com/atom/ns#" term="PHP" /><category scheme="http://www.blogger.com/atom/ns#" term="Best Practice" /><category scheme="http://www.blogger.com/atom/ns#" term="Phing" /><title>Six valuable Phing build file refactorings</title><content type="html">Some &lt;strike&gt;weeks&lt;/strike&gt; months ago I finally got my hands on the &lt;a href="http://www.pragprog.com/titles/twa/thoughtworks-anthology"&gt;ThoughtWorks Anthology&lt;/a&gt; and got immediately hooked on one of the featured essays called 'Refactoring Ant Build Files' contributed by Julian Simpson aka the &lt;a href="http://www.build-doctor.com"&gt;build doctor&lt;/a&gt;. After absorbing and studying the provided catalogue of overall 24 refactorings, I spent some time to transform a few health-promoting ones to the &lt;a href="http://phing.info/trac/"&gt;Phing&lt;/a&gt; universe. So the following post will outline six &lt;strike&gt;five&lt;/strike&gt; basic, but valuable Phing build file refactorings by showing the smelly example first, followed by the scentless one and a closing refactoring description.&lt;h4 class="custom"&gt;Making build files 'first-class' codebase citizens&lt;/h4&gt;You might ask yourself if there even is any need to refactor and care about mostly poorly treated project artifacts like build files. Well according to the book market there are growing needs and catalogues for refactoring non-sourcecode matters like &lt;a href="http://www.amazon.com/Refactoring-Databases-Evolutionary-Addison-Wesley-Signature/dp/0321293533/"&gt;databases&lt;/a&gt; and nowadays even &lt;a href="http://www.amazon.com/Refactoring-HTML-Improving-Applications-Addison-Wesle/dp/0321503635"&gt;(X)HTML&lt;/a&gt;, and as build tools are often used to automate the whole build and delivery process of complete projects, their feed build files should be readable and clear, painless maintainable and easily customizable as the controlled project takes new directions and hurdles. &lt;br /&gt;&lt;br /&gt;Today build files are also often used to drive the Continuous Integration(CI) process and are heavily used to run local development builds prior to commiting the finished development tasks, therefor messy and clotty build files will have a counterproductive impact on the build management lifecycle and on making required changes to it. So when striving for codebase citizen equality agree upon a build file coding standard (e.g. &lt;a href="#ref5"&gt;5. Introduce distinct target naming&lt;/a&gt; or a common element indentation), reside all build files of a project in a SCM system like Subversion or &lt;a href="http://www.pragprog.com/titles/tsgit"&gt;Git&lt;/a&gt; and try not to neglect the build files constantly just because they aren't actual business logic.&lt;h4 class="custom"&gt;Trapeze balancing without a safety net&lt;/h4&gt;While developing an applications business logic 'ideally' automated behaviour verifying tests are written, whether in a test-first or test-last approach, and these are building the implicit and required safety net for all follow-up refactorings. For build files there are currently no tailored testing tools/frameworks available to warrant their external behaviour during and after a refactoring, though it might be possible to create a basic safety net by using for example a combination of PHPUnit's &lt;em&gt;assertFileExists&lt;/em&gt; and &lt;em&gt;assertContains&lt;/em&gt; assertions. And even if there were such tools available, it's rather questionable that these would be applied to test-drive mostly simple starting and incremental evolving build files. So currently this flavour of refactoring needs to be applied with much more descipline and caution than classic sourcecode refactorings, even 'old-school' manual tests have to be run frequently and only with a proceeding practice more and more agressive refactorings will become an everyday tool. After this short note of caution, let us jump right into the refactoring catalogue extract.&lt;h4 class="custom"&gt;1. Extract target&lt;/h4&gt;&lt;div class="refactoringDescription"&gt;Take parts of a large target, declare them as independent targets and preserve dependencies by using the &lt;br /&gt;target &lt;em&gt;depends&lt;/em&gt; attribute.&lt;/div&gt;&lt;div class="refactoringStatus"&gt;Before&lt;/div&gt;&lt;pre class="codeSnippetRefactoring"&gt;&amp;lt;target name="what-a-clew"&amp;gt;&lt;br /&gt;  &lt;br /&gt;  &amp;lt;phplint&amp;gt;&lt;br /&gt;    &amp;lt;fileset dir="${build.src}"&amp;gt;&lt;br /&gt;      &amp;lt;include name="**/*.php"/&amp;gt;&lt;br /&gt;    &amp;lt;/fileset&amp;gt;&lt;br /&gt;  &amp;lt;/phplint&amp;gt;&lt;br /&gt;  &lt;br /&gt;  &amp;lt;phpcodesniffer standard="PEAR" format="summary"&amp;gt;&lt;br /&gt;    &amp;lt;fileset dir="${build.src}"&amp;gt;&lt;br /&gt;      &amp;lt;include name="**/*.php"/&amp;gt;&lt;br /&gt;    &amp;lt;/fileset&amp;gt;&lt;br /&gt;  &amp;lt;/phpcodesniffer&amp;gt;&lt;br /&gt;  &lt;br /&gt;  &amp;lt;phpunit&amp;gt;&lt;br /&gt;    &amp;lt;formatter todir="reports" type="xml"/&amp;gt;&lt;br /&gt;    &amp;lt;batchtest&amp;gt;&lt;br /&gt;      &amp;lt;fileset dir="="${build.tests}"&amp;gt;&lt;br /&gt;        &amp;lt;include name="**/*Test*.php"/&amp;gt;&lt;br /&gt;      &amp;lt;/fileset&amp;gt;&lt;br /&gt;    &amp;lt;/batchtest&amp;gt;&lt;br /&gt;  &amp;lt;/phpunit&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;/target&amp;gt;&lt;/pre&gt;&lt;div class="refactoringStatus"&gt;After:&lt;/div&gt;&lt;pre class="codeSnippetRefactoring"&gt;&amp;lt;target name="phplint-report"&amp;gt;&lt;br /&gt;  &amp;lt;phplint&amp;gt;&lt;br /&gt;    ....&lt;br /&gt;  &amp;lt;/phplint&amp;gt;&lt;br /&gt;&amp;lt;/target&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;target name="sniff-report" depends="phplint-report"&amp;gt;&lt;br /&gt;  &amp;lt;phpcodesniffer standard="PEAR" format="summary"&amp;gt;&lt;br /&gt;    ....&lt;br /&gt;  &amp;lt;/phpcodesniffer&amp;gt;&lt;br /&gt;&amp;lt;/target&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;target name="test-report" depends="sniff-report"&amp;gt;&lt;br /&gt;  &amp;lt;phpunit&amp;gt;&lt;br /&gt;    ....&lt;br /&gt;  &amp;lt;/phpunit&amp;gt;&lt;br /&gt;&amp;lt;/target&amp;gt;&lt;/pre&gt;The &lt;em&gt;Extract target&lt;/em&gt; refactoring is similar to the well-known &lt;a href="http://www.refactoring.com/catalog/extractMethod.html"&gt;Extract method&lt;/a&gt; refactoring catalogued by Martin Fowler and should be applied to unclutter long targets, which can become hard to understand and troubleshoot while maintaining or extending a build file. This refactoring is achieved by taken each atomic task (e.g. phplint) of the cluttered target and provide them each a own target (e.g. phplint-report) while the former tasks execution sequence can be obtained by utilizing the target &lt;em&gt;depends&lt;/em&gt; attribute. You can compare this refactoring to the technique of tackling a method that's to large and infringes upon the &lt;a href="http://en.wikipedia.org/wiki/Single_responsibility_principle"&gt; single responsibility principle&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;While twiddling with this refactoring I came up with a follow-up and hand in hand refactoring that might go by the name of &lt;em&gt;Introduce facade target&lt;/em&gt;, which simply orchestrates the target execution sequence so you can remove the &lt;em&gt;depends&lt;/em&gt; attribute of all orchestrated targets and thereby use them separately if needed and advisable. The following build file extract shows the result of this refactoring in action.&lt;h4 class="custom"&gt;1.1 Introduce facade target&lt;/h4&gt;&lt;div class="refactoringDescription"&gt;Provide a facade target to obtain the task execution sequence and to make each involved target a single callable unit.&lt;/div&gt;&lt;div class="refactoringStatus"&gt;After:&lt;/div&gt;&lt;pre class="codeSnippetRefactoring"&gt;&amp;lt;target name="phplint-report"&amp;gt;&lt;br /&gt;  &amp;lt;phplint&amp;gt;&lt;br /&gt;    ....&lt;br /&gt;  &amp;lt;/phplint&amp;gt;&lt;br /&gt;&amp;lt;/target&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;target name="sniff-report" &lt;strike&gt;depends="phplint-report"&lt;/strike&gt;&amp;gt;&lt;br /&gt;  &amp;lt;phpcodesniffer standard="PEAR" format="summary"&amp;gt;&lt;br /&gt;    ....&lt;br /&gt;  &amp;lt;/phpcodesniffer&amp;gt;&lt;br /&gt;&amp;lt;/target&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;target name="test-report" &lt;strike&gt;depends="sniff-report"&lt;/strike&gt;&amp;gt;&lt;br /&gt;  &amp;lt;phpunit&amp;gt;&lt;br /&gt;    ....&lt;br /&gt;  &amp;lt;/phpunit&amp;gt;&lt;br /&gt;&amp;lt;/target&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;target name="quality-report" depends="lint-report, sniff-report, test-report" &lt;br /&gt;        description="Generates the overall projects quality report" /&amp;gt;&lt;/pre&gt;&lt;h4 class="custom"&gt;2. Introduce property file&lt;/h4&gt;&lt;div class="refactoringDescription"&gt;Move infrequently changing properties from the build file body to a flat file.&lt;/div&gt;&lt;div class="refactoringStatus"&gt;Before:&lt;/div&gt;&lt;pre class="codeSnippetRefactoring"&gt;&amp;lt;property name="db.port" value="3306" /&amp;gt;&lt;br /&gt;&amp;lt;property name="db.name" value="example" /&amp;gt;&lt;br /&gt;&amp;lt;property name="db.user" value="funkdoc" /&amp;gt;&lt;br /&gt;....&lt;br /&gt;&amp;lt;property name="runtime.property.x" value="default" /&amp;gt;&lt;br /&gt;....&lt;/pre&gt;&lt;div class="refactoringStatus"&gt;After:&lt;/div&gt;&lt;pre class="codeSnippetRefactoring"&gt;&lt;br /&gt;build.poperties file&lt;br /&gt;[example properties]&lt;br /&gt;db.port = 3306&lt;br /&gt;db.name = example&lt;br /&gt;db.user = funkdoc&lt;br /&gt;....&lt;br /&gt;&lt;br /&gt;build file&lt;br /&gt;&amp;lt;property file="build.properties" /&amp;gt;&lt;br /&gt;&amp;lt;property name="runtime.property.x" value="default" /&amp;gt;&lt;br /&gt;....&lt;/pre&gt;The &lt;em&gt;Introduce property file&lt;/em&gt; refactoring can be applied for moving infrequently changing or static properties out of the main build file body to raise the overall legibility and keep them distinct from runtime properties. The downside of this refactoring is a lost of property visibility and breaking up the former single build file into multiple units, which is contradictory to the third &lt;a href="http://www.onjava.com/pub/a/onjava/2003/12/17/ant_bestpractices.html"&gt;ANT best practice&lt;/a&gt; named 'Prefer a Single Buildfile' of an older best practice catalogue compiled by Eric M. Burke. So in this case, like in any case, you have to make your own choice based on your needs and requirements.&lt;h4 class="custom"&gt;3. Replace comment with description&lt;/h4&gt;&lt;div class="refactoringDescription"&gt;Annotate elements(targets) with the description attribute instead of XML comments.&lt;/div&gt;&lt;div class="refactoringStatus"&gt;Before:&lt;/div&gt;&lt;pre class="codeSnippetRefactoring"&gt;&amp;lt;!-- This target runs the PHP_Codesniffer task and reports coding standard violations --!&amp;gt;&lt;br /&gt;&amp;lt;target name="sniff-report"&amp;gt;&lt;br /&gt;  ....&lt;br /&gt;&amp;lt;/target&amp;gt;&lt;/pre&gt;&lt;div class="refactoringStatus"&gt;After:&lt;/div&gt;&lt;pre class="codeSnippetRefactoring"&gt;&amp;lt;target name="sniff-report" description="Runs the PHP_Codesniffer task and reports coding standard violations"&amp;gt;&lt;br /&gt;  ....&lt;br /&gt;&amp;lt;/target&amp;gt;&lt;/pre&gt;Often build files are accentuated with plain XML comments to retain the mechanics and purpose of build file elements(i.e. targets) and can become a diversionary/obscuring source while maintaining or extending a build file. By using the available &lt;em&gt;description&lt;/em&gt; attribute of the target element to annotate its purpose it's possible to reduce that kind of noise and even better, if used constantly, they can provide valuable &lt;a href="http://raphaelstolt.blogspot.com/2008/03/getting-overview-of-all-targets.html"&gt;information about all accumulated&lt;/a&gt; targets of a build file when phing is called with the &lt;em&gt;-l(ist)&lt;/em&gt; option. As you can see the &lt;em&gt;Replace comment with description&lt;/em&gt; refactoring requires a minimum of effort/investment to achieve a very valuable impact.&lt;h4 class="custom"&gt;4. Reuse elements by id&lt;/h4&gt;&lt;div class="refactoringDescription"&gt;Declare an instance e.g. a fileset once and make references to it elsewhere to reduce duplication and increase clarity.&lt;/div&gt;&lt;div class="refactoringStatus"&gt;Before:&lt;/div&gt;&lt;pre class="codeSnippetRefactoring"&gt;&amp;lt;target name="phplint-report"&amp;gt;&lt;br /&gt;  &amp;lt;phplint&amp;gt;&lt;br /&gt;    &amp;lt;fileset dir="${build.src}"&amp;gt;&lt;br /&gt;      &amp;lt;include name="**/*.php"/&amp;gt;&lt;br /&gt;    &amp;lt;/fileset&amp;gt;&lt;br /&gt;  &amp;lt;/phplint&amp;gt;&lt;br /&gt;&amp;lt;/target&amp;gt; &lt;br /&gt;&lt;br /&gt;&amp;lt;target name="sniff-report" depends="phplint-report"&amp;gt;&lt;br /&gt;  &amp;lt;phpcodesniffer standard="PEAR" format="summary"&amp;gt;&lt;br /&gt;    &amp;lt;fileset dir="${build.src}"&amp;gt;&lt;br /&gt;      &amp;lt;include name="**/*.php"/&amp;gt;&lt;br /&gt;    &amp;lt;/fileset&amp;gt;&lt;br /&gt;  &amp;lt;/phpcodesniffer&amp;gt;&lt;br /&gt;&amp;lt;/target&amp;gt;&lt;/pre&gt;&lt;div class="refactoringStatus"&gt;After:&lt;/div&gt;&lt;pre class="codeSnippetRefactoring"&gt;&amp;lt;fileset id="src_artifacts" dir="${build.src}"&amp;gt;&lt;br /&gt;  &amp;lt;include name="**/*.php"/&amp;gt;&lt;br /&gt;&amp;lt;/fileset&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;target name="phplint-report"&amp;gt;&lt;br /&gt;  &amp;lt;phplint&amp;gt;&lt;br /&gt;    &amp;lt;fileset refid="src_artifacts" /&amp;gt;&lt;br /&gt;  &amp;lt;/phplint&amp;gt;&lt;br /&gt;&amp;lt;/target&amp;gt; &lt;br /&gt;&lt;br /&gt;&amp;lt;target name="sniff-report" depends="phplint-report"&amp;gt;&lt;br /&gt;  &amp;lt;phpcodesniffer standard="PEAR" format="summary"&amp;gt;&lt;br /&gt;    &amp;lt;fileset refid="src_artifacts" /&amp;gt;&lt;br /&gt;  &amp;lt;/phpcodesniffer&amp;gt;&lt;br /&gt;&amp;lt;/target&amp;gt;&lt;/pre&gt;The &lt;em&gt;Reuse elements by id&lt;/em&gt; refactoring is, as the short description states, tailor-made to increase clarity while reducing code duplication, which is when present a risk that an alternation made to one element will be skipped for the other duplicates, by declaring top-level elements once by assigning an &lt;em&gt;id&lt;/em&gt; attribute to it and then referring to it thoughout the rest of the build file. This refactoring is best compared to the classic sourcecode refactoring called &lt;a href="http://www.refactoring.com/catalog/pullUpMethod.html"&gt;Pull Up Method&lt;/a&gt; also catalogued by Martin Fowler and moreover it enforces the compliance with the &lt;a href="http://en.wikipedia.org/wiki/Don%27t_repeat_yourself"&gt;DRY principle&lt;/a&gt; by providing a single point of change for futurities alternations.&lt;a name="ref5"&gt;&lt;/a&gt;&lt;h4 class="custom"&gt;5. Introduce distinct target naming&lt;/h4&gt;&lt;div class="refactoringDescription"&gt;Use a different punctuation for targets and properties to enhance readability.&lt;/div&gt;&lt;div class="refactoringStatus"&gt;Before:&lt;/div&gt;&lt;pre class="codeSnippetRefactoring"&gt;&amp;lt;property name="example.property1" value="abc" /&amp;gt;&lt;br /&gt;&amp;lt;property name="example_property2" value="def" /&amp;gt;&lt;br /&gt;&amp;lt;property name="example-Property3"  value="ghi" /&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;target name="example.target1"&amp;gt;&lt;br /&gt;   &amp;lt;echo msg="${example.property1}" /&amp;gt;&lt;br /&gt;   ....&lt;br /&gt;&amp;lt;/target&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;target name="example-target2"&amp;gt;&lt;br /&gt;   &amp;lt;echo msg="${example-Property3}" /&amp;gt;&lt;br /&gt;   ....&lt;br /&gt;&amp;lt;/target&amp;gt;&lt;/pre&gt;&lt;div class="refactoringStatus"&gt;After:&lt;/div&gt;&lt;pre class="codeSnippetRefactoring"&gt;&amp;lt;property name="example.property1" value="abc" /&amp;gt;&lt;br /&gt;&amp;lt;property name="example.property2" value="def" /&amp;gt;&lt;br /&gt;&amp;lt;property name="example.property3" value="ghi" /&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;target name="example-target1"&amp;gt;&lt;br /&gt;   &amp;lt;echo msg="${example.property1}" /&amp;gt;&lt;br /&gt;   ....&lt;br /&gt;&amp;lt;/target&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;target name="example-target2"&amp;gt;&lt;br /&gt;   &amp;lt;echo msg="${example.property3}" /&amp;gt;&lt;br /&gt;   ....&lt;br /&gt;&amp;lt;/target&amp;gt;&lt;/pre&gt;The &lt;em&gt;Introduce distinct target naming&lt;/em&gt; refactoring once again tackles the improvement of readability in a build file by applying a constant and different punctuation on the common elements: targets and properties. The appliance of this refactoring leaves you and coworkers with an immediate reply whether you're looking at a property value or a target and can lead towards an agreed upon in-house/project &lt;a href="http://wiki.apache.org/ant/TheElementsOfAntStyle"&gt;build file coding standard&lt;/a&gt;. For target names underscores and dashes are suitable, although dashes are preferred by me as a hypen is used when &lt;a href="http://raphaelstolt.blogspot.com/2008/03/getting-overview-of-all-targets.html"&gt;enforcing internal targets&lt;/a&gt;, while for the build properties dots should be considered to build namespaces and their names should 'always' be lowercased except for environment variables/properties.&lt;br /&gt;&lt;br /&gt;In case this blog post whetted your appetite for more, heavier and here uncovered build file refactorings, you might consider &lt;a href="http://www.build-doctor.com"&gt;picking up&lt;/a&gt; a copy of the ThoughtWorks Anthology book. Last but not least a shout out goes to the build doctor for the remix permission and until the next post I'm ghost like dog.&lt;br /&gt;&lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8420118650236071171-6867408109374725659?l=raphaelstolt.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/raphaelonphp/~4/GAdkdRDjd3Q" height="1" width="1"/&gt;</content><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=8420118650236071171&amp;postID=6867408109374725659" title="8 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/6867408109374725659?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/6867408109374725659?v=2" /><link rel="alternate" type="text/html" href="http://raphaelstolt.blogspot.com/2008/07/six-valuable-phing-build-file.html" title="Six valuable Phing build file refactorings" /><author><name>Raphael Stolt</name><uri>http://www.blogger.com/profile/07949831701855458792</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="14172620075949142098" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">8</thr:total></entry><entry gd:etag="W/&quot;DkIERH4yeip7ImA9WxZbE0Q.&quot;"><id>tag:blogger.com,1999:blog-8420118650236071171.post-6391166006609930210</id><published>2008-04-17T00:37:00.009Z</published><updated>2008-04-17T01:08:25.092Z</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2008-04-17T01:08:25.092Z</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="PHP" /><category scheme="http://www.blogger.com/atom/ns#" term="Xinc" /><category scheme="http://www.blogger.com/atom/ns#" term="Continuous Integration" /><title>Hooking a Growl publisher plugin into Xinc</title><content type="html">This week I finally had the time to setup &lt;a href="http://code.google.com/p/xinc/" target="_blank"&gt;Xinc&lt;/a&gt;, PHP's shiny new Continuous Integration(CI) server, on my &lt;u&gt;MacBook&lt;/u&gt; and started to fiddle with it's publisher plugins and plugin architecture. While still down with 'tool envy' influenza I recently came across some nice and inspiring blog posts from the Ruby/Rails &lt;i&gt;camp&lt;/i&gt;, showing how to employ &lt;a href="http://growl.info/about.php"&gt;Growl&lt;/a&gt; as an &lt;a href="http://railstips.org/2007/7/23/autotest-growl-pass-fail-notifications" target="_blank"&gt;useful&lt;/a&gt; and &lt;a href="http://szeryf.wordpress.com/2007/07/30/way-beyond-cool-autotest-growl-doomguy/" target="_blank"&gt;fun&lt;/a&gt; feedback radiator for test/spec results. Since then the idea of building a Growl publisher plugin for Xinc was travelling my mind repeatedly, so the following post will break this circle and show a possible approach to build such a plugin, which can be used to notify the build result for continuously integrated projects and thereby provide an on-point/immediate feedback.&lt;br /&gt;&lt;h4 class="custom"&gt;Building the Growl publisher plugin&lt;/h4&gt;Xinc comes with an elegant plugin system, allowing you to easily &lt;a href="http://code.google.com/p/xinc/wiki/HowToCreateAPlugin" target="_blank"&gt; role your own&lt;/a&gt; one. The following code shows the plugin class and is the place where the actual Growl notifications are executed/raised by calling the growlnotify CLI. The images in the public &lt;i&gt;growl()&lt;/i&gt; method, used to accent the two main build statuses(failure and success) in the Growl notification, are borrowed from &lt;a href="http://www.thelucid.com/articles/2007/07/30/autotest-growl-fail-pass-smilies" target="_blank"&gt;this&lt;/a&gt; blog post and should be located in the &lt;em&gt;$HOME/Pictures&lt;/em&gt; directory.&lt;pre class="codeSnippet"&gt;&amp;lt;?php&lt;br /&gt;/**&lt;br /&gt; * A Growl publisher plugin for Xinc&lt;br /&gt; *&lt;br /&gt; * @package Xinc.Plugin&lt;br /&gt; * @author Raphael Stolt&lt;br /&gt; * @version 2.0&lt;br /&gt; * @copyright 2008 Raphael Stolt, Constance&lt;br /&gt; * @license  http://www.gnu.org/copyleft/lgpl.html GNU/LGPL, see license.php&lt;br /&gt; */&lt;br /&gt;require_once 'Xinc/Plugin/Base.php';&lt;br /&gt;require_once 'Xinc/Plugin/Repos/Publisher/Growl/Task.php';&lt;br /&gt;&lt;br /&gt;class Xinc_Plugin_Repos_Publisher_Growl extends Xinc_Plugin_Base&lt;br /&gt;{&lt;br /&gt;    public function validate()&lt;br /&gt;    {&lt;br /&gt;        return true;&lt;br /&gt;    }&lt;br /&gt;    public function getTaskDefinitions()&lt;br /&gt;    {&lt;br /&gt;        return array(new Xinc_Plugin_Repos_Publisher_Growl_Task($this));&lt;br /&gt;    }&lt;br /&gt;    private function _sendGrowlNotification($message, $image, $name)&lt;br /&gt;    {&lt;br /&gt;        $command = "growlnotify -w -m '{$message}' "&lt;br /&gt;                 . "-n '{$name}' "&lt;br /&gt;                 . "-p 2 --image {$image}";&lt;br /&gt;&lt;br /&gt;        Xinc_Logger::getInstance()-&gt;info('Executed growlnotify command: ' . $command);&lt;br /&gt;&lt;br /&gt;        exec($command, $response, $return);&lt;br /&gt;&lt;br /&gt;        if ($return === 0) {&lt;br /&gt;            return true;&lt;br /&gt;        }&lt;br /&gt;        return false;&lt;br /&gt;    }&lt;br /&gt;    public function growl(Xinc_Project &amp;$project, $message, $buildstatus, $name = 'Xinc')&lt;br /&gt;    {&lt;br /&gt;        if ($buildstatus === Xinc_Build_Interface::PASSED) {&lt;br /&gt;            $image = '$HOME/Pictures/pass.png';&lt;br /&gt;            $buildstatus = 'PASSED';&lt;br /&gt;        } elseif ($buildstatus === Xinc_Build_Interface::FAILED) {&lt;br /&gt;            $image = '$HOME/Pictures/fail.png';&lt;br /&gt;            $buildstatus = 'FAILED';&lt;br /&gt;        }&lt;br /&gt;&lt;br /&gt;        $project-&gt;info('Executing Growl publisher with content '&lt;br /&gt;                      ."\nMessage: " . $message&lt;br /&gt;                      ."\nBuildstatus: " . $buildstatus&lt;br /&gt;                      ."\nImage: " . $image);&lt;br /&gt;&lt;br /&gt;        return $this-&gt;_sendGrowlNotification($message, $image, $name);&lt;br /&gt;    }&lt;br /&gt;}&lt;/pre&gt;The next big code chunk shows the task definition of the plugin and is the place where the publisher plugin name and is acceptable and required attributes are programmaticly defined, as you can see the Growl task currently only takes a required message attribute which can be set for the Growl publisher within the Xinc &lt;a href="#conf"&gt;project configuration file&lt;/a&gt;.&lt;pre class="codeSnippet"&gt;&amp;lt;?php&lt;br /&gt;/**&lt;br /&gt; * A Growl publisher plugin task for Xinc&lt;br /&gt; *&lt;br /&gt; * @package Xinc.Plugin&lt;br /&gt; * @author Raphael Stolt&lt;br /&gt; * @version 2.0&lt;br /&gt; * @copyright 2008 Raphael Stolt, Constance&lt;br /&gt; * @license  http://www.gnu.org/copyleft/lgpl.html GNU/LGPL, see license.php&lt;br /&gt; */&lt;br /&gt;require_once 'Xinc/Plugin/Repos/Publisher/AbstractTask.php';&lt;br /&gt;&lt;br /&gt;class Xinc_Plugin_Repos_Publisher_Growl_Task extends Xinc_Plugin_Repos_Publisher_AbstractTask&lt;br /&gt;{&lt;br /&gt;    private $_message;&lt;br /&gt;&lt;br /&gt;    public function setMessage($message)&lt;br /&gt;    {&lt;br /&gt;        $this-&gt;_message = $message;&lt;br /&gt;    }&lt;br /&gt;    public function getName()&lt;br /&gt;    {&lt;br /&gt;        return 'growl';&lt;br /&gt;    }&lt;br /&gt;    private function _isGrowlnotifyAvailable()&lt;br /&gt;    {&lt;br /&gt;        exec('growlnotify -v', $reponse, $return);&lt;br /&gt;&lt;br /&gt;        if ($return === 0) {&lt;br /&gt;            return true;&lt;br /&gt;        }&lt;br /&gt;        return false;&lt;br /&gt;    }&lt;br /&gt;    public function validateTask()&lt;br /&gt;    {&lt;br /&gt;        if (!$this-&gt;_isGrowlnotifyAvailable()) {&lt;br /&gt;            $message = 'The growlnotify command seems to be not available '&lt;br /&gt;                     . 'on this system';&lt;br /&gt;            Xinc_Logger::getInstance()-&gt;error($message);&lt;br /&gt;            throw new RuntimeException($message);&lt;br /&gt;        }&lt;br /&gt;&lt;br /&gt;        if (!isset($this-&gt;_message)) {&lt;br /&gt;              $message = 'Element publisher/growl - required attribute '&lt;br /&gt;                       . '\'message\' is not set';&lt;br /&gt;              throw new Xinc_Exception_MalformedConfig($message);&lt;br /&gt;        }&lt;br /&gt;        return true;&lt;br /&gt;    }&lt;br /&gt;    public function publish(Xinc_Build_Interface &amp;$build)&lt;br /&gt;    {&lt;br /&gt;        $statusBefore = $build-&gt;getStatus();&lt;br /&gt;        $res = $this-&gt;_plugin-&gt;growl($build-&gt;getProject(), $this-&gt;_message, $build-&gt;getStatus());&lt;br /&gt;        if (!$res &amp;&amp; $statusBefore == Xinc_Build_Interface::PASSED ) {&lt;br /&gt;            /**&lt;br /&gt;             * Status was PASSED, but now the publish process made it fail&lt;br /&gt;             */&lt;br /&gt;            $build-&gt;setStatus(Xinc_Build_Interface::FAILED);&lt;br /&gt;        }&lt;br /&gt;    }&lt;br /&gt;}&lt;/pre&gt;&lt;h4 class="custom"&gt;Hooking the Growl publisher plugin into the build system&lt;/h4&gt;To make the just crafted Growl publisher plugin available to the Xinc build system you have to add the plugin file and class name/path to the &lt;em&gt;/etc/xinc/system.xml&lt;/em&gt; file. After restarting the Xinc server the Growl publisher should be available to use and also be listed in the section of the &lt;em&gt;/var/log/xinc.log&lt;/em&gt; file stating all registered plugins.&lt;br /&gt;&lt;br /&gt;The next listing shows the Growl publisher plugin added to the aforementioned XML file.&lt;pre class="codeSnippet"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;br /&gt;&amp;lt;xinc&amp;gt;&lt;br /&gt;  &amp;lt;configuration&amp;gt;&lt;br /&gt;    &amp;lt;setting name="loglevel" value="2"/&amp;gt;&lt;br /&gt;    &amp;lt;setting name="timezone" value="Europe/Berlin"/&amp;gt;&lt;br /&gt;  &amp;lt;/configuration&amp;gt;&lt;br /&gt;  &amp;lt;plugins&amp;gt;&lt;br /&gt;    &amp;lt;plugin filename="Xinc/Plugin/Repos/ModificationSet.php" classname="Xinc_Plugin_Repos_ModificationSet"/&amp;gt;&lt;br /&gt;    ...&lt;br /&gt;    &lt;b&gt;&amp;lt;plugin filename="Xinc/Plugin/Repos/Publisher/Growl.php" classname="Xinc_Plugin_Repos_Publisher_Growl"/&amp;gt;&lt;/b&gt;&lt;br /&gt;    ...&lt;br /&gt;    &amp;lt;plugin filename="Xinc/Contrib/Warko/Plugin/ModificationSet/SvnTag.php"&lt;br /&gt;            classname="Xinc_Contrib_Warko_Plugin_ModificationSet_SvnTag"/&amp;gt;&lt;br /&gt;  &amp;lt;/plugins&amp;gt;&lt;br /&gt;&lt;br /&gt;  &amp;lt;engines&amp;gt;&lt;br /&gt;    &amp;lt;engine classname="Xinc_Engine_Sunrise" filename="Xinc/Engine/Sunrise.php" default="default"/&amp;gt;&lt;br /&gt;  &amp;lt;/engines&amp;gt;&lt;br /&gt;&amp;lt;/xinc&amp;gt;&lt;/pre&gt;&lt;h4 class="custom"&gt;Putting the Xinc Growl publisher plugin to action&lt;/h4&gt;Now that the Growl publisher plugin is available it can be used in the build cycle. The following Xinc project configuration file shows the utilization of the Growl publisher plugin in both of the main publisher realms &lt;em&gt;onsuccess&lt;/em&gt; and &lt;em&gt;onfailure&lt;/em&gt;.&lt;a name="conf"&gt;&lt;/a&gt;&lt;pre class="codeSnippet"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;br /&gt;&amp;lt;xinc engine="Sunrise"&amp;gt;&lt;br /&gt;  &amp;lt;project name="recordshelf"&amp;gt;&lt;br /&gt;    &amp;lt;configuration&amp;gt;&lt;br /&gt;      &amp;lt;setting name="loglevel" value="2"/&amp;gt;&lt;br /&gt;    &amp;lt;/configuration&amp;gt;&lt;br /&gt;    &amp;lt;property name="dir" value="${projectdir}/${project.name}" /&amp;gt;&lt;br /&gt;    &amp;lt;property name="build.failure.message" value="Build for project ${project.name} failed!"/&amp;gt;&lt;br /&gt;    &amp;lt;schedule interval="180" /&amp;gt;&lt;br /&gt;    &amp;lt;modificationset&amp;gt;&lt;br /&gt;      &amp;lt;svn directory="${dir}" update="true" /&amp;gt;&lt;br /&gt;    &amp;lt;/modificationset&amp;gt;&lt;br /&gt;    &amp;lt;builders&amp;gt;&lt;br /&gt;      &amp;lt;phingbuilder buildfile="${dir}/build.xml" target="main"/&amp;gt;&lt;br /&gt;    &amp;lt;/builders&amp;gt;&lt;br /&gt;    &amp;lt;publishers&amp;gt;&lt;br /&gt;      &amp;lt;onfailure&amp;gt;&lt;br /&gt;        &amp;lt;email to="example@example.com" subject="[${project.name}] Build failure"&lt;br /&gt;               message="${build.failure.message}" /&amp;gt;&lt;br /&gt;        &lt;b&gt;&amp;lt;growl message="${build.failure.message}" /&amp;gt;&lt;/b&gt;&lt;br /&gt;      &amp;lt;/onfailure&amp;gt;&lt;br /&gt;      &amp;lt;onsuccess&amp;gt;&lt;br /&gt;        &amp;lt;email to="example@example.com" subject="[${project.name}] Build success"&lt;br /&gt;               message="Build for project was successful" /&amp;gt;&lt;br /&gt;        &lt;b&gt;&amp;lt;growl message="Build for project ${project.name} was successful." /&amp;gt;&lt;/b&gt;&lt;br /&gt;      &amp;lt;/onsuccess&amp;gt;&lt;br /&gt;    &amp;lt;/publishers&amp;gt;&lt;br /&gt;  &amp;lt;/project&amp;gt;&lt;br /&gt;&amp;lt;/xinc&amp;gt;&lt;/pre&gt;The next two images are finally showing the Growl notifications for a successful and a failed build. To minimize the notification noise it's a good and common practice to only 'ring the alarm' for failed builds, which can be achieved by using the Growl publisher only in the &lt;em&gt;onfailure&lt;/em&gt; publisher realm of the Xinc project configuration file. Nuff talk, happy Xincing!&lt;br /&gt;&lt;a href="http://www.flickr.com/photos/raphaelstolt/2419110337/" title="Growl notification for a successful build"&gt;&lt;br /&gt;    &lt;img src="http://farm3.static.flickr.com/2398/2419110337_5f3df4f7ac_o.jpg" width="370" height="96" alt="Growl notification for a successful build" border="0" /&gt;&lt;br /&gt;&lt;/a&gt;&lt;a href="http://www.flickr.com/photos/raphaelstolt/2419924614/" title="Growl notification for failed build"&gt;&lt;br /&gt;    &lt;img src="http://farm3.static.flickr.com/2271/2419924614_acb6372459_o.jpg" width="370" height="96" alt="Growl notification for failed build" border="0" /&gt;&lt;br /&gt;&lt;/a&gt;&lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8420118650236071171-6391166006609930210?l=raphaelstolt.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/raphaelonphp/~4/7Kh9H0mbOdw" height="1" width="1"/&gt;</content><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=8420118650236071171&amp;postID=6391166006609930210" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/6391166006609930210?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/6391166006609930210?v=2" /><link rel="alternate" type="text/html" href="http://raphaelstolt.blogspot.com/2008/04/hooking-growl-publisher-plugin-into.html" title="Hooking a Growl publisher plugin into Xinc" /><author><name>Raphael Stolt</name><uri>http://www.blogger.com/profile/07949831701855458792</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="14172620075949142098" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total></entry><entry gd:etag="W/&quot;Ck8AQH8-eSp7ImA9WxJTEEs.&quot;"><id>tag:blogger.com,1999:blog-8420118650236071171.post-7078640050186351982</id><published>2008-03-29T18:04:00.006Z</published><updated>2009-04-18T13:20:41.151Z</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-04-18T13:20:41.151Z</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="Phing" /><title>Getting an overview of all targets accumulated in a Phing build file</title><content type="html">Looking for an equivalent to &lt;a href="http://ant.apache.org" target="_blank"&gt;Ant&lt;/a&gt;'s &lt;i&gt;-p(rojecthelp)&lt;/i&gt; command-line option in Phing, I dug up that you can use &lt;a href="http://phing.info/trac/" target="_blank"&gt;Phing&lt;/a&gt;'s &lt;i&gt;-l(ist)&lt;/i&gt; option to get a quick overview of all targets hosted in a given build file. This comes in handy when you are maintaining build files or have to get a raw picture of the provided targets of a project specific build file. The following console output shows the targets hosted in a example build file. As you will see, by using the -l command-line option you get an overview of the default target, the main targets and further subtargets.&lt;pre class="consoleOutput"&gt;triton:work stolt$ phing -l&lt;br /&gt;Buildfile: /Volumes/USB DISK/work/build.xml&lt;br /&gt;Build file for recordshelf project&lt;br /&gt;Default target:&lt;br /&gt;-------------------------------------------------------------------------------&lt;br /&gt; help                Displays the help for this build file&lt;br /&gt;&lt;br /&gt;Main targets:&lt;br /&gt;-------------------------------------------------------------------------------&lt;br /&gt; build               Builds the project&lt;br /&gt; clean               Removes data left over by former build&lt;br /&gt; cs-coding-standard  Runs the coding standard code inspection&lt;br /&gt; help                Display the help for this build file&lt;br /&gt; init                Initializes the build process&lt;br /&gt; layout              Creates the project layout for the build&lt;br /&gt; lint-project        Lints all code artifacts&lt;br /&gt; test-components     Runs the automated component tests&lt;br /&gt; test-integration    Runs the automated integration tests&lt;br /&gt; test-units          Runs the automated unit tests&lt;br /&gt;&lt;br /&gt;Subtargets:&lt;br /&gt;-------------------------------------------------------------------------------&lt;br /&gt; build-initial-model&lt;/pre&gt;A target becomes a main target when it has an non-empty &lt;em&gt;description&lt;/em&gt; attribute, otherwise it will be a subtarget. This can be used to define the visibility and thereby 'callability' of targets, similar as you would do when defining access specifiers for class members of a PHP class. Main targets would become the public targets of your build file and all subtargets could be considered as private targets, which shouldn't be called directly as they might/should be used to provide specific helper functionality to public main targets or target wrappers. To definitively veto the call to a target, which never should be run directly via the Phing CLI(e.g. &lt;em&gt;phing tragetname&lt;/em&gt;), just define an invalid target name(i.e. &lt;em&gt;-build-initial-model&lt;/em&gt; instead of build-initial-model) so the Phing CLI will respond with an error and thereby not run the build file. The next console output shows the Phing CLI response when calling a private target.&lt;pre class="consoleOutput"&gt;triton:work stolt$ phing -build-initial-model&lt;br /&gt;&lt;em&gt;Unknown argument: -build-initial-model&lt;/em&gt;&lt;br /&gt;phing [options] [target [target2 [target3] ...]]&lt;br /&gt;Options:&lt;br /&gt;  -h -help               print this message&lt;br /&gt;  -l -list               list available targets in this project&lt;br /&gt;  -v -version            print the version information and exit&lt;br /&gt;  -q -quiet              be extra quiet&lt;br /&gt;  -verbose               be extra verbose&lt;br /&gt;  -debug                 print debugging information&lt;br /&gt;  -logfile &amp;lt;file&amp;gt;        use given file for log&lt;br /&gt;  -logger &amp;lt;classname&amp;gt;    the class which is to perform logging&lt;br /&gt;  -f -buildfile &amp;lt;file&amp;gt;   use given buildfile&lt;br /&gt;  -D&amp;lt;property&amp;gt;=&amp;lt;value&amp;gt;   use value for given property&lt;br /&gt;  -find &amp;lt;file&amp;gt;           search for buildfile towards the root of the&lt;br /&gt;                         filesystem and use it&lt;br /&gt;  -inputhandler &amp;lt;file&amp;gt;   the class to use to handle user input&lt;br /&gt;&lt;br /&gt;Report bugs to &amp;lt;dev@phing.tigris.org&amp;gt;&lt;/pre&gt;This post is somehow a partial and PHP flavoured remix of a &lt;a href="http://www.build-doctor.com/2008/03/ant-best-practices-provide-good-help.html" target="_blank"&gt;post&lt;/a&gt; by Julian Simpson, who runs the &lt;a href="http://www.build-doctor.com/" target="_blank"&gt;build doctor&lt;/a&gt; blog and is also the author of the '&lt;i&gt;Refactoring Ant Build Files&lt;/i&gt;' contribution to the upcoming &lt;a href="http://www.pragprog.com/titles/twa" target="_blank"&gt;ThoughtWorks Anthology&lt;/a&gt; book.&lt;br /&gt;&lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8420118650236071171-7078640050186351982?l=raphaelstolt.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/raphaelonphp/~4/1vNrX7uNcQE" height="1" width="1"/&gt;</content><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=8420118650236071171&amp;postID=7078640050186351982" title="2 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/7078640050186351982?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/7078640050186351982?v=2" /><link rel="alternate" type="text/html" href="http://raphaelstolt.blogspot.com/2008/03/getting-overview-of-all-targets.html" title="Getting an overview of all targets accumulated in a Phing build file" /><author><name>Raphael Stolt</name><uri>http://www.blogger.com/profile/07949831701855458792</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="14172620075949142098" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">2</thr:total></entry><entry gd:etag="W/&quot;CEICQ3w-fSp7ImA9WxZWEkk.&quot;"><id>tag:blogger.com,1999:blog-8420118650236071171.post-2117725052931856596</id><published>2008-03-11T14:30:00.001Z</published><updated>2008-03-11T13:36:02.255Z</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2008-03-11T13:36:02.255Z</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="Refactoring" /><category scheme="http://www.blogger.com/atom/ns#" term="PHP" /><category scheme="http://www.blogger.com/atom/ns#" term="PHP_CodeSniffer" /><title>Sniffing refactoring needs</title><content type="html">While still messing around with the &lt;a href="http://pear.php.net/package/PHP_CodeSniffer" target="_blank"&gt;PHP_CodeSniffer&lt;/a&gt; Pear package, I took a somehow jealous 1000 feet look at some prominent code inspection tools of the Java community: &lt;a href="http://pmd.sourceforge.net" target="_blank"&gt;PMD&lt;/a&gt; and &lt;a href="http://checkstyle.sourceforge.net/" target="_blank"&gt;Checkstyle&lt;/a&gt;. Browsing their available &lt;a href="http://pmd.sourceforge.net/rules/index.html" target="_blank"&gt;rule sets&lt;/a&gt;/&lt;a href="http://checkstyle.sourceforge.net/availablechecks.html" target="_blank"&gt;checks&lt;/a&gt; I soon recognized that guaranteeing the coding standard adherence is just a partial aspect of these tools. The following post will focus on one of these additional aspects, which is finding problems related to the code size of the inspected code artifacts, by showing how to port two selected rules to sniffs for utilization with the PHP_CodeSniffer tool. These ported sniffs can be used to automatically spot obvious code smells in the code base and to signal the need to apply the appropriate and odour reducing activity known as refactoring. Further more a complete set of code size sniffs, going beyond the trageted realm of the sniffs to come, could be used to speed up the feedback loop and to reduce the effort for manual code reviews.&lt;br /&gt;&lt;br /&gt;PMD has a rule named &lt;a href="http://pmd.sourceforge.net/rules/codesize.html#TooManyMethods" target="_blank"&gt;TooManyMethods&lt;/a&gt; which will fail when the inspected class is housing to much methods. In this case the class is probably breaking the &lt;a href="http://en.wikipedia.org/wiki/Single_responsibility_principle" target="_blank"&gt;Single responsibility principle&lt;/a&gt; and a refactoring i.e. &lt;a href="http://www.refactoring.com/catalog/extractClass.html" target="_blank"&gt;Extract Class&lt;/a&gt; should be applied to reduce complexity and take along the additional advantages like an increased testability and a clear separation of concerns.&lt;br /&gt;&lt;br /&gt;The following port of this rule simply counts the methods of the class under inspection and decides upon this count whether to raise an error, which will be reported, or not. It does so by analyzing the tokenised representation of the class under inspection and validating the relevant results against programmatically defined sniff criterions. For detailed information about how to roll your own sniffs take a look at the available &lt;a href="http://pear.php.net/manual/en/package.php.php-codesniffer.coding-standard-tutorial.php" target="_blank"&gt;tutorial&lt;/a&gt; of the PHP_CodeSniffer package.&lt;pre class="codeSnippet"&gt;&amp;lt;?php&lt;br /&gt;/**&lt;br /&gt; * 'File-level' PHPDoc Block&lt;br /&gt; */&lt;br /&gt;&lt;br /&gt;/**&lt;br /&gt; * This sniff counts the defined methods of a class and raises an error when&lt;br /&gt; * the configurable method amount per class is exceeded.&lt;br /&gt; *&lt;br /&gt; * An example of the targeted code size violation is:&lt;br /&gt; *&lt;br /&gt; * &amp;lt;code&amp;gt;&lt;br /&gt; *  class One&lt;br /&gt; *  {&lt;br /&gt; *      public function method1()&lt;br /&gt; *      {&lt;br /&gt; *          ...&lt;br /&gt; *      }&lt;br /&gt; *      ... // more methods&lt;br /&gt; *      public function methodN()&lt;br /&gt; *      {&lt;br /&gt; *          ...&lt;br /&gt; *      }&lt;br /&gt; *  }&lt;br /&gt; * &amp;lt;/code&amp;gt;&lt;br /&gt; *&lt;br /&gt; * @category  PHP&lt;br /&gt; * @package   PHP_CodeSniffer&lt;br /&gt; * @author    Raphael Stolt &amp;lt;raphael.stolt@gmail.com&amp;gt;&lt;br /&gt; * @license   http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence&lt;br /&gt; * @version   Release: @package_version@&lt;br /&gt; * @link      http://pear.php.net/package/PHP_CodeSniffer&lt;br /&gt; */&lt;br /&gt;class Example_Sniffs_CodeSize_ToManyMethodsSniff implements PHP_CodeSniffer_Sniff&lt;br /&gt;{&lt;br /&gt;    /**&lt;br /&gt;     * Defines the maximum of allowed methods in a inspected class.&lt;br /&gt;     *&lt;br /&gt;     * @var integer&lt;br /&gt;     */&lt;br /&gt;    private $_maxMethodsPerClass = 10;&lt;br /&gt;&lt;br /&gt;    /**&lt;br /&gt;     * Returns the token types this sniff is interested in.&lt;br /&gt;     * For an overview of available tokens take a look at the&lt;br /&gt;     * PHP_CodeSniffer_Tokens class.&lt;br /&gt;     *&lt;br /&gt;     * @return array(int)&lt;br /&gt;     */&lt;br /&gt;    public function register()&lt;br /&gt;    {&lt;br /&gt;        return array(T_CLASS);&lt;br /&gt;    }&lt;br /&gt;    /**&lt;br /&gt;     * Processes the tokens that this sniff is interested in.&lt;br /&gt;     *&lt;br /&gt;     * @param PHP_CodeSniffer_File $phpcsFile The file where the token was found.&lt;br /&gt;     * @param int                  $stackPtr  The position in the stack where&lt;br /&gt;     *                                        the token was found.&lt;br /&gt;     *&lt;br /&gt;     * @return void&lt;br /&gt;     */&lt;br /&gt;    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)&lt;br /&gt;    {&lt;br /&gt;        $classTokenIndex = $phpcsFile-&gt;findNext(array(T_CLASS), $stackPtr);&lt;br /&gt;        $classNameTokenIndex = $phpcsFile-&gt;findNext(array(T_STRING), $classTokenIndex);&lt;br /&gt;&lt;br /&gt;        $className = $phpcsFile-&gt;getTokensAsString($classNameTokenIndex, 1);&lt;br /&gt;&lt;br /&gt;        $methodTokenIndex = $stackPtr;&lt;br /&gt;&lt;br /&gt;        $foundMethodTokens = 0;&lt;br /&gt;&lt;br /&gt;        // Get all methods of class and increase counter on appearance&lt;br /&gt;&lt;br /&gt;        while ($methodTokenIndex = $phpcsFile-&gt;findNext(array(T_FUNCTION), $methodTokenIndex + 1)) {&lt;br /&gt;            if ($methodNameTokenIndex = $phpcsFile-&gt;findNext(array(T_STRING), $stackPtr)) {&lt;br /&gt;                $foundMethodTokens+=1;&lt;br /&gt;            }&lt;br /&gt;        }&lt;br /&gt;&lt;br /&gt;        // Decide if there is a sniff violation and raise error in case of&lt;br /&gt;&lt;br /&gt;        if ($foundMethodTokens &gt; $this-&gt;_maxMethodsPerClass) {&lt;br /&gt;            $message = "Method count in class '{$className}' exceeded "&lt;br /&gt;                     . "the defined maximum of {$this-&gt;_maxMethodsPerClass}"&lt;br /&gt;                     . ". Found {$foundMethodTokens} methods";&lt;br /&gt;            $phpcsFile-&gt;addError($message, $stackPtr);&lt;br /&gt;        }&lt;br /&gt;    }&lt;br /&gt;}&lt;br /&gt;?&amp;gt;&lt;/pre&gt;Another rule of PMD, falling in the rule set targeting code size violations, is called &lt;a href="http://pmd.sourceforge.net/rules/codesize.html#ExcessiveMethodLength" target="_blank"&gt;ExcessiveMethodLength&lt;/a&gt; and can also be easily ported to a PHP_CodeSniffer sniff. A violation of this rule/sniff, by exceeding the configurable method length, indicates again the need to put on the refactoring hat for keeping the code base in a non broken window state. The suitable refactorings in this case would be &lt;a href="http://www.refactoring.com/catalog/extractMethod.html" target="_blank"&gt;Extract Method&lt;/a&gt; or &lt;a href="http://www.refactoring.com/catalog/moveMethod.html" target="_blank"&gt;Move Method&lt;/a&gt;.&lt;pre class="codeSnippet"&gt;&amp;lt;?php&lt;br /&gt;/**&lt;br /&gt; * 'File-level' PHPDoc Block&lt;br /&gt; */&lt;br /&gt;&lt;br /&gt;/**&lt;br /&gt; * This sniff counts the lines of a defined method and raises an error when&lt;br /&gt; * the configurable method loc is exceeded.&lt;br /&gt; *&lt;br /&gt; * An example of the targeted code size violation is:&lt;br /&gt; *&lt;br /&gt; * &amp;lt;code&amp;gt;&lt;br /&gt; *  class One&lt;br /&gt; *  {&lt;br /&gt; *      public function toLongMethod()&lt;br /&gt; *      {&lt;br /&gt; *          // lots of code exceeding the configurable method loc&lt;br /&gt; *      }&lt;br /&gt; *  }&lt;br /&gt; * &amp;lt;/code&amp;gt;&lt;br /&gt; *&lt;br /&gt; * @category  PHP&lt;br /&gt; * @package   PHP_CodeSniffer&lt;br /&gt; * @author    Raphael Stolt &amp;lt;raphael.stolt@gmail.com&amp;gt;&lt;br /&gt; * @license   http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence&lt;br /&gt; * @version   Release: @package_version@&lt;br /&gt; * @link      http://pear.php.net/package/PHP_CodeSniffer&lt;br /&gt; */&lt;br /&gt;class Example_Sniffs_CodeSize_MethodLengthSniff implements PHP_CodeSniffer_Sniff&lt;br /&gt;{&lt;br /&gt;    /**&lt;br /&gt;     * Defines the maximum of allowed lines in an inspected method.&lt;br /&gt;     *&lt;br /&gt;     * @var integer&lt;br /&gt;     */&lt;br /&gt;    private $_maxMethodLoc = 100;&lt;br /&gt;    /**&lt;br /&gt;     * Defines whether to exclude comments from the method loc.&lt;br /&gt;     *&lt;br /&gt;     * @var boolean&lt;br /&gt;     */&lt;br /&gt;    private $_exludeComments = true;&lt;br /&gt;    /**&lt;br /&gt;     * Defines whether to exclude empty lines from the method loc.&lt;br /&gt;     *&lt;br /&gt;     * @var boolean&lt;br /&gt;     */&lt;br /&gt;    private $_excludeEmptyLines = true;&lt;br /&gt;&lt;br /&gt;    /**&lt;br /&gt;     * Returns the token types this sniff is interested in.&lt;br /&gt;     * For an overview of available tokens take a look at the&lt;br /&gt;     * PHP_CodeSniffer_Tokens class.&lt;br /&gt;     *&lt;br /&gt;     * @return array(int)&lt;br /&gt;     */&lt;br /&gt;    public function register() {&lt;br /&gt;        return array(T_FUNCTION);&lt;br /&gt;    }&lt;br /&gt;    /**&lt;br /&gt;     * Processes the tokens that this sniff is interested in.&lt;br /&gt;     *&lt;br /&gt;     * @param PHP_CodeSniffer_File $phpcsFile The file where the token was found.&lt;br /&gt;     * @param int                  $stackPtr  The position in the stack where&lt;br /&gt;     *                                        the token was found.&lt;br /&gt;     *&lt;br /&gt;     * @return void&lt;br /&gt;     */&lt;br /&gt;    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)&lt;br /&gt;    {&lt;br /&gt;        $tokens = $phpcsFile-&gt;getTokens();&lt;br /&gt;        $methodName = $phpcsFile-&gt;getDeclarationName($stackPtr);&lt;br /&gt;&lt;br /&gt;        $methodBeginScope = $tokens[$stackPtr]['scope_opener'] + 1;&lt;br /&gt;        $methodClosingScope = $tokens[$stackPtr]['scope_closer'];&lt;br /&gt;&lt;br /&gt;        // Get method body&lt;br /&gt;&lt;br /&gt;        $methodBody = $phpcsFile-&gt;getTokensAsString($methodBeginScope,&lt;br /&gt;            $methodClosingScope - $methodBeginScope);&lt;br /&gt;&lt;br /&gt;        $methodLines = explode("\n", $methodBody);&lt;br /&gt;&lt;br /&gt;        $funtionalMethodLines = array();&lt;br /&gt;&lt;br /&gt;        if ($this-&gt;_exludeComments) {&lt;br /&gt;            $commentPattern = '/^#.*' .         // Hash comment&lt;br /&gt;                              '|^\/\/.*' .      // Opening comment&lt;br /&gt;                              '|^\*.*' .        // Continuing multiple line comment&lt;br /&gt;                              '|^\*\/' .        // Closing comment&lt;br /&gt;                              '|^\/\*\*.*' .    // Opening comment/doc&lt;br /&gt;                              '|^\/\*.*\*\//i'; // Single line comment&lt;br /&gt;        }&lt;br /&gt;&lt;br /&gt;        // Handle each method line&lt;br /&gt;&lt;br /&gt;        foreach ($methodLines as $methodLine) {&lt;br /&gt;&lt;br /&gt;            $methodLine = str_replace(array(" ", "\t", "{"), '',&lt;br /&gt;                $methodLine);&lt;br /&gt;&lt;br /&gt;            if ($this-&gt;_exludeComments) {&lt;br /&gt;&lt;br /&gt;                preg_match($commentPattern, $methodLine, $matches);&lt;br /&gt;&lt;br /&gt;                if (count($matches) &gt; 0) {&lt;br /&gt;                    continue;&lt;br /&gt;                }&lt;br /&gt;            }&lt;br /&gt;            if ($this-&gt;_excludeEmptyLines) {&lt;br /&gt;                if (strlen($methodLine) &gt; 1) {&lt;br /&gt;                  $funtionalMethodLines[] = $methodLine;&lt;br /&gt;                }&lt;br /&gt;            } else {&lt;br /&gt;                $funtionalMethodLines[] = $methodLine;&lt;br /&gt;            }&lt;br /&gt;        }&lt;br /&gt;&lt;br /&gt;        $methodLoc = count($funtionalMethodLines);&lt;br /&gt;&lt;br /&gt;        // Decide if there is a sniff violation and raise error in case of&lt;br /&gt;&lt;br /&gt;        if ($methodLoc &gt; $this-&gt;_maxMethodLoc) {&lt;br /&gt;            $message = "Method '{$methodName}' LOC exceeded the defined "&lt;br /&gt;                     . "maximum of {$this-&gt;_maxMethodLoc} lines. Found "&lt;br /&gt;                     . "{$methodLoc} LOC";&lt;br /&gt;            $phpcsFile-&gt;addError($message, $stackPtr);&lt;br /&gt;        }&lt;br /&gt;    }&lt;br /&gt;}&lt;br /&gt;?&amp;gt;&lt;/pre&gt;In case you want to twiddle around with these exploratory sniffs you have to put the sniff definitions in the appropriate directory i.e. &lt;em&gt;PEAR/PHP/CodeSniffer/Standards/Example/Sniffs/CodeSize&lt;/em&gt; and create a 'coding standard' i.e. &lt;i&gt;PHP_CodeSniffer_Standards_Example_ExampleCodingStandard&lt;/i&gt; for making the sniffs runnable and available to the inspection tool. &lt;br /&gt;&lt;br /&gt;The closing console dump shows the two previous listed code size sniffs in action and the errors raised, after sniffing obvious refactoring needs in an inspected class.&lt;pre class="consoleOutput"&gt;triton:tmp stolt$ phpcs --standard=Example Test.php&lt;br /&gt;&lt;br /&gt;FILE: /Volumes/USB DISK/tmp/Test.php&lt;br /&gt;--------------------------------------------------------------------------------&lt;br /&gt;FOUND 2 ERROR(S) AND 0 WARNING(S) AFFECTING 2 LINE(S)&lt;br /&gt;--------------------------------------------------------------------------------&lt;br /&gt; 2 | ERROR | Method count in class 'Test' exceeded the defined maximum of 10.&lt;br /&gt;   |       | Found 12 methods&lt;br /&gt; 5 | ERROR | Method 'longMethod' LOC exceeded the defined maximum of 100 lines.&lt;br /&gt;   |       | Found 120 LOC&lt;br /&gt;--------------------------------------------------------------------------------&lt;br /&gt;&lt;/pre&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8420118650236071171-2117725052931856596?l=raphaelstolt.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/raphaelonphp/~4/F3FsutUkcN8" height="1" width="1"/&gt;</content><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=8420118650236071171&amp;postID=2117725052931856596" title="5 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/2117725052931856596?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/2117725052931856596?v=2" /><link rel="alternate" type="text/html" href="http://raphaelstolt.blogspot.com/2008/03/sniffing-refactoring-needs.html" title="Sniffing refactoring needs" /><author><name>Raphael Stolt</name><uri>http://www.blogger.com/profile/07949831701855458792</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="14172620075949142098" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">5</thr:total></entry><entry gd:etag="W/&quot;C0UNSXozcCp7ImA9WxZXFU4.&quot;"><id>tag:blogger.com,1999:blog-8420118650236071171.post-6129670849004983685</id><published>2008-02-28T11:43:00.009Z</published><updated>2008-03-03T08:01:38.488Z</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2008-03-03T08:01:38.488Z</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="Zend Framework" /><category scheme="http://www.blogger.com/atom/ns#" term="Book reviews" /><title>'Zend Framework Das Entwickler-Handbuch' Buch Rezension</title><content type="html">&lt;a href="http://www.flickr.com/photos/raphaelstolt/2298021234/" title="Zend Framework Entwickler Handbuch by Raphael Stolt, on Flickr"&gt;&lt;img src="http://farm4.static.flickr.com/3049/2298021234_0a053efe87_o.png" width="160" height="211" alt="Zend Framework Entwickler Handbuch" style="float:left; margin:0 10px 10px 0;cursor:pointer; cursor:hand;"/&gt;&lt;/a&gt;Yesterday I received the book &lt;a href="http://www.galileocomputing.de/katalog/buecher/titel/gp/titelID-1540" target="_blank"&gt;'Zend Framework Das Entwickler-Handbuch'&lt;/a&gt; written by Carsten Möhrke and published via &lt;a href="http://www.galileo-press.de/" target="_blank"&gt;Galileo Press&lt;/a&gt;. As the targeted audience of this book will be mainly german-speaking readers and the publisher has currently no intention to publish it in other languages, I will switch the language for this book review.&lt;br /&gt;&lt;br /&gt;'Zend Framework Das Entwickler-Handbuch' bietet auf insgesamt 420 Seiten einen guten Einstieg in die Anwendungsentwicklung mit dem Zend Framework bis zu der Version 1.0.3. In den einführenden Kapiteln werden grundlegende Themen wie die Installation des Frameworks, die Theorie des MVC Patterns und dessen Realisierung mit den entsprechenden Komponenten des Zend Framework behandelt. Die folgenden Kapitel des Buches sind dann meist vom MVC Kontext losgelöst und bieten daher gerade für Einsteiger die Möglichkeit weitere Komponenten wie z.B. Zend_Db und Zend_Db_Table für den Datenbankzugriff oder Zend_Auth für die Benutzerauthentifikation isoliert zu erkunden und deren Prinzipien zu verstehen. Das Spektrum der im Buch behandelten Komponenten erstreckt sich von Zend_Acl bis Zend_XmlRpc und bietet somit einen guten Gesamtüberblick auf die vorhandenen und nutzbaren Funktionalitäten des Frameworks. Die einzelnen Komponenten werden dem Leser jeweils durch meist adäquate Code-Listings und die in den Kontext passende Theorie nähergebracht. Aufgrund der raschen technischen Entwicklung und dem etwas unglücklichen gewählten Veröffentlichungstermins werden Komponenten wie z.B. Zend_Layout und Zend_Form aus dem Zend Framework 1.5.0 Preview Release leider nicht behandelt.&lt;br /&gt;&lt;br /&gt;Das Buch wendet sich meiner Meinung nach in erster Linie an Einsteiger, welche am Zend Framework interessiert sind und einen schnellen, portablen und deutschsprachigen Einstieg in dessen Magie haben wollen. Für diese Zielgruppe ist das Buch eine klare Empfehlung.&lt;br /&gt;&lt;br /&gt;Für Leser die sich schon gute Kenntnisse im Umgang mit dem Framework erarbeitet haben und Besitzer eines permanenten Onlinezugangs sind bietet dieses Buch meiner Meinung nach leider keinen großen Mehrwert, da es ein sehr gutes und kostenloses &lt;a href="http://framework.zend.com/manual/de/" target="_blank"&gt;Referenzhandbuch&lt;/a&gt; gibt und auftretende Detailfragen meist mittels diesem, der vorbildlichen &lt;a href="http://framework.zend.com/apidoc/core/" target="_blank"&gt;API Dokumentation&lt;/a&gt; oder in letzter Instanz mit der Hilfe der rasch reagierend &lt;a href="http://framework.zend.com/wiki/display/ZFDEV/Contributing+to+Zend+Framework#ContributingtoZendFramework-Subscribetotheappropriatemailinglists" target="_blank"&gt;Mailinglist&lt;/a&gt; Community geklärt werden können. Für diese Zielgruppe ist meine Empfehlung es einmal Probe zu lesen und dann selbst zu entscheiden.&lt;br /&gt;&lt;br /&gt;Als einen kaufentscheidenden Mehrwert für erfahrene Leser hätte ich mir z.B. ein grundlegendes Kapitel über das Testen von Zend Framework Anwendungen mit einem xUnit Framework z.B. PHPUnit gewünscht, dieser Aspekt wird von dem Autor leider total ausgeklammert.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8420118650236071171-6129670849004983685?l=raphaelstolt.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/raphaelonphp/~4/w1h8INL6W_w" height="1" width="1"/&gt;</content><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=8420118650236071171&amp;postID=6129670849004983685" title="1 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/6129670849004983685?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/6129670849004983685?v=2" /><link rel="alternate" type="text/html" href="http://raphaelstolt.blogspot.com/2008/02/zend-framework-das-entwickler-handbuch.html" title="'Zend Framework Das Entwickler-Handbuch' Buch Rezension" /><author><name>Raphael Stolt</name><uri>http://www.blogger.com/profile/07949831701855458792</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="14172620075949142098" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">1</thr:total></entry><entry gd:etag="W/&quot;C08EQXk7eip7ImA9WxVREUg.&quot;"><id>tag:blogger.com,1999:blog-8420118650236071171.post-4554946441614576706</id><published>2008-02-18T20:36:00.006Z</published><updated>2009-01-17T00:23:20.702Z</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-01-17T00:23:20.702Z</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="Zend Framework" /><category scheme="http://www.blogger.com/atom/ns#" term="TextMate" /><title>Creating Zend Framework snippets for TextMate</title><content type="html">After finally converting to Mac OS X, I really couldn't resist to fall for the famous &lt;a href="http://macromates.com/" target="_blank"&gt;TextMate&lt;/a&gt; editor. The editor comes with a nice PHP bundle authored by &lt;a href="http://ciaranwal.sh/" target="_blank"&gt;Ciarán Walsh&lt;/a&gt; that eases the typing and creation of common language constructs like classes and control structures by entering pre-defined keys and hitting the tab key to insert and customize them. To reduce the typing effort for the most common tasks in creating a Zend Framework based application, which are creating action controllers including their hosted actions and creating new models for accessing the underlying database, I spent some minutes to figure out how to create and add these valuable snippets to the default PHP bundle.&lt;br /&gt;&lt;h4 class="custom"&gt;Adding the controller and model snippets&lt;/h4&gt;The Bundle Editor of TextMate is a starter aid that can be used to create the intended Zend Framework snippets and categorize them. To add a new snippet simply select the PHP bundle and hit the most left button in the lower left of the Bundle Editor window. Now you are able to create new snippets, edit their content and assign an activation key to them i.e. &lt;em&gt;zfc&lt;/em&gt; and also to define the snippet scope i.e. source.php.&lt;br /&gt;&lt;br /&gt;To trigger the automatic insertion of the new snippet you just have to type &lt;em&gt;zfc&lt;/em&gt; and hit the tab key in an openend PHP file. The next listing shows the snippet code for the creation of a Zend Framework action controller and as you will notice it contains several at first glance variable look-a-likes. All variables prefixed with TM are definable via the TextMate preferences while the numeric indexed variables are used to tab through the code after insertion and to modify the custom parts like the name for the controller class. They start by an index of 1 for the first tab position after the snippet insertion and end with an index of 0 for the last tab position. Thereby the first two tab strikes will allow you to modify the PHPDoc block while the third tab strike will allow you to edit the controller name e.g. News, which whould make the current code artifact your NewsController class.&lt;br /&gt;&lt;pre class="codeSnippet"&gt;/**&lt;br /&gt; * ${1:'File-level' PHPDoc Block} &lt;br /&gt; *&lt;br /&gt; * @author  ${PHPDOC_AUTHOR:$TM_FULLNAME} &amp;lt;$TM_ORGANIZATION_EMAIL&amp;gt;&lt;br /&gt; */&lt;br /&gt;&lt;br /&gt;/**&lt;br /&gt; * ${2:'Class-level' PHPDoc Block}&lt;br /&gt; *&lt;br /&gt; * @author  ${PHPDOC_AUTHOR:$TM_FULLNAME} &amp;lt;$TM_ORGANIZATION_EMAIL&amp;gt;&lt;br /&gt; */&lt;br /&gt;class ${3:Name}Controller extends Zend_Controller_Action&lt;br /&gt;{&lt;br /&gt;    /**&lt;br /&gt;     * ${4:'Method-level' PHPDoc Block}&lt;br /&gt;     */ &lt;br /&gt;    public function ${5:name}Action()&lt;br /&gt;    {&lt;br /&gt;        $6&lt;br /&gt;    }&lt;br /&gt;    $0&lt;br /&gt;}//Activation key: zfc tab&lt;/pre&gt;As an action contoller surely will increase all allong by several more actions the next listing shows the snippet code to add a single action to a currently opened action controller class by simply entering &lt;em&gt;zfca&lt;/em&gt; and striking the tab key.&lt;br /&gt;&lt;pre class="codeSnippet"&gt;/**&lt;br /&gt; * ${1:'Method-level' PHPDoc Block}&lt;br /&gt; */&lt;br /&gt;public function ${2:name}Action()&lt;br /&gt;{&lt;br /&gt;    $0&lt;br /&gt;}//Activation key: zfca tab&lt;/pre&gt;The next &lt;strike&gt;last valuable&lt;/strike&gt; snippet would refer to the model part of a Zend Framework application, so the listing shows a snippet for a table class, which automatically sets the table name property to the lowercased and prior entered class name. This snippet would be inserted into the current PHP file by entering the activation key &lt;em&gt;zfm&lt;/em&gt; and striking the tab key again. &lt;pre class="codeSnippet"&gt;/**&lt;br /&gt; * ${1:'File-level' PHPDoc Block}&lt;br /&gt; * @author ${PHPDOC_AUTHOR:$TM_FULLNAME} &amp;lt;$TM_ORGANIZATION_EMAIL&amp;gt;&lt;br /&gt; */&lt;br /&gt;&lt;br /&gt;/**&lt;br /&gt; * ${2:'Class-level' PHPDoc Block}&lt;br /&gt; * @author ${PHPDOC_AUTHOR:$TM_FULLNAME} &amp;lt;$TM_ORGANIZATION_EMAIL&amp;gt;&lt;br /&gt; */&lt;br /&gt;class ${3:Name} extends Zend_Db_Table_Abstract&lt;br /&gt;{&lt;br /&gt;    protected \$_name = '${3/./\l$0/}';&lt;br /&gt;}//Activation key: zfm tab&lt;/pre&gt;&lt;h4 class="custom"&gt;Adding a View Helper snippet&lt;/h4&gt;The next snippet was provided by a reader called Jeff Federmann, who contacted me via mail as Google ate his code he tried to contribute in a comment. This snippet can get you up to speed when developing custom View Helpers. The workflow for this snippet is to create a new file for the helper, saving it with a purpose indicating name, which will auto-determine the View Helper class name on code insertion, and trigger the snippet by entering the activation key i.e. &lt;em&gt;zfvh&lt;/em&gt; plus hitting the tab again. So nuff talk. Here is the snippet contributed by Jeef.&lt;pre class="codeSnippet"&gt;/**&lt;br /&gt;* ${1:'File-level' PHPDoc Block} &lt;br /&gt;*&lt;br /&gt;* @author  ${PHPDOC_AUTHOR:$TM_FULLNAME} &lt;$TM_ORGANIZATION_EMAIL&gt;&lt;br /&gt;*/&lt;br /&gt;&lt;br /&gt;/**&lt;br /&gt;* ${2:'Class-level' PHPDoc Block}&lt;br /&gt;*&lt;br /&gt;* @author  ${PHPDOC_AUTHOR:$TM_FULLNAME} &lt;$TM_ORGANIZATION_EMAIL&gt;&lt;br /&gt;*/&lt;br /&gt;class Zend_View_Helper_${3:`#!/usr/bin/env php &lt;br /&gt;&lt;?php&lt;br /&gt;$filename = $_ENV['TM_FILENAME'];&lt;br /&gt;$filename = str_replace('.php', '', $filename);&lt;br /&gt;echo $filename;&lt;br /&gt;?&gt;`} extends Zend_View_Helper_Abstract&lt;br /&gt;{&lt;br /&gt;   /**&lt;br /&gt;    * ${4:'Method-level' PHPDoc Block}&lt;br /&gt;    */ &lt;br /&gt;   public function ${3/./\l$0/}()&lt;br /&gt;   {&lt;br /&gt;       $5&lt;br /&gt;   }&lt;br /&gt;   $0&lt;br /&gt;}&lt;/pre&gt;&lt;h4 class="custom"&gt;Housing the Zend Framework snippets&lt;/h4&gt;To finally categorize the Zend Framework snippets and avoid bloating the default PHP Bundle the Bundle Editor comes to rescue again, as it allows to add new categories to an existing bundle in which the shown snippets and other compatible and common snippets, e.g. Zend_Registry access to acquire the application's Zend_Log instance, can be housed.&lt;br /&gt;&lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8420118650236071171-4554946441614576706?l=raphaelstolt.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/raphaelonphp/~4/ti6Pokdf5Ac" height="1" width="1"/&gt;</content><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=8420118650236071171&amp;postID=4554946441614576706" title="2 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/4554946441614576706?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/4554946441614576706?v=2" /><link rel="alternate" type="text/html" href="http://raphaelstolt.blogspot.com/2008/02/creating-zend-framework-snippets-for.html" title="Creating Zend Framework snippets for TextMate" /><author><name>Raphael Stolt</name><uri>http://www.blogger.com/profile/07949831701855458792</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="14172620075949142098" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">2</thr:total></entry><entry gd:etag="W/&quot;Ck8ERXk4eip7ImA9WxZRF0g.&quot;"><id>tag:blogger.com,1999:blog-8420118650236071171.post-6469202502515353390</id><published>2008-02-11T18:26:00.000Z</published><updated>2008-02-11T17:26:44.732Z</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2008-02-11T17:26:44.732Z</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="Zend Framework" /><title>Zend Framework coding standards on one page</title><content type="html">Before jumping into the development of a Zend Framework coding standard for the &lt;a href="http://pear.php.net/package/PHP_CodeSniffer" target="_blank"&gt;PHP_CodeSniffer&lt;/a&gt; Pear package, I spent some time revisiting and compiling the available Zend Framework &lt;a href="http://framework.zend.com/manual/en/coding-standard.html" target="_blank"&gt;coding standards&lt;/a&gt; into a handy one-paged Pdf document.&lt;br /&gt;&lt;br /&gt;In case somebody is interrested it's available &lt;a href="http://raphael.stolt.googlepages.com/ZendFramework_coding_standards_on_on.pdf"&gt;here&lt;/a&gt;.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8420118650236071171-6469202502515353390?l=raphaelstolt.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/raphaelonphp/~4/QH_zdvKIOrI" height="1" width="1"/&gt;</content><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=8420118650236071171&amp;postID=6469202502515353390" title="1 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/6469202502515353390?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/6469202502515353390?v=2" /><link rel="alternate" type="text/html" href="http://raphaelstolt.blogspot.com/2008/02/zend-framework-coding-standards-on-one.html" title="Zend Framework coding standards on one page" /><author><name>Raphael Stolt</name><uri>http://www.blogger.com/profile/07949831701855458792</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="14172620075949142098" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">1</thr:total></entry><entry gd:etag="W/&quot;C0EEQX0_cCp7ImA9WxZRGEw.&quot;"><id>tag:blogger.com,1999:blog-8420118650236071171.post-2108576691785337878</id><published>2008-02-08T13:32:00.000Z</published><updated>2008-02-12T10:20:00.348Z</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2008-02-12T10:20:00.348Z</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="PHP_CodeSniffer" /><category scheme="http://www.blogger.com/atom/ns#" term="Phing" /><category scheme="http://www.blogger.com/atom/ns#" term="Continuous Integration" /><title>PHP_CodeSniffer task is in the current Phing branch</title><content type="html">A few days ago I was evaluating the need for writing a Phing task for the awesome &lt;a href="http://pear.php.net/package/PHP_CodeSniffer" target="_blank"&gt;PHP_CodeSniffer&lt;/a&gt; Pear package and noticed that &lt;a href="http://www.4wdmedia.de/unternehmen/dirk-thomas.html" target="_blank"&gt;Dirk Thomas&lt;/a&gt; blessedly already &lt;a href="http://phing.info/trac/changeset/328" target="_blank"&gt;added&lt;/a&gt; one to the current Phing Svn branch. That's again extending the range of available tools that PHP's Continuous Integration(CI) toolchain has to offer and I quess there are more to follow, as Manuel Pichler just announced the development start of &lt;a href="http://www.manuel-pichler.de/pages/php_depend.html" target="_blank"&gt;PHP_Depend&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;To get the 2.3 Phing branch including the mentioned PHP_CodeSniffer task and it's documentation simply run a &lt;em&gt;svn checkout http://svn.phing.info/branches/2.3 /path/to/phing-checkout-dir&lt;/em&gt; and you're ready to add continuous monitoring of coding standard adherence to your builds. &lt;br /&gt;&lt;br /&gt;For simplifying the integration of the just checked out Phing 'branch' release into an existing Pear environment you can build a new Pear package by calling the buildfile in &lt;em&gt;/path/to/phing-checkout-dir/pear&lt;/em&gt;. The built Pear package will be located in &lt;em&gt;/path/to/phing-checkout-dir/pear/build&lt;/em&gt; and can than be used to switch the installed Phing Pear release. &lt;br /&gt;&lt;br /&gt;Also make sure you have installed the underlying PHP_CodeSniffer Pear package via the Pear Installer.&lt;br /&gt;&lt;br /&gt;Although the task is very well documented the next code snippet shows the shiny PHP_CodeSniffer task in an example build file.&lt;pre class="codeSnippet"&gt;&amp;lt;?xml version="1.0"?&amp;gt;&lt;br /&gt;&amp;lt;project name="example" default="build" basedir="."&amp;gt;&lt;br /&gt;&lt;br /&gt;   &amp;lt;target name="build" depends="clean, svn-checkout, code-inspection, test"&amp;gt;&lt;br /&gt;&lt;br /&gt;   ...&lt;br /&gt;&lt;br /&gt;   &amp;lt;/target&amp;gt;&lt;br /&gt;&lt;br /&gt;   ...&lt;br /&gt;&lt;br /&gt;   &amp;lt;target name="code-inspection" description="runs code inspection on the stated directory"&amp;gt;&lt;br /&gt;     &amp;lt;phpcodesniffer standard="PEAR"&lt;br /&gt;                     format="summary"&lt;br /&gt;                     tabWidth="4"&lt;br /&gt;                     file="/path/to/source-files"&lt;br /&gt;                     allowedFileExtensions="php"/&amp;gt;&lt;br /&gt;     &amp;lt;/phpcodesniffer&amp;gt;&lt;br /&gt;   &amp;lt;/target&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;/project&amp;gt;&lt;br /&gt;&lt;/pre&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8420118650236071171-2108576691785337878?l=raphaelstolt.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/raphaelonphp/~4/XPG_PqLIe3E" height="1" width="1"/&gt;</content><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=8420118650236071171&amp;postID=2108576691785337878" title="3 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/2108576691785337878?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/2108576691785337878?v=2" /><link rel="alternate" type="text/html" href="http://raphaelstolt.blogspot.com/2008/02/phpcodesniffer-task-is-in-current-phing.html" title="PHP_CodeSniffer task is in the current Phing branch" /><author><name>Raphael Stolt</name><uri>http://www.blogger.com/profile/07949831701855458792</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="14172620075949142098" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">3</thr:total></entry><entry gd:etag="W/&quot;CUAHR3k4fip7ImA9WB9VFkQ.&quot;"><id>tag:blogger.com,1999:blog-8420118650236071171.post-753906140768189337</id><published>2007-12-03T11:40:00.000Z</published><updated>2007-12-03T14:55:36.736Z</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2007-12-03T14:55:36.736Z</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="Zend Framework" /><category scheme="http://www.blogger.com/atom/ns#" term="Book reviews" /><title>Zend Framework in Action Pre-review</title><content type="html">&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://farm3.static.flickr.com/2131/1590115009_a35ba148aa_m.jpg"&gt;&lt;img style="margin: 0pt 10px 10px 0pt; float: left; cursor: pointer; width: 192px; height: 240px;" src="http://farm3.static.flickr.com/2131/1590115009_a35ba148aa_m.jpg" alt="Zend Framework in Action" border="0" /&gt;&lt;/a&gt;For the last few days I've been reading the MEAP &lt;a href="http://manning.com/allen/" target="_blank"&gt;release&lt;/a&gt; of Zend Framework in Action from Rob Allen and Nick Lo published by Manning Publications and as a pre-review I d'like to share my reading experience. Currently there are 6 of totally 16 intended chapters available, to find out if them are already worth the money you might want to read further.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;What's in it?&lt;/b&gt;&lt;br /&gt;The first two chapters are introductional and outline what's in the Zend Framework and how it can support and ease the development of (enterprise) web applications. The obligatory Hello World application is used to provide a quick introduction to the MVC pattern and how it's implemented by the framework's controller system and how each part of the MVC pattern is represented in the code artifacts of a application. Next the anatomy of a Zend Framework based application (directory structure, bootstrapping and the request routing to the various components) is layed out to the reader.&lt;br /&gt;&lt;br /&gt;After prodviding this 3,000 feet view a community application is introduced, providing the stories and models which will be implemented with the appropriate Zend Framework components and evolve with the coming close-up view chapters. The book carries on to get the guiding application initially going and introduces techniques how to unclutter views through the Two-Step View design pattern and how to build a growing safety-net for your application and it's further evolution via unit tests written in PHPUnit.&lt;br /&gt;&lt;br /&gt;The next chapter opens the close-up views by looking at Ajax and how it fits within Zend Framework based applications. The connection of the views to the server/model code is demonstrated first via handcrafted code and next by using two common JavaScript libraries named Prototype and Yahoo! User Interface (YUI). Additional JSON, an important connector in nowadays Ajax application, is covered by taking a look at the Zend_Json component.&lt;br /&gt;&lt;br /&gt;Next the focus descends two layers deeper into the model, hosting the specific business logic of an application, by covering the way to establish a connection to the database via the Zend_Db_Adapter and how to perform CRUD actions against it with the Zend_Db_Table component. The linking of tables together is explained by a closer look at the table relationship feature of the Zend_Db_Table component. As a very valuable goodie the authors provide their additional knowledge about testing Zend Framework models. &lt;br /&gt;&lt;br /&gt;The last available chapter introduces the concepts of authorisation and authentication and goes on providing the needed theoretical background in addition with explanatory implementations for both scenarios.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Conclusion&lt;/b&gt;&lt;br /&gt;As this book is co-authored by Rob Allen, who provides the very valuable &lt;a href="http://akrabat.com/zend-framework-tutorial/" target="_blank"&gt;introduction&lt;/a&gt; I assume everyone getting started with the Zend Framework has used or at least noticed, Zend Framework in Action is the more detailed and naturally matured sequel of it. Though the book isn't released yet the provided knowledge is already valueable whether for novice or more advanced learners/developers whose thirst for knowledge isn't fully covered by the Zend Framework manual. Personally I liked the fact that the authors share their knowledge about unit testing Zend Framework applications and that the reader is guided along an example application, which might motivate to further exploratory coding activities. Reading only these first available chapters leaves you waiting for the rest of it.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8420118650236071171-753906140768189337?l=raphaelstolt.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/raphaelonphp/~4/2vWONH1izno" height="1" width="1"/&gt;</content><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=8420118650236071171&amp;postID=753906140768189337" title="1 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/753906140768189337?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/753906140768189337?v=2" /><link rel="alternate" type="text/html" href="http://raphaelstolt.blogspot.com/2007/12/zend-framework-in-action-pre-review.html" title="Zend Framework in Action Pre-review" /><author><name>Raphael Stolt</name><uri>http://www.blogger.com/profile/07949831701855458792</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="14172620075949142098" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">1</thr:total></entry><entry gd:etag="W/&quot;DU8FSXc4eyp7ImA9WB9XFEs.&quot;"><id>tag:blogger.com,1999:blog-8420118650236071171.post-2433610817461404334</id><published>2007-11-07T17:16:00.000Z</published><updated>2007-11-07T20:36:58.933Z</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2007-11-07T20:36:58.933Z</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="BDD" /><category scheme="http://www.blogger.com/atom/ns#" term="XP" /><category scheme="http://www.blogger.com/atom/ns#" term="Best Practice" /><category scheme="http://www.blogger.com/atom/ns#" term="TDD" /><title>Utilizing designer toys as an extreme feedback device</title><content type="html">&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://farm2.static.flickr.com/1086/748163626_39b91085f3_m.jpg"&gt;&lt;img style="margin: 0pt 10px 10px 0pt; float: left; cursor: pointer; width: 200px;" src="http://farm2.static.flickr.com/1086/748163626_39b91085f3_m.jpg" alt="extreme feedback device" border="0" /&gt;&lt;/a&gt;In response to Nick Halstead's &lt;a href="http://blog.assembleron.com/2007/11/07/programming-tips-competition/" target="_blank"&gt;Programming Tips Competition&lt;/a&gt; I dug a bit in my blog's idea queue for something related. The idea I picked includes some thoughts on how to gently prod the implementation of developer testing/specifying as an everyday team practice, assuming a head-nod/good-to-go from management. So today you won't see any code, as this is more related to the area of team building and process adoption. Instead of starting with the 'dictatorial' hammer and thereby possibly discouraging &lt;a href="http://www.developertesting.com/archives/month200701/20070126-developer%20testing%20test-infected%20gene.html" target="_blank"&gt;T2&lt;/a&gt; developers or scaring away &lt;a href="http://www.developertesting.com/archives/month200701/20070126-developer%20testing%20test-infected%20gene.html" target="_blank"&gt;T3&lt;/a&gt; developers, it would be wisely to use a fun and far more important attention creating approach. As this idea popped up on my mind I found a really suitable designer toy called &lt;a href="http://www.kidrobot.com/products2.cfm?ID=4529&amp;cfid=3823243&amp;cftoken=21317341&amp;nav_chooser=" target="_blank"&gt;Totem Doppelganger&lt;/a&gt; from Anton Ginzburg, consisting of three stackable look-a-like ghosts. I guess an excellent match to disenchant the initial 'mystics' of developer testing/specifying. As them are separable they can be used to accompany and indicate the ranged skill-development of each involved developer/team and thereby act as an &lt;a href="http://www.developertesting.com/archives/month200404/20040401-eXtremeFeedbackForSoftwareDevelopment.html" target="_blank"&gt;extreme feedback device&lt;/a&gt; for both parties.&lt;br /&gt;&lt;br /&gt;An example scenario could be:&lt;br /&gt;&lt;br /&gt;At the beginning of process adoption every developer/team without or less previous knowledge might get two feedback devices(ghosts), which will be reduced to one on the path to the expected knowledge. One device might remain to signal the willingness to spread the gained knowledge to other struggling with adoption the practices or new team members, this tends to the direction of building a &lt;a href="http://www.artima.com/weblogs/viewpost.jsp?thread=196922" target="_blank"&gt;Developer Testing Master&lt;/a&gt;. In the case of not practicing pair-programming they can even act as a reminder to apply developer testing/specifying until it becomes a flesh-and-blood habit.&lt;br /&gt;&lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8420118650236071171-2433610817461404334?l=raphaelstolt.blogspot.com'/&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/raphaelonphp/~4/zbYponYpcKU" height="1" width="1"/&gt;</content><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=8420118650236071171&amp;postID=2433610817461404334" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/2433610817461404334?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/8420118650236071171/posts/default/2433610817461404334?v=2" /><link rel="alternate" type="text/html" href="http://raphaelstolt.blogspot.com/2007/11/utilizing-designer-toys-as-extreme.html" title="Utilizing designer toys as an extreme feedback device" /><author><name>Raphael Stolt</name><uri>http://www.blogger.com/profile/07949831701855458792</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="14172620075949142098" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total></entry></feed>
