VirgenTech Blog Hector Virgen's Blog Fri, 12 Mar 2010 01:35:20 -0800 Zend_Feed_Writer 1.10.0beta (http://framework.zend.com) http://www.virgentech.com/blog/entries djvirgen@gmail.com (Hector Virgen) Hector Virgen Improved Comment Support Commenting on my blog just got easier!

]]>
Sat, 13 Feb 2010 22:24:42 -0800 http://www.virgentech.com/blog/2010/02/improved-comment-support.html http://www.virgentech.com/blog/2010/02/improved-comment-support.html djvirgen@gmail.com (Hector Virgen) Hector Virgen I've improved the comment support today for this site. The first change allows visitors to post a comment without requiring logging in. But to help protect against those evil spammers, I've added a captcha and set up Akismet to help keep the spam low.

I've also made it easier for authenticated users to comment by no longer asking for your name, e-mail address, or website. I plan to pull that information eventually though OpenID -- as long as the user agrees to it, of course.

I've also improved the JavaScript in the comments area. If you haven't posted a comment yet, you'll some nice Ajax and jQuery going on. Nothing fancy, really, just an overall better user experience in my opinion.

If you run into any issues with this site or have a suggestion for a new blog post, just let me know!

]]>
0
Building an Identity Map in PHP An identity map is a useful tool to keep track of all of the domain entities that are created in PHP. Here's an easy way to add one to your projects.

]]>
Tue, 09 Feb 2010 20:45:13 -0800 http://www.virgentech.com/blog/2010/02/building-identity-map-php.html http://www.virgentech.com/blog/2010/02/building-identity-map-php.html djvirgen@gmail.com (Hector Virgen) Hector Virgen I have been using an identity map in my web applications for quite some time. It's useful to keep track of all of the domain entities that have been created throughout the life of the request. It keeps me from loading the same object twice and makes it easier to do strict comparisons (===).

Without an identity map, you can easily run into problems because you may have more than one object that references the same domain entity.

$userA = $userMapper->find(123); // new object created
$userB = $userMapper->find(123); // new object created

echo $userA->getName(); // Hector
echo $userB->getName(); // Hector

$userA->setName('Bob');
echo $userA->getName(); // Bob
echo $userB->getName(); // Hector ?!?

The identity map solves this problem by acting as a registry for all loaded domain instances.

To build my identity map, the first thing I did was to make sure that each of my entities had a unique ID to distinguish it from other entities of the same type. To enforce this, I created an interface for my entities to implement:

interface Virgen_Entity_Interface
{
    public function getId();
}

The next step was to update my domain entities to implement this interface.

class Default_Model_User implements Virgen_Entity_Interface
{
    protected $_id;

    public function setId($id)
    {
        $this->_id = (int) $id;
    }

    public function getId()
    {
        return $this->_id;
    }
}

The ID can be anything, but because I'm using a relational database with an auto-incrementing primary key, I can use that value as my ID because I know it will be unique for each user.

Next comes the Identity Map. It will act as a registry to store entities so I can load it again later. To ensure that we don't end up with duplicates, we'll use a static array property to store the entities:

class Virgen_Entity_IdentityMap
{
    protected static $_identities = array();

    public static function loadEntity($class, $id)
    {
        $key = self::_generateKey($class, $id);
        if (isset(self::$_identities[$key])) {
            return self::$_identities[$key];
        }
        return false;
    }

    public static function storeEntity(Virgen_Entity_Interface $entity)
    {
        $class = get_class($entity);
        $id = $entity->getId();
        $key = self::_generateKey($class, $id);
        self::$_identities[$key] = $entity;
    }

    protected static function _generateKey($class, $id)
    {
        return $class . '-' . $id;
    }
}

The identity map has two public static functions that you can use to load and store entities.

You'll notice that the loadEntity() method accepts a class name and an ID. The reason for this is you don't want to have to instantiate the object before checking if you need to instantiate it. By passing in the class name and ID, you can query the identity map for the entity. If it returns an object, then you're good to go. If it's not already loaded, the identity map will return false and you'll need to create your object manually.

The storeEntity() method accepts an instance of Virgen_Entity_Interface, and calls its getId() method to help it create a unique key for the domain entity. The unique key is the entity's class name joined to the ID with a hyphen.

The IdentityMap works best with data mappers. Here's an example data mapper for the "user" entity.

class Default_Model_Mapper_User
{
    public function find($id)
    {
        if (false !== ($user = Virgen_Entity_IdentityMap::loadEntity('Default_Model_User', $id))) {
            // Found existing user, return it.
            return $user;
        }
        
        // Existing user not loaded
        $user = new Default_Model_User();
        
        // Populate $user with data from persistant storage
        /* ... */

        // Store user in identity map
        Virgen_Entity_IdentityMap::storeEntity($user);

        return $user;
    }
}

The first time you try to find the user ID 123, the identity map will return false and the mapper will do it's work of constructing it from the data in the persistant storage. But the second time you call find with that same ID, the identity map will return the object created earlier.

$mapper = new Default_Model_Mapper_User();

$userA = $mapper->find(123); // new object created
$userB = $mapper->find(123); // same object return

echo $userA->getName(); // Hector
echo $userB->getName(); // Hector

$userA->setName('Bob');

echo $userA->getName(); // Bob
echo $userB->getName(); // Bob

Much better :)

If your data mapper is also responsible for saving domain entities in the persistant storage, you will also want to update your save() method to store the entity in the identity map after a successful save. This way, you can be sure that if you want to find that entity again later it will be pulled from the identity map:

class Default_Model_Mapper_User
{
    /* ... */

    public function save(Default_Model_User $user)
    {
        if (null !== ($id = $user->getId())) {
            // update persistant storage
        } else {
            // insert into persistant storage and capture ID
        }

        Virgen_Entity_IdentityMap::storeEntity($user);
    }
}

By using an identity map you can be confident that your domain entity is shared throughout your application for the duration of the request.

Note that using an identity map is not the same as adding a cache layer to your mappers. Although caching is useful and encouraged, it can still produce duplicate objects for the same domain entity.

However, caching can further improve your data mappers but its important to note the order:

  1. Check the identity map first.
  2. If the entity is not found, check the cache.
  3. If not in the cache, load it from persistant storage.

By following the order above you can reap the benefits of both identity maps and caching systems.

]]>
0
New Site is Now Live! The new VirgenTech Website is now live!

]]>
Thu, 04 Feb 2010 19:37:19 -0800 http://www.virgentech.com/blog/2010/02/new-site-is-now-live.html http://www.virgentech.com/blog/2010/02/new-site-is-now-live.html djvirgen@gmail.com (Hector Virgen) Hector Virgen I just pushed the new website to my main server -- so whatcha think?

There are still a few edges to polish but overall the basic functionality I need is in. Here's what's new:

Rebuilt from scratch.

The entire site was rebuilt using the latest conventions of the Zend Framework, including the usage of Zend_Tool and Zend_Application. This means a much cleaner architecture and I got to learn a lot about it on the way.

New Code Section.

I rebuilt the code section to make it easy for me to create code entries from the web. My previous site was admittedly lacking in this area, and I needed to SSH into my web server in order to update code samples.

Blogs Rebuilt.

The entire engine powering this blog has been rebuilt using the data mapper pattern and Zend_Acl. This makes it easier for me to maintain, meaning more features will be coming soon (like tags).

I'm also using a lot of the latest components like Zend_Navigation, Zend_Paginator, and Zend_Feed_Writer.

Overall this has been a great learning experience. The Zend Framework has come a long way since I first started using it back in the 1.0 days. Using it's new features has helped me appreciate it that much more.

Some of the things I plan on working on next are guest comment support and maybe, just maybe, tree-based comments.

]]>
0
Login is Now Working You can now log in to VirgenTech, and the "code" tab is up and running!

]]>
Mon, 01 Feb 2010 21:33:18 -0800 http://www.virgentech.com/blog/2010/02/login-is-now-working.html http://www.virgentech.com/blog/2010/02/login-is-now-working.html djvirgen@gmail.com (Hector Virgen) Hector Virgen You can now log in to virgentech! In order to support OpenID, I had to recompile PHP from source to include the gmp extension.

The only issue I'm running into now is getting tidy to work. It seems that I need to also compile tidy from source, but the website was down the last time I looked. Hopefully I'll get that up and running soon to ensure that comments come in as clean HTML.

