Revisiting saveAll() and HABTM

During my months of winter hibernation, or maybe due to some oversight in prior review of the test cases… I’ve missed a very important and a great feature, which is that saveAll() can also work with HABTM data.

Let’s see a quick example based on the provided test cases.

We have the following models:

Tag HABTM Article
Article HABTM Tag
Article hasMany Comment
… and both Article and Comment belongsTo User

First, we’ll build a form (view add.ctp), so we can store an Article with some Tags as well as relevant Comment, all belonging to some User. All in one go with the lovely saveAll()

<?php
    echo $form->create();
    
    echo $form->input('Article.title');
    echo $form->input('Article.body');
    echo $form->hidden('Article.user_id', array('value' => $session->read('User.id')));
    
    echo $form->input('Tag', array('options'=>array(15,20,30), 'multiple'=>'checkbox'));
    
    echo $form->input('Comment.comment');
    echo $form->hidden('Comment.user_id', array('value' => $session->read('User.id')));
    
    echo $form->end('Add Aricle with Tags and Comment');

?>

Pretty simple form. I am reading User.id from some variable that was previously stored in the session (you could actually modify the data array in the controller before saving, rather than passing it as a hidden field). Also, I just created a “fake” array of Tag id’s, in 'options'=>array(15,20,30)… in real life the values would likely come from $this->Article->Tag->find('list');

Once the form is done, the controller action to save all of this data in one shot, couldn’t be simpler:

function add() {
    if(!empty($this->data)) {
       $this->Article->saveAll($this->data);
    }
} 

Just for fun here’s the resulting SQL:

START TRANSACTION		
INSERT INTO `articles` (`body`, `user_id`, `updated`, `created`) VALUES ('This is a great article with tags and comments', 5, '2009-03-30 11:54:54', '2009-03-30 11:54:54')	
SELECT LAST_INSERT_ID() AS insertID	
SELECT `ArticlesTag`.`tag_id` FROM `articles_tags` AS `ArticlesTag` WHERE `ArticlesTag`.`article_id` = 6 	
INSERT INTO `articles_tags` (`article_id`,`tag_id`) VALUES (6,'0'), (6,'1'), (6,'2')		
INSERT INTO `comments` (`comment`, `user_id`) VALUES ('NO comment', 6)	
SELECT LAST_INSERT_ID() AS insertID		
COMMIT

Pretty powerful stuff, with just a couple of lines of code.

P.S. My previous post, which mentioned the problems with saveAll() and HABTM had been corrected.

Paginate associated model’s data in CakePHP

This is a pretty simple tip, but I’ve noticed some beginners are having a problem with this scenario.

We are in the Users Controller and need to paginate all Comments for a given User (i.e. User.id = 5), well and probably display the User info while we are at it…
For this example we keep in mind that User hasMany Comment and Comment belongsTo User.

First, we prepare our controller to handle Comment pagination:

class UsersController extends AppController
{
    var $name = 'Users';
    
    var $paginate = array('Comment'=>array('limit'=>5));
}

Yep, even though we are in the Users Controller, we will be paginating the Comment model. (Obviously you can add other options, but we’ll just add a simple ‘limit’ for this example).

So what’s left to do?
Tell our paginate() method that we’ll be paginating the Comment model and pass appropriate User.id in the array of conditions:

function index() {        
   $this->set('comments', $this->paginate($this->User->Comment, array('User.id'=>5)));        
}

Remember, that because Comment belongsTo User, cake will build a JOIN and pass the correct condition to retrieve all of the relevant comments for this User.

… and for a little extra bonus let’s throw in a simple view:

<?php echo $paginator->numbers(); ?>

<?php foreach($comments as $comment): ?>

<div>
    <?php echo $comment&#91;'Comment'&#93;&#91;'comment'&#93;; ?>
</div>

<?php endforeach; ?>

saveAll() with multiple records AND for multiple models

This question kept coming up on IRC over the last few days, so I’ve decided to give this a shot as well…

Since I didn’t find anything specific in the test cases, I had to rely on some trickery (so, if someone has a more elegant approach, please share).

Let’s say we have User hasMany Comment, we’d like to store a couple of new users and for each one add a comment.

First let’s build our form (view):


<?php
    echo $form->create();

    echo $form->input('1.User.name');
    echo $form->input('1.Comment.0.comment');
    
    echo $form->input('2.User.name');    
    echo $form->input('2.Comment.0.comment'); 
       
    echo $form->end('Save');

?>

Notice a slightly unusual naming of our fields.
Starting the field name with a numeric key index will help us to get the data array that will ultimately look something like below, when it arrives from our form to the controller (remember that you should not start a field name with a zero, especially when including the Model name… *see below for a quick explanation):

Array
(
    [1] => Array
        (
            [User] => Array
                (
                    [name] => bob
                )

            [Comment] => Array
                (
                    [0] => Array
                        (
                            [comment] => nice guy
                        )

                )

        )

    [2] => Array
        (
            [User] => Array
                (
                    [name] => dave
                )

            [Comment] => Array
                (
                    [0] => Array
                        (
                            [comment] => pain in the ass
                        )

                )

        )

)

I hope you see where I’m headed with this now, if not … well, here’s our add() action in the controller:

function add() {               
        if(!empty($this->data)) {
            foreach($this->data as $data) {
           	    $this->User->saveAll($data);
            }            
        }
    }

A little dirty? Yes…
But it wraps each model and associated model into a nice transaction and saves multiple records and models in a relatively simple manner.

—————
* So why can’t (or shouldn’t) we start our input with a zero?

Let’s try:

<?php echo $form->input('0.User.name'); ?>

The result:

<input name="data&#91;User&#93;&#91;User&#93;&#91;name&#93;" type="text" maxlength="100" value="" id="UserUserName" />

Definitely not what was intended and likely an undesired effect for majority of cases.

Run multiple CakePHP apps side-by-side

… Well then you’ve stumbled onto the right place.

In this quick, introductory how-to, I’ll show you my approach for using a rather simple setup to run as many CakePHP apps as you’d like on the same development server.
The directions provided below are for windoze (yeah, yeah… I know), but I’m sure you’ll figure out how to make the same approach work for your favorite OS.

Step 1 – Get your Apache/PHP/MySQL running

I prefer to use XAMPP to get my development environment up and running. To keep things simple (and to deal with some Microsoft crap), I like to install it into C:\xampp.

With a quick download and a couple of installer clicks, you should be good to go within a few minutes.

Step 2 – Grab the CakePHP core

Since we are talking about a development setup, I recommend you get the nightly (or HEAD revision) from SVN. If you don’t have an SVN setup, you can just download the nightly zip from cakephp.org.
Again, to keep it simple I install the CakePHP core in C:\cake\cake. The second “cake” dir is the actual location of where all of your core files/libs are located. (All of your apps are going to live under C:\cake, we’ll see that in a minute).

Step 3 – Get the first app prepared

Now that we have a web server a database and the CakePHP core installed, we are ready to get our first app up and running.
It’s a good idea to get the skeleton (or empty) app from cakephp.org as well, since every so often there are subtle changes made to the app files to keep up with the latest and the greatest. I like to setup an SVN checkout to always get the latest “app” and then copy it over to the new location, that is, whenever I’m starting the work on a new application…
So, with that little preface, let’s assume you’ve got the skeleton app downloaded (or checked out from SVN) and you’ve placed it (as mentioned in Step 2) in C:\cake\app.

Step 4 – Add an entry to your hosts file

Ultimately we’d like to be able to access our app by going to http://app/, for this we need to add an entry to the “hosts” file. Please take a second to google where you can find this little file, which will certainly depend on what OS you are using.
That being said, all we need to do is add a new line, just like so:
127.0.0.1 app

Step 5 – Setup a new virtual host for the app

Last, but not least, we’ll use Apache’s virtual hosts for each one of our apps. By following this rather simple method we can get as many apps as our dev server can handle, all up and running without much pain.
Assuming that the location of our XAMPP install is as described above, the httpd-vhosts file will be found in C:\xampp\apache\conf\extra.

Let’s add the following to entry to this file:


NameVirtualHost 127.0.0.1

<VirtualHost 127.0.0.1>
    DocumentRoot C:\cake\app\webroot
    ServerName app

    <Directory "C:\cake\app\webroot">
    	Options Indexes FollowSymLinks Includes ExecCGI
	AllowOverride All
	Order allow,deny
	Allow from all
    </Directory>
</VirtualHost>

Let me quickly explain what’s going on here (although, I hope it is a little obvious)…

  1. The document root is where our images, javascript, css and other web accessible files are located. C:\cake\app\webroot is a standard cake app location, so we went ahead and specified it in our vhosts file.
  2. We gave our server a “very creative” name: “app”. This is also needed so we can go ahead and access the application by going to http://app/
  3. We’ve used the Apache’s “Directory” directive to specify some options for this app, namely AllowOverride All

And that’s about it… let’s restart (or start) the web server and if all went according to plan you should now be able to go to: http://app/ and observe CakePHP’s default homepage.

Step 6 – Getting additional apps up and running

So we’ve gone through all this trouble to get the first app running, but the good news is that setting up additional apps now should be a … piece of cake (pun intended).

Let’s say we are now ready to get a new app setup called “mousetrap”.
Just repeat steps 3 – 5, and you’ll be ready to rock.
Let’s sum things up really quickly:

  1. A new skeleton (empty) app will be placed in C:\cake\mousetrap
  2. We’ll add a new entry to our hosts file: 127.0.0.1 mousetrap
  3. Copy/paste our existing entry for “app” in the vhosts file and modify the relevant paths to C:\cake\mousetrap (Note, NameVirtualHost is only needed once in the file, so don’t copy that part)
  4. Let’s not forget to change the ServerName to “mousetrap”
  5. Restart the web server
  6. Go to http://mousetrap and see the new app ready to go

And that’s the end of this story… Hopefully the approach that I’m using for my dev environment makes sense and serves as good reference for CakePHP beginners and those that are starting to deal with multi-app setups.

Blacklist your model fields for save()…

You’ve probably heard that in order to make your save() more secure, you can pass-in a third parameter of only those fields that you’d like to save (all others will be ignored).

However, in some cases it would make more sense to “blacklist” one or two fields, rather than “whitelist” a whole bunch of required fields.

An awesome tip from Nate, shows just how to accomplish this…


$blackList = array('protected', 'fields', 'here');
$this->Model->save($this->data, true, array_diff(array_keys($this-> Model->schema()), $blackList);

Quite clever indeed.