debuggable

 
Contact Us
 

How to bend Cake's Model::find() method to your needs

Posted on 21/6/08 by Tim Koschützki

Hey folks,

CakePHP allows you to use your own "find-types" for the Model::find() methodology. Those of your who are familiar with the find() method know that there are currently four types in the core: 'list', 'all', 'first' and 'count'. However, sometimes it is nice to specify your own type so you can have a call like this:

$this->Comment->find('pending');

So how would you go about implementing this? Correct, you would overwrite the find() method in your model and provide the default functionality if there is one of the default types used. Let's have a look at how you could implement this pending find from above:

class Comment extends AppModel {
  var $belongsTo = array('User');

  function find($type, $queryData = array()) {
    switch ($type) {
      case 'pending':
        return $this->find('all', array(
          'fields' => array('User.email', 'User.first_name', 'User.last_name', 'Comment.name')
          , 'conditions' => array(
            'Comment.active' => 0
            , 'Comment.blocked' => 0
          )
          , 'contain' => array('User')
        ));
      default:
        return parent::find($type, $queryData);
    }
  }
}

So this is actually not very difficult code, but it is very powerful. You could specify different sets of conditions, fields, orders, groups by's and containments just by adding a new entry to the switch statement. Please note that there is no break needed within the switch as we return home.

Let's add some code for finding pending users that were invited by the currently logged-in user. This is what we could use in the controller:

$this->Comment->find('pending', array('created_by_id' => User::get('id')));

Now for the model we would need to add some code that tracks if $queryData['conditions'] is set, and use that as well in conjunction with the conditions specified in our find type. While we are at it, let's also add some code that would handle fields, order, group, recursive and contain statements:

class Comment extends AppModel {
  var $belongsTo = array('User');

  function find($type, $queryData = array()) {
    $defaults = array(
      'conditions' => null
      , 'fields' => array()
      , 'order' => null
      , 'recursive' => null
      , 'contain' => null
    );
    $queryData = am($defaults, $queryData);

    switch ($type) {
      case 'pending':
        return $this->find('all', array(
          'fields' => am(
            array('User.email', 'User.first_name', 'User.last_name', 'Comment.name')
            , $queryData['fields']
          )
          , 'conditions' => am(array(
            'Comment.active' => 0
            , 'Comment.blocked' => 0
          ), $queryData['conditions'])
          , 'contain' => am(array('User'), $queryData['contain'])
        ));
      default:
        return parent::find($type, $queryData);
    }
  }
}

With some default values we can happily use our custom find type alongside dynamic conditions inserted by our controllers. If you think this am() stuff is overkill, you could as well just provide another find type:

$this->Comment->find('pending-created-by-logged-in-user');
class Comment extends AppModel {
  var $belongsTo = array('User');

  function find($type, $queryData = array()) {
    switch ($type) {
      case 'pending':
        return $this->find('all', array(
          'fields' => array('User.email', 'User.first_name', 'User.last_name', 'Comment.name')
          , 'conditions' => array(
            'Comment.active' => 0
            , 'Comment.blocked' => 0
          )
          , 'contain' => array('User')
        ));
      case 'pending-created-by-logged-in-user':
        return $this->find('all', array(
          'fields' => array('User.email', 'User.first_name', 'User.last_name')
          , 'conditions' => array(
            'Comment.active' => 0
            , 'Comment.blocked' => 0
            , 'Comment.created_by_id' => User::get('id')
          )
          , 'contain' => array('User')
        ));
      default:
        return parent::find($type, $queryData);
    }
  }
}

... well you get the idea. By the way, do you notice how useful using a static functions like this User::get() methods are for fetching properties and data from the currently logged in user? More on that later.

Anybody willing to share some dough on this one?

-- Tim Koschuetzki aka DarkAngelBGE

 
&nsbp;

You can skip to the end and add a comment.

Eelco Wiersma said on Jun 21, 2008:

Nice approach, I can see me using this.

Thanks!

Dardo Sordi  said on Jun 21, 2008:

This is the third time this get blogged in CakeLand(TM):

http://c7y.phparch.com/c/entry/1/art,mvc_and_cake
http://cakebaker.42dh.com/2008/03/23/defining-custom-find-types/

Anyway, good post.

Peter Goodman said on Jun 21, 2008:

I find this adds too much power to your find() function and needlessly complicates it. If we imagine a scenario where I'm looking at the documentation for this model, I need to know that specific arguments will trigger functionality that is inconsistent with the find() methods of every other model.

Why not simply have a findPending() function? It make your life simpler because it removed the unnecessary logic from find() into a specific function and the purpose of the function is *immediately* obvious.

ryan  said on Jun 21, 2008:

i like it; good post

phpcurious said on Jun 22, 2008:

Nice one! I also like to implement this on one of my projects!

Jeremy  said on Jun 22, 2008:

Nice and inspired,

Thanks

Tim Koschützki said on Jun 22, 2008:

Eelco Wiersma: :)

Dardo Sordi: Argh, wasn't aware of these. Well, it's IMO a good technique, so why not spread the word some more.

Peter Goodman: Your approach works as well. However, with the approach from the article you can remove a lot of extra functions. I agree however, that this can add quite some overhead if they need additional logic / complexity.

Good objection.

Grant Cox  said on Jun 23, 2008:

I've used this for find('sql') when I want to get the SQL query returned, rather than the have the query executed and the result returned. I've written a Bakery article, which is waiting approval.

I agree with Peter that I wouldn't use it for individual cases - only where the functionality is appropriate for all models.

Old Cake User  said on Jun 23, 2008:

* Override all core functions
* Let other programmers to debug

* Make the other programmers mad

* Spread and blog the great Cake power

* Kill the web

* Done

Felix Geisendörfer said on Jun 23, 2008:

@Old Cake User and fellow conservatives: Static function signatures have been around for a good part of the last century. Yes, I will miss them too. But no, just because there are currently more tools and assumptions based on the old way of thinking that doesn't mean new approaches should not be used. And this isn't even a CakePHP thing. Take a look over to jQuery land. They have taken the exact same approach for jQuery UI, where one method per plugin / widget delegates all calls. In the long run this keeps the API clean, removes the need of explaining PHP's overload() concept for simple tasks and is just much more flexible.

Aaron  said on Jun 24, 2008:

Hi, interesting post.. I am still a beginner at this, can anyone enlighten me what does an am() does?

Tim Koschützki said on Jun 24, 2008:

am() is a cake shorty for array_merge().

PHP 4 Hater  said on Jun 26, 2008:

@Felix Geisendörfer what about scalability and performance? have you guys tested that stuff on a high traffic website? This keeps the API clean but also creates a "big ball of mud" inside MyModel::find()

Felix Geisendörfer said on Jun 26, 2008:

PHP 4 Hater: If this becomes your performance bottleneck in any reasonably complex application I would like to congratulate you ; ). Seriously, this is not going to become performance relevant in any scenario I can think of.

Stepan Stolyarov  said on Aug 25, 2008:

Actually, I was wondering about "how useful using a static functions like this User::get() methods are for fetching properties and data from the currently logged in user". :)

Could you please explain this part in a few words until "more on that later" comes?

Tim Koschützki said on Aug 25, 2008:

Stepan Stolyarov: In the Auth system that we use and made, all your User relevant data can be fetched with User::get('id'), User::get('name'), User::get('Profile.id'), etc. By that you ensure you don't have to use all this set('logged_in_user', $user) stuff in your app controller and just call these functions right of your views, too.

And not only that, you can call it from anywhere. You don't need to pass around your user id or anything fancy.

brian  said on Aug 25, 2008:

will cake's find / relationships not handle belongsTo more than 2 levels deep, in one single query? If Model1 hasMany Model2 and Model2 hasMany Model3, and the associated belongsTo relations are in place, why can't we just do Model3->find('all') and it "back into" all three models with a single query?

Christoph Tavan said on Aug 26, 2008:

Hey Tim, I like your approach and just implemented it in an application. It works perfectly if the application exclusively uses the new type-style find()-calls. However I got confronted with the problem, that the old-style find()-calls (and along with that stuff like read() ), that I still find handy from time to time were broken. It can easily be fixed by allowing two more parameters and not overwriting the queryData-array:

function find($type = null, $fields = array(), $order = null, $recursive = null) {
[...]

$queryData = am($defaults, $fields);

[... here goes the switch ...]

default:

return parent::find($type, $fields, $order, $recursive);

Felix Geisendörfer said on Aug 27, 2008:

Christoph Tavan: http://gist.github.com/7451

rafaelbandeira3 said on Aug 28, 2008:

trackback:
"...read on phparch, cakebaker and debuggable about

how to define diferent find types, but *happily* they are all “deprecated” now, since

nate’s revision - see changelog [6481] - made 6 months ago,

customizing find methods is now much more fun, clean..."

http://rafaelbandeira3.wordpress.com/2008/08/28/the-new-way-to-define-find-methods/

Daniel Watson  said on Sep 17, 2008:

I love how you overwrite the find() class, however there is an issue when you try to use this methodology with paginate().

Refer to the bug report

Here is my work-around. Not very elegant, but it should work...


// App Model

var $find_type = 'all';

// Widget Model Class...

function paginateCount($conditions = null, $recursive = 0) {
if ($this->find_type == 'custom_find') {

....

return count(custom search conditions)

}

return count(find(all))

}

function find($type,$conditions) {
if ($type == 'custom_find') {

....

return custom search conditions

}

return find($type)

}

// Widget Control Class

$this->Widget->find_type = 'custom_find';
$this->paginate = array('Widget' => array($this->Widget->find_type));

$results = $this->paginate('Widget');

$this->Widget->find_type = 'all';

-dw

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.