I've also updated the "Code" tab. I finished adding support to create code entries and edit them. I plan on using the "code" tab to showcase some of the code I'm working on.

]]>
0
Building a Smarter Model Layer My experiences with using the Data Mapper pattern in Zend Framework applications.

]]>
Mon, 01 Feb 2010 21:10:12 -0800 http://www.virgentech.com/blog/2010/02/building-smarter-model-layer.html http://www.virgentech.com/blog/2010/02/building-smarter-model-layer.html djvirgen@gmail.com (Hector Virgen) Hector Virgen I have been spending the last several months learning the "Data Mapper" pattern as suggested in the Zend Framework Quickstart. At first glance it seems simple, intuitive, and straight-forward -- the model contains the business rules, and the mapper is used to persist the model in the database. This separation of concerns can make the task of refactoring later much simpler and also allows for easier unit testing.

But when tasked at building a site that is more complex than a simple guestbook, the path is less clear.

My first attempt at implementing this pattern in my model layer at first proved to be useful. I started off by creating a series of "models", or objects that could be represented with nouns such as "user", "blog post", and "comment". I built an abstract model class that provided magic getters/setters. I created mappers for each model to persist them in a database. I created lazy-loading collections and reference objects so that I could traverse through related models as needed. And, finally, I introduced a service layer to create a single point of entry to my models.

// Abstract model class (simplified)
abstract class Virgen_Model_Abstract
{
    protected $_data = array();
    
    public function __get($key)
    {
        return $this->_data[$key];
    }
    
    public function __set($key, $value)
    {
        $this->_data[$key] = $value;
    }
}

class Default_Model_BlogPost extends Virgen_Model_Abstract
{
    protected $_data = array(
        'id' => null,
        'user' => null,
        /* ... */
    );
}

All was well -- or so it seemed.

When all was said and done, I noticed that my models were relatively thin. Most of them ended up as "dumb" containers of data and, thanks to my magic getters and setters, were more like glorified associative arrays. My service layer, however, was weighing in at a few thousand lines of code. So what happened?

It turns out that my models were suffering from anemia.

After reading Fowler's blog about anemic domain models, I decided to take a step back and start over. I wanted to be sure that if I'm going to use the data mapper pattern that I'm going to use it correctly.

With this in mind I put together two objectives:

  1. Remove the abstract model class. While useful, all that magic was making it too easy to be lazy.
  2. Clean up the service layer by moving all model-specific code into the model itself.

For the first part, I removed the abstract model class and redesigned my models to use real getters/setters for each property. This allowed me to enforce certain things like the user of a blog must be an instance of Default_Model_User.

// New blog post model (excerpt)
class Default_Model_BlogPost
{
    protected $_user;
    
    /* ... */
    
    public function setUser(Default_Model_User $user)
    {
        $this->_user = $user;
    }
    
    /* ... */
}

Now that my models have true getters/setters, I can be sure to include type hinting and even provide some functionality to them.

One of the areas that I've improved upon is the editing of the models through the use of forms. In my previous code, I used a service layer to create and populate a form:

$blogsService = new Default_Service_Blogs();
$blog = $blogsService->find(123);
$form = $blogsService->getEditForm($blog);

I've now moved that functionality into the model itself, meaning that a model is able to create and populate a form for editing itself:

$blogsService = new Default_Service_Blogs();
$blog = $blogsService->find(123);
$form = $blog->getForm();

This makes a big difference in what my blog model can do. Since it has access to it's own form, I can now use it for validation and also create new blog posts just as easily:

$blog = new Default_Model_BlogPost();
$form = $blog->getForm(); // returns empty form

Some of the things I am still working on is:

  • How to handle ACL checks. I previously had my service layer handle this, but with a thinning service layer I may need to support this directly in the model.
  • How to handle lazy-loading references for "belongs-to" or "has-one" relationships.

Overall, this new design appears to be overall more solid than my previous one. I am currently using it in a newly created VirgenTech website. I'm building it form the ground up using this design to see how well it works in a not-so-simple website.

]]>
0
New Site Coming Soon! The new VirgenTech website is nearly ready for launch!

]]>
Sat, 30 Jan 2010 16:08:26 -0800 http://www.virgentech.com/blog/2010/01/new-site-coming-soon.html http://www.virgentech.com/blog/2010/01/new-site-coming-soon.html djvirgen@gmail.com (Hector Virgen) Hector Virgen I've been busy the last few months building a new VirgenTech website. I plan to use this site to host my blog and code samples.

I'm currently working on fixing the login functionality. It seems that the OpenID component of the Zend Framework requires PHP to be compiled with GMP, so I'll need to get that working before login will work.

Once the login works I can spend more time blogging about the changes and posting code samples. I'll also be importing my old blog entries from Blogger.

]]>
0
Lazy Loading and Data Mappers How to use lazy loading to write efficient data mappers.

]]>
Mon, 18 Jan 2010 21:11:17 -0800 http://www.virgentech.com/blog/2010/01/lazy-loading-and-data-mappers.html http://www.virgentech.com/blog/2010/01/lazy-loading-and-data-mappers.html djvirgen@gmail.com (Hector Virgen) Hector Virgen Lazy loading is a simple yet powerful tool in any developer's tool box, and its knack for procrastination is especially useful in domain modeling.

Let's say we're building a simple blogging application, and each blog post can have 0 or more comments. So we may have models like this:

class Blog
{
    protected $_title;
    protected $_body;
    protected $_comments = array();
}

class Comment
{
    protected $_blog;
    protected $_from;
    protected $_body;
}

When developing your data mapper, you may intuitively want to load all the comments for the blog post when loading a blog from the database. However, you may not need all the comments. For example, you may simply be showing a list of all blog posts and are not displaying the comments at all. It would be unnecessary to load the comments, but your data mapper won't know how much information is needed when you request the object.

This is where lazy loading can help. To solve this, you'll want to crate a lazy-loading iterator. Initially, this iterator would be given the information it needs to build the collection, without actually building the collection itself.

So what kind of information does the iterator need? Only two things: a data mapper class (or instance), and a list of IDs. When the iterator is iterated, the instance is fetched by calling find() on the mapper with the current iteration's ID. Here's an example:

class CommentCollection implements SeekableIterator
{
    protected $_ids = array();
    protected $_mapper;
    protected $_instances = array();
    protected $_position = 0;

    public function setIds(array $ids)
    {
        $this->_ids = $ids;
        $this->_instances = array();
    }

    public function getIds()
    {
        return $this->_ids;
    }

    public function setMapper($mapper)
    {
        $this->_mapper = $mapper;
    }

    public function getMapper()
    {
        if (is_string($this->_mapper)) {
            $this->_mapper = new $this->_mapper;
        }
        return $this->_mapper;
    }

    public function key()
    {
        return $this->_position;
    }

    public function next()
    {
        ++$this->_position;
    }

    public function rewind()
    {
        $this->_position = 0;
    }

    public function valid()
    {
        return array_key_exists($this->_position, $this->_ids);
    }

    public function seek($position)
    {
        $this->_position = (int) $position;
    }

    public function current()
    {
        if (array_key_exists($this->_position, $this->_instances)) {
            $this->_instances[$this->_position] = $this->getMapper()->find($this->_ids[$this->_position]);
        }
    return $this->_instances[$this->_position];
}
}

Now, instead of passing an array of fully instantiated comments to your Blog, you can pass in this iterator. But, as you may have noticed, in order for this to work, the iterator must have a list of comment IDs. Since we may not be displaying the comments at all, let's take this one step further and make the comment IDs lazy-loaded, too. In order to do this, we'll create a new class that uses this one. But instead of giving it an array of IDs, we'll give it a callback function that it can use when first iterating through it.

class CommentCollectionLoader implements SeekableIterator
{
    protected $_collection;
    protected $_mapper;
    protected $_arguments = array();
    protected $_method;

    public function setIds(array $ids)
    {
        $this->getCollection()->setIds(array $ids);
    }

    public function getIds()
    {
        return $this->getCollection()->getIds();
    }

    public function setMapper($mapper)
    {
        $this->_mapper = $mapper;
    }

    public function getMapper()
    {
        if (is_string($this->_mapper)) {
            $this->_mapper = new $this->_mapper;
        }
        return $this->_mapper;
    }

    public function setMethod($method)
    {
        $this->_method = $method;
    }

    public function getMethod()
    {
        return $this->_method;
    }

    public function setArguments(array $arguments)
    {
        $this->_arguments = $arguments;
    }

    public function getArguments()
    {
        return $this->_arguments;
    }

    public function getCollection()
    {
        if (null === $this->_collection) {
            $this->_collection = new CommentCollection();
            $ids = call_user_func_array(array($this->getMapper(), $this->_method), $this->_arguments);
            $this->_collection->setIds($ids);
        }
        return $this->_collection;
    }

    public function key()
    {
        return $this->getCollection()->key();
    }

    public function next()
    {
        $this->getCollection()->next();
    }

