Normalizing CakePHP model records

Posted by Felix Geisendörfer, on Aug 29, 2008 - in PHP & CakePHP » DataSources, Models & Behaviors

Hey folks,

here we go with post #10 of my 30 day challenge from sunny Atlanta.

one problem I often face when writing model functions is that I got to handle different ways of passing a model record. Imagine a simple Post hasMany Comment association that you are looping through and calling a function on.

php
  1. $out = array();
  2. foreach ($posts as $post) {
  3.   $out[] = sprintf('<h1>%s</h1>', $post['Post']['title']);
  4.   foreach ($post['Comment'] as $comment) {
  5.     $out[] = sprintf('<img src="%s">', Comment::gravatarUrl($comment));
  6.   }
  7. }
  8. echo join("\n", $out);

If you were now to code the Comment::gravatarUrl function and you wanted it to be fairly flexible as to what the $comment parameter is (an array, either with or without the 'Comment' key included or the $id of the record), you'd have to always code something like this:

php
  1. class Comment extends AppModel{
  2.   static function gravatarUrl($comment) {
  3.     if (!is_array($comment))) {
  4.       $comment = ClassRegistry::init('Comment')->findById($comment);
  5.     }
  6.     if (empty($comment)) {
  7.       return false;
  8.     }
  9.     if (!isset($comment['Comment'])) {
  10.       $comment = array('Comment' => $comment);
  11.     }
  12.     return sprintf('http://www.gravatar.com/avatar/%s.jpg', md5($comment['Comment']['author_email']));
  13.   }
  14. }

So in order to avoid duplicating this kind of logic throughout my apps, I decided to abstract it into a generic AppModel::normalize function that looks like this:

php
  1. class AppModel extends Model{
  2.   static function normalize($model, $id, $query = array()) {
  3.     if (empty($id)) {
  4.       return false;
  5.     }
  6.     if (is_array($id)) {
  7.       $record = $id;
  8.       if (!isset($record[$model])) {
  9.         $record = array($model => $record);
  10.       }
  11.     }
  12.     if (!isset($record)) {
  13.       $Model = ClassRegistry::init($model);
  14.       $record = $Model->find('first', am(array(
  15.         'conditions' => array($model.'.'.$model->primaryKey => $id),
  16.         'contain' => false,
  17.       ), $query));
  18.     }
  19.     if (empty($record)) {
  20.       return false;
  21.     }
  22.     return $record;
  23.   }
  24. }

Usage is fairly simple. Applying the function to the behavior above will make the function look like this:

php
  1. class Comment extends AppModel{
  2.   static function gravatarUrl($comment) {
  3.     $comment = AppModel::normalize('Comment', $comment);
  4.     return ($comment)
  5.       ? sprintf('http://www.gravatar.com/avatar/%s.jpg', md5($comment['Comment']['author_email']))
  6.       : false;
  7.   }
  8. }

And as you can see there is also a 3rd parameter called $query that you can use to create more complex normalization rules if an $id is passed in instead of an array. Now of course this isn't perfect yet. It doesn't verify complex association structures and I spend a lot of thought on how to possibly do that. But at some point I kind of concluded that the performance implications probably wouldn't be worth it. Another thing I'd really love is if I was able to detect the name of the class the static function was called upon inside the normalize function, but so far I think PHP simply can't do it. What I mean is that I'd like to use Comment::normalize / Post::normalize etc. without having to re-define the function in each model, so if anybody has an idea I'd love to hear it!

Feedback as always is very welcome! Please comment!

HTH,
-- Felix Geisendörfer

Print this Post | Digg This | Stumble It | Delicious

13 Comments

Matt Curry on Aug 29, 2008:

Hey Felix,
I was wondering why you made this a static method? Since Comment extends AppModel couldn't you just do $this->normalize and not have to pass in the model name?

Tim Koschützki on Aug 29, 2008:

Matt Curry: You are very right. However, by making it static you can also use it in views - when you want to create normalized versions of your data.

Imagine simple creation of links, with $comment['Comment']['id'] and $comment['id'].

Dardo Sordi on Aug 29, 2008:

> What I mean is that I'd like to use Comment::normalize / Post::normalize etc.
> without having to re-define the function in each model, so if anybody has an idea
> I'd love to hear it!

As of PHP 5.3.0 you can use late static binding: http://us.php.net/oop5.late-static-bindings

Mark Story on Aug 29, 2008:

Felix, I think in PHP 5.3 you can use Late Static Binding to get the name of the class you are in with __CLASS__. So something like

static function getClass() {
return __CLASS__
}

static::getClass(); would get your class name. But have to wait for PHP 5.3 or PHP 6 for cool new toys.

Mark Story on Aug 29, 2008:

Dardo Sordi: beat me to it, your comment hadn't been posted when I first read the article :)

Felix Geisendörfer on Aug 29, 2008:

Dardo Sordi / Mark Story: Yes, I had looked at that before - but I want PHP 5.2 to work for now : ). I'm really excited about 5.3 so, looks like its going to be an amazing release.

rafaelbandeira3 on Aug 29, 2008:

It's a nice approach.

Just a update:
'conditions' => array($model.'.id' => $id)
Should be
'conditions' => array($model.'.' .$Model->primaryKey => $id)

Piccolo Principe on Aug 29, 2008:

But you are using $this in a static method:
> static function gravatarUrl($comment) {
> if (!is_array($comment))) {
> $comment = $this->findById($comment);
> }
Isn't that a violation of [strict] standards?

Thomas on Aug 30, 2008:

fascinating, that you are realy using objects from the model-level in templates. and again, against its purpose by defintion.
it's a realy interessesting way of work. and a violation of the mvc-pattern of course. but maybe you can't go to abstract on php and had to work like this.

Felix Geisendörfer on Aug 30, 2008:

Matt Curry / Thomas: normalize is static because the methods build on top of it are static and used inside views. Normalize itself *is not* meant to be used inside of views.

Piccolo Principe: Thanks for catching that. Was an oversight when coming up with the example.

rafaelbandeira3: Good idea, put it in.

rafaelbandeira3 on Aug 30, 2008:

Thomas: Show me the law please...
I'm dieing to see this...

Where, and in what specification did you that it wasn't *"ALLOWED"*?

Jaik on Sep 03, 2008:

Is it me or do you have a superfluous 'return' on line 5 in the last block of code?

Felix Geisendörfer on Sep 03, 2008:

Jaik: Nice catch, fixed.

Add a comment