debuggable

 
Contact Us
 

Containable 2.0 BETA

Posted on 14/6/07 by Felix Geisendörfer

Deprecated post

The authors of this post have marked it as deprecated. This means the information displayed is most likely outdated, inaccurate, boring or a combination of all three.

Policy: We never delete deprecated posts, but they are not listed in our categories or show up in the search anymore.

Comments: You can continue to leave comments on this post, but please consult Google or our search first if you want to get an answer ; ).

PLEASE READ: CakePHP now includes a version of this behavior natively. Please use that version instead of the one downloadable here as it contains many bug fixes and additional features.

Hi folks,

sorry I've taken so long to get a new version of my Containable Behavior released, but believe me I've not been slacking this time. Rather I noticed that my initial plan of refactoring, adding a couple features and unit testing the behavior (especially the later) turned out to be much more ambitious than I thought it would be. In fact I'm releasing the new version as a BETA right now since I'm still not 100% satisfied with the result and not all features have made it in yet, but I felt the need for iterating. However, the new version should be a big step up from this initial one and I hopefully bug free.

Where is the code?

Note: I just tried running the unit test in PHP4 and it's failing. Will need to investigate what's going on there. So for now I only recommend usage for people running PHP5 would love PHP4 users to test the behavior and let me know if it works for them or not.
Update: My initial investigation has shown that it might be impossible to write a useful unit test for this behavior in PHP4. Apparently PHP4 chokes on verifying that two self-containing class instances are identical:

debug($this->User === $this->User->Post->User);

Generates the following error in PHP4: "Fatal error: Nesting level too deep - recursive dependency?".

So what does this mean for PHP4 users? Well give the behavior a try and see if it does what it's supposed to. I think it should be working regardless of the problems above but I have no rational way to verify it at this point.

What has improved?

  • Performance be better then in version 1.0
  • A new function containments() has been added that flattens the assoc tree and makes debugging easier.
  • Support for dynamic field containments
  • Support for different field containments for the main model and later assoc references to it

How to use the new Goodies?

Containable 2.0 is 100% syntax compatible to version 1.0. This means you can keep all your existing code based on it without having to change anything. However, additionally you are now able to specify what fields to include for what associations dynamically:

The most verbose way to do it looks like this. The following code will only fetch only the Group.id's associated with the current User + the User data itself. Group.name and other fields will not be returned:

$this->User->contain(array('Group' => array('fields' => array('id'))));

Now of course you can get a little more succinct then the above:

$this->User->contain(array('Group.fields' => array('id')));

Or even better:

$this->User->contain(array('Group' => array('id')));

Still not the shortest:

$this->User->contain('Group.fields.id');

And yup, you've guessed it, this is for the laziest amongst us:

$this->User->contain('Group.id');

Which syntax is the best? Well I usually like it short, but I know you can easily loose the overview between included models and fields in a big containment array so I've decided to leave you with the freedom to specify a 'fields' array for your field containments where you see fit.

One thing that's important to know is that you can also specify the $fields parameter to use for your main model find call with the new behavior, which you shouldn't mix up with the above:

$this->User->contain('group_id');

You also need to know that the Containable behavior tries to be a smart ass. So if you try to trick it with something like this:

$this->User->contain('id', 'Group');

Then it will notice that you forgot to include the User.group_id field which is necessary to fetch the Group association and the behavior will automatically add it for you.

What other features are missing / planned?

  • Support for setting all other association fields like 'order', 'conditions', 'limit' etc. on the fly
  • Configuration options for the behavior, i.e. making it behave persistently and not reset after a find call automatically
  • A more elegant unit test. The current one isn't bad and was hard to do, but I hope to be able to come up with something better.
  • I'm open for suggestions about other features, drop a comment!

All right I hope you enjoy this new version of the behavior and can help me with testing it on the battlefield a little more then I was able to do so far and I'm interested in hearing your feedback.

-- Felix Geisendörfer aka the_undefined

 
&nsbp;

You can skip to the end and add a comment.

Tarique Sani said on Jun 14, 2007:

"Support for setting all other association fields like 'order', 'conditions', 'limit' etc. on the fly" - this would be on my wanted list before I start using this over an older method :) Thanks for sharing

Nao  said on Jun 14, 2007:

Very thanks Felix! I test it now!

Features wished :

- support for infinite assoc : allow to not unbind child of one or more association with suffix by ‘*’ on last level association.
- support for self jointed assoc : ‘@’ suffix allow to repeat association expected for self jointed model. Or may be it do automatically if assoc is suffixed by "*".