    public function rewind()
    {
        $this->getCollection()->rewind();
    }

    public function valid()
    {
        return $this->getCollection()->valid();
    }

    public function seek($position)
    {
        $this->getCollection()->seek($position);
    }

    public function current()
    {
        return $this->getCollection()->current();
    }
}

The difference is that now this new collection can be instantiated and passed in directly to the Blog instance without invoking any additional SQL queries until the very moment you need it.

$blog = new Blog();
$comments = new CommentCollectionLoader();
$comments->setMapper('CommentMapper');
$comments->setMethod('findByBlog');
$comments->setArguments(array($blog));
$blog->setComments($comments);

foreach ($blog->getComments as $comment) {
    assert($comment instanceof Comment); // true
}

By following this pattern throughout your data mappers, you can effectively traverse throughout the entire domain from just a single instance, and only the required queries will run.

]]>
0
Building an Object-Oriented jQuery Plugin Learn how to use your OOP skills in jQuery.

]]>
Sun, 04 Oct 2009 21:13:18 -0700 http://www.virgentech.com/blog/2009/10/building-object-oriented-jquery-plugin.html http://www.virgentech.com/blog/2009/10/building-object-oriented-jquery-plugin.html djvirgen@gmail.com (Hector Virgen) Hector Virgen So you've been using jQuery as your Javascript framework and now you need to write a plugin. If you come from an Object-Oriented background like me, you may feel that jQuery's plugins leave a lot to be desired.

The basic formula to create a jQuery plugin is to extend the plugin namespace with a single method:

#myplugin.js

jQuery.fn.myplugin = function()
{
   // Do some cool stuff here
}

While that seems all fine and dandy for simple plugins, you may need to create more robust plugins that do many things, often in a non-linear fashion.

Some plugins get around this by adding tons of methods to jQuery's plugin namespace.

$('#test').plugin();
$('#test').pluginAdd('stuff');
$('#test').pluginRemove('other stuff');
$('#test').pluginDoSomethingCool();

I personally don't like that approach because it pollutes the jQuery plugin namespace with lots of methods. I personally like to stick to just one plugin method per plugin.

Other plugins use the first parameter of the plugin to call methods:

$('#test').plugin();
$('#test').plugin('add', 'stuff');
$('#test').plugin('remove', 'other stuff');
$('#test').plugin('doSomethingCool');

I think this approach is a little awkward, especially if the plugin accepts an options object the first time it is created. This approachs means you would have to either write a switch of all the methods you want to expose, or blindly accept any string as a method name.

To get around these hurdles, I've created a basic template for jQuery plugins that provides access to an Object-Oriented interface if needed while still maintaining jQuery's simplicity of a single method in the plugin namespace.

The first thing you need to do is wrap all your plugin code in an anonymous function. This will help keep things nice and tidy without creating global variables.

#myplugin.js

(function($){
   // Your plugin code goes here
})(jQuery);

Next, create your plugin as a class, where the first parameter is a single DOM element.

#myplugin.js

(function($){
   var MyPlugin = function(element)
   {
       element = $(element);
       var obj = this;

       // Public method
       this.publicMethod = function()
       {
           console.log('publicMethod() called!');
       };
   };
})(jQuery);

To make your new object-oriented class available as a jQuery plugin, write a simple wrapper function in the plugin namespace:

#myplugin.js

(function($){
   var MyPlugin = function(element)
   {
       element = $(element);
       var obj = this;

       // Public method
       this.publicMethod = function()
       {
           console.log('publicMethod() called!');
       };
   };

   $.fn.myplugin = function()
   {
       return this.each(function()
       {
           var myplugin = new MyPlugin(this);
       });
   };
})(jQuery);

Now, when you call $(element).myplugin(), the jQuery plugin instantiates an instance of MyPlugin, passing the element as the first argument.

But now there's a problem of how to get the object "myplugin" once it's been created. For this, I usually store the object in the elements data. This provides easy access to the object while allowing you to prevent accidental double instantiation in the event that the plugin was called again on the same element.

#myplugin.js

(function($){
   var MyPlugin = function(element)
   {
       element = $(element);
       var obj = this;

       // Public method
       this.publicMethod = function()
       {
           console.log('publicMethod() called!');
       };
   };

   $.fn.myplugin = function()
   {
       return this.each(function()
       {
           var element = $(this);
          
           // Return early if this element already has a plugin instance
           if (element.data('myplugin')) return;

           var myplugin = new MyPlugin(this);

           // Store plugin object in this element's data
           element.data('myplugin', myplugin);
       });
   };
})(jQuery);

Now you have easy access to the object should you need to run methods on it.

$('#test').myplugin();
var myplugin = $('#test').data('myplugin');
myplugin.publicMethod(); // prints "publicMethod() called!" to console

If you need to get fancy and add options parameter or other required parameters, just pass them from the jQuery plugin to your plugin's constructor:

#myplugin.js

(function($){
   var MyPlugin = function(element, options)
   {
       element = $(element);
       var obj = this;

       // Merge options with defaults
       var settings = $.extend({
           param: 'defaultValue'
       }, options || {});

       // Public method
       this.publicMethod = function()
       {
           console.log('publicMethod() called!');
       };
   };

   $.fn.myplugin = function(options)
   {
       return this.each(function()
       {
           var element = $(this);
          
           // Return early if this element already has a plugin instance
           if (element.data('myplugin')) return;

           // pass options to plugin constructor
           var myplugin = new MyPlugin(this, options);

           // Store plugin object in this element's data
           element.data('myplugin', myplugin);
       });
   };
})(jQuery);

You may also want to expose some of your object's methods while keeping others private. To make a private method, create a local function within your object using the var keyword:

#myplugin.js

(function($){
   var MyPlugin = function(element, options)
   {
       element = $(element);
       var obj = this;
       var settings = $.extend({
           param: 'defaultValue'
       }, options || {});
       
       // Public method - can be called from client code
       this.publicMethod = function()
       {
           console.log('public method called!');
       };

       // Private method - can only be called from within this object
       var privateMethod = function()
       {
           console.log('private method called!');
       };
   };

   $.fn.myplugin = function(options)
   {
       return this.each(function()
       {
           var element = $(this);
          
           // Return early if this element already has a plugin instance
           if (element.data('myplugin')) return;

           // pass options to plugin constructor
           var myplugin = new MyPlugin(this, options);

           // Store plugin object in this element's data
           element.data('myplugin', myplugin);
       });
   };
})(jQuery);

To see an example of a plugin I wrote that uses this template, check out my Tagger plugin.

]]>
0
Lazy Loading and Traversables in PHP5 How to take advantage of PHP5's iterators by using lazy loading.

]]>
Tue, 08 Sep 2009 21:15:10 -0700 http://www.virgentech.com/blog/2009/09/lazy-loading-traversables-php5.html http://www.virgentech.com/blog/2009/09/lazy-loading-traversables-php5.html djvirgen@gmail.com (Hector Virgen) Hector Virgen In my previous blog post, I demonstrated a simple implementation of lazy loading and how it can be used to load resources on demand. While the concept of lazy loading is simple, it can be used to solve many problems in real world applications.

For example, let's say you are building a simple quiz application. Each quiz can have multiple questions, and each question can have multiple answer choices. When it comes to domain modelling, you may want to access a quiz's questions directly from its model like this:

$id = 123;
$quiz = new Quiz($id);
$questions = $quiz->getQuestions(); // returns array of Question objects

Taking a closer look at the Quiz class, we can see what is happening when the getQuestions is called:

class Quiz
{
    /* ... */
    protected $_questions = null;

    public function getQuestions()
    {
        if (null === $this->_questions) {
            // Perform some magic to load the questions
        }
        return $this->_questions;
    }
}

While this is a form of lazy loading, it may still be providing us with too much information. For example, let's say the questions are displayed to the user one at a time. There's certainly no need to load and instantiate all of those Question objects if we only need to display one.

Part of the problem is that, currently, $this->_questions is an array, and arrays are not flexible enough for lazy loading. That's where the Iterator interface comes into play.

By using the Iterator as a container for the questions and adding a lazy-loading mechanism to the current() method, we can improve the efficiency of container by only loading what we need when we need it.

class QuestionsContainer implements Iterator
{
    protected $_questionIds = array();

    protected $_questionInstances = array();

    protected $_position = 0;

    public function __construct(array $ids)
    {
        $this->_questionIds = array_values($ids);
    }

    public function rewind()
    {
        $this->_position = 0;
    }

    public function next()
    {
        ++$this->_position;
    }

    public function key()
    {
        return $this->_position;
    }

    public function valid()
    {
        return isset($this->_questionIds[$this->_position]);
    }

