Google Ad

The Power of CakePHP aliases

Date: Tue, Mar 23rd 2010, 01:20 Author: nick Views: 19553 Comments share
While working on many projects over the years I've come to the conclusion people don't take advantage of CakePHP's alias attribute nearly enough. As such I've decided to go a bit into one of my personal experiences to show you the power of $this->alias. We may even get you laid in the process.....

Case Study

Recently I've had the (dis)pleasure of working on a legacy CakePHP app. Judging by the code quality, I'd guesitmate it was probably their first web app. No worries there, thinking back to my first web app, it was rather a mess as well. However, what really caught my attention was the amount of model repetition that littered the app. This app in particular had four almost identical models (ShippingAddress, BillingAddress, MailingAddress, CorporateAddress). Each model had a different table (shipping_addresses, billing_addresses, etc..), and each table had identical fields (id, name_line, street, city, state, zip). On top of that, each model had the exact same beforeSave() function that tested for a custom zipcode within city validation.

Here is the before shot of one of those four models:
  1. class ShippingAddress extends AppModel {
  2.   var $name = 'ShippingAddress';
  3.   var $validate = array(
  4.     'name_line' => 'notEmpty',
  5.     'street' => 'notEmpty',
  6.     'city' => 'notEmpty',
  7.     'state' => 'notEmpty',
  8.     'zip' => 'notEmpty',
  9.   );
  11.   function beforeSave(){
  12.     if(isset($this->data['ShippingAddress']['city'])
  13.        && isset($this->data['ShippingAddress']['zip'])){
  14.        $city = $this->data['ShippingAddress']['city'];
  15.        $zip = $this->data['ShippingAddress']['zip']);
  17.        $City = ClassRegistry::init('City');
  18.        if($City->hasZip($city, $zip)){
  19.          return true;
  20.        } else {
  21.          $this->validationErrors['zip'] = 'Zipcode does not exist in city';
  22.          return false;
  23.        }
  24.     }
  25.     return true;
  26.   }
  27. }

The other three models were almost identical -- simply substituting 'ShippingAddress' for 'BillingAddress' (or 'MailingAddress' etc..). The reason for the separation (I believe) was to give context to the type of address you're dealing with in regards to model relations. In this example, an Order has a ShippingAddress and BillingAddress, a Customer has a MailingAddress, and a Vendor has a CorporateAddress.

Those model relations are keen, because its nice to have that separation of context within our app. It's also nice for our form helper. Forms in views are easily defined like so:
  1. $form->create('Order');
  2. $form->input('ShippingAddress.name_line');
  3. $form->input('ShippingAddress.street');
  4. ....
  5. $form->input('BillingAddress.name_line');
  6. $form->input('BillingAddress.street');

With that said, CakePHP gives us a better way to get this separation without defining four completely identical models and having four identical tables. In fact, it is my goal to cut the codebase to 1/4.... and get you laid, of course!

Don't Repeat Yourself (DRY)

CakePHP's conventions, and any sane programmer, try to adhere to the DRY principle (Don't Repeat Yourself). With that in-mind lets rework ShippingAddress to be much more generic so that we can alias it by any other model wishing to have belongTo an address.

First, lets rename our class to a more generic Address.
  1. class Address extends AppModel{
  2.   var $name = 'Address';
  3. }

Next lets get rid of that ugly beforeSave() validation hack and make a proper custom validation function.
  1. class Address extends AppModel {
  2.   var $name = 'Address';
  3.   var $validate = array(
  4.     'zip' => array(
  5.       'format' => array(
  6.         'rule' => array('postal', null, 'us'),
  7.         'message' => 'Please enter a valid zipcode'
  8.       ),
  9.       'valid' => array(
  10.         'rule' => 'inCity',
  11.         'message' => 'Zipcode not in city.'
  12.       )
  13.     ),
  14.   );
  16. /**
  17.   * Validation rule for zipcode, should be in city inputed.
  18.   *
  19.   * @return boolean true if validation passes, or false if it doesnt
  20.   */
  21.   function inCity($check = null){
  22.     if(isset($this->data[$this->alias]['city'])
  23.       && isset($this->data[$this->alias]['zip'])){
  24.        $city = $this->data[$this->alias]['city'];
  25.        $zip = $this->data[$this->alias]['zip'];
  26.        return ClassRegistry::init('City')->hasZip($city, $zip);
  27.     }
  28.     return true;
  29.   }
  30. }

Note: I've left out the other validation rules for simplicity, in the real Address all other fields are just 'notEmpty' rules.

Looking deeper into the inCity() validation function, we see our first $this->alias reference. This is important, this is how we'll be able to use this method no matter what name our other Models give our generic Address (eg ShippingAddress, BillingAddress, etc..).

model relations....... Ladies ;)

So now that we have our new generic Address model with a single addresses table its time to start linking them in to our other Models like before.

Lets start with Order:
  1. class Order extends AppModel {
  2.   var $name = 'Order';
  3.   var $belongsTo = array(
  4.     'ShippingAddress' => array(
  5.       'className' => 'Address',
  6.       'foreignKey' => 'shipping_address_id'
  7.     ),
  8.     'BillingAddress' => array(
  9.       'className' => 'Address',
  10.       'foreignKey' => 'billing_address_id'
  11.     ),
  12.   );
  13. }

'className' => 'Address' is where the magic is. ShippingAddress and BillingAddress are now aliases of the class Address. Each will be loaded separately. will correspond to their respective billing/shipping_address_id foreignKeys. This is all in our addresses table (single table), but CakePHP gives us a level of contextual separation. We can do the exact same thing in our view as before ($form->input('ShippingAddress.name_line'), etc..) because as far as Order is concerned, ShippingAddress and BillingAddress are two separate models. Slick huh?

Lets finish out this case study with Vendor and Customer:
  1. //vendor.php
  2. class Vendor extends AppModel {
  3.   var $name = 'Vendor';
  4.   var $belongsTo = array(
  5.     'CorporateAddress' => array(
  6.       'className' => 'Address',
  7.       'foreignKey' => 'corporate_address_id'
  8.     )
  9.   );
  10. }
  12. //customer.php
  13. class Customer extends AppModel {
  14.   var $name = 'Customer';
  15.   var $belongsTo = array(
  16.     'MailingAddress' => array(
  17.       'className' => 'Address',
  18.       'foreignKey' => 'mailing_address_id'
  19.     )
  20.   );
  21. }

women love a slim codebase

That's it. We won, we just cut our codebase by 75% and our boss is taking us out for drinks! And look, that hot blonde in the corner is checking you out! Told ya we might get you laid.... Thanks CakePHP!