(- ability to set "modifiers" for each assoc)

See
http://www.thinkingphp.org/2007/05/13/bringing-the-cold-war-to-cakephp-12-the-containable-behavior/#comment-68499

and

http://www.thinkingphp.org/2007/05/13/bringing-the-cold-war-to-cakephp-12-the-containable-behavior/#comment-68503

nao  said on Jun 14, 2007:

An other think for feature wich allow to repeat associations expected, $recursive param is need to set how many deep you want to repeat.

poLK said on Jun 14, 2007:

Well done, Felix! :)

PHPDeveloper.org said on Jun 14, 2007:

Felix Geisendörfer's Blog: Containable 2.0 BETA...

...

[...] Felix Geisendörfer has officially released the latest version of his Containable Behavior for the CakePHP framework: Sorry I’ve taken so long to get a new version of my Containable Behavior released, but believe me I’ve not been slacking this time. […] In fact I’m releasing the new version as a BETA right now since I’m still not 100% satisfied with the result and not all features have made it in yet, but I felt the need for iterating. However, the new version should be a big step up from this initial one and I hopefully bug free. [...]

nao  said on Jun 16, 2007:

I test your behaviour and I like your implementation and syntax which prepare smartly future enhancement.

Suggestions :

* When you use not existing association it cause lots of notices and
fatal error. Warning should be better.*

* When custom fields are set, I preconise to auto-include primary

key and foreign keys fields to prevent association expected which

use their (think "hasOne" assoc). The best is to include only

foreign keys fields needed, but may be difficult.

Bugs (just one in reality :-) :

* Association type hasMany cause bug, foreign key is not handle by
current model but by associate model so :

Change line 169 :
if (in_array($assocType, array('hasOne', 'belongsTo')) && isset($model->{$assocType}[$assoc])) {

by

if ($assocType == 'belongsTo' && isset($model->{$assocType}[$assoc])) {

I tell you if I find others, thanks again for your great work.

Felix Geisendörfer said on Jun 17, 2007:

nao: Yeah I also got your email about it - thanks for spotting this one. Just updated the post with the fix. Will take a look at your suggestions and see what I can implement in the next release.

Eric Winchell  said on Jun 25, 2007:

Not sure what it means but if I var_dump() the result of a Method::find* it outputs two times.

Eric Winchell  said on Jun 25, 2007:

Oops, my post above isn't related to Containable.

Brandon  said on Jun 25, 2007:

What is the status of this behavior and Controller::paginate()? I am using the 1.0 version and getting some very unexpected (well expected because I know whats most likely happening) behavior. For example:

ModelA->contain('ModelB');
$data = $this->paginate('ModelA');

?>

$data - in this example has all the associations (notedly the hasMany ModelC). I assume this has to do with binding the models that are reset after the findCount() that the Controller::paginate().

What is the best way to work around this?

Brandon  said on Jun 25, 2007:

Oh, looks like it didn't like my model definitions:

ModelA belongsTo ModelB
ModelA hasMany ModelC

Felix Geisendörfer said on Jun 25, 2007:

Brandon, first of all: Don't bother we with 1.0 stuff, this is (web) 2.0 so you better get it!!111!!!

*g* just kidding it's my 4th beer tonight. Other then that I have not played around with cake 1.2's paginate yet. However I think you are right about the find found, and I think I know how to fix it. I don't have much time to get it up in the next 2-3 days, but just look through my (2.0) code for where it reads:

$assocModel->unbindModel($unbind);

replace that with:

$assocModel->unbindModel($unbind, false);

And your bindings should stick persistently. If for some reason you need to get them back for another query I suggest you to use this little hack for now until I can come up with something more formalized:

http://bin.cakephp.org/view/1876932255

HTH

jcsiegrist  said on Jun 26, 2007:

Brandon & Felix,

I hacked the containable behavior for paginate by adding a reset property with a setter method:

var $reset = true;

function setContainReset($reset) {
$this->reset = $reset;

}

and changing the line:

$assocModel->unbindModel($unbind);

to

$assocModel->unbindModel($unbind, $this->reset);

so if I want the bindings to be sticky I call setContainReset(false).

Not very elegant and just a quick hack...

[...] So stay tuned for upcoming stuff like the finished Containable behavior, some other code and posts I got in my backlog as well as the launch of my little web application. [...]

Joel Stein  said on Jul 05, 2007:

My biggest request is to make it compatible with paginate(). I tried the "reset" hack above, but it doesn't really keep the fields around. Plus, when using Containable with paginate, for some reason all of my pagination data is lost, so I can't go past the first page. I think testing this against paginate would help a lot of us. Thanks, Felix!

speedmax  said on Jul 18, 2007:

I have really good experience with Containable, just like everything with problem with Controller::paginate() ..

I found the setContainRest method is pretty pointless when you can modify Model property straight away.

In the case using paginate.

$this->Person->reset = false;
$this->Person->contain('name', 'user_name', 'Profile', 'Activity');

pr($this->paginate());

In the case of normal Model::find() or Model::findAll()

$this->Person->contain('real_name', 'Activity.name', 'Activity.location');
pr($this->Person->findAll());

this is not a bad hack.. with the stablize of cakephp 1.2 trunk, hopefully "the_undefined" can release a better solution.

Felix Geisendörfer said on Jul 18, 2007:

speedmax: I don't think there is a $reset property for Model? Do you use any behavior for that? Please clarify since this looks like a cool solution but I can't find any reference to it. (Anyway I might implement it like this).

Thanks, Felix

Eric  said on Jul 18, 2007:

This is really useful.....continue developing it?

Felix Geisendörfer said on Jul 18, 2007:

Eric: Yes.

speedmax  said on Jul 31, 2007:

@Felix Geisendörfer

introduce the reset property inside ContainableBehavior, and passit to unbindModel as second parameter

...
class ContainableBehavior extends ModelBehavior {

var $runtime = array();

var $reset = true;

...

function contain(&$model, $associations = array()) {
...

$assocModel->unbindModel($unbind, $this->reset);

...

}

speedmax  said on Jul 31, 2007:

Sorry my eariler example wouldn't work, since property of Model and Behavior obviously are not the same...

my bad...

to implement this.. one line of hack.

in ContainableBehavior::contain method change to following
$assocModel->unbindModel($unbind, isset($assocModel->reset));

Sorry this looks too hacky, i am sure you can set those things in setup callback, or even use a method to change the reset flag...but you know..

@Felix Geisendörfer i tried to get in inside irc, but no reply.. anyway may be next time.

Chris Domigan  said on Aug 15, 2007:

Felix, when using Containable with $persistModel = true, I get the following error:

Warning (2): call_user_func_array() [function.call-user-func-array]: First argument is expected to be a valid callback, '__PHP_Incomplete_Class::contain' was given [CORE\cake\libs\model\model.php, line 484]

Have other users been successful in using the two together?

Felix Geisendörfer said on Aug 15, 2007:

Chris: Hm you might be the first one to run this setup. From the sounds of it, it may be a general problem with behaviors. Do other behaviors conflict with $persistModel = true for you?

nao  said on Aug 17, 2007:

Very useful, I like it! New features soon ?! ^_^

Marton Sari  said on Aug 24, 2007:

Hi, this code is very useful, thanks.

However it would be great if I can use this in "just add" mode; ie. if I set my model's recursive to 1, but I want to bind just one more depth in one of my first level bindings, then it would look like this:

$this->model->recursive = 1;
$this->model->addContain('Bike.Category');

Now you have to list all of your other first level default bindings to achieve this:
$this->model->contain('Bike.Category', 'Car', 'House', 'Motorcycle' etc.);

I think the logic would be that unbind only those models which are in a lower level than the root model's recursive value; but I couldn't implement it.

Felix Geisendörfer said on Aug 25, 2007:

Marton: Good point. Maybe I can reuse some of the code from here:

http://www.thinkingphp.org/2006/10/03/a-lightweight-approach-to-acl-the-33-lines-of-magic/

To achieve an advanced conain() syntax like:

$this->model->contain('*', 'Bike.Category');

or if you want to exclude the Car assoc from the first level:

$this->model->contain('*', '!Car', 'Bike.Category');

or if I want to make things even more DRY:

$this->model->contain('*,!Car,Bike.Category');

I'll have to think about this. It certainly seems to be an interesting idea, but also has its down-sides. For example if you keep adding model assocs during development and use a 'star' selector, you may be fetching more assocs then you intended to later on.

Anyway, I'll see what I can do for the next version.

speedmax  said on Sep 04, 2007:

@Felix
1.

That is very interesting idea, some kind of lightweight syntax to perform bind and unbind.

I kinda enjoy i can do following to unbind all assoc..
$this->model->contain()

2.
The reset hack is confirmed working even for pagination data set, nice

3.
Current movement in r5600+ renders Containable bind the wrong association, it could be results from the changes in model lately.

I am looking forward for new release.
:)

Joel Stein  said on Sep 11, 2007:

I love this behavior. It works perfectly on PHP5, but it doesn't seem to work at all in PHP4. Do you know why that is?

Felix Geisendörfer said on Sep 12, 2007:

Joel: Unfortunately I don't know why it fails in PHP4. When I find time to rewrite the tests for it I'll make some tests I can use for PHP4 so I can get it to work there.

Joel Stein  said on Sep 13, 2007:

Thanks, Felix... if there's anything I can do to help, please let me know. I find this behavior most useful.

Eric  said on Sep 19, 2007:

Controller::paginate() doesn't play with Contain. Just for testing I set $model->unbindModel($unbind, false); so that it always makes the (un)bindings stick. Paginate refuses to return anything beyond the first level of results.

However, I can take out the 'false' argument and call contain just before paginate's findAll in controller.php, and it looks fine. So something is going on when $reset is false that prevents paginate going beyond the first level.

Can anyone second this?

Eric  said on Sep 19, 2007:

Oops, I was using an older Containable version. Seems to work now. Did we decide on the best way to implement $reset?

Tim Daldini  said on Oct 05, 2007:

Could anyone be more specific about what exactly fails under PHP4?

I would use the behavior only to reset all assocations using contains with no args (I prefer this to crazylegs' function because the latter is using the private __backassociation member)

However, I don't like the idea of breaking PHP4 compatibility even though I'm using PHP5.

Felix Geisendörfer said on Oct 05, 2007:

Tim: Why not set Model::recursive to -1 instead?

Tim Daldini  said on Oct 05, 2007:

Hold, I just noticed that you are using __backassociations as well, I'll stick with the crazylegs unbindall.

Tim Daldini  said on Oct 05, 2007:

Hard to explain why I can't use recursive - 1, I'm trying to add a couple of custom LEFT JOIN clausules in a beforefind call and in order to let them appear in the resulting statement I must use recursive = 0;

I'm adding LEFT JOIN so I can retrieve certain data in a single query.

francky06l  said on Nov 05, 2007:

Hello

Great behaviors, I have been trying it .. I just can;t get it work with the reset hacks, it seems the paginateCount just reset the associations.
Does anyone had problem with this ? I am using the 1.2 from the Branch.

Thanks

Eric  said on Dec 11, 2007:

Should it work if the desired model to be included is a 'with' association?

Eric  said on Dec 14, 2007:

Also, it doesn't like 'DISTINCT Model.field' which is ok to do according to nate

Eric  said on Jan 17, 2008:

I'm trying to call contain() twice on the same model in an action, but the second call doesn't seem to make the model "forget" the associations set in the first call. Is there a way around this?

Eric  said on Jan 17, 2008:

My solution is to aggregate the fields that could ever be needed for the model in the action and only call contain() once, but my previous post might be a good improvement. Thanks Felix for Containable!

Bram  said on Jun 07, 2008:

Great behavior,

Can it be used to filter fields from a second assoc table. I am using the default cake contain and it tells me the table is not associated to the current model.

So is this possible with the latest version?

Felix Geisendörfer said on Jun 08, 2008:

Bram: Please download the latest CakePHP 1.2 RC1 release, it includes a new version of the behavior with the functionality you need. Also see: http://cakebaker.42dh.com/2008/05/18/new-core-behavior-containable/

Marek  said on Jul 04, 2008:

I have the same problem as Eric, if I call find('all'..) with 'contain' set twice, only the first calls give me right results. But with Cake 1.2RC1 everything was fine, the error comes when I change to Cake 1.2 RC2 today. It seems that find ignores the 'contain' order in the second call.

Marek  said on Jul 04, 2008:

ok, i find a solution ( https://trac.cakephp.org/ticket/4934 ), it seems Containable overwrites the bindings permanently.

$bT = $this->Adresse->belongsTo;
find('all', 'contain' =>....);

$this->Adresse->belongsTo=$bT;

polutan said on Aug 06, 2008:

Hello... i heard from someone, Containable was the new Extendable? Is that true? i'am confuse.. please tell me the true..
Because i need to Combining Containable and Extendable behavior.

Extendable for handling different user type tasks. and Containable for filtering tasks. If it's true, how to use Containable for handling different user type task like what Extendable can do..

Thank you :)

Felix Geisendörfer said on Aug 06, 2008:

polutan: The version of containable that comes with the latest 1.2 release can do everything containable and expandable used to do. However, the containable version talked about on this site is over a year old and has nothing to do with it!

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.