    public function current()
    {
        if (!isset($this->_questionInstances[$this->_position])) {
            $id = $this->_questionIds[$this->_position];
            $this->_questionInstances[$this->_position] = new Question($id);
        }
        return $this->_questionInstances[$this->_position];
    }
}

Now your collection is ready to go, so long as you provide it with an array of IDs to work with. You can use foreach() on this container and only with each iteration will a Question object be instantiated.

Room for Improvement?

Depending on your circumstances you may need to be able to count the items in your container. To do this, simply implement the Countable interface and add a count() method:

class QuestionsContainer implements Iterator, Countable
{
    /* ... */
    public function count()
    {
        return count($this->_questionIds);
    }
}

Note: Be sure to count the number of IDs, not the number of instances, or else you may end up with a smaller count than expected!

One of the benefits of using a lazy-loading iterator is that you can easily paginate through thousands of results and only show 10 or 20 at time.

Any Drawbacks or Limitations?

There are a few drawbacks to using this pattern over a traditional array, but depending on your use the strengths may easily outweigh the weaknesses. The weakness I've noticed are:

  • Since the container object is not a true array, it's array capabilities are limited. For example, you cannot perform an array_merge() on this container, but you can implement your own method that merges in another array (or even another container).
  • This pattern is susceptible to the N+1 problem, where the number of queries is equal to the number of items plus one. However, usually the queries required for the instantiation of a Question object are minimal.

In my next post, I will show you how you can combine these ideas with the Data Mapper pattern to keep your business logic and persistence logic separated.

]]>
0
Lazy Loading Resources in PHP Lazy loading is a great tool to keep applications running quickly.

]]>
Wed, 19 Aug 2009 21:16:40 -0700 http://www.virgentech.com/blog/2009/08/lazy-loading-resources-in-php.html http://www.virgentech.com/blog/2009/08/lazy-loading-resources-in-php.html djvirgen@gmail.com (Hector Virgen) Hector Virgen In most PHP applications it is common to have several resources. The database connection, the user session, the server-side cache object and helper classes are eventually used in parts of the application. But often times, certain pages do not need all of the resources. In this tutorial, I'll show you how to use lazy loading to create the resources only when you need them.

What is Lazy Loading?

Lazy loading is based on the concept that your resources should only be created when needed, and re-used if needed more than once. It's fairly straight-forward to implement and can also help with unit tests (more on this later).

How Lazy Loading Works

Lazy loading works best in object-oriented code, so we'll begin with a class. Let's say we're building a simple database connection class and want to lazy load the actual connection. A typical class might look like this:

<?php

class DbConnection
{
    protected $_connection;

    public function __construct($dsn, $username = null, $password = null, array $options = array())
    {
        $this->_connection = new Pdo($dsn, $username, $password, $options);
    }

    public function getConnection()
    {
        return $this->_connection;
    }
}

As you can see, the connection to the database is made immediately when the DbConnection class is instantiated. Normally this type of object is created in the application's bootstrap so that all pages will have access to a database connection.

But what happens when you have a page that doesn't need a database connection? That resource is now wasted CPU cycles. How can this be improved by lazy loading?

Simple -- just move the connection code into the getConnection() method. You can then test if the connection has already been made and, if not, create a new one. Keep in mind that you'll also need to store the constructor arguments so that they are available when you need to make the connection.

<?php

class DbConnection
{
    protected $_connection;
    protected $_dsn;
    protected $_username;
    protected $_password;
    protected $_options = array();

    public function __construct($dsn, $username = null, $password = null, array $options = array())
    {
        $this->_dsn = $dsn;
        $this->_username = $username;
        $this->_password = $password;
        $this->_options = $options;
    }

    public function getConnection()
    {
        if (null === $this->_connection) {
            $this->_connection = new Pdo($this->_dsn, $this->_username, $this->_password, $this->_options);
        }
        return $this->_connection;
    }
}

Now your database connection won't be opened until you call DbConnection::getConnection(). This can help improve overall application performance if many of your pages do not require a database connection.

Unit Testing with Lazy Loading

Lazy loading compliments unit-testing because you can stub in a mock connection before calling DbConnection::getConnection(), allowing you to test your application without the need of a real database connection.

This is as simple as adding a setConnection() method:

<?php

class DbConnection
{
    /* ... */
    public function setConnection(Pdo $connection)
    {
        $this->_connection = $connection;
    }
}

What else is Lazy Loading good for?

Lazy loading isn't just for database connections. Virtually any resource can be lazy-loaded to help improve performance. For example, I use lazy loading often in my Zend Framework applications to load things like forms, table classes, and service layers.

The way I see it, there's no need to load a service if it's not going to be used, and there's no need to load the same service more than once when the same instance will do.

]]>
0
Zend_Db_Table Enhancements Add Modified-Preorder-Tree-Traversal support to your tables with ease.

]]>
Thu, 23 Jul 2009 21:18:28 -0700 http://www.virgentech.com/blog/2009/07/zend-db-table-enhancements.html http://www.virgentech.com/blog/2009/07/zend-db-table-enhancements.html djvirgen@gmail.com (Hector Virgen) Hector Virgen The Zend Framework's Zend_Db_Table class offers plenty of features to make working with tables a breeze in PHP. You can easily insert, update, and delete rows, along with build complex select queries with Zend_Db_Table_Select.

I have subclassed Zend_Db_Table_Abstract to add my own commonly-used features, like preInsert() and preUpdate() methods, and automated support for tables using the Modified Preorder Tree Traversal algorithm.

Modified Preorder Tree Traversal

To use the modified preorder tree traversal algorithm in your table, you will initially have to do just a little bit of work but once it is set up everything should be automated for you.

