debuggable

 
Contact Us
 

Normalizing CakePHP model records

Posted on 29/8/08 by Felix Geisendörfer

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.

$out = array();
foreach ($posts as $post) {
  $out[] = sprintf('<h1>%s</h1>', $post['Post']['title']);
  foreach ($post['Comment'] as $comment) {
    $out[] = sprintf('<img src="%s">', Comment::gravatarUrl($comment));
  }
}
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:

class Comment extends AppModel{
  static function gravatarUrl($comment) {
    if (!is_array($comment))) {
      $comment = ClassRegistry::init('Comment')->findById($comment);
    }
    if (empty($comment)) {
      return false;
    }
    if (!isset($comment['Comment'])) {
      $comment = array('Comment' => $comment);
    }
    return sprintf('http://www.gravatar.com/avatar/%s.jpg', md5($comment['Comment']['author_email']));
  }
}

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:

class AppModel extends Model{
  static function normalize($model, $id, $query = array()) {
    if (empty($id)) {
      return false;
    }
    if (is_array($id)) {
      $record = $id;
      if (!isset($record[$model])) {
        $record = array($model => $record);
      }
    }
    if (!isset($record)) {
      $Model = ClassRegistry::init($model);
      $record = $Model->find('first', am(array(
        'conditions' => array($model.'.'.$model->primaryKey => $id),
        'contain' => false,
      ), $query));
    }
    if (empty($record)) {
      return false;
    }
    return $record;
  }
}

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

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

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

 
&nsbp;

You can skip to the end and add a comment.

Matt Curry said 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 said 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  said 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 said 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 said 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 said 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 said 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  said 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  said 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 said 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 said 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  said 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 said on Sep 03, 2008:

Jaik: Nice catch, fixed.

This post is too old. We do not allow comments here anymore in order to fight spam. If you have real feedback or questions for the post, please contact us.