Forcing cake to use “dashed” HTML DOM ID’s

Why bother?

I guess just to keep CSS purists happy, we should have our DOM id’s appear as “some-field-id” rather than CakePHP’s default “SomeFieldId”.
(Also, it’s just a matter of consistency and most certainly personal preference).
There is no defined rule for this, but in overwhelming CSS examples and tutorials it is quite common to see “dashed” names rather than camel cased.

Thanks to a tip from Mark Story, we can override the default behavior by creating an app_helper.php with the following method:

public function domId($options = null, $id = 'id') {
  $view =& ClassRegistry::getObject('view');
  if (is_array($options) && array_key_exists($id, $options) && $options[$id] === null) {
    unset($options[$id]);
    return $options;
  } elseif (!is_array($options) && $options !== null) {
    $this->setEntity($options);
    return $this->domId();
  }

  $entity = $view->entity();
  $model = array_shift($entity);
  $replacement = array($model .' '. implode('-', $entity));
  $dom = $model . join('', array_map(array('Inflector', 'camelize'), $entity));	
  $dom =  preg_replace('/(?<=[^A-Z])([A-Z])/', ' $1', $replacement);
  $dom = strtolower(implode('-', Inflector::slug($dom, '-')));
  if (is_array($options) && !array_key_exists($id, $options)) {
    $options[$id] = $dom;
  } elseif ($options === null) {
    return $dom;
  }

  return $options;	
}

This overrides Helper::domId() and gives us nice “model-field-name” ID’s with dashes rather than “CamelCase”.
The actual change happens in lines 13 – 16 (if you care), the rest of the method remains the same as the core.

Important database session limitation

If you are using a database to keep your sessions, there is one rather significant limitation which you should be aware of.

It does depend on the specifics of your DB, but I am going to guess that in most cases it is something that could happen to any database…

For example, we were using MySQL to keep the session data.
Looking at the table you’ll see that session data is serialized and stored in the “data” field…

By default that field is set to MySQL’s “TEXT” type, which has a size limitation of ~ 65K.

I know, I know… why would you want sessions larger than 65K?
Well…

“640k ought to be enough for anybody”

So, just in case, you do need a larger session the remedy is pretty simple, change the field type to “MEDIUMTEXT”, which effectively gives you ~ 16MB.

p.s. If you need even a larger size you can go with “LONGTEXT”.

For your reference, here’s the semi-official breakdown of different text-type fields in MySQL and their limitations:

TEXT
A BLOB or TEXT column with a maximum length of 65535 (2^16 – 1) characters

MEDIUMTEXT
A BLOB or TEXT column with a maximum length of 16777215 (2^24 – 1) characters

LONGTEXT
A BLOB or TEXT column with a maximum length of 4294967295 (2^32 – 1) characters

Couple of convenience methods to add to your bootstrap.php

Although most of the convenience methods will be removed from CakePHP going forward (see the note about basics.php), and I say good riddance, you still have the opportunity to add your own to bootstrap.php.

For example a couple that I like are wrappers for var_dump(). The main reason being is that var_dump() will properly display NULL and FALSE. Unlike debug() and pr() (alias to) print_r(), which will show… well… nothing.

So here you go:

function vd($var) {
  if(Configure::read() > 0) {
    echo '<pre>';
      echo var_dump($var);
    echo '</pre>';
  }
}

Or, if you’d like to die() out:

function vdd($var) {
  if(Configure::read() > 0) {    
      die(var_dump($var));
  }
}

Putting semi-RESTful API development to… rest

With some of the older web API standards subsiding, such as SOAP based interfaces, the new buzz seems to be all about REST (for the last few years at least).
Not that SOAP is “bad” in any way, as a matter of fact self-discovery via WSDL, and the standard itself is quite beneficial in many cases. It’s just that… programmers are notoriously lazy, and bulkiness of deploying a SOAP-like API makes most of us cringe.

So, let’s take a deeper look at REST for a second.

I think one of the most important principles behind the REST, is the fact that:

… [it] depends on HTTP headers for direction.