First, you will need to create your table in MySQL and add two columns for the "left" and "right" values. Let's create a comments table as an example. Since "left" and "right" are reserved words in SQL, let's name these columns "lt" and "rt", but you can name them whatever you choose. You will also need to add a "parent_id" column, which references this same table's "id" column.

 
CREATE TABLE `comments` (
  `id` bigint(20) unsigned NOT NULL auto_increment,
  `parent_id` bigint(20) unsigned default NULL,
  `name` varchar(255) NOT NULL,
  `subject` varchar(255) NOT NULL,
  `comment` text NOT NULL,
  `created` timestamp NOT NULL default CURRENT_TIMESTAMP,
  `modified` datetime default NULL,
  `lt` bigint(20) unsigned NOT NULL,
  `rt` bigint(20) unsigned NOT NULL,
  PRIMARY KEY  (`id`),
  KEY `parent_id` (`parent_id`),
  KEY `lt` (`lt`,`rt`),
  KEY `rt` (`rt`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

Next, you will need to create your table class by extending Virgen_Db_Table, and declare the traversal properties.

 
<?php
 
class Model_Comments extends Virgen_Db_Table
{
    protected $_name = 'comments';
    
    protected $_traversal = array(
        'left'          => 'lt',
        'right'         => 'rt',
        'column'        => 'id',
        'refColumn'     => 'parent_id'
    );
}

That's it! Now when you insert a new record, the "lt" and "rt" columns will be updated as necessary to reflect the new preorder tree.

If you already have data in your table or want to rebuild the entire tree, you can use the rebuildTreeTraversal() method. Please note on large tables, this may take some time to complete.

 
<?php
 
$comments = new Model_Comments();
$comments->rebuildTreeTraversal();

Fetching Descendents of a Given Node

Once your tree is built, you can fetch all descendents of a node with fetchAllDescendents(). The first argument is the node to fetch the descendents of. The node can be either an instance of Zend_Db_Table_Row_Abstract or the string/numeric value of the columns id (based on $_traversal['column']). You can optionally pass in a select object to use as the second argument, which will be used when selecting the descendents.

 
<?php
 
$node = $comments->find(17)->current();
$descendents = $comments->fetchAllDescendents($node);
// Identical to:
$descendents = $comments->fetchAllDescendents(17);

// With optional select object
$select = $comments->select()->where('name = ?', 'jennifer')->limit(5);
$descendends = $comments->fetchAllDescendents($node, $select);

Fetching Ancestors of a Given Node

You can also fetch the ancestors just as easily with fetchAllAncestors(). All ancestors from the immediate parent up to the root of the tree will be returned.

 
<?php
 
$ancestors = $comments->fetchAllAncestors($node);

Fetching Nodes as a Tree

You can fetch nodes as a tree by calling $table->fetchTree(). Its functionality is similar to fetchDescendents, except that it returns a modified rowset in that each row contains a tree_depth value.

 
<?php
 
$tree = $comments->fetchTree();
 
foreach ($tree as $node) {
    echo str_repeat(' ', $node->tree_depth * 4) . $node->id . PHP_EOL;
}

Class: Virgen_Db_Table

Here's the complete Virgen_Db_Table class:

 
<?php
 
/**
 * Enhancements to Zend_Db_Table
 * @author Hector Virgen
 * 
 */
require_once 'Zend/Db/Table/Abstract.php';
 
class Virgen_Db_Table extends Zend_Db_Table_Abstract
{
    /**
     * Traversal tree information for
     * Modified Preorder Tree Traversal Model
     * 
     * http://www.sitepoint.com/print/hierarchical-data-database
     * 
     * Values:
     *  'left'          => column name for left value
     *  'right'         => column name for right value
     *  'column'        => column name for identifying row (primary key assumed)
     *  'refColumn'     => column name for parent id (if not set, will look in reference map for own table match)
     *  'order'         => order by for rebuilding tree (e.g. "`name` ASC, `age` DESC")
     *
     * @var array $_traversal
     */
    protected $_traversal = array();
    
    /**
     * Automatically is set to true once traversal info is set and verified
     *
     * @var boolean $_isTraversable
     */
    protected $_isTraversable = false;
    
    /**
     * Modified to initialize traversal
     *
     */
    public function __construct($config = array())
    {
        parent::__construct($config);
        $this->_initTraversal();
    }
    
    /**
     * Returns columns names
     *
     * @return array columns
     */
    public function getColumns()
    {
        return $this->info(Zend_Db_Table_Abstract::COLS);
    }
    
    /**
     * Returns metadata value for index or entire array
     *
     * @param index $key
     * @return value | array
     */
    public function getMetadata($key = null)
    {
        if (null === $key) return $this->_metadata;
        if (!array_key_exists($key, $this->_metadata)) {
            require_once 'Zend/Db/Table/Exception.php';
            throw new Zend_Db_Table_Exception("Key '{$key}' not found in metadata");
        }
        return $this->_metadata[$key];
    }
    
    /**
     * Returns the table name and schema separated by a dot for use in sql queries
     *
     * @return string schema.name || name
     */
    public function getName()
    {
        return $this->_schema ? $this->_schema . '.' . $this->_name : $this->_name;
    }
    
    /**
     * Is Duplicate - Checks for a duplicate value in the database
     *
     * @param string $column - column name
     * @param string $value - value to search for
     * @return boolean
     */
    public function isDuplicate($column, $match)
    {
        $select = $this->select()->limit(1);
        
        if (is_string($match) OR is_numeric($match)) {
            $select->where("{$column} = ?", $match);
        } else if (is_array($match)) {
            $select->where("{$column} IN (?)", $match);
        } else {
            require_once 'Zend/Db/Table/Exception.php';
            throw new Zend_Db_Table_Exception("Match value must be a string, numeric, or array");
        }
        
        return (null !== $this->fetchRow($select)) ? true : false;
    }
    
    /**
     * Fetches duplicate entries based on column name
     *
     * @param string $column - column name
     * @param string $match - optional match value
     * @return Zend_Db_Table_Rowset
     */
    public function fetchDuplicates($column, $match = null)
    {
        $select = $this->select()
        ->from(
            $this->getName(), 
            array(
                'value'         => $column, 
                'duplicates'    => new Zend_Db_Expr('COUNT(*)')
            )
        )
        ->group($column)
        ->having('duplicates > ?', 1)
        ;
        
        if (is_string($match) OR is_numeric($match)) {
            $select->where("{$column} = ?", $match);
        } else if (is_array($match)) {
            $select->where("{$column} IN (?)", $match);
        }
        
        return $this->fetchAll($select);
    }
    
    /**
     * Is Valid - Checks if a field is valid based on its validator
     *
     * @param string $field
     * @param string|int $value
     * @return boolean
     */
    public function isValid($field, $value)
    {
        if (!array_key_exists($field, $this->_validators)) return true;
        
        foreach($this->_validators[$field] as $validator) {
            if (!array_key_exists('name', $validator)) {
                require_once 'Zend/Db/Table/Exception.php';
                throw new Zend_Db_Table_Exception("Validators must contain a name.");
            }
            $name = $validator['name'];
            $arguments = array_key_exists('arguments', $validator) ? $validator['arguments'] : array();
            if (!Zend_Validate::is($value, $name, $arguments)) {
                return false;
            }
        }
        
        return true;
    }
    
    /**
     * Counts the number of rows for a given select statement.
     * Accepts instances of Zend_Db_Table_Select, Zend_Db_Select,
     * an array of WHERE clauses, or null to return a total
     * count of all rows in the table.
     *
     * @param Zend_Db_Table_Select|string|array $select
     * @return int theCount
     */
    public function count($select = null)
    {
        // Count using instance of Zend_Db_Table_Select
        if ($select instanceof Zend_Db_Table_Select) {
            $_select = clone $select;
            $result = $this->_countSelect($_select);
            
        // Count using array or count all
        } else if(null === $select OR is_string($select) OR is_array($select)) {
            $result = $this->_countWhere($select);
 
        // Invalid parameter
        } else {
            require_once 'Zend/Db/Table/Exception.php';
            throw new Zend_Db_Table_Exception('Invalid parameter passed to count() method');
        }
        
        return $result;
    }
    
    /**
     * Counts the number of rows using an instance of 
     * Zend_Db_Table_Select.
     *
     * @param Zend_Db_Table_Select $select
     * @return double theCount
     */
    protected function _countSelect(Zend_Db_Table_Select $select)
    {
        $s = clone $select;
        
        // Remove any existing limits, offsets, and orders
        $s->reset('order');
        $s->reset('limitcount');
        $s->reset('limitoffset');
        
        
        $_select = $this->getAdapter()
        ->select()
        ->from(
            array('c' => $s),
            array('theCount' => 'COUNT(*)')
        )
        ;
        
        $row = $this->getAdapter()->fetchRow($_select);
        
        return (double) $row['theCount'];
    }
    
    /**
     * Counts the number of rows using an array or string
     * of where clauses, or null to count all rows in the 
     * table.
     *
     * @param array|string $where
     * @return double theCount
     */
    protected function _countWhere($where = null)
    {
        $select = $this->select();
        if (is_array($where)) {
            foreach ($where as $key => $value) {
                if (is_int($key)) {
                    $select->where($value);
                } else {
                    $select->where($key, $value);
                }
            }
        } else if (is_string($where)) {
            $select->where($where);
        }
        
        return (double) $this->_countSelect($select);
    }
    
    /**
     * Returns the number of rows from the last SQL_CALC_FOUND_ROWS query
     *
     * @return double - found rows
     */
    public function getCalcFoundRows()
    {
        $sql = "SELECT FOUND_ROWS() AS theCount";
        $stmt = $this->_db->query($sql);
        $row = $stmt->fetch();
        
        return (double) $row['theCount'];
    }
    
    /**
     * Pre-insert hook allows for data validation / filtering on a per-class basis
     *
     * @param array $data
     * @return array
     */
    public function preInsert($data)
    {
        return $data;
    }
    
    /**
     * Pre-update hook allows for data validation / filtering on a per-class basis
     *
     * @param array $data
     * @return array
     */
    public function preUpdate($data)
    {
        return $data;
    }
    
    /**
     * Override insert method to include pre-insert hook
     *
     * @param mixed $data
     * @return primary key
     */
    public function insert(array $data)
    {
        $data = $this->preInsert($data);
        
        return $this->_isTraversable ? $this->_insertTraversable($data) : parent::insert($data);
    }
    
    /**
     * Override update method to include pre-update hook
     *
     * @param mixed $data
     * @param mixed $where
     * @return int
     */
    public function update(array $data, $where)
    {
        $data = $this->preUpdate($data);
        
        return parent::update($data, $where);
    }
    
    /**
     * Factory method to return instances of reference tables
     *
     * @param string $name
     * @param array $options for constructor
     * @return Virgen_Db_Table $instance
     */
    public function getReferenceInstance($ruleKey, array $options = array())
    {
        if (!array_key_exists($ruleKey, $this->_referenceMap)) {
            require_once 'Zend/Db/Table/Exception.php';
            throw new Zend_Db_Table_Exception("Reference key {$ruleKey} not found in " . __CLASS__);
        }
        
        $className = $this->_referenceMap[$ruleKey]['refTableClass'];
        
        // Check for self-references
        if (!array_key_exists($className, self::$_referenceInstances)) {
            self::$_referenceInstances[$className] = ($className == __CLASS__) ?
                $this:
                new $className($options);
        }
        
        return self::$_referenceInstances[$className];
    }
    
    /**
     * Factory method to return instances of dependent tables
     *
     * @param string $name - class name of dependent table
     * @param array $options - options to pass to constructor
     * @return Virgen_Db_Table $instance
     */
    public function getDependentInstance($className, array $options = array())
    {
        if (!in_array($className, $this->_dependentTables)) {
            require_once 'Zend/Db/Table/Exception.php';
            throw new Zend_Db_Table_Exception("Dependent table {$className} not found in " . __CLASS__);
        }
        
        if (!array_key_exists($className, self::$_dependentInstances)) {
            self::$_dependentInstances[$className] = ($className == __CLASS__) ?
                $this:
                new $className($options);
        }
        
        return self::$_dependentInstances[$className];
    }
    
    /**
     * Returns all reference instances
     *
     * @return array - reference instances
     */
    public function getReferenceInstances()
    {
        return self::$_dependentInstances;
    }
    
    /**
     * Returns all dependent instances
     *
     * @return array - dependent instances
     */
    public function getDependentInstances()
    {
        return self::$_dependentInstances;
    }
    
    /**
     * Public function to rebuild tree traversal. The recursive function
     * _rebuildTreeTraversal() must be called without arguments.
     *
     * @return $this - Fluent interface
     */
    public function rebuildTreeTraversal()
    {
        $this->_rebuildTreeTraversal();
        
        return $this;
    }
    
    /**
     * Recursively rebuilds the modified preorder tree traversal
     * data based on a parent id column
     *
     * @param int $parentId
     * @param int $leftValue
     * @return int new right value
     */
    protected function _rebuildTreeTraversal($parentId = null, $leftValue = 0)
    {
        $this->_verifyTraversable();
        
        $select = $this->select();
        
        if ($parentId > 0) {
            $select->where("{$this->_traversal['refColumn']} = ?", $parentId);
        } else {
            $select->where("{$this->_traversal['refColumn']} IS NULL OR {$this->_traversal['refColumn']} = 0");
        }
        
        if (array_key_exists('order', $this->_traversal)) {
            $select->order($this->_traversal['order']);
        }
        
        $rightValue = $leftValue + 1;
        
        $rowset = $this->fetchAll($select);
        foreach ($rowset as $row) {
            $rightValue = $this->_rebuildTreeTraversal($row->{$this->_traversal['column']}, $rightValue);
        }
        
        if ($parentId > 0) {
            $node = $this->fetchRow($this->select()->where("{$this->_traversal['column']} = ?", $parentId));
            if (null !== $node) {
                $node->{$this->_traversal['left']} = $leftValue;
                $node->{$this->_traversal['right']} = $rightValue;
                $node->save();
            }
        }
        
        return $rightValue + 1;
    }
    
    /**
     * Calculates left and right values for new row and inserts it.
     * Also adjusts all rows to make room for the new row.
     *
     * @param array $data
     * @return int $id
     */
    protected function _insertTraversable($data)
    {
        $this->_verifyTraversable();
        
        // Disable traversable flag to prevent automatic traversable manipulation during updates.
        $isTraversable = $this->_isTraversable;
        $this->_isTraversable = false;
        
        if (array_key_exists($this->_traversal['refColumn'], $data) && $data[$this->_traversal['refColumn']] > 0) {
            // Find parent row
            $parent_id = $data[$this->_traversal['refColumn']];
            $parent = $this->find($parent_id)->current();
            if (null === $parent) {
                require_once 'Zend/Db/Table/Exception.php';
                throw new Zend_Db_Table_Exception("Traversable error: Parent id {$parent_id} not found");
            }
            
            $lt = (double) $parent->{$this->_traversal['left']};
            $rt = (double) $parent->{$this->_traversal['right']};
            
            // Make room for the new node
            parent::update(
                array(
                    $this->_traversal['left'] => new Zend_Db_Expr($this->getAdapter()->quoteInto("{$this->_traversal['left']} + ?", 2)),
                ),
                array(
                    $this->getAdapter()->quoteInto("{$this->_traversal['left'] > ?", $lt)
                )
            );
            
            parent::update(
                array(
                    $this->_traversal['right'] => new Zend_Db_Expr($this->getAdapter()->quoteInto("{$this->_traversal['right']} + ?", 2)),
                ),
                array(
                    $this->getAdapter()->quoteInto("{$this->_traversal['right']} > ?", $lt)
                )
            );
            
            $data[$this->_traversal['left']] = $lt + 1;
            $data[$this->_traversal['right']] = $lt + 2;
        } else {
            $maxRt = (double) $this->fetchRow($this->select()->from($this, array('theMax' => "MAX({$this->_traversal['right']})")))->theMax;
            $data[$this->_traversal['left']] = $maxRt + 1;
            $data[$this->_traversal['right']] = $maxRt + 2;
        }
        
        // Do insert
        $id = $this->insert($data);
        
        // Reset isTraversable flag to previous value.
        $this->_isTraversable = $isTraversable;
        
        return $id;
    }
    
    /**
     * Fetches all descendents of a given node
     *
     * @param Zend_Db_Table_Row_Abstract|string $row - Row object or value of row id
     * @param Zend_Db_Select $select - optional custom select object
     * @return Zend_Db_Table_Rowset|null
     */
    public function fetchAllDescendents($row, Zend_Db_Select $select = null)
    {
        $this->_verifyTraversable();
                
        if ($row instanceof Zend_Db_Table_Row_Abstract) {
            $_row = $row;
        } else if (is_string($row) OR is_numeric($row)) {
            $_row = $this->fetchRow($this->select()->where($this->_traversal['column'] . ' = ?', $row));
            if (null === $_row) {
                require_once 'Zend/Db/Table/Exception.php';
                throw new Zend_Db_Table_Exception("Cannot find row '{$this->_traversal['column']}' = {$row}");
            }
        } else {
            require_once 'Zend/Db/Table/Exception.php';
            throw new Zend_Db_Table_Exception("Expecting instance of Zend_Db_Table_Row_Abstract, a string, or numeric");
        }
        
        $left = $_row->{$this->_traversal['left']};
        $right = $_row->{$this->_traversal['right']};
        
        if (null === $select) {
            $select = $this->select();
        }
        
        $select->where("{$this->_traversal['left']} > ?", (double) $left)
        ->where("{$this->_traversal['left']} < ?", (double) $right)
        ;
        
        $orderPart = $select->getPart('order');
        if (empty($orderPart)) $select->order($this->_traversal['left']);
        
        return $this->fetchAll($select);
    }
    
    /**
     * Fetches all descendents of a given node and returns them as a tree
     *
     * @param Zend_Db_Table_Row_Abstract|string|int $rows- Row object or value of row id or array of rows
     * @param Zend_Db_Select $select - optional select object
     * @return Zend_Db_Table_Rowset|null
     */
    public function fetchTree($row = null, Zend_Db_Select $select = null)
    {
        $this->_verifyTraversable();
        
        if (null === $select) {
            $select = $this->select();
        }
        
        $select->setIntegrityCheck(false)
        ->from(array('node' => $this->getName()))
        ->join(array('parent' => $this->getName()),
            null,
            array(
                'tree_depth' => new Zend_Db_Expr("COUNT(parent.{$this->_traversal['refColumn']})")
            )
        )
        ->group("node.{$this->_traversal['column']}")
        ;
        
        if (null !== $row) {
            if ($row instanceof Zend_Db_Table_Row_Abstract) {
                $_row = $row;
            } else if (is_string($row) OR is_numeric($row)) {
                $_row = $this->fetchRow($this->select()->where($this->_traversal['column'] . ' = ?', $row));
                if (null === $_row) {
                    require_once 'Zend/Db/Table/Exception.php';
                    throw new Zend_Db_Table_Exception("Cannot find row '{$this->_traversal['column']}' = {$row}");
                }
            } else {
                require_once 'Zend/Db/Table/Exception.php';
                throw new Zend_Db_Table_Exception("Expecting instance of Zend_Db_Table_Row_Abstract, a string, or numeric");
            }
            
            $left = (double) $_row->{$this->_traversal['left']};
            $right = (double) $_row->{$this->_traversal['right']};
            
            if ($left > 0 AND $right > 0) {
                $select->where("node.{$this->_traversal['left']} >= {$left} AND node.{$this->_traversal['left']} < {$right}");
            } else {
                // Traversal information is bad, throw an exception
                $id = $_row->{$this->_traversal['column']};
                require_once 'Zend/Db/Table/Exception.php';
                throw new Zend_Db_Table_Exception("Left/right values for row '{$this->_traversal['column']}' = '{$id}' in table '{$this->_name}' must be greater than zero to fetch tree.");
            }
        }
        
        $select->where("node.{$this->_traversal['left']} BETWEEN parent.{$this->_traversal['left']} AND parent.{$this->_traversal['right']}");
        
        $orderPart = $select->getPart('order');
        if (empty($orderPart)) {
            $select->order("node.{$this->_traversal['left']}");
        }
        
        return $this->fetchAll($select);
    }
    
    /**
     * Fetches all ancestors of a given node
     *
     * @param Zend_Db_Table_Row_Abstract|string $row - Row object or value of row id
     * @param Zend_Db_Select $select - optional custom select object
     * @return Zend_Db_Table_Rowset|null
     */
    public function fetchAllAncestors($row, Zend_Db_Select $select = null)
    {
        $this->_verifyTraversable();
        
        if ($row instanceof Zend_Db_Table_Row_Abstract) {
            $_row = $row;
        } else if (is_string($row) OR is_numeric($row)) {
            $_row = $this->fetchRow($this->select()->where($this->_traversal['column'] . ' = ?', $row));
            if (null === $_row) {
                require_once 'Zend/Db/Table/Exception.php';
                throw new Zend_Db_Table_Exception("Cannot find row '{$this->_traversal['column']}' = {$row}");
            }
        } else {
            require_once 'Zend/Db/Table/Exception.php';
            throw new Zend_Db_Table_Exception("Expecting instance of Zend_Db_Table_Row_Abstract, a string, or numeric");
        }
        
        $left = $_row->{$this->_traversal['left']};
        $right = $_row->{$this->_traversal['left']};
        
        if (null === $select) {
            $select = $this->select();
        }
        
        $select->where("{$this->_traversal['left']} < ?", (double) $left)
        ->where("{$this->_traversal['right']} > ?", (double) $right)
        ;
 
        $orderPart = $select->getPart('order');
        if (empty($orderPart)) {
            $select->order($this->_traversal['left']);
        }
        
        return $this->fetchAll($select);
    }
    
    /**
     * Prepares the traversal information
     *
     */
    protected function _initTraversal()
    {
        if (empty($this->_traversal)) return;
        
        $columns = $this->getColumns();
        
        // Verify 'left' value and column
        if (!isset($this->_traversal['left'])) {
            require_once 'Zend/Db/Table/Exception.php';
            throw new Zend_Db_Table_Exception("'left' value must be specified for tree traversal");
        }
        
        if (!in_array($this->_traversal['left'], $columns)) {
            require_once 'Zend/Db/Table/Exception.php';
            throw new Zend_Db_Table_Exception("Column '" . $this->_traversal['left'] . "' not found in table for tree traversal");
        }
        
        // Verify 'right' value and column
        if (!isset($this->_traversal['right'])) {
            require_once 'Zend/Db/Table/Exception.php';
            throw new Zend_Db_Table_Exception("'right' value must be specified for tree traversal");
        }
        
        if (!in_array($this->_traversal['right'], $columns)) {
            require_once 'Zend/Db/Table/Exception.php';
            throw new Zend_Db_Table_Exception("Column '" . $this->_traversal['right'] . "' not found in table for tree traversal");
        }
        
        // Check for identifying column
        if (!isset($this->_traversal['column'])) {
            if (!isset($this->_primary)) {
                require_once 'Zend/Db/Table/Exception.php';
                throw new Zend_Db_Table_Exception("Unable to determine primary key for tree traversal");
            }
            
            if (count($this->_primary) > 1) {
                require_once 'Zend/Db/Table/Exception.php';
                throw new Zend_Db_Table_Exception("Cannot use compound primary key as identifying column for tree traversal, please specify the column manually");
            }
            
            $this->_traversal['column'] = current((array) $this->_primary);
        }
        
        // Check for reference column
        if (!isset($this->_traversal['refColumn'])) {
            if (!array_key_exists('Parent', $this->_referenceMap)) {
                require_once 'Zend/Db/Table/Exception.php';
                throw new Zend_Db_Table_Exception("Unable to determine reference column for traversal, and did not find reference rule 'Parent' in reference map");
            }
            
            $refColumn = $this->_referenceMap['Parent']['refColumns'];
            if (!is_string($refColumn) AND count($refColumn) > 1) {
                require_once 'Zend/Db/Table/Exception.php';
                throw new Zend_Db_Table_Exception("Cannot use compound primary key as reference column for tree traversal, please specify the reference column manually");
            }
            
            $this->_traversal['refColumn'] = $refColumn;
        }
        
        $this->_isTraversable = true;
    }
    
    /**
     * Verifies that the current table is a traversable
     * 
     * @throws Zend_Db_Exception - Table is not traversable
     */
    protected function _verifyTraversable()
    {
        if (!$this->_isTraversable) {
            require_once 'Zend/Db/Table/Exception.php';
            throw new Zend_Db_Table_Exception("Table {$this->_name} is not traversable");
        }
    }
}
]]>
0
Invisible Buttons are a Bad Idea One of my pet peeve's are buttons that you can't see.

]]>
Thu, 25 Jun 2009 21:20:03 -0700 http://www.virgentech.com/blog/2009/06/invisible-buttons-are-a-bad-idea.html http://www.virgentech.com/blog/2009/06/invisible-buttons-are-a-bad-idea.html djvirgen@gmail.com (Hector Virgen) Hector Virgen Good user-interface design involves a lot of factors including clear responsive feedback. A good example of this is making a hyperlink brighter when the cursor is placed over it. This also works well for any "clickable" object like form buttons, images, tabs, etc.

