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:

[sourcecode language=”sql”]
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
)
[/cc]

[sourcecode language=”sql”]
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,
`created` DATETIME NOT NULL
)
[/cc]

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('Company.name', array('label'=>'Company name'));
echo $form->input('Company.description');
echo $form->input('Company.location');

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

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(
              'conditions'=>array(
                'Account.id'=> $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 Account.id 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',
                    'action'=>'edit'));

echo $form->input('Company.name');
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 ('Company.id');
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.

%d bloggers like this: