VirgenTech Blog Hector Virgen's Blog Tue, 07 Feb 2012 05:12:43 -0800 Zend_Feed_Writer 1.10.0beta (http://framework.zend.com) http://www.virgentech.com/blog/entries djvirgen@gmail.com (Hector Virgen) Hector Virgen Extending Navigation View Helpers in Zend Framework A question that comes up from time to time in the Zend Framework mailing list is how to extend Zend Framework's Navigation view helpers such as Menu and Breadcrumbs. This quick tutorial will show you how.

]]>
Tue, 10 May 2011 22:17:24 -0700 http://www.virgentech.com/blog/2011/05/extending-navigation-view-helpers-zend-framework.html http://www.virgentech.com/blog/2011/05/extending-navigation-view-helpers-zend-framework.html djvirgen@gmail.com (Hector Virgen) Hector Virgen A question that comes up from time to time in the Zend Framework mailing list is how to extend Zend Framework's Navigation view helpers. By following these simple steps, you'll be able provide your own Navigation view helpers while still taking advantage of what Zend Framework has provided.

How Most Helpers are Extended

Most of Zend Framework's view helpers can easily be extended by providing a helper with the same name within your application. Since Zend_View uses a plugin loader to locate view helpers, you can use the same helper name while avoid conflicts. Here's a basic example:

Let's say you want to extend the Json view helper so that it always pretty prints the JSON. You could simply create your own "Json" view helper, extend Zend's Json view helper, and save it at application/views/helpers/Json.php:

<?php

class Default_View_Helper_Json extends Zend_View_Helper_Json
{
    public function json($data, $keepLayouts = false)
    {
        $json = parent::json($data, $kepLayouts);
        return Zend_Json::prettyPrint($json);
    }
}

Additionally, you'll want to be sure that your view can find your new view helper, so you'll need to add this line to your application/configs/application.ini under the production section:

resources.view.helperPath.Default_View_Helper = APPLICATION_PATH "/views/helpers"

Now, whenever your view scripts call $this->json(), your improved view helper will be used. Seems easy enough!

What About Them Navigation Helpers?

So now let's say that you want to improve ZF's "menu" view helper. For example, you want to output an HTML comment whenever we call the menu helper's renderMenu() method. Why would you want to do this? I have no idea, but for the purposes of this tutorial let's say that's what we want to do.

So let's follow the same steps as we did with our custom Json view helper and see what happens. So first we define our helper and save it at application/views/helpers/Menu.php:

<?php

class Default_View_Helper_Menu extends Zend_View_Helper_Navigation_Menu
{
    public function renderMenu(Zend_Navigation_Container $container, array $options = array())
    {
        return '' . parent::renderMenu($container, $options);
    }
}

Again, we make sure our application's configuration includes the path to this helper:

resources.view.helperPath.Default_View_Helper = APPLICATION_PATH "/views/helpers"

And then we update our layout to call the view helper:

<?php echo $this->navigation()->menu()->renderMenu(); ?>

We then visit our page in a browser and look for the comment -- but it's not there! Why did it not pick up our custom Menu helper, while it picked up our custom Json helper with no problem?

What's Going On? I'm scared!

There's no need to be afraid! Let's just take a look under the hood to see what's happening.

First of all, you might have noticed that Zend's "menu" view helper is not located in the same place as all the other Zend view helpers. Most of them (including the Json view helper) are located here:

Zend/View/Helper

But the Menu view helper is located here:

Zend/View/Helper/Navigation

In order for Zend_View to load the navigation helpers, a new prefix path must be added to your view instance. The parent helper "Navigation" is smart enough to add this prefix path for us, but often times this happens very late in the request's life time (after your view has been bootstrapped). This results in the prefix paths being set up thusly:

  • Zend/View/Helper
  • application/views/helpers
  • Zend/View/Helper/Navigation

Since Zend_View uses a Last In, First Out (LIFO) stack to find view helpers, this means the path Zend/View/Helper/Navigation is the first place it looks for the Menu helper. Since ZF's helper exists there, it ends up being used and your custom Menu helper never gets called.

