Behaviors on model associations
I made a simple behavior that serializes (beforeSave) and deserializes (afterFind) data to and from the database.
The behavior works fine as long as I do a find directly over the model where my behavior is attached, but if I do a find through associated models using the containable behavior, my behavior is not triggered at all and I have to deserialize the data manually after the find, which is very uncomfortable.
Model behavior afterFind is triggered
$this->Model->find(...);
Model behavior afterFind isn’t triggered
$this->OtherModel->contain('Model');
$this->OtherModel->find(...);
I found a ticket and a thread concerning this topic, and it turns out that this is an existing problem with CakePHP for a longer time already.
Because I use the latest CakePHP 1.2.5 and couldn’t find any working patch for it, I adapted the patch found in the thread to this version. I am not very familiar with the core, so it’s still quick&dirty and I cannot tell if it works for every setup. Anyway I hope this still helps other people that run into the same problem.
Here are the changes that have to be done in the cake\libs\model\datasources\dbo_source.php, just place the code after the lines:
Line 877: $resultSet[$i][$association] = $linkModel->afterFind($resultSet[$i][$association]);
foreach ($linkModel->Behaviors->attached() as $behavior) {
if ($behavior != 'Containable') {
$data = array(array($association => $resultSet[$i][$association]));
$filtered_data = $linkModel->Behaviors->{$behavior}->afterFind($linkModel, $data, false);
if ($filtered_data) {
$resultSet[$i][$association] = $filtered_data[0][$association];
}
}
}
Line 741: function queryAssociation(&$model, &$linkModel, $type, $association, $assocData, &$queryData, $external = false, &$resultSet, $recursive, $stack) {
foreach ($linkModel->Behaviors->attached() as $behavior) {
if ($behavior != 'Containable') {
$return = $linkModel->Behaviors->{$behavior}->beforeFind($linkModel, $assocData);
$assocData = (is_array($return)) ? $return : $assocData;
}
}
Line 716: $data = $model->{$className}->afterFind(array(array($className => $results[$i][$className])), false);
foreach ($model->{$className}->Behaviors->attached() as $behavior) {
if ($behavior != 'Containable') {
$filtered_data = $model->{$className}->Behaviors->{$behavior}->afterFind($model->{$className}, $data, false);
if ($filtered_data) {
$data = $filtered_data;
}
}
}
edit:
I added support for the beforeFind callback, because I needed this on a softdelete behavior to filter associated deleted data as well.
I also found out that you don’t have to really hack the core, instead just copy the cake\libs\model\datasources\dbo_source.php into your app app\models\datasources\dbo_source.php and change this file instead.
Fast random find with CakePHP
I want to share my experience with random finds in CakePHP because they can be very slow done the wrong way.
In this example we have the model Audioplaylist which hasAndBelongsToMany Audio and we want to get a random playlist of Audios. We assume the table for Audio is pretty large and we also need the data of other related models as result, let’s say the owner’s username and the title of the related album. All models are set to $recursive = -1 and the ContainableBehavior is used to contain the data.
$audios = $this->Audioplaylist->Audio->find('all', array('contain' => array('User.name',
'Album.title'),
'order' => 'RAND()',
'limit' => $count));
Happy waiting!
You have to know that DB queries ordered by RAND() get slower the more fields are selected, and in this case we even select all fields of the Audio model.
$audios = $this->Audioplaylist->Audio->find('all', array('contain' => array('User.name',
'Album.title'),
'fields' => 'id',
'order' => 'RAND()',
'limit' => $count));
This is already a lot faster than before, but still not very applicable because it still slows down with the amount of related data.
$randomAudioIds = $this->Audioplaylist->Audio->find('list', array('fields' => 'id',
'order' => 'RAND()',
'limit' => $count));
$audios = $this->Audioplaylist->Audio->find('all', array('contain' => array('User.name',
'Album.title'),
'conditions' => array('Audio.id' => $randomAudioIds),
'order' => 'RAND()'));
Sometimes more is less, especially if you need alot of data from related models, you will appreciate the performance of these two queries.
Pausing remoteTimer
This is a simple example on how to create a remoteTimer that can be paused by the user, in this case this is done over a checkbox.
Because the ajax-Helper offers little help in this case, we need the help of javascript. We also have to simulate pausing with start and stop, because the prototype remoteTimer doesn’t offer it.
Javascript
First of all we need a variable to store the timer, in order to be able to stop it later.
var mytimer = null;
Next we need a function to start the timer with the important parameters.
function startTimer(url, update, frequency) {
mytimer = new PeriodicalExecuter(function() {
new Ajax.Updater(update, url , {asynchronous:true, evalScripts:true,
requestHeaders:['X-Update', update]})}, frequency);
}
And for last we need a function to stop the timer.
function stopTimer() {
mytimer.stop();
}
View
Now we can use the javascript-functions in the view.
echo $javascript->codeBlock("startTimer('/yourapp/posts/view', 'mydiv', 5)");
echo $form->checkbox('pause', array('checked' => true,
'onclick' => "if (this.checked){
startTimer('/yourapp/posts/view', 'mydiv', 5);
} else {
stopTimer();
}"));
Multiple ajax requests problems and AjaxQueue as solution
For my app I had to load many import processes at once by Ajax requests, so I ran into some serious problems.
1. Session data was not available each second request
I used the database option for Sessions, and that seemed to be the problem in this case. Because I don’t worry much about how sessions are saved, I changed it to cake in core.php and it solved this problem, not a really good solution, but I’m fine with it.
2. Timed out Socket connections
During the import process I had to make some Socket Connections and however there suddenly was no connection possible anymore after 10-15 requests, so the following ran into timeout.
3. The solution: AjaxQueue
After some search and search and search, I finally found a script called AjaxQueue posted on a mailing list. There you can set the maximum amout of simultaneous Ajax requests, exactly what I was looking for. After some testing it turned out to do a really wonderful job and all my problems were solved without loosing much of performance.
The following code is for the Prototype framework, but it should be no problem to adapt it to other frameworks in replacing the “Ajax.”-statements to similar ones of another framework.
var AjaxQueue = {
batchSize: 1, //No.of simultaneous AJAX requests allowed, Default : 1
urlQueue: [], //Request URLs will be pushed into this array
elementsQueue: [], //Element IDs of elements to be updated on completion of a request ( as in Ajax.Updater )
optionsQueue: [], //Request options will be pushed into this array
setBatchSize: function(bSize){ //Method to set a different batch size. Recommended: Set batchSize before making requests
this.batchSize = bSize;
},
push: function(url, options, elementID){ //Push the request in the queue. elementID is optional and required only for Ajax.Updater calls
this.urlQueue.push(url);
this.optionsQueue.push(options);
if(elementID!=null){
this.elementsQueue.push(elementID);
} else {
this.elementsQueue.push("NOTSPECIFIED");
}
this._processNext();
},
_processNext: function() { // Method for processing the requests in the queue. Private method. Don't call it explicitly
if(Ajax.activeRequestCount < AjaxQueue.batchSize) // Check if the currently processing request count is less than batch size
{
if(AjaxQueue.elementsQueue.first()=="NOTSPECIFIED") { //Check if an elementID was specified
// Call Ajax.Request if no ElementID specified
//Call Ajax.Request on the first item in the queue and remove it from the queue
new Ajax.Request(AjaxQueue.urlQueue.shift(), AjaxQueue.optionsQueue.shift());
var junk = AjaxQueue.elementsQueue.shift();
} else {
// Call Ajax.Updater if an ElementID was specified.
//Call Ajax.Updater on the first item in the queue and remove it from the queue
new Ajax.Updater(AjaxQueue.elementsQueue.shift(), AjaxQueue.urlQueue.shift(), AjaxQueue.optionsQueue.shift());
}
}
}
};
Ajax.Responders.register({
//Call AjaxQueue._processNext on completion ( success / failure) of any AJAX call.
onComplete: AjaxQueue._processNext
});
/************* SYNTAX ***************
AjaxQueue.setBatchSize(size);
AjaxQueue.push(URL , OPTIONS, [ElementID]);
************** USAGE ***************
AjaxQueue.setBatchSize(4);
AjaxQueue.push("http://www.testingqueue.com/process/",{onSucess: funcSuccess, onfailure: funcFailure});
AjaxQueue.push("http://www.testingqueue.com/process1/",{onSucess: funcSuccess1, onfailure: funcFailure1}, "myDiv");
AjaxQueue.push("http://www.testingqueue.com/process2/",{onSucess: funcSuccess2, onfailure: funcFailure2});
AjaxQueue.push("http://www.testingqueue.com/process3/",{onSucess: funcSuccess3, onfailure: funcFailure3});
AjaxQueue.push("http://www.testingqueue.com/process4/",{onSucess: funcSuccess4, onfailure: funcFailure4});
AjaxQueue.push("http://www.testingqueue.com/process5/",{onSucess: funcSuccess5, onfailure: funcFailure5});
**********************************/
Update multiple fields with one ajax request response
I searched for a possibility to update multiple fields with each ajax request made by a remoteTimer, but didn’t find any satisfying explanation on the web. To get that right, each field should get updated with the same response of a request.
So I had a look closer into the ajax helper and the prototype framework and came up with a little hack that suffice my needs and might be also helpful for others.
In the ajax helper (cake/libs/helpers/ajax.php) we first have to look into what happens when the options[update] parameter is set to an array with the different fields we want to update. That leads us to the ‘remoteFunction’ where following happens in that case:
$func = "new Ajax.Updater(document.createElement('div'),";
I have no clue what the creation of the div is really good for, but anyway we have to replace it with a JavaScript array holding the id’s of our fields:
$update = '[';
foreach($options['update'] as $option) {
$update .= "'" . $option . "',";
}
$update .= ']';
$func = "new Ajax.Updater({$update},";
Now we must have a look into the prototype framework (app/webroot/js/prototype.js) and make sure the function Ajax.Updater can handle that array. The important function there is called updateContent:
Ajax.Updater = Class.create();
Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
[...]
updateContent: function() {
[...]
if (receiver = $(receiver)) {
if (this.options.insertion)
new this.options.insertion(receiver, response);
else
receiver.update(response);
}
[...]
}
});
Because the receiver is now our array, we must step through it and send a response to each of it:
Ajax.Updater = Class.create();
Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
[...]
updateContent: function() {
[...]
if (receiver.constructor.toString().indexOf("Array") != -1) {
for(var i = 0; i < receiver.length; i++) {
if(r = $(receiver[i])) {
if (this.options.insertion)
new this.options.insertion(r, response);
else
r.update(response);
}
}
} else {
if (receiver = $(receiver)) {
if (this.options.insertion)
new this.options.insertion(receiver, response);
else
receiver.update(response);
}
}
[...]
}
});
After checking if the receiver is actually an array, we step through it and send the response to every our fields.
Finished!
You can now use it like:
<?php echo $ajax->remoteTimer(array('url' => 'controller/action', 'update' => array('field1', 'field2', 'field3'), 'frequency' => '5')); ?>
Sortable table rows with ajax helper
Most examples for drag&drop sorting with the ajax helper use lists (<ul>, <ol>) to demonstrate, but if you have several columns each row, you really would like to use a table and drag&drop it’s rows.
There is a solution mentioned at script.aculo.us, where you just have to use the HTML 4.0 specified table looking like:
<table> <thead><tr><td></td></tr></thead> <tfoot><tr><td></td></tr></tfoot> <tbody><tr><td></td></tr></tbody> </table>
Now you can use the tbody as parent in the ajax sortable function and set the ‘tag’-option to ‘tr’.Here is a small example in cakePHP:
<table>
<thead>
<tr><th>Sortable Table</th></tr>
</thead>
<tbody id="sortable_table">
<tr><td>row 1</td></tr>
<tr><td>row 2</td></tr>
<tr><td>row 3</td></tr>
</tbody>
</table>
<?php echo $ajax->sortable('sortable_table', array('tag' => 'tr')); ?>
Because the table rows are not set to float, you don’t see any moving effect while dragging a row. I found no way to make it float without the table beeing messed up.
Eclipse Code Completion for cakePHP
Code Completion in Controllers
Add this to your app_controller.php for every Component and Model you want to have completed by eclipse:
/** * @var CookieComponent */ var $Cookie; . . . /** * @var Yourmodel */ var $Yourmodel; . . .
Eclipse will use the PHPdoc for code completion.
Code Completion in Views
Add this to an extra file e.g. eclipse_cc.php:
$html = new HtmlHelper(); $javascript = new JavascriptHelper(); . . .
There is no need to include this file, just put it in your app folder and it will fool eclipse.
Init Weblog
Because I recently started to bake a Website with cakePHP, I now want to write down my experiences with that very cool piece of Framework during the further development.