Dealing with errors in CakePHP

Chances are if your app is already out in production, you have your debug level set to ‘0’. This is probably good news, since the users will not be seeing any nasty errors and in the worst case, they’ll see a blank page or a ‘not found’ page in case of an application error. (You might have noticed that when running with debug = 0, PHP errors are disabled and all application errors will point the user to a standard 404 page).

However, this may not be good enough for you and you’d like to extend the error handling capabilities of your app…

First, let’s take a look at what types of errors we should handle, and how to deal with them properly.

Technically we have two types of errors: PHP errors and CakePHP errors. CakePHP errors (application errors) would be something that is triggered by your application, for example a missing view file or a missing database table, PHP errors would be something like a fatal error that’s caused by a missing bracket or a notice of an undefined variable.
It goes without saying that in production your code should not have any missing brackets or missing views, but there are cases (i.e. division by zero) where a problem can slip by even with rigorous testing. Surely, you don’t want your users to see it, but it would be good for the developer/manager to know if that happens.

Let’s take a look at what to do with our PHP errors… It is important to realize that PHP errors should be dealt with completely separately from the CakePHP errors. Thankfully you can pretty easily control what happens when a PHP error arises by using our good ol’ .htaccess or php.ini. A simple google search will teach you all you need to know about how to ensure that PHP errors are logged, but not displayed to the user.

So what about CakePHP errors?
To extend CakePHP’s error handling you should create a file called app_error.php in your /app/ directory.

You then neeed to create a class there: class AppError extends ErrorHandler…

Now, you can override any default CakePHP error handling methods, to suit your own needs.

Here’s a trick to allow CakePHP errors to be emailed to the developer, while running with debug = 0, set in core.php:

class AppError extends ErrorHandler {

function __construct($method, $messages) {
   Configure::write('debug', 1);
   parent::__construct($method, $messages);

function _outputMessage($template) {

   App::import('Core', 'Email');

   $email = new EmailComponent;

   $email->from = 'CakePHP <>';
   $email->to = 'Developer <>';
   $email->sendAs = 'html';
   $email->subject = 'Error in my CakePHP app';



So now, the end-user will not see any errors (PHP or CakePHP), but the developer should have PHP errors logged on the server and CakePHP errors sent to him in an email.

It is also a good idea to add some output so that the user is not stuck looking at a blank page. You can easily extend above method to have:

$this->controller->output = null;
echo $this->controller->output;

You could just as easily log the errors to a file or define your own error methods (see cake’s default error.php) to add improved error logging and handling.

I’m sure this approach goes against some rules, but I have not seen a different way to handle this situation… so any suggestions/improvements are certainly welcomed.

primeminister pointed out that by using Object::cakeError() (see API) as well as defining some methods in App Error, one can easily add a more robust error handling to deal with various situations that can arise in your application. In a way you can think of using cakeError() within your app similarly to how you’d use trigger_error() in good ol’ PHP.

Dynamic menus without requestAction() in CakePHP 1.2

You’ve probably heard time and time again (especially in a few recent and popular blog posts) that using requestAction() is, generally speaking, considered to be a “bad practice”, “last resort”, “hackish” way of doing things in cake.

Let’s consider one common use to see if we can achieve our goals without using requestAction()…

We have a Company model and our goal is to build a navigational menu consisting of all the companies in our table. We would create an element called ‘company_nav.ctp’ and somehow load the relevant data from our Company model.
The first, obvious, approach is to use requsestAction() in an element, to get the required data. This, however, is something we would like to avoid.

The approach I offer below relies on cache, or cached model’s data to be precise…

We can agree that our menu or navigation would only change if a company has been added, modified or deleted. Therefore we use our Company model’s callback methods such as afterSave() and afterDelete() to properly cache the required data for the menu.

So, assuming we have a Company model, we can add the following to handle the data caching:

function afterSave() {
function afterDelete() {
function _cacheNav() {
$companies = $this->find('list');

Cache::config(null, array('engine'=>'File', 'path'=>CACHE));
Cache::write('companies', $companies, array('duration'=>7200, 'config'=>null));

The code is pretty simple, but let me explain it just a little…

We rely on afterSave() and afterDelete() to call our custom method _cacheNav(), which handles the actual caching of the Company data.

In this case we only need and for our menu, so using find(‘list’) works perfectly well for that.
We then call Cache::config() to ensure that we write to the correct location (the constant CACHE is defined for us by cake core) and that we use the correct storage engine (I use File, but you can use any supported engine). I found that specifying the ‘path’ is a good idea, since Cache can write to different locations depending on the context, therefore it’s best to be explicit about it.

Alright, now that our data is cached, we can easily build our element (company_nav.ctp):


Cache::config(null, array('engine'=>'File', 'path'=>CACHE));

$companyData = Cache::read('companies');

if(!empty($companyData)) {
   foreach($companyData as $key => $value) {
      echo '<div>'.$html->link($value, array('action'=>'view', $key)).'</div>';


I think the above should be pretty much self-explanatory, we are simply reading the data we’ve cached earlier in our model and building a menu of links.
Again, I prefer to specify the ‘path’ to ensure we attempting to read from the correct cache path.

Before we finish up, I wanted to point out a few important issues:

1. The duration of the cache is limited
Of course you can increase it to some unrealistically large value to cache forever, or you could come up with some fall-back mechanism (i.e. the good ol’ requestAction()) if cache data is not available.
At any rate, this is just something to be aware of and it can be easily fine-tuned to fit your specific needs.

2. Breaking of MVC (?)
In theory the view (element) should not access the data directly, and I’m not 100% sure if using Model’s cached data is somehow an exception to this rule. Looking at the benefits, however, I feel that this approach is justified. Cache is accessible to all of our application objects, therefore we could at least say that we are not breaking the rules, but rather bending them slightly.

Controller makeover (from ugly to beautiful)

Let’s take a controller ridden with problems and see how we can improve it. Hopefully this little experiment can help you beautify your code and optimize your app…

This is our ugly Users controller:

class UsersController extends AppController {

var $name = 'Users';
var $uses = array('User', 'Profile');

function add() {
   if(!empty($_POST)) {

      if($this->User->validates()) {
            //create the process_type hash
            $this->data['User']['process_type_hash'] = md5($this->data['User']['process_type']);

           //prepare user code messages
          switch($this->data['User']['error_code']) {

                   $this->data['User']['error_message'] = 'Success';

                   $this->data['User']['error_message'] = 'User suspended';

                   $this->data['User']['error_message'] = 'User account cancelled';

                   $this->data['User']['error_message'] = 'User not verified';

         $userId = $this->User->getLastInsertId();

         if($this->Profile->validates()) {
                 $this->data['Profile']['user_id'] = $userId;
                 $this->data['Profile']['temp_id'] = $this->generateTempId();

function view($id=null) {
        $this->set('users', $this->User->findAll('id='.$id));
        $this->set('profiles', $this->Profile->findAll('user_id ='.$id));

Let’s analyze the problems line by line and see what we can do it make it better…

Line 4
The $uses array is not needed (as in the vast majority of cases). Our models are related, therefore both User and Profile are already available to your controller

Line 7
When checking for POST’ed data, it’s best to rely on $this->data rather than $_POST

Line 8
No need to create() a model in this case, save() will handle that for you later on.

Line 9
This line really depends on the validates() method. Generally speaking there is no need to call this method separately, since it is called by save() for you, therefore setting the model’s data is no longer necessary.

Line 11
Based on the point above, we will not call validates() method, but will rely on save() instead.
Well, what about the data we need to prepare before saving?…

Lines 12 – 33
This work should never be done in the controller, and definitely not in this manner. Considering our points above, whenever we have any data, which has to be processed/prepared prior to save(), we should build a beforeSave() method in a given model to accomplish that.

Line 35
Calling save() without a false param, (as in save($data, false)) will trigger validation again. Either we get rid of the validates() method and rely on save() to handle validation for us, or we accept that data is already validated and we use ‘false’. Regardless of the approach, certainly there is no need to validate the same data twice.

Line 36
There is no need to call getLastInsertId(); After saving model’s id is automatically set to the last inserted record’s id. So in our case $this->User->id is already set.

Lines 38 – 45
Related models can be easily saved in one shot with saveAll(), there is simply no need of all this extra code.

Line 42
Looks like we’ve made some utility function in App Controller to generate our temp id. There are a few problems with this approach… First of all, the function is not protected, so we could access it by a URL, which is probably not desired. An easy way to avoid this would be to rename the function to function _generateTempId(), then you’d call it like $this->_generateTempId();. Still, this type of processing is better done in the model’s beforeSave() method.

Lines 51-52
We’ve got a few problems with our view() action. First of all our models are related, so we can easily retrieve the information for both by using $this->User->find(‘all’); Secondly, findAll() is deprecated so it’s best to use find(‘all’) instead. Third, the conditions, while working, are written poorly. A better syntax would be: find(‘all’, array(‘conditions’=>array(‘’=>$id)));
And last, but not least, we are not checking for the value of $id prior to find(). If $id is indeed ‘null’, we should not call find() at all (a simple if() statement should handle it for us).

So what would a fixed-up controller look like?

class UsersController extends AppController {

    var $name = 'Users';

    function add() {
        if(!empty($this->data)) {
           $this->User->saveAll($this->data, array('validate'=>'first'));	

    function view($id=null) {
        if(!empty($id)) {
           $this->set('users', $this->User->find('all', array('conditions'=>array(''=>$id))));


That’s all folks. A lot cleaner and prettier.

Well, what happened with all the code?

saveAll() will handle validation and saving of our User and Profile models, so all that code above can be easily condensed into a single line of code.

The data preparation i.e. the hash and error message creation will be moved into the beforeSave() of the User model.
And the preparation of temporary id will be moved to beforeSave() of the Profile model.

One note here, is that placing such a switch statement directly into your beforeSave() method is ugly. Instead, make a little utility function that you could use to process your data, for example:
$this->data = $this->_processErrorMsgs($this->data); … and move your switch code block into that utility function, it will make your code more flexible in the long run.

The main point here is that if you feel/see that your controllers are getting a little fat and your actions seems to have all sorts of ‘if’ statements, consider taking a step back and evaluating whether some of the functionality should be offloaded to models, utility methods or plain old simplified by using cake’s built-in tools.

Automagical selects or checkboxes

A quick example of some automagic goodness…

Let’s say we need to to build an “add Post” form and allow the user to pick some tags for the Post.

In our Posts controller’s add() action, we’d probably do something like this:

$this->set('tags', $this->Post->Tag->find('list', array('fields'=>array('Tag.tag'))));

Then in the view, while building the form, all we need to do is:

echo $form->input('Tag', array('multiple'=>'checkbox'));

Yep, cake will automagically recognize the $tags variable, which we set in the controller, realize that we have Post->Tag models, find the ‘Tag’ field and decide that what we really wanted to do is build a list of checkboxes from our array of $tags.

You can experiment with field names for other cases… things like ‘tags’ or ‘tag_ids’ for the field name should work as well.

Filtering results returned by Containable behavior

First of all, let me warn you that what I’m describing here is more or less a hack and is not the most efficient way of handling such things, especially if you are dealing with large volumes of data. It is really here for educational purposes and as food for thought…

Now that I got that out of the way, let’s use our favorite Post HABTM Tag example, and try to do something like this…

We need to get all Posts whose titles start with a ‘t’ and all related Tags, which start with an ‘n’.

This seems like a perfect job for the Containable behavior:

$this->Post->find('all', array(
                                   'conditions'=>array('Post.title LIKE'=>'t%'),
                                                              'conditions' => array('Tag.tag LIKE'=>'n%')

The problem is that we will get back all Posts that start with a ‘t’, but if there is no matching Tag that starts with an ‘n’ we will simply get an empty array.

The returned data might looks something like:

    [0] => Array
            [Post] => Array
                    [id] => 4
                    [title] => test
                    [post] => 
                    [created] => 2008-06-20 16:06:53
                    [modified] => 2008-06-20 16:06:53
                    [user_id] => 0

            [Tag] => Array


    [1] => Array
            [Post] => Array
                    [id] => 99
                    [title] => testing 00777
                    [post] => 
                    [created] => 2008-07-18 15:14:24
                    [modified] => 2008-07-18 15:14:24
                    [user_id] => 0

            [Tag] => Array
                    [0] => Array
                            [id] => 12
                            [tag] => new
                            [status] => 0
                            [PostsTag] => Array
                                    [id] => 159
                                    [post_id] => 99
                                    [tag_id] => 12
                                    [status] => 




Well, the second record looks like what we really need, but we certainly don’t need the first one.

Let’s use our Post model’s afterFind() method to filter out unwanted data:

function afterFind($results, $primary=false) {
    if($primary == true) {
        foreach($results as $key => $value) {
            if(empty($value['Tag'])) {
    return $results;  

Yeah, not the prettiest solution by any means, but it gets the job done relatively painlessly.

Remember that afterFind() will be executed after each find() call, so be mindful of what you are doing, because you may tamper with results that really did not need any filtering.

HABTM and JOIN trickery with CakePHP

Update (10/7/2009)… this functionality has been rolled into Habtamable behavior

An interesting question came up on IRC today…

If I have Post HABTM Tag, how do I select only Posts that have Tags “new” AND “cakephp”?

A seemingly simple problem, actually required a pretty tricky solution. I do have to say that it’s not because of some cake limitation, but really (at least for me) it’s not at all straight forward to do that type of query in good ol’ SQL (yeah, I don’t really know SQL all that well… good thing we’ve got google).

Before going further, I’ll assume that you have a solid understanding of the way HABTM works, concepts of join tables, auto “with” models and cake conventions. If not, you should probably refer to my previous posts about HABTM and/or read up in the manual.

Alright, let’s analyze the problem for a second… basically we need to grab all Posts where in the join table a single post_id would match two tag_id’s (i.e. the tag_id’s of “new” and “cakephp”). Note, that we cannot match just one or the other, we must have both tag_id’s matching the same post_id. Not only that, we don’t know tag_id’s as we only know the search terms (“new” and “cakephp”).

Looks like we’ll need some creative SQL and JOINs to get this working right…

Surely, we could rely on Model::query(), but let’s see if we can get this working with find() instead.

First of all we have to force cake to build a join query.

So let’s do something like this:

$searchTerms = array('cakephp', 'new');

                                                         'conditions'=>array('PostsTag.post_id =')
                                                              ' = PostsTag.tag_id',

I will briefly explain, what’s going on here (if you need more details, see the post linked above on how to force CakePHP to do a JOIN)…

We are telling cake to JOIN our Post model with the PostsTag model (join table: posts_tags) and then JOIN our PostTag model with our Tag model (tag table: tags).
The JOIN conditions are pretty simple, we ensure that matches the PostsTag.post_id and matches PostsTag.tag_id. Of course we need to also ensure that we only grab the tags where Tag.tag is IN our search terms (see the $searchTerms array).

Once all of that is accomplished, we build our find() method:

$this->Post->find('all', array(
                              'group' => array('','Post.title HAVING COUNT(*) = '.$numCount)))


Let’s break it down…

If we were to do a simple find(‘all’) we’d get all Posts that happen to have’s matching either one of our search terms. This is not what we need.

By adding the GROUP BY and HAVING COUNT(*) = $numCount, we ensure that we match both of our Tag ids and not just one or the other. In other words, COUNT (*) must equal to the number of search terms.

OK, so what is $numCount?
In our example we know that we searched for two terms ($searchTerms array), therefore we could have done:


However, if our $searchTerms array had an unknown number of items, we’d do something like this prior to our find() call, to determine how many’s must be matched in order for our query to be correct:

$numCount = count($searchTerms);

I can imagine that this is probably a bit over the top, but it certainly taught me a few things and reinforced some others, hopefully you’ll learn a thing or two from this as well ;)

‘fields’, ‘conditions’ and associated models in CakePHP 1.2

Sometimes you see code that tries to do something like this:

$this->Company->find('all', array('conditions' =>array(''=>'test'),

Of course most of the time you’ll hear: “Well, this doesn’t work!”
Yet, some people will disagree: “Works for me!”

So, what’s really going on here?

Whether the above syntax works or not, would depend on your model association. Or, more specifically, whether or not an SQL JOIN is built.

If we assume that Company hasOne Profile, then the above find() will build a query like:

   `profiles` AS `Profile` 
   (`Profile`.`company_id` = `Company`.`id`) 
   `Profile`.`name` = 'test'

… which is perfectly legal.

Of course if you attempt to do this for hasMany or HABTM, the query is going to fail.
You’ve probably guessed that the same works for belongsTo.

Practical use of saveAll() (part 2, notes and tips)

In part 1, I explained how to use saveAll() in order to save multiple models at the same time, if you haven’t had a chance to read that post please do so before starting on this one.

1. Saving multiple records for the same model.

Using our models from the previous example, we can build a form to save multiple Accounts at the same time (accounts/add.ctp):

echo $form->create();

echo $form->input('');
echo $form->input('Account.0.username');
echo $form->input('');
echo $form->input('Account.0.company_id', array('type'=>'hidden', 'value'=>1));

echo $form->input('');
echo $form->input('Account.1.username');
echo $form->input('');
echo $form->input('Account.1.company_id', array('type'=>'hidden', 'value'=>1));

echo $form->end('Add');

Now, in your Accounts controller’s add() action you would do something like this:

function add() {
   if(!empty($this->data)) {
        $this->Account->saveAll($this->data['Account'], array('validate'=>'first'));

Notice, that I am passing $this->data[‘Account’] to saveAll(), rather than just $this->data. This is necessary for saveAll() to work properly when saving multiple records for a single model.

2. Any chance to save deep bindings?

No, at least currently, if you have Company->Account->Profile, you cannot save all three models in one go.

3. What if Company hasMany Account and Company hasMany Profile?

This is possible, you can save all three models at the same time. It is very similar to the way we’ve saved Company and Account models before. Just add Profile model’s fields to the form, named as (for example).

4. Make sure your DB supports transactions

You’ve probably noticed that saveAll() uses transactions to ensure data integrity. I, however, made a little mistake ( and forgot that MySQL’s default MyISAM storage engine does not support transactions. So, make sure that you use InnoDB in MySQL to enable transaction support (if you use another DB, double check that transactions are supported).

5. Is it possible to save HABTM models with saveAll()?
Update (3/30/2009): It is now working just fine, please see this post for more details.

I have not found a way (or a supporting test case) to save multiple HABTM models at once, for example, if you wanted to create a new Post and some new Tags and save all that goodness in one shot.
It is possible with save() to create a new post and assign some Tags to it, but you need to know the ids of the Tags before the save(). (See this post on saving HABTM data for more details).

6. What if Company hasOne Account?

This is still possible, but since you can only have one Account per Company, you should name your Account model fields as usual, i.e. (no need for, since only one Account is allowed by the relationship).

Well, that pretty much concludes the majority of things you should know about saveAll(), hopefully now, you should be able to handle and troubleshoot a variety of cases where saving of multiple models or records is needed.

Practical use of saveAll() (part 1, working with multiple models)

(Part 2 is here)

I would like to provide some insight on how to use the saveAll() method, with a few real-world examples. First, I’m going to cover how to use saveAll() when working with two related models.

Imagine we are building a CRM of some sort. We have a Company model, which holds some generic Company information. Company hasMany Account, which would hold the information about various users who can access the CRM.

Let’s go ahead and create our tables:

CREATE TABLE `companies` (
  `id` int(11) NOT NULL auto_increment PRIMARY KEY,
  `name` varchar(200) NOT NULL,
  `description` varchar(200) NOT NULL,
  `location` varchar(200) NOT NULL,
  `created` datetime NOT NULL
 CREATE TABLE `accounts` (
`id` INT NOT NULL auto_increment PRIMARY KEY,
`company_id` int(11) NOT NULL,
`name` VARCHAR( 200 ) NOT NULL,
`username` VARCHAR( 200 ) NOT NULL,
`email` VARCHAR( 200 ) NOT NULL,

Then we can setup the Company and Account models as follows:

class Company extends AppModel {
	var $name = 'Company';	

	var $hasMany= array('Account');

	var $validate = array(
            'name' => array('rule' => array('notEmpty')),
            'description' => array('rule' => array('notEmpty'))
class Account extends AppModel {
	var $name = 'Account';	

	var $belongsTo = array('Company');

	var $validate = array(
            'name' => array('rule' => 'notEmpty'),
            'username' => array('rule' => 'notEmpty'),
	    'email' => array('rule' => 'email')


As you can see, the models are pretty simplistic, for example the Account model is missing an obvious password field, but I’m leaving it up to you to extend the models as you wish. At least we’ve added some validation and all in all that should suffice for the purposes of this example.
Please note, that I’m using a new (at the time of writing) validation rule ‘notEmpty’ if you do not have the latest version of cake (nightly build of 07/31/2008 or later), this rule may not be available in your core.

Next, let’s create a Companies controller, we’ll leave it empty for now:

class CompaniesController extends AppController {
 var $name = 'Companies';

Now that we have our models and a controller, our goal is to build a form where some CRM user would setup a company and a first default account at the same time. This is where saveAll() comes in very handy, because it allows us to save both models without any effort.

So, let’s build a form, which would allow us to create a Company and an Account (create a file called /companies/add.ctp):

echo $form->create();
echo $form->input('', array('label'=>'Company name'));
echo $form->input('Company.description');
echo $form->input('Company.location');

echo $form->input('', array('label'=>'Account name'));
echo $form->input('Account.0.username');
echo $form->input('');

echo $form->end('Add');

Let’s take a look at what’s going on here. We consider Company to be our main model, therefore the form by default will post to Companies controller’s add action (i.e. /companies/add/).
Take a look at the way I named the form fields for the Account model. If Company is our main model saveAll() will expect the related model’s (Account) data to arrive in a specific format. And having Account.0.fieldName is exactly what we need (this is only true for hasMany relationship, for hasOne the fields would follow Account.fieldName format… I hope it makes sense as to why).
Having the label for the two fields allows us to be more descriptive, otherwise CakePHP would label both fields as just “Name” by default, which would be confusing to the user.

Now, in our Companies controller we can create an add() action:

function add() {
   if(!empty($this->data)) {
      $this->Company->saveAll($this->data, array('validate'=>'first'));

Is that easy or what?

A quick thing to point out here, is the use of array(‘validate’=>’first’), this option will ensure that both of our models are validated. You can refer to the API for other options that saveAll() accepts, but this is good enough for the current example (and most similar cases).

Now let’s take our tiny app for a test drive and try to submit the form with empty data. If all goes well, the validation should take place for both models and you should see our relevant fields being invalidated.

Go ahead, and try to save some data. Looking at your SQL debug, you’ll see that cake saved both models and established the correct relationship by saving company_id field with the correct id into our accounts table. Ah, the wonders of automagic…

Well, we are not done just yet. Let’s now build a form and a related action for editing our Company and Account.

For the purposes of our example let’s do something like this for the edit action:

function edit() {
   if(!empty($this->data)) {
      $this->Company->saveAll($this->data, array('validate'=>'first'));
   else {
      $this->Session->write('AccountId', 2);
      $this->data = $this->Company->Account->find('first', array(
						  	''=> $this->Session->read('AccountId');

We’ll imagine that at this point the user (or technically Account) is logged into our application and we’ve stored the into the session by using: $this->Session->write(‘AccountId’, 2);

In your case this id might be different, so please ensure that it’s a valid one by looking at your DB.

Of course in reality you would rely on CakePHP’s auth or some other log-in method, but that would be beyond the scope of this article.

Lastly let’s build our edit form:

echo $form->create('Company', array('controller'=>'companies',

echo $form->input('');
echo $form->input('Company.description');
echo $form->input('Company.location');

echo $form->input('Account.'.$session->read('AccountId').'.name', array('label'=>'Account name'));
echo $form->input('Account.'.$session->read('AccountId').'.username');
echo $form->input('Account.'.$session->read('AccountId').'.email');

echo $form->input ('');
echo $form->input('Account.'.$session->read('AccountId').'.id');

echo $form->end('Edit');


Update: In CakePHP 1.2 RC3+ you can name your edit form fields with any key (not necessarily the model ID).

Although using model ID for the key name still works, it is no longer necessary to correctly display the validation errors. The code above and text below refers to the older way of doing things, but it should still give you an idea of how to approach the edit form.

There are a couple of things to point out here, first you’ll notice that in this form I’ve named the Account model fields by using $session->read(‘AccountId’) as the array key. This is needed in order to properly display the validation errors.
If you are unsure about what I mean pr($this->validationErrors) in your view and pay attention to the value of the key, which holds the errors for the Account model. You’ll notice that it will be matching the id of the model we are currently editing.

If you think back to our add() action, the Account model had no data (and therefore no id) and we were just creating one and only account, therefore Account.0.fieldName would work just fine.

In the case of edit() we already know the id of the Account model, therefore the field names must match the actual id from the database. Of course, we know that in this example it was 2, but we do have to make our form dynamic, so by using $session->read(‘AccountId’) we ensure that our fields will always be named correctly regardless of whether the actual value of the id is 2 or 76.

Next thing to notice is that we’ve added two inputs, which hold the id’s of our models. This is needed to ensure that an UPDATE query is triggered, rather than an INSERT. And yes, cake is smart enough to make those inputs as hidden fields.

Well, if you’ve managed to follow all along, congratulations we are done. I hope that now you can see how saveAll() can make your life so much easier with so little effort.