The solution is to take back control of the view's helper paths by manually adding Zend/View/Helper/Navigation to the stack before your custom helper path. You can do this by modifying your application.ini like this:

resources.view.helperPath.Zend_View_Helper_Navigation = "Zend/View/Helper/Navigation"
resources.view.helperPath.Default_View_Helper = APPLICATION_PATH "/views/helpers"

This results in a change in the LIFO stack, which now looks like this:

  • Zend/View/Helper
  • Zend/View/Helper/Navigation
  • application/views/helpers

Now your view will look in your custom helpers before checking the Zend directories for that Menu helper.

Conclusion

Zend's Navigation view helpers can be a bit tricky to extend at first glance, but once you see how the order of the plugin paths matter it starts to make a lot more sense.

One of the great things about Zend Framework is its extensibility, but sometimes it can be frustrating when things don't work as you'd expect. Sometimes all you need to do is just peek through the source to find your answers.

]]>
0
ZendCon 2010 - Reusable Bootstrap Resources Slides from my talk on Reusable Bootstrap Resources at ZendCon 2010.

]]>
Wed, 10 Nov 2010 08:07:40 -0800 http://www.virgentech.com/blog/2010/11/zendcon-reusable-bootstrap-resources.html http://www.virgentech.com/blog/2010/11/zendcon-reusable-bootstrap-resources.html djvirgen@gmail.com (Hector Virgen) Hector Virgen Here are the slides from my talk on Reusable Bootstrap Resources, presented on the second day of ZendCon 2010.

]]>
0
Why Singletons Are Evil Singletons can cause more problems than they solve.

]]>
Thu, 26 Aug 2010 16:56:56 -0700 http://www.virgentech.com/blog/2010/08/why-singletons-are-evil.html http://www.virgentech.com/blog/2010/08/why-singletons-are-evil.html djvirgen@gmail.com (Hector Virgen) Hector Virgen One of the nice things about design patterns is that once you learn them they are really quite easy to implement. One of the simplest design patterns is the Singleton. Not only does it comprise of a single class, but it can be built and used very quickly.

The basic idea behind a Singleton is to limit the number of instances allowed throughout the application. Usually they're limited to a single instance (hence the name "Singleton") but can be modified to support any number of instances.

Here's a Singleton implementation in PHP:

class MySingleton {
    protected static $_instance;
    
    protected function __construct() {}
    
    protected function __clone() {}
    
    public static function getInstance()
    {
        if (null === self::$_instance) {
            self::$_instance = new self();
        }
        return self::$_instance;
    }
}

Starting at the top, we have a single static property called $_instance. This will eventually hold our singleton instance.

Continuing on, you'll notice that __construct() and __clone() are protected. This prevents these methods from being called from anywhere outside the class. In other words, you won't be able to call the code new MySingleton(); unless you're inside the class. The same is true for cloning -- it can only be done inside of the class. Any attempt to instantiate or clone this class from the outside will cauase a fatal error in PHP.

$mySingleton = new MySingleton();
// Fatal error!

Lastly, there is a static getInstance() method. Note that it's marked public, meaning it can be called from the outside. Inside this method, it checks the $_instance property to see if its null and, if it is, lazy-loads the instance into that property.

$mySingleton = MySingleton::getInstance();
// This works!

If you were to call that code again later, the same instance would be returned instead of a new one. This pattern can be used to ensure that the application isn't creating multiple objects unneccessarily. For example, you probably don't want multiple logger instances throughout your app if they're all writing to the same log.

This pattern is as elegant as it is simple, but it comes with a price.

One of the major problems with singletons is that they are very sneaky -- just like global variables. Consider this portion of a class that uses a database singleton:

class Users
{
    public function fetchAll()
    {
        // Singleton access to DB
        $db = MyDatabase::getInstance();
        
        // Fetch all users
        return $db->fetchAll('SELECT * FROM users WHERE 1');
    }
}

From the outside world, a client developer would probably write code something like this:

$users = new Users();
$allUsers = $users->fetchAll();

