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:
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:
Now of course you can get a little more succinct then the above:
Or even better:
Still not the shortest:
And yup, you've guessed it, this is for the laziest amongst us:
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:
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:
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
You can skip to the end and add a comment.
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
An other think for feature wich allow to repeat associations expected, $recursive param is need to set how many deep you want to repeat.
Well done, Felix! :)
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. [...]
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.
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.
Not sure what it means but if I var_dump() the result of a Method::find* it outputs two times.
Oops, my post above isn't related to Containable.
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?
Oh, looks like it didn't like my model definitions:
ModelA belongsTo ModelB
ModelA hasMany ModelC
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
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. [...]
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!
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.
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
This is really useful.....continue developing it?
Eric: Yes.
@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);
...
}
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.
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?
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?
Very useful, I like it! New features soon ?! ^_^
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.
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.
@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.
:)
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?
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.
Thanks, Felix... if there's anything I can do to help, please let me know. I find this behavior most useful.
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?
Oops, I was using an older Containable version. Seems to work now. Did we decide on the best way to implement $reset?
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.
Tim: Why not set Model::recursive to -1 instead?
Hold, I just noticed that you are using __backassociations as well, I'll stick with the crazylegs unbindall.
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.
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
Should it work if the desired model to be included is a 'with' association?
Also, it doesn't like 'DISTINCT Model.field' which is ok to do according to nate
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?
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!
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?
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/
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.
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;
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 :)
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.
"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