One thing I've been seeing a lot lately is invisible buttons -- they remain invisible until the cursor approaches the button, which reveals the button so it can be clicked.

While this may help make things look tidy in screenshots, it can quickly become problematic to the user.

According to The essential guide to user interface design By Wilbert O. Galitz (ISBN: 0470053429, 9780470053423), '"Invisible" buttons must never exist'.

The biggest problem with invisible buttons is that they are invisible. How would know the buttons are there if you can't see them? What if the button does something destructive, like delete a file or close a window? Now a seemingly safe place to click is a hotspot for unintentional and sometimes unrecoverable damage.

I recently downloaded Apple Safari 4 for PC, and started browsing and opening tabs. The browser itself is great -- quick, responsive, snappy. Each tab, however, has an invisible close button on the right side of each tab. The button remains invisible until your mouse cursor is placed directly on the tab. Once the cursor is "within range", the close button appears.

When you need to quickly switch tabs using the mouse, it becomes dangerously easy to close the tab instead of switching to it. If you're a very fast clicker, it's possible to accidentally close the tab without even seeing the close button appear, especially when approaching the tab from the bottom, top, or right side of the tab. Your tab just now disappeared when trying to switch to it. Additionally, if there were a tab to the right, it will be pushed over to the left in place of tab that you just unknowningly closed, causing even more confusion.