While that looks simple enough, the client developer has no idea that a database is involved unless she examines the source code. This is what's known as a hidden dependency.

Additionally, this makes the Users class very difficult to unit test. How would you write a test that uses a mock database? There's no way to inject a mock database unless you create a duplicate MyDatabase class.

A better alternative would be to require that all dependencies be injected instead of statically loaded:

class Users
{
    protected $_db;
    
    public function __construct(MyDatabase $db)
    {
        $this->_db = $db;
    }

    public function fetchAll()
    {
        // Fetch all users
        return $this->_db->fetchAll('SELECT * FROM users WHERE 1');
    }
}

Let's see how this changes the client's code:

$db = MyDatabase::getInstance();
$users = new Users($db);
$allUsers = $users->fetchAll();

What a difference! Now it's obvious that a database is involved without having to look at the source of the Users class. Additionally, it provides us with a way to inject a mock database for unit testing.

But we're still using a singleton, and it's all too tempting to revert back to sprinkling our classes with calls to its getInstance method. Additionally, when unit testing, it becomes a task to have to reset all of our singletons between each test. Is there a better way?

Lately I've been avoiding singletons entirely and sticking to basic classes. In order to ensure that only one instance is used, I bootstrap that instance. For example, in the Zend Framework I create a bootstrap resource class that can load my single instance. Whenever I need the resource, I pull it from the bootstrap and inject it into the class that needs it. This means I can inject stubs into the bootstrap to test my application and don't have to worry about hidden dependencies causing failures.

]]>
0
Taming Getters and Setters Getters and Setters can be evil, but with a little bit of love (and a long whip), they can be tamed.

]]>
Mon, 26 Apr 2010 13:43:22 -0700 http://www.virgentech.com/blog/2010/04/taming-getters-and-setters.html http://www.virgentech.com/blog/2010/04/taming-getters-and-setters.html djvirgen@gmail.com (Hector Virgen) Hector Virgen It has recently come to my attention that getters and setters are evil. No, they won't run off with your first-born child, nor will they pour gasoline into your cereal. But, according to other programmers on the interwebs, they can lead to poor object-oriented design.

However, problems can be found with just about any single line of code in your application. As a PHP developer I've even heard complaints about the opening PHP tag itself!

But I want to look more closely at getters and setters to find out why they are evil and what we can do to tame them.

Based on various web blogs, getters are referred to as a "code smell" that can easily cause problems down the road. Most of these articles are referring to Java, but since I program in PHP I wanted to see if it applies. Let's use a simple User object to see what havoc it can wreak:

class User
{
    protected $_firstName;
    protected $_lastName;

    public function getFirstName()
    {
        return $this->_firstName;
    }

    public function getLastName()
    {
        return $this->_lastName;
    }
}

So here we have a simple User class that appears to politely return the first and last name of the user on demand. I don't see any horns, hooves, nor a tail, so how can this be evil?

What we mere mortals tend to forget is that evil is the master of deception. It won't become obvious that we've raised hell in code by committing this innocent-looking class to our repository.

However, let's see what happens when we start using this class. Let's say we're building a simple profile page and need to display the user's full name wrapped in a fancy H1:

$user = new User();
echo "<h1>{$user->getFirstName()} {$user->getLastName()}</h1>";

Seems simple enough. Just get the first and last name and put a space in between. You test it, everything works, you can commit your code and go home for the day.

But the problem here is that your view is determining which data constitutes the user's full name. And this becomes a bigger problem when you have many views all trying to show the same full name.

Since this is really a business rule, it belongs in the User class. Behold:

class User
{
    protected $_firstName;
    protected $_lastName;

    public function getFullName()
    {
        return "{$this->_firstName} {$this->_lastName}";
    }
}

With this small change to our User class, we can guarantee that the full name will always be rendered the same. In other words, we don't need no stinking getters.

So we've successfully converted our evil class into something slightly less evil. We've removed those pesky getters and encapsulated the full name in a method. But we still have a long way to go before we can sleep peacefully.