What that simply means is that a server, which exposes its API and expects a RESTful request, should rely on the HTTP headers (sent by the client) to make certain assumptions on what exactly the client is expecting to do (or acquire from the server).

CakePHP already does and excellent job of handling this issue for you. Let’s take a look at the mapping table from cake’s manual. (The table represents a specific HTTP method being used to send a request, and how it would be mapped, or actually expected to be mapped to a CakePHP action is some controller).
http://book.cakephp.org/view/1239/The-Simple-Setup

HTTP Method URL.method Controller action invoked
GET /recipes.method RecipesController::index()
GET /recipes/123.method RecipesController::view(123)
POST /recipes.method RecipesController::add()
PUT /recipes/123.method RecipesController::edit(123)
DELETE /recipes/123.method RecipesController::delete(123)
POST /recipes/123.method RecipesController::edit(123)

This is just my opinion, but while the REST ideology makes a lot of sense, it doesn’t always work out exactly as intended in the real world. That being said, REST is not meant to be a rule or a standard, rather it is a great set of suggestions on how to make your web app communicate efficiently with third-parties without much effort.

I guess, this is the point where I should explain the “semi-RESTful” part of the article…

According to the REST manifest (and as seen from the above mapping table), a POST action should be sent when any sort of data is supposed to be added (…again, as seen above, CakePHP will handle it perfectly well for you, out of the box).

To cut to the chase, a RESTful API will provide an opportunity for another web app to access yours and perform certain tasks.
As an example, some partner site might wish to send over the user credentials in order to gain certain information about the user attempting log-in.

Surely, issuing a GET request with such sensitive information wouldn’t be acceptable at all. Based on the above requirements we would have to send the data as POST (preferably over HTTPS). And thus our API deviates a bit from the REST approach, but… only slightly so.

… let’s keep this in mind and switch gears for a second.

Setting up a simple RESTful API in CakePHP will only take you a few minutes. (By following the manual and quite a few great tutorials out there).

Let’s see how to set things up when we need to stray away from the basics a little.

To illustrate a pseudo-real-world example, let’s consider the scenario mentioned above.
(A partner site is going to log-in into your application and get some data for the user).

I am going to do things a little backwards and present the following method for your consideration:
(this is going to be a part of our Users Controller):


public function api_info() {

Configure::write('debug', 0);

$result = array('response' => $this->User->find('first', 
array(
 'conditions' => array(
   'User.username' => $_POST['username'],
   'User.password' => $_POST['password']
),
 'fields' => array(
   'id', 'first_name', 'last_name', 'email',  'phone', 'dob', 'stats' 
),
 'recursive' => -1,
 'callbacks' => FALSE
)));

if($result['response'] === FALSE) {
  $result = array('response' => 'no user found');
}
		
$this->set(compact('result'));		
}

So the idea, is quite simple here. Some remote application will access the given method with the POST’ed credentials and receive back some information about a user (as provided in our fields key)… agreeably so, in an XML format.

Here’s what would happen in real life:

1. Do a form post with username and password, as seen above…
2. To the following URL: https://www.example.com/api/users/info.xml
3. Get back the information about the user (or receive a “no user found” response) if the credentials are wrong.

(There are a few ways to make such access more secure, via IP restriction for example, but I don’t feel that is extremely relevant at this point. Just something to keep in mind).

Technically all we would need at this point is a view and the following code added to the method:
$this->RequestHandler->respondAs('xml');
The RequestHandler component, in theory and according to the manual, would set the proper headers and provide a response, as expected in the XML format.
Well, for the life of me, I could not get this this to work… i.e. RequestHandler would not set the headers properly, thus a well formed XML string would be returned, as expected, but without telling the requesting application that it is an XML response, indeed (the magic is in the HTTP header).
(I hope someone can give me a guiding light…)

Nevertheless, this is not something to cry over as we can still fulfill our requirements, albeit in the “not-so-perfect-cake-way”.

Let’s take a look at the rest of the setup to make things a little more clear…

In order to respond with XML we will need to create the following structure in our views:
app/views/users/xml/api_info.ctp

