Archive of July 2009

Learning CakePHP: Validation

I started developing for the web sometime in 1995 while I was still in the military. I “turned pro” in 1997. At that time there were two methods of data validation: client-side and server-side. Yeah, I know, that’s still all we have, but I think that Ajax has helped to blur the line between the two – perceptually, I mean, not technically.

Then

Even back then I hated duplication. Frankly, I hated doing validation at all; I damn sure hated doing it twice. Moreover, I hated server-side validation because doing it (without also doing a whole lot of extra work) involved:

  1. Submitting a form to an action page
  2. Having that action page validate the form values
  3. Displaying (what was usually) an ugly error message to the user
  4. Asking the user to use their Back button to return to the form
  5. Expecting the user to remember the errors that had been displayed
  6. Asking the user correct each error
  7. Rinse
  8. Repeat

That’s a lot of lousy user experience. Since that sucked so mightily, I chose client-side. With Javascript, I could validate user input without having to make the round trip to the server. Talk to me all you want about graceful degradation, but first of all, that concept didn’t exist in 1996 and second…this is Javascript we’re talking about.

Now

With the relatively recent proliferation of API‘s, server-side validation has taken on a whole new urgency. With multiple gateways into an application’s business core, it’s more critical than ever that validation be moved as far down the application stack as possible in order to eliminate redundancy and, in the process, ensure consistency. For most applications that I’ve built in the last 5 years or so, there’s been some kind of API involved.

With CakePHP

Once I had worked with CakePHP for a few months, I got comfortable enough to start thinking about validation and how I wanted it to work from end-to-end. I had a few requirements:

  1. I only wanted to write my rules and messages once. Do something once and you don’t have to worry about consistency. One less pain point is always going to be a good thing.
  2. I wanted to be able to validate without necessarily saving. Most of the time I’d be doing both, but decoupling them is just a good idea.
  3. Hide from the user the fact that my validation was happening on the server. To them, generating and displaying error messages should be a smooth and seamless experience.

I came up with something that seems reasonably elegant and has met my needs to date.

The Basics

Although I’m not a big fan of the semantics, CakePHP nicely decouples the validation and save actions. I would expect this of any framework, of course, but that’s a place to start. If I’m validating something independent of a save operation, I usually want that validation to return one or more error messages. CakePHP makes this easy and even offers me a convenient means of using my preferred semantics.

Never, Ever Mess With the Core

This is one of my basic tenets for developing with third-party libraries. Messing with core code is almost certain to destroy your upgrade path. Find another way.

In this case, I want to be able to validate model data based on the rules defined by that model and I want this validation to be available to all models in my application in exactly the same way. To do that, I created a copy of CakePHP’s app_model.php file as app/app_model.php. By creating a copy in my app/ directory, I can preserve my upgrade path, but still inherit from the proper parent class.

Validate

In the newly copied AppModel class, I added this function:

/**
 * function validate
 * 
 * Validates model data. This function can be called independently
 * on any model for validation independent of a save operation. It
 * can also be overridden, for example, by a model requiring more
 * complex validation.
 *
 * param   $data
 * return  array     An array of error messages or an empty array
 *                   if the data validates properly.
 */
public function validate ( $data = array() ) { $this->set ( $data );

/** * Corrected based on input from Miles Johnson in the comments * below. */ return !$this->validates() ? $this->validationErrors : array(); }

That snippet alone provides basic validation across my entire application with a simple line of code:

Model->validate ( $this->data )

Complex validation

Something of a misnomer since this isn’t very complex, but there are times when more is required. For example, if I have customers and vendors that have addresses, I usually want to break the address out so that the data structure can be shared. Separate table, separate model. The data is abstracted, but I can’t really have a valid customer or vendor unless their address also validates. To do that, I need to override my simple validate() method in the Vendor (or Customer) model so that I can do just a little bit more:

public function validate ( $data = array() ) {
   return array_merge ( $this->Address->validate ( $data ), parent::validate ( $data ) );
}

User Interface

As I alluded to initially, what I really want to do is to perform server-side validation while providing the illusion of client-side validation. To do that, I employ the appropriate controller. If I’m able to submit the entire form via Ajax, this becomes trivial because the same request can perform both actions (provided no errors are reported). Here’s how it might look on a vendor application page (/vendors/apply):

public function apply() {
   $errors = $this->Vendor->validate ( $this->data );

if ( !empty ( $errors ) ) { /** Package the errors for an ajax return */ echo json_encode ( array ( ‘errors’ => $errors ); ); exit(); } /** If there are no errors…save */ exit(); }

If I want to validate first (again, via Ajax) and then submit to a different action page, all I have to do is create a validate() method in the controller, make the Ajax call to that and, if no errors are returned, submit the form to the preferred action. If errors are returned, I can use jQuery’s JSON parser to extract the messages and drop them on the screen.

With this technique, I can consolidate my validation, rarely write more than one block of code to access that validation and present the results to a user attractively without that user ever being aware that anything more than client-side error handling has been done. What technique(s) do you employ to minimize your validation headaches?

Labels. They're Not Just for Forms Anymore.

Think semantically, not dogmatically. Labels can be used to describe data as well as form fields. I can’t tell you how often I’ve seen something like this:

<div id="image-info">
   <span class="label">Name:</span><span>myimage.jpg</span>
   <span class="label">Size:</span><span>5KB</span>
   <span class="label">MIME Type:</span><span>image/jpg</span>
</div>

The obvious red flag here is that the name of the class is also the name of a tag, but a lot of examples aren’t quite so obvious. Nonetheless, when labeling data, use the label tag to do it. I’ve never seen any indication that it’s incorrect in any way.

Disable the System Bell in iTerm

Spend enough time in a terminal session and eventually the system “bell” will drive you nuts. I honestly don’t remember it being this much of an issue on my old Macbook Pro, but it’s been maddening since I got my new one a few weeks ago. Because of its bookmarks feature, iTerm is my emulator of choice and there’s nothing in its preferences (I’m using Build 0.9.6.20090415) that even acknowledges a system bell exists, much less allows me to disable it. I did a clean install when I got my new machine, so maybe this is a recent change. I haven’t looked at the release history to determine why it’s not there, I can only be sure that it’s not.

In a fit of desperation this morning, I decided to scour the plist file to see if there was anything I could do at a slightly lower level to quiet my terminal sessions. Fortunately, I found an answer:

  1. Navigate to ~/Library/Preferences.
  2. Open net.sourceforge.iTerm.plist in your favorite plist file editor. I use Property List Editor.app because I have XCode installed and the app is available to me. There are other plist editors out there or you can just open the file in a text editor – it’s just an XML file with a fancy extension.
  3. Navigate the XML nodes (different editors may offer different means of drilling down) to Root > Terminals > Default > Silence Bell
  4. Click the checkbox to enable that property.
  5. Save the change.
  6. Restart iTerm.

Enjoy the silence.

MacPorts, MySQL 5 and the Launch Daemons

Update, 7/29/2009: In response to my question about this on StackOverflow, Mike Richards offered an infinitely better solution. Apparently MacPorts is effectively deprecating the mysql5 +server path in favor of a new mysql-server package. I can’t confirm this personally, but it sounds reasonable enough.

That sounds a little bit like a Harry Potter title, but the content isn’t nearly as entertaining. For the past year or two, I’ve been using a MySQL installed via MacPorts, the (pseudo-) apt repository for Mac ports (get it?) of Unix applications and utilities. MacPorts has been fantastic and I haven’t regretted the decision to move away from either OS X’s native MySQL install or from MAMP, an all-in-one solution that I had used previously. The last few times I’ve installed MySQL, though, I’ve noticed that I haven’t been able to get MySQL to start automatically when I login.

Following Chad Kieffer’s excellent tutorial for installing & configuring a MacPorts MySQL install, I would get myself to the point where I execute launchctl to load the plist file that will start MySQL automatically:

$ sudo launchctl load -w /Library/LaunchDaemons/org.macports.mysql5.plist

Unlike Chad, I want MySQL to start automatically. Admittedly, my work-life balance sucks; I’m more than likely doing something work-related if I’m sitting behind the keyboard. Given that, the server might as well be ready to respond, right? Except that the plist I’m trying to load…isn’t there to be loaded.

The first time that I did the install, the plist was there and loaded as expected, but the last 2 or 3 times that has not been the case. I don’t know what changed with the MacPorts bundle, but that plist simply isn’t there. Fortunately, I still have my old install around, so I faked it.

If anyone else is having the same issue, here’s how you too can fake it:

  1. Create a directory for the launch scripts.
    $ mkdir -p /opt/local/etc/LaunchDaemons/org.macports.mysql5
  2. Download the files that no longer get installed, mysql5.wrapper and org.macports.mysql5.plist. I’m making mine available since I don’t know where else to get them. Save both files to the directory you just created.
  3. Set the proper ownership and permissions.
    $ sudo chown root:wheel /opt/local/etc/LaunchDaemons/org.macports.mysql5/*
    $ sudo chmod 755 /opt/local/etc/LaunchDaemons/org.macports.mysql5/mysql5.wrapper
    $ sudo chmod 644 /opt/local/etc/LaunchDaemons/org.macports.mysql5/org.macports.mysql5.plist
  4. Create a soft link to the newly downloaded plist file in /Library/LaunchDaemons.
    $ cd /Library/LaunchDaemons
    $ ln -s /opt/local/etc/LaunchDaemons/org.macports.mysql5/org.macports.mysql5.plist org.macports.mysql5.plist
  5. Load the plist file, as indicated in Chad’s instructions and duplicated above. For the sake of keeping it all in one place:
    $ sudo launchctl load -w /Library/LaunchDaemons/org.macports.mysql5.plist
  6. Reboot.
  7. Verify that MySQL has started.
    $ sudo ps -ef | grep mysql

You should see output that looks something like this:

    0    65     1   0   0:00.00 ??         0:00.00 /opt/local/bin/daemondo --label=mysql5 --start-cmd /opt/local/etc/LaunchDaemons/org.macports.mysql5/mysql5.wrapper start ; --stop-cmd /opt/local/etc/LaunchDaemons/org.macports.mysql5/mysql5.wrapper stop ; --restart-cmd /opt/local/etc/LaunchDaemons/org.macports.mysql5/mysql5.wrapper restart ; --pid=none
    0    85     1   0   0:00.01 ??         0:00.01 /bin/sh /opt/local/lib/mysql5/bin/mysqld_safe --datadir=/opt/local/var/db/mysql5 --pid-file=/opt/local/var/db/mysql5/rob17.local.pid
   74   111    85   0   0:07.48 ??         0:19.75 /opt/local/libexec/mysqld --basedir=/opt/local --datadir=/opt/local/var/db/mysql5 --user=mysql --pid-file=/opt/local/var/db/mysql5/rob17.local.pid --socket=/tmp/mysql.sock
  501  3370  3145   0   0:00.00 ttys003    0:00.00 grep mysql

If you do, then you’re golden. If you don’t, then you probably made a mistake. If the mistake is mine, please let me know in the comments and I’ll make the appropriate adjustments.