By now you may be asking yourself "how does the first and last name properties get populated?" That's a very good question!

If you've been following my blog you may have noticed that I am a fan of the Data Mapper pattern. It separates business logic from persistence logic, allowing you to swap out persistence logic at any time. This means in order to persist that user object, the mapper needs to somehow access the $_firstName and $_lastName properties.

Can this be done without getters? Sure, why not?

class User
{
    /* ... */

    public function getData()
    {
        return array(
            'firstName' => $this->_firstName,
            'lastName' => $this->_lastName
        );
    }
}

But is this any better? Now it's just a "super-getter" that returns all the properties at once. So this doesn't really solve the problem, it just disguises it under a different method. So you might as well keep all those getters in there because your mapper is going to need to access them anyways.

What about setters?

Another problem you may run into is when a user decides to change her name (maybe she just got married and wants to update her last name). How would you accomplish this without a setter?

What you might want to do is avoid using a set* method. Maybe we'll call it "updateLastName":

class User
{
    /* ... */

    public function updateLastName($newLastName)
    {
        $this->_lastName = $newLastName;
    }
}

In reality, this method is identical to what a setLastName method would be. So how is this any better?

It's not. Especially when you consider how the mapper is supposed to load a user from persistent storage. For example, it will need to create a new User object and then "set" all of its properties based on the persisted data. In other words, you're going to need setters.

Not-So-Conclusive Conclusion

When it comes to domain entities, like the aforementioned User object, some parts of your application are going to need the getters and setters, while other parts should be working with just the "business" methods.

In other words, getters and setters are a necessary evil that helps you accomplish certain tasks, but they must be used wisely.

Some OO purists may say that getters and setters are a "code smell", but I say that using only getters and setters is where the real stink is.

]]>
0
My Favorite Gmail Label Labels in Gmail are awesome as is, but this label has to be the best!

]]>
Sat, 24 Apr 2010 16:38:39 -0700 http://www.virgentech.com/blog/2010/04/favorite-gmail-label.html http://www.virgentech.com/blog/2010/04/favorite-gmail-label.html djvirgen@gmail.com (Hector Virgen) Hector Virgen Ever since I discovered labels in Gmail, I've been using them like crazy to organize my ever-growing inbox. The best part about labels is that you can create filters to automatically attach labels to new messages as they come in. Additionally, you can attach more than one label to a conversation.

My favorite label is one called "Me". I know, I'm so self-centered!

But it comes in so handy! What I did was I created a filter that matches messages where the "from" address is my own e-mail address and applies the label "Me".

Matches: from([my e-mail address])
Do this: apply label "Me"

Now, each time I reply to an e-mail or write a new one, the label "Me" is automatically applied.

This means when I look at my inbox I can easily see which conversations I was a part of because they will have the "Me" label. This is especially useful because I belong to a lot of mailing lists, but only participate in a handful of the threads.

So when I see a new message show up at the top of my inbox and it has the label "Me", I immediately know that I was part of that conversation and am more than likely to be very interested in reading the new reply.

However, clicking the label to view all messages labeled with "Me" is virtually identical to viewing the "Sent Mail" folder. So viewing this label on its own isn't all that useful. 

To add your own "Me" label to Gmail, follow these quick steps:

  1. Log into Gmail and click the "Settings" link
  2. Click the "Filters" tab 
  3. Scroll down and click the "Create a new filter" link
  4. In the "From" box, enter your own Gmail address
  5. Click the "Next Step" button
  6. Check the box next to "Apply the label"
  7. Click the "Choose label" dropdown box and select "New Label"
  8. Type in "Me" (or whatever you want to call it) and click "OK"
  9. Optionally select "Apply filter to x conversations below" (recommended)
  10. Click Create Filter

That's it! Now every single conversation you are a part of will be labeled. 

]]>
0
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 Update: Jamie Talbot has written an excellent article that improves this method and is definitely worth a read.

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)
   {
       var elem = $(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)
   {
       var elem = $(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)
   {
       var elem = $(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)
   {
       var elem = $(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)
   {
       var elem = $(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