New core behavior: Containable

Published on and tagged with behavior  cakephp  feature  model

With changeset 6918 a new behavior has been introduced to CakePHP: Containable. It’s a mix of the original Containable behavior by Felix Geisendörfer (aka the_undefined) and the Bindable behavior by Mariano Iglesias.

To use the new behavior, you either have to add it to the $actsAs property of your model:

class Post extends AppModel {
    var $actsAs = array('Containable');
}

or you can attach the behavior on the fly with:

$this->Post->Behaviors->attach('Containable');

Ok, now let’s have a look at some examples. For the examples I will use the three models Post, Comment, and Tag, with the associations: Post hasMany Comments, Post hasAndBelongsToMany Tags.

If we perform a simple find all statement, then we get back all posts plus all associated records, something like:

debug($this->Post->find('all'));

[0] => Array
        (
            [Post] => Array
                (
                    [id] => 1
                    [title] => First article
                    [content] => aaa
                    [created] => 2008-05-18 00:00:00
                )
            [Comment] => Array
                (
                    [0] => Array
                        (
                            [id] => 1
                            [post_id] => 1
                            [author] => Daniel
                            [email] => dan@example.com
                            [website] => http://example.com
                            [comment] => First comment
                            [created] => 2008-05-18 00:00:00
                        )
                    [1] => Array
                        (
                            [id] => 2
                            [post_id] => 1
                            [author] => Sam
                            [email] => sam@example.net
                            [website] => http://example.net
                            [comment] => Second comment
                            [created] => 2008-05-18 00:00:00
                        )
                )
            [Tag] => Array
                (
                    [0] => Array
                        (
                            [id] => 1
                            [name] => A
                        )
                    [1] => Array
                        (
                            [id] => 2
                            [name] => B
                        )
                )
        )

But often you don’t want to get all those data, and that’s when the Containable behavior comes to the rescue.

To get only the posts, you can do the following:

$this->Post->contain();
debug($this->Post->find('all'));

or

debug($this->Post->find('all', array('contain' => false)));

or without the Containable behavior

$this->Post->recursive = -1;
debug($this->Post->find('all'));

With the contain() method resp. the “contain” option we specify from which of the associated models we want to get data. If we want to get all posts plus the associated tags (without the comments), we would do it in the following way:

$this->Post->contain('Tag'); // we could also use an array
debug($this->Post->find('all'));

or

debug($this->Post->find('all', array('contain' => 'Tag'))); // we could also use an array

or without the Containable behavior

$this->Post->unbindModel(array('hasMany' => array('Comment')));
debug($this->Post->find('all'));

As you can see, the code using the Containable behavior is much cleaner than the code without it.

But that’s not all the Containable behavior can do for you. You can also filter the data of the associated models. If you are interested in the posts and the names of the comment authors, you could write:

$this->Post->contain('Comment.author');
debug($this->Post->find('all'));

or

debug($this->Post->find('all', array('contain' => 'Comment.author')));

[0] => Array
        (
            [Post] => Array
                (
                    [id] => 1
                    [title] => First article
                    [content] => aaa
                    [created] => 2008-05-18 00:00:00
                )
            [Comment] => Array
                (
                    [0] => Array
                        (
                            [author] => Daniel
                            [post_id] => 1
                        )
                    [1] => Array
                        (
                            [author] => Sam
                            [post_id] => 1
                        )
                )
        )

As you can see, the comment arrays only contain the author plus the post_id (which is needed by Cake to map the results).

You can also filter the (comment) data by using a condition:

$this->Post->contain('Comment.author = "Daniel"');
debug($this->Post->find('all'));

or 

debug($this->Post->find('all', array('contain' => 'Comment.author = "Daniel"')));

[0] => Array
        (
            [Post] => Array
                (
                    [id] => 1
                    [title] => First article
                    [content] => aaa
                    [created] => 2008-05-18 00:00:00
                )
            [Comment] => Array
                (
                    [0] => Array
                        (
                            [id] => 1
                            [post_id] => 1
                            [author] => Daniel
                            [email] => dan@example.com
                            [website] => http://example.com
                            [comment] => First comment
                            [created] => 2008-05-18 00:00:00
                        )
                )
        )

As you can see from these examples, the Containable behavior is very powerful, and I recommend to have a look at the tests.

Anyway, it’s very cool to have this behavior in the core :)