See, the xml directory under our standard app/views/users will hold the view, which will be used to send the XML response back to the requestor. And, as you can see, the actual view api_info.ctp will match the action name, as usual.
If we had to send a JSON response the following structure would be needed:
app/views/users/json/api_info.ctp…. now, that everything is coming together let’s take a look at the view (api_info.ctp).

<?php 
if(!empty($result)) {
  echo '<?xml version="1.0" encoding="UTF-8"?>';
  echo $this->Xml->serialize($result, array('format' => 'tags'));
}
?>

I am not going to dwell on the details here, but basically we’ve got our $result from the controller and now present one as XML in the view. (I surely hope that the code above is quite easy to grasp).

Now, remember my complaint about the XML headers above?

Here is the necessary remedy… in the layouts we do have to create the following:
app/views/layouts/xml/api.ctp
… and here it is in all its glory:

<?php
  header ("content-type: text/xml");
  echo $content_for_layout;
?>

As mentioned, I’ve abandoned my failed attempts with RequestHandler and set the header manually in the layout. (Not much harm done).

The next two steps, to complete our semi-RESTful API are as follows:

1. Enable parsing of extenstions in the routes.php file: Router::parseExtensions();
2. And… to use the proper prefix routing, i.e. transform the api_info() method into a cake-accessible URL such as outlined above: https://www.example.com/api/users/info.xml&#8230; we’ll need yet another addition to the routes.php file:
Router::connect('/:controller/:id', array('prefix' => 'api', 'action' => 'info', '[method]' => 'POST'), array('id' => '[0-9]+'));. (I believe this neat little trick was “inspired” by cakebaker, so please award the required props to him).

Hooray! We are done now. To further extend your application and open up certain functionality to some third-party apps,
all that needs to be done now is to create a few, api_some_methods() and appropriate views.

Let’s talk about the “Search”…

Not sure how to get this post started exactly, but let me first say that: “I am not a Doctor… Err… Search Engine specialist”…

Now, I think, it is an interesting topic that a few people might find helpful and perhaps (and hopefully) some could even provide a little further insight in the comments as my dear readers often do…

So does your app need a search feature?.. More than often — it does.
(Do I need to mention that there are about 92308.6147 solutions that exist out there?)

I am going to go over some of the findings, headaches and success we’ve had while implementing a super-cool-and-robust search feature.

However, before proceeding, let’s take a few things into consideration:

  1. The search feature has to be fast and flexible; a simple “LIKE” is not going to cut it in this case.
    If this is beyond your current needs you might stop reading here :)
  2. MySQL’s, MyISAM engine has a lovely full-text search capability, but it is lacking in some areas. (Unfortunately too many details to list, but they are out there).
  3. MySQL’s, InnoDB does not have a lovely full-text search capability, but it does offer better performance (i.e. no table locks) and is the only supported option (at least at the time of this writing) on Amazon’s RDS.
  4. No need to try and beat Google at this game.

Given all the potential options, as always, it is extremely important to use the right tool for the right job. (So anything below may be a guiding light for your project or a complete dead-end, but at least, and hopefully, you’ll get to that realization sooner than later).

Let’s plow on…

I should share some of the high-level specifics of the app where the super-duper search engine was required.

In general, it is a rather active (yet simple) forum application (CakePHP, of course) with over a million comments, and with a new post or comment coming in approximately every few seconds or so (Decent LAMP stack powered by AWS).