A viable and aesthetic alternative would be to make the buttons less noticeable until the user places the cursor over the actual button itself. Less noticeable does not mean invisible or near-invisible, just let it blend more naturally as to not be too distracting. Once the cursor is over the button, it can glow or change colors to indicate that clicking the mouse will cause something to happen.

Google Chrome does a great job of this by making the close buttons appear as little gray x's on the tabs. The x's are always visible so you always know they are there. When the mouse is directly over the x it becomes a red circle with a white x within it, indicating that something "destructive" will happen if you click at that moment. This helps make it very obvious that if you're going to quickly target the tab with your mouse to select it that you should aim for the middle or left side of the tab.

]]>
0
"Conjunction" View Helper for the Zend Framework Joins many items together using a conjunction for prettier messages.

]]>
Wed, 03 Jun 2009 21:25:02 -0700 http://www.virgentech.com/blog/2009/06/conjunction-view-helper-for-zend-framework.html http://www.virgentech.com/blog/2009/06/conjunction-view-helper-for-zend-framework.html djvirgen@gmail.com (Hector Virgen) Hector Virgen Many times in dynamic web sites you will need to list items in a sentence, but if you don't know how many items there are then it can be tedious to join them with a conjunction. That's where the Conjunction view helper for the Zend Framework comes in. It accepts an array of items and joins them with commas except for the last item, which is prefixed with "and" or any other conjunction of your choice.

Usage (Within a View Script)

 
<?php
 
$fruits = array(
    'apples',
    'bananas',
    'oranges',
    'lemons'
);
 
echo $this->conjunction($fruits);
// Outputs "apples, bananas, oranges and lemons"
 
echo $this->conjunction($fruits, 'or');
// Outputs "apples, bananas, oranges or lemons"

Class: Virgen_View_Helper_Conjunction

 
<?php
 
/**
 * Uses a conjunction to join items in the English language as in the sentence:
 * 
 * "Red, blue and green are all colors."
 *
 */
class Virgen_View_Helper_Conjunction
{
    public function conjunction($items, $type = 'and')
    {
        // Return empty string if no items are in array
        if (count($items) == 0) return '';
        
        // Return first item if only 1 item
        if (count($items) == 1) return $items[0];
        
        // Build conjunction
        $last = array_pop($items);
        $first = implode(', ', $items);
        
        return "{$first} {$type} {$last}";
    }
}
]]>
0
"Percent" View Helper for the Zend Framework Easily display percentages using this view helper for the Zend Framework.

]]>
Wed, 03 Jun 2009 21:23:22 -0700 http://www.virgentech.com/blog/2009/06/percent-view-helper-for-zend-framework.html http://www.virgentech.com/blog/2009/06/percent-view-helper-for-zend-framework.html djvirgen@gmail.com (Hector Virgen) Hector Virgen Lately I've been working on a very data-intensive website where things like averages and percentages are very common. While it is pretty simple to display an average, the "percent" format is a little awkward to display easily. Usually, a percent value is calculated within the SQL query and displayed in the view.

 
SELECT      id,
            name,
            CONCAT(score / max_score * 100, '%', 3) as percent
FROM        my_table
WHERE       id = 123;

However, I consider "percent" to be a display format, and asking the database to render a "for display" value can lead to design issues in the future.

For example, let's say your client wants to display the score alongside the percent value. Simple enough, just modify your SQL query right? But doing so would mean that a programmer would have be making simple design changes... is there a better way?

One solution would be to fetch the raw values and calculate them in PHP within the view script:

 
# controller
$this-view->exam = $myTable->find(123)->current();
# view
<?= number_format($this->exam->score / $this->exam->max_score * 100, 3) ?>%

While that may seem like a pretty simple bit of code, it can be quite tedious to have to write out that formula each time you need a percent value. Additionally, percents like 92.5% would be displayed as 92.500% due to how number_format treats trailing zeroes. To remove trailing zeroes and the possible trailing dot, you must wrap the number_format() function above in two rtrim() functions.

 
<?= rtrim(rtrim(number_format($this->exam->score / $this->exam->max_score * 100, 3), '0'), '.') ?>%

As you can see, this is starting to look pretty ugly.

That's where the Zend Framework's view helpers come in. They are perfect for these types of things.

Usage

Usage is pretty straightforward. Just pass in an array of values representing the score and max_score values, and an optional integer for the number of digits to keep after the dot.

 
<?= $this->percent(array($this->exam->score, $this->exam->max_score), 3) ?>

That's it! The view helper automatically handles the calculations for you, trims off any trailing zeroes, and appends a '%' to the end.

The output for the code above would look like 92.5%.

In the event that you already have a percent value (perhaps from a precalculated database field) and just need to have it formatted, pass in the percent value instead of an array. The view helper will detect that it's numeric and just do the trimming and toss a percent sign at the end.

To create percentages in other locales, you can assign your own characters to use for the percent sign, thousands separator, and decimal separator.

 
<?= $this->percent()
    ->setPercentSymbol('#')
    ->setThousandsSeparator('.')
    ->setDecimalSeparator(',')
    ->percent(array($this->exam->score, $this->exam->max_score), 3) ?>

The output for the above would look like 92,5#.

Here's the complete Percent view helper for the Zend Framework.

Virgen_View_Helper_Percent

 
<?php
 
/**
 * Formats a number to pretty percent notation.
 * Accepts an array of numbers to calculate on-the-fly
 * 
 * @author Hector Virgen
 */
class Virgen_View_Helper_Percent
{
    /**
     * Flag to enable/disable trimming of trailing zeroes and dot
     *
     * @var boolean
     */
    protected $_trimTrail = true;
    
    /**
     * Percent symbol to append to formatted number
     *
     * @var string
     */
    protected $_symbol = '%';
    
    /**
     * Character to use as thousands separator
     *
     * @var string
     */
    protected $_thousandsSep = ',';
    
    /**
     * Character to use as decimal point
     *
     * @var string
     */
    protected $_decPoint = '.';
    
    /**
     * Formats a number or array of numbers to percent notation
     *
     * @param numeric|array $data - Percent value or array of numbers to calculate percent
     * @param int $digits - Number of digits to display after the dot
     * @return string - Formatted percent
     */
    public function percent($data = null, $digits = 0)
    {
        // Return this if no parameters were passed
        if (null === $data) {
            return $this;
        }
        
        // Determine percent value
        if (is_array($data)) {
            list($val, $maxval) = $data;
            $percent = $this->calcPercent($val, $maxval);
        } else if (is_numeric($data)) {
            $percent = (float) $data;
        } else {
            throw new Zend_View_Exception("Data must be a numeric or array of value, maxvalue.");
        }
        
        $percent = (string) number_format($percent, (int) $digits, $this->_decPoint, $this->_thousandsSep);
        
        // Remove trailing zeroes and dot
        if ($this->_trimTrail AND $digits > 0) {
            $percent = rtrim($percent, '0');
            $percent = rtrim($percent, $this->_decPoint);
        }
        
        // Append percent symbol
        $percent .= $this->_symbol;
        
        return $percent;
    }
    
    /**
     * Calculates the percent based on value and maxvalue.
     *
     * @param numeric $val - Current value
     * @param numeric $maxval - Total value
     * @return float - percent of total value
     */
    public function calcPercent($val, $maxval)
    {
        $maxval = (float) $maxval;
        if (0 == $maxval) {
            throw new Zend_View_Exception("Maxval must be a non-zero value.");
        }
        
        return (float) $val / $maxval * 100;
    }
    
    /**
     * Enables or disabled the trimming of trailing zeroes and dots
     *
     * @param boolean $flag - true or false
     * @return $this - Fluent interface
     */
    public function setTrimTrail($flag)
    {
        $this->_trimTrail = (bool) $flag;
        
        return $this;
    }
    
    /**
     * Sets the symbol to append to the percent value
     *
     * @param string $symbol - Percent symbol
     * @return $this - Fluent interface
     */
    public function setSymbol($symbol)
    {
        $this->_symbol = (string) $symbol;
        
        return $this;
    }
    
    /**
     * Sets the decimal point character
     *
     * @param string $char - Decimal point character
     * @return $this- Fluent interface
     */
    public function setDecPoint($char)
    {
        $this->_decPoint = (string) $char;
        if (empty($this->_decPoint)) {
            throw new Zend_View_Exception("Decimal point character cannot be empty.");
        }
        
        return $this;
    }
    
    /**
     * Sets the thousands separator to use
     *
     * @param string $sep
     * @return $this - Fluent interface
     */
    public function setThousandsSep($sep)
    {
        $this->_thousandsSep = $sep;
        
        return $this;
    }
}
]]>
0
"Truncate" View Helper for the Zend Framework Ever needed to truncate a long string for display purposes? This helper can make it easy.

]]>
Tue, 02 Jun 2009 21:28:57 -0700 http://www.virgentech.com/blog/2009/06/truncate-view-helper-for-zend-framework.html http://www.virgentech.com/blog/2009/06/truncate-view-helper-for-zend-framework.html djvirgen@gmail.com (Hector Virgen) Hector Virgen This simple view helper for the Zend Framework will truncate a string to the desired length and automatically add customizable prefixes and postfixes if the string was truncated.

Usage Example

This view helper works similarly to substr, with two additional parameters for specifying the prefix and postfix.

 
<h1>My Truncated Blog Post</h1>
<p><?= $this->truncate($this->blog, 0, 40, '', '... [more]') ?></p>

If your blog post was very long (like this one), the result would be something like this:

 
<h1>My Truncated Blog Post</h1>
<p>This simple view helper for the Zend Fra... [more]</p>

Due to the fact this this helper works on the string directly, you may need to strip html tags first if your string is HTML, otherwise you will end up with a lot of broken tags!

Class: Virgen_View_Helper_Truncate

 
<?php
 
class Virgen_View_Helper_Truncate
{
    public function truncate($string, $start = 0, $length = 100, $prefix = '...', $postfix = '...')
    {
        $truncated = trim($string);
        $start = (int) $start;
        $length = (int) $length;
        
        // Return original string if max length is 0
        if ($length < 1) return $truncated;
        
        $full_length = iconv_strlen($truncated);
        
        // Truncate if necessary
        if ($full_length > $length) {
            // Right-clipped
            if ($length + $start > $full_length) {
                $start = $full_length - $length;
                $postfix = '';
            }
            
            // Left-clipped
            if ($start == 0) $prefix = '';
            
            // Do truncate!
            $truncated = $prefix . trim(substr($truncated, $start, $length)) . $postfix;
        }
        
        return $truncated;
    }
}
]]>
0