39 comments baked

  • Sebastian

    Wow. In my opinion this was the only thing missing in CakePHP.
    This is incredible, I’m very happy to see this new feature.
    Congratulations to the CakePHP team for this great work.

  • Jonathan Snook

    Ageed! I could’ve used this very feature just last week. It’s like the “expects” methods that was in the bakery and that I’ve used from time to time. But the optional filtering features are awesome. I do have some questions about it such as, how do you filter AND limit the fields returned in the associated model? (I’m guessing something like array(‘Comment.email’, ‘Comment.email = “filter”‘) although the overloaded parameter format always unsettles me (in that a parameter may do multiple things based on its format).

  • wangbo

    hi, dho. I want to know why did cakephp.org remove your blog link?

  • Felix Geisendörfer

    Hey Daniel, glad you like the new behavior and thanks a lot for this nice introductory post. Just a little correction: contain => null / false will set recursive to -1 not 0 as indicated in your example.

  • Felix Geisendörfer

    @Jonathan: You can do: Comment => array(conditions => …, fields => …).

  • John

    Daniel, mind if I use this as a basis for a page in the manual?

  • cakebaker

    @all: Thanks for your comments!

    @Jonathan: Yes, the overloaded parameter format is a bit confusing, and it is probably better to use the more verbose format shown by Felix.

    @wangbo: Hm, I don’t know what the reason is for removing the link to this blog, but I guess someone from the Cake team doesn’t like what I write ;-)

    @Felix: Thanks for the hint, it is now fixed in the article (even though I think it doesn’t matter in my example, as I get the same results)

    @John: No, I don’t mind, use it if you think it is useful for the manual.

  • tkenoid

    Gotta say that I’ve been waiting for this for a long, long time now… However I was not able to filter the results as in the last example.

    I have a User with hasMany Account.

    Trying to do:

    $this->User->find(‘all’, array(‘contain’=>’Account.id=19′));

    The returned result was not just one User who has Account ID = 19, but rather all users with an empty array for Account data and one user with account data where ID was 19 (as expected).

    It seems that cake still runs this condition as two queries, one to get all users and the second one two find the correct account.

  • teknoid

    The other note is that it does not seem to work with pagination as Bindable did…

    I used to have:
    $this->User->restrict();
    $this->set(‘users’, $this->paginate(‘User’));

    which worked very nicely to restrict only the User model for pagination.

    Trying to do:
    $thi->User->contain();
    $this->set(‘users’, $this->paginate(‘User’));

    Does not seem to have any effect, and all related models to User are still being queried.

  • Andreas

    @teknoid

    Looking through the tests for the ConaintableBehaviour (in /cake/tests/cases/libs/model/behaviours/containable.test.php)) I found one example for paginate:

    $Controller->paginate = array(‘Article’ => array(‘fields’ => array(‘title’)));
    $Controller->Article->contain(false, array(‘User(user)’));
    $result = $Controller->paginate(‘Article’);
    $Controller->Article->resetBindings();

    So my guess would be something like this:
    $this->User->contain(false)
    $this->paginate….

    So the trick is propably to use the behaviour with reset=false – don’t know how exactly I can achieve this :)

  • zonium

    The entry to your blog is no more listed on official CakePhp.org ???

  • Abhimanyu Grover

    WOW, I just loved the filtering part of Containable.

  • cakebaker

    @teknoid: I guess it is intentional that the filter only works on the associated data (in your case Account), i.e. it is thought for the use case: “Return all users plus all associated accounts with id 19″. On the other hand your use case would also make sense: “Return only those users with an account with id 19″. Maybe it is worth an enhancement ticket?

    And regarding the issue with pagination, it seems it has been fixed in changeset 6979.

    @zonium: Yes, it has been removed from the “Read” section of cakephp.org…

  • teknoid

    @cakebaker
    To me the real power of such filter would be if you have a chain of models where you’d like to filter the result set on multiple models.

    Let’s say:
    User->Product->ProductFeatures->Category

    I’d love to be able to only grab Users who are “developers”, with Products that are newer than “one month ago”, which ProductFeatures happen to be “open source” and belong to Category of “games”.

    The way it seems now, containable will return me to much extra data, and I have to do some dirty work to arrive at just the result set I need.

    I know that a lot of people have been looking for something like that. Perhaps you are right and enhancement ticket is in order :)

  • links for 2008-05-21 « Richard@Home

    [...] New core behavior: Containable – cakebaker CakePHP gains another powerful behaviour. Good stuff. (tags: cakephp containable behavior) [...]

  • cakebaker

    @teknoid: Yeah, such a feature would be nice :)

  • Cássio Talle

    Hello, i’m from Brasil, sorry my inglish!
    Very good that post, was really needing this information.
    Now I would like to know using this->paginate() this code?

    $this->Post->Behaviors->attach(‘Containable’);
    $this->Post->contain(‘Comentario.autor’,’Categoria.nomeCategoria’);
    $fields = array(‘Post.titulo’, ‘Post.created’);
    $this->set(‘dados’, $this->paginate(‘Post’));
    $this->set(‘dados’,$this->Post->find(‘all’,array(‘fields’=>$fields,’order’=>’Post.id DESC’)));

    thanks!

  • cakebaker

    @Cássio: You have to add the data for the containable behavior to the $paginate variable, for example:

    public $paginate = array('limit' => 10, 'contain' => array('Comentario.autor', 'Categoria.nomeCategoria'));
    

    Hope that helps!

  • Aaron

    great post! thanks..

  • Matt Huggins

    Just curious… I’ve been using Mariano Iglesias’ bindable behavior for awhile now, so I’m pretty familiar with this functionality. However, the conditional containable methodology seems redundant to me. How does this…

    $this->Post->find(‘all’, array(‘contain’ => ‘Comment.author = “Daniel”‘)))

    …differ from this…

    $this->Post->find(‘all’, array(‘contain’ => ‘Comment’, ‘conditions’ => array(‘Comment.author’ => ‘Daniel’)));

    …?

  • cakebaker

    @Matt: The difference is in the place the conditions are applied. If you have Post hasMany Comments, then a find(‘all’) performs two SQL statements: the first one to get all Posts, and the second to get the corresponding Comments.

    In your first example, the condition is applied to the second SQL statement, and the SQL looks like:

    SELECT `Post`.`id`, `Post`.`title`FROM `posts` AS `Post` WHERE 1 = 1
    SELECT `Comment`.`id`, `Comment`.`post_id`, `Comment`.`author` FROM `comments` AS `Comment` WHERE `Comment`.`author` = "Daniel" AND `Comment`.`post_id` IN (1, 2)
    

    In the second example, the condition is applied to the first SQL statement (which causes an error, as the column doesn’t exist):

    SELECT `Post`.`id`, `Post`.`title` FROM `posts` AS `Post` WHERE `Comment`.`author` = 'Daniel'
    

    Hope this explains the difference ;-)

  • Matt Huggins

    Thanks Daniel, that makes sense. :)

  • Matt Huggins

    Unless I just don’t know what it is, it’s too bad there’s not a way to say NOT to contain something. In other words, let the model go ahead and recurse as many times as it normally would while avoiding a specific model (and in turn its associated models).

    If you have a user model with 20 associations, but you want 19 of those associations, then you have to type out contain=>array(‘model1′,’model2′,…,’model19′). It would be nice to do something like contain=>array(‘not model20′) or something like that.

  • Rich Yumul

    @Matt
    Couldn’t you just use $model->unbindModel() for your one model that you want to exclude?

  • Steve Oliveira

    [...] CakePHP RC1 was released on June 4th. At the moment I haven’t had much time to play with it as I wrap up a few projects using 1.2 beta. One thing I did notice was their inclusion of the Containable Behaviour. This is a powerful behaviour that helps in limiting the data you want to grab from your models. I’ve used both Mariano’s and Felix’s behaviours and I’m glad those forces were combined to produce this, and then some. [...]

  • cakebaker

    @Matt: As Rich already pointed out, you can use Model::unbindModel() for this purpose. On the other hand I like your idea of using the containable behavior not only for specifying what you want, but also for specifying what you don’t want.

  • redchair

    hi
    may I translate this article into my language
    and post in my blog? ^^

  • cakebaker

    @redchair: Sure, as long as you mention somewhere the original source that’s no problem.

  • Elmo

    Thanks a lot! This is great. From the test cases you can see that they’ve accounted for recursion a couple of levels in. :) This is what I needed. In particular, second level containable behavior:
    $this->Client->recursive = 2;
    $this->Client->contain(‘Incident’, array(‘Incident’=> array(‘User’,’Action’,’Outcome’,’Journal.id’)));

  • cakebaker

    @Elmo: Thanks for your comment! And yes, the Containable behavior is a very useful new feature.

  • Promet CakePHP Source» Blog Archive » Learn Containable Behavior by Example

    [...] CakeBaker: New Core Behavior – Containable [...]

  • Terr

    Thanks everyone for the clear and concise examples, both in the post and in the comments.

  • cakebaker

    @Terr: You are welcome :)

  • Han Xu

    Just wanted to verify something about the containable behavior:

    What’s the difference between using containable and simply specifying the fields that you want in the find command?

    Since I ran into this problem today, is it because the fields property only applies to the original model that the find is executed on and not the associated model?

    Thanks!

  • cakebaker

    @Han Xu: Yes, the fields option is for the original model whereas the containable behavior is for the associated models.

    Hope that helps!

  • Sebastien G.

    hmmm…. when using the ‘contain’ array in the $paginate variable, how do I attach the ‘Containable’ behavior on the paginated model ?

    thks in advance

  • cakebaker

    @Sebastien: I’m not sure I understood what the problem is. Did you try the following (replace the XXX with what should be “contained”)?

    $this->paginate = array('YourModel' => array('contain' => array(XXX)));
    

    Hope that helps!

  • Sebastien G.

    hi thanks I did resolve my problem…

    i’m still getting another problem here….
    let me explain:

    Im tryin to find a single user in my database. I need to get his country, region and city.

    $this->User->Behaviors->attach('Containable');
    $user = $this->User->find('first',array(
    			'conditions'=>array(
    				'User.id'=>$id,
    				'User.activate_account'=>1
    			),
    			'contain'=> array('Country','Region','City'),
    ));

    And it gets me an error :
    SQL Error: 1054: Unknown column ‘Country.id’ in ‘field list’

    All my relations are made correctly… I think this is because Countries,Regions and Cities are in another database… how can I contain different database in one find method ??

  • cakebaker

    @Sebastien: Hm, I have no idea what the problem is. I tried it here with two models, each in its own database, and it worked fine (using the latest CakePHP version from the repository). What happens, if you perform the find without the “contain” option? Does it return any data?

Bake a comment




(for code please use <code>...</code> [no escaping necessary])

© daniel hofstetter. Licensed under a Creative Commons License