Therefore we had to evaluate some options to make sure (as mentioned) we use right tool for the right job.

  1. MyISAM full-text search. Excellent for simple needs, not powerful enough for our requirements. Let me say a few quick points about it… Default index is four characters, and we actually had a business requirement to have at least a two character search, under any circumstances. Yes, you can index with 2 chars, but the performance comes to a grinding halt (OK, I am being a bit dramatic here, but it does nothing to improve what is inherently “not so great”). Not to mention that after moving to InnoDB we simply could not use MyISAM anymore.
  2. Google’s custom search. Why not let the best in business handle your needs?
    Well, to fully utilize the best of the best, you need to have a business account (to allow for customized branding, no-ads, etc. which is very important in some cases). You can certainly look-up some of the pros and cons and options at the Google’s custom search pages. In most cases I would stop right here and suggest that people give it a shot. (But then the post would be too boring)…
  3. Google Appliance. One of the issues that we faced right off the bat, was the fact that in order to access the app (and consequently the search feature) you had to be authenticated. Which, from my research (albeit somewhat limited) could only be done by hosting the google appliance. Obviously having your own little piece of google is cool, but the price and overhead of maintenance are certain drawbacks, which we weren’t ready to face, at least just yet.
  4. Lucene. A very popular and quite powerful tool for database indexing and searching. There are a few implementations out there… The most important for us, cake-people, is the Zend PHP version. While the tool is great (and is actually used by CakePHP’s own manual) it has a few extremely strange behavioral problems. Without going into much detail, there are 3 magic numbers (options), which control the auto-optimization of your index.
    Again, I am not a Lucene expert, but after trying out Lucene in a real-world application the gravity of attempts to properly adjust the auto-optimization options in order to keep the application performing smoothly seemed liked trying to control a magic carpet flight.
    Alrighty, to be fair, if you have a rather static site (similar to cake’s manual) there should be no issue with using Lucene. Needless to say, there are a few CakePHP-specific ways, which show you how to integrate Lucene into your environment. However, for a highly dynamic site (i.e. the forum) the auto-optimization of the index would bring down the application and make everyone cry, not to mention the indexing of the existing content would take hours upon initial installation.
  5. Sphinx. While hesitant at first, we’ve decided to give it a shot… One issue is that you have to install sphinx as as service (daemon) running on your web server. In many cases (smaller applications or shared hosting) it could be a showstopper. That being said, the installation is quite simple, and very little effort is required as far as any maintenance. (It also has a very low foot-print and so far, after a few months of real-world testing, it has not impacted server performance in any way).

Well… now you can probably guess that Sphinx was our final candidate and ultimately the chosen solution. A few things that made my colleagues and I very impressed was that the indexing is extremely fast. Compared to Lucene the initial indexing only took minutes for about one million records.
As well, and quite importantly, updating the index for any new posts/comments is also very fast.

By default the results are filtered by relevance as well as the date of the post (well, at least in our app it had an importance).
For example, most relevant and recent posts would be at the top of the resultset of a search, while older, yet still relevant results would appear lower.
(Word highlighting and other “neat” features are also available, but justly so… with other search tools as well).

Now then, what about the actual implementation?

Once the decision has been made to try out Sphinx, it was actually rather simple…

First of all a HUGE “thank you”, to the creator of this excellent behavior.
It has performed flawlessly in both CakePHP 1.2 and 1.3… (certainly some adjustments might be required for anyone’s specific needs, but the foundation, which has been laid down, is outstanding).

Once the behavior is properly attached to the required models, the Sphinx configuration couldn’t be easier:
(I will skip over the defaults and just point out what was required to get this thing off the ground)

source main
This is where the overall configuration of the Sphinx search engine is stored, as well as our initialization query to get thing up and running:

sql_query_pre = SET NAMES utf8
sql_query_pre = REPLACE INTO forum_counter SELECT 1, MAX(id) FROM forum_comments
sql_query = SELECT id, category_id, topic_id, user_id, body, UNIX_TIMESTAMP(created) AS created FROM forum_comments WHERE active = 1

sql_attr_uint = topic_id
sql_attr_timestamp = created

sql_query_info = SELECT * FROM forum_comments WHERE id = $id

Yep, besides any server-side defaults this is all that was custom-tailored and needed to get things going.
I hope you see how simple the queries are and can utilize the setup in your app.
(Notice, that we are using cake’s excellent counter cache here).

source delta : main

{
sql_query_pre = SET NAMES utf8
sql_query = SELECT id, category_id, topic_id, user_id, body, UNIX_TIMESTAMP(created) AS created FROM forum_comments WHERE active = 1 AND id > ( SELECT max_doc_id FROM forum_counter WHERE counter_id = 1)
}

This little snippet controls the “delta”, i.e. the difference between the original index and, well, any new additions to the forum.
Notice the max_doc_id, which is referring to the Sphinx index.

Again, besides the defaults (and attaching the above-mentioned behavior), this is all that was needed to be done to get a really great search engine working in our app.

I know that this has become a rather long post already, so I’d like to cut it short right about now…

  1. Please ask any specific questions in the comments. I do realize that everything above is quite a generic overview.
  2. Sphinx doesn’t play well with UUID’s, hopefully someone will prove me wrong ;)
  3. Default search return is 1,000 results… in most cases this is more than plenty, but hopefully someone can show us a cake-way to bring back even more.

Well then, if you’ve made it this far, next round of beers is on me ;)

A question to my readers..?

What is your approach and strategy for building the views in your applications?

If it’s a-one-man show… well, that’s certainly is admirable, but often a little extra help is needed.

Let’s say we have a designer who generally does the visuals, the mark-up including some mock-up functionality and based on that I can add CakePHP features, but even more importantly all the rich-application jQuery programming, since ideally we’d like to keep CakePHP logic in the views to the bare-minimum…

  1. Is this a common approach, something that you also practice in development?
  2. Have you got some great cutting-edge ideas, you’d love to share?
  3. Perhaps some Q&A to reach mutual goals?

Well… almost anything goes, but I’d love to see how are others handle similar situations in the MVC (well, forget the “M”) multi-developer environments.

Help me, help you

You might have noticed that my posts are not as frequent as they used to be.

Granted, I try to be more elaborate (and boy do I have a few things baking in the oven at the moment).

So with this idea, I will open up this post as Q&A “type-of-thing”…

If you are having difficulty with something in CakePHP post away in the comments with your questions and I will do my best to solve it (or at least guide you in the right direction).

I do have a couple of requests/rules though:

  1. Please don’t copy paste code in the comments. Use http://bin.cakephp.org/ or your favorite paste-bin to post the code.
  2. Review the manual before you post your question. Seriously. If the question can be answered by skimming through the cookbook… I’ll be sure to come up with some sort of “punishment” :)
  3. Please don’t ask me to write an entire app for you
  4. Keep the questions short, relevant and to the point
  5. Please understand that I cannot respond immediately, but I promise to make a timely answer, or contact you otherwise

Well that’s about it from my side. Hopefully this little endeavor will benefit both sides (I learn something new every day when I try to solve the problems for others… so yes, I am getting something out of it).

On top of that, I would like to reconnect with my readers, whom I felt neglected in the last few months (sorry, but work has to pay the bills and the habits).

Alright… let’s give it a shot… the forum is open :)!

Use CakePHP + jQuery to build dynamic selects…

First, I can’t believe I’ve missed a whole month of posting…. damn 28 days :(

Anyway, a recent post on the bakery http://bakery.cakephp.org/articles/view/dynamic-select-boxes-with-ajax-jquery prompted me to show a slightly more accurate approach on working with the given scenario.

(I don’t mean to piggy-back on someone’s work, but I feel it deserves a little “touch-up”).

If you don’t feel like reading the other post, the basic idea is to build a dynamic select list using CakePHP + jQuery.

For this example we’ll first select a car make and then build a select list of available models using jQuery.

In order to accomplish this, first of all, the appropriate association should be established between the models:
Car hasMany CarModel

Based on that we can have two controllers:

  1. cars_controller.php
  2. car_models_controller.php

Next, of course, we’ll need some actions and views…

(The simple add/edit/etc… you could easily “bake”, so I’ll just focus on jQuery and relevant views at this point).

In CarsController we’ll add a list_models() method…

Now let’s take a look at the relevant view (list_models.ctp).
Again, here we are only focusing on the two drop-downs.

<?php $this->Html->script('views/cars/list_models.js', array('inline' => FALSE)); ?>

<?php 
  echo $this->Form->input('Car.name', array('empty' => 'Select One', 'options' => $names, 'id' => 'car-name'));  
?>

<div id="car-models" style="display: none;">
  <?php echo $this->Form->input('CarModel.name', array('type' => 'select', 'id' => 'car-model-name')); ?>
</div>

First, we’ll load up the jQuery script, which is relevant to the view. Despite my previous conventions, I find it much easier to replicate the structure of your JS file placement exactly as you’d do for the views. With one obvious difference, that all JS goes under /webroot/js/views/some_controller/same_as_view_name.js

You’ll notice that I wrapped the second select input into a div, which is hidden by default.
This is just one approach, but you certainly could leave it visible in your UI and populate it with an:
'empty' => 'Select Car First' … just a matter of choice here, I guess.

Next, comes our cars_controller.php:
I’m only showing the “interesting” actions.

  public function list_models() {
    $this->set('names', $this->Car->find('list'));
  }
  
  public function get_models_ajax() {
   Configure::write('debug', 0);
   if($this->RequestHandler->isAjax()) {
     $this->set('carModels', $this->Car->CarModel->find('list', 
                            array('conditions' => 
                                        array('CarModel.car_id' => $this->params['url']['carId']),
                                  'recursive' => -1)));
   }
 }

Let’s review the code a little… The list_models() method doesn’t really do anything special, it simply sets the car names to be used for the first select list in the view.

The get_models_ajax() will be called via jQuery in order to build our second select input. We are turning off debug here, so that any “extra” output does not mess with the returned data…

Yet, a side note… I am referring to SQL debug, officially produced by cake, or timestamp…
Keep the debug “on” and the resulting output (in case of errors) will be seen in the firebug console… and if you don’t have firebug, then I don’t know how to debug AJAX stuff.

Also, note the $this->params['url']['carId']. This value will come from our first select list, which lists the car names with the corresponding ID’s from the database. That is because we’ve previously established a proper model association, therefore finding all the models for a given car (car_id) is no trouble at all now. (Oh, and please don’t forget to include RequestHandler in your list of required components, see the manual for more info).

Next, we still need a view for our get_models_ajax() action. The purpose of that view would be to return all the $carModels, which as you see we are setting in the controller.

Here it is, get_models_ajax.ctp:

<?php
  if(isset($carModels)) {
    echo $this->Js->object($carModels);
  }
?> 

(Too much for such a simple task (view and all)?… well, respect MVC and it will not come back to bite you in the ass later.)

The view is not terribly interesting, but one thing to note is that $this->Js->object($carModels); will convert the array of data, which is returned by the find('list') in the controller, into a JSON object.

Mental note… You certainly don’t have to work with JSON and any type of data can be returned back to the client, but for simple AJAX communication between the client and the server I find JSON to be most convenient format.

Alright, last, but not least let’s see the jQuery snippet that makes all the magic happen.

list_models.js

$(document).ready(function(){
  $('#car-name').live('change', function() {
    if($(this).val().length != 0) {
      $.getJSON('/cars/get_models_ajax', 
                  {carId: $(this).val()},
                  function(carModels) {
                    if(carModels !== null) {
                      populateCarModelList(carModels);
                    }
        });
      }
    });
});

function populateCarModelList(carModels) {
  var options = '';
  
  $.each(carModels, function(index, carModel) {
    options += '<option value="' + index + '">' + carModel + '</option>';
  });
  $('#car-model-name').html(options);
  $('#car-models').show();
  
}

Unfortunately it would take a few more days to explain every line of code in detail, and there are quite a few jQuery tutorials our there that will do a better job of explaining it, so I hope a little googl’ing will answer any outstanding questions.
… but I do want to point out a few things.

First, we are using jQuery’s handy $.getJSON, which does a GET request to a given URL with some data and returns the results back to our client.
Remember this piece: $this->params['url']['carId']? Well, that’s exactly where the carId value is coming from… i.e. the select input value, as specified between the curly brackets. Of course, there is no point in sending empty values to the server, therefore we wrap the entire chunk of AJAX code into if($(this).val().length != 0)… this will prevent jQuery making the extra call to the server if the “empty” option is selected.

Next, we already know that the data returned from the server will be a JSON object. So, before attempting to do anything with the returned data we check for some valid/good/existing data with:
if(carModels !== null)
In this example carModels is our JSON object, which is returned by CakePHP back to jQuery.

When all said and done, we use yet another awesome tool $.each to traverse the JSON object (i.e. carModels) and build our options list.
Finally, we add the freshly built HTML options list to the contents of our second select input and display it to the user.

We are pretty much done now, but just for some more detailed Q&A you can read further, if interested.

Q. Why use .live('change'... instead of just .change?

A. .live is a great tool to use if you are manipulating the DOM in some way and need to work with freshly inserted element. Granted in this example it is not necessary, but I wanted to show it off anyway. Just keep in mind that this approach is available and could be a life-saver at times.

Q. Why create populateCarModelList() function?
A. I like to keep things separated as much as possible, and who knows this function might come in handy for other reasons in a more complex application.

Q. Shouldn’t the get_models_ajax() action go into the CarModels Controller ?
A. Truth be told… it should. For the sake of simplicity I kept it in the same controller as the other method, but it would be “more proper” to place it in the CarModels Controller.

Q. Why did I assign DOM ID’s to the drop down elements, doesn’t cake do that automagically?
A. It does indeed, but cake’s DOM ID’s look like SomeModelThenField. In the world of CSS it is almost an unwritten rule that ID’s most often represented as some-model-then-field… so that’s my basic goal there. Thanks to a tip from Mark Story I promise to show in an upcoming post how to override the default CamelCasedID’s with dash-separated-ones.

Let’s help out CakePHP’s pagination

While it would be sweet, if the paginated data was usually not much more involved than a simple SELECT * FROM some_table… Unfortunately it is not.

In many cases there are a few, JOINs to related models, and likely a GROUP BY statement somewhere (maybe DISTINCT ?, who knows).

What I am getting at, is an SQL issue if that’s what you’d call it…

count(*) (which CakePHP uses properly, indeed) will yield pretty strange results if you have JOINs and GROUP BY (or any variation thereof… or “worse” yet, things like HAVING, etc.).

The display of records to the end-user will “disagree” with the number, which count(*) produces.
This is because count(*) actually counts everything (meaning across various models in the case of a JOIN).
The main issue comes up when one attempts to paginate the result-set from a complex query. You might see only 20 records displayed due to GROUP BY, but count(*) will actually return 43 (for example) because it does not actually count in the same way SELECT returns the records.

However, with a little help this problem is quite easily resolved…

Remember, we can always override the paginateCount() method inside a model.

For example, if we have an Order Model, just to get the expected count of records we’ll add:

public function paginateCount($conditions = null, $recursive = 0, $extra = array()) {
    if($this->statusId) {
      $conditions = array('conditions' => array('Order.order_status_id' => $this->statusId));
    }
    $results = $this->find('count', array_merge(array('recursive' => -1), $conditions));
  	return $results;
  }

With this scenario, we can get an accurate count of all orders, or just the ones that have some specific status.

The bottom line is that we know exactly what needs to be counted, and overriding paginateCount() allows us to do just that, regardless of what the actual record fetching query is like.

Make updateAll() fire behavior callbacks

For a while now updateAll() would not trigger any of the model’s behavior callbacks.

This presents a problem for a couple of reasons:

  1. The updateAll() method allows to easily do things like field_value = field_value + 1, because unlike other similar methods it does not escape fields/values
  2. Secondly we cannot rely on proper Behavior execution, which can lead to some unexpected results

What we have below is a simple override (and a very rough patch at this point) that will trigger afterSave() of the attached behavior as expected.

Take the code below and place into the model, which uses updateAll() at some point.

public function updateAll($fields, $conditions = true) {
    $db =& ConnectionManager::getDataSource($this->useDbConfig);
    $created = FALSE;
    $options = array();
    if($db->update($this, $fields, null, $conditions)) {  
      $created = TRUE;      
      $this->Behaviors->trigger($this, 'afterSave', array($created, $options));
      $this->afterSave($created);
      $this->_clearCache();
      $this->id = false;
      return true;
    }
  return FALSE;
 }

Again, this works for my immediate needs and doesn’t break any tests. However there are a few things that can be improved to make it more consistent with other methods like save(), for example.

Well, either way I am hoping that this will help someone.