SaveAll with CakePHP (part2)

Since the new beta release for CakePHP there is a nice feature called saveAll and, like the name says, it saves all model data in a form. What this means is that you don’t have to do nasty stuff in the controllers (like looping) and it saves related model data automagically!

Lets make a more advanced todo list to demonstrate.

Tables

Todo hasMany Tasks.

First we need a table named todos.

CREATE TABLE `todos` (
  `id` int(11) NOT NULL auto_increment,
  `name` varchar(255) NOT NULL default '',
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1 ;

We also need a table tasks

CREATE TABLE `tasks` (
  `id` int(11) NOT NULL auto_increment,
  `todo_id` int(11) NOT NULL default '0',
  `name` varchar(255) NOT NULL default '',
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1 ;

Models

First we create the model todo (in /app/models/todo.php)

<?php
class Todo extends AppModel
{
 var $name = 'Todo';
 var $hasMany = array('Task', array('dependent'=>true));
}
?>

Now we create the model task (in /app/models/task.php)

<?php
class Task extends AppModel
{
 var $name = 'Task';
 var $belongsTo = array('Todo');
}
?>

Controller

Next we set up the tasks controller (in /app/controller/tasks_controller.php)

<?php
class TasksController extends AppController {
	var $name = 'Tasks';
	var $paginate = array('limit' => 50, 'page' => 1);	
	
	function index(){
		$this->set('tasks', $this->paginate('Task'));
	}
	
	function add() {
		if (!empty($this->data)) {
			$this->cleanUpFields();
			$this->Task->create();
			if ($this->Task->saveAll($this->data)) {
				$this->Session->setFlash('List saved');
				$this->redirect(array('action'=>'index'), null, true);
			} else {
				$this->Session->setFlash('List could not be saved');
				$this->redirect(array('action'=>'index'), null, true);
			}
		}		
	}

}
?>

You can see that there is nothing different here except for the $this->Task->saveAll($this->data);
That is where most of the magic happens. The other magic is in the views, lets look at add.ctp

Views

(/app/views/tasks/add.ctp)

<div class="todos form">
<?php echo $form->create('Task');?>
	<fieldset>
 		<legend><?php __('Add Todo');?></legend>
	<?php
		echo $form->input('Todo.name');
	?>
	</fieldset>
	<fieldset>
 		<legend><?php __('Add Tasks for todo');?></legend>
	<?php
		echo $form->input('Task.name');
	?>
	</fieldset>
<?php echo $form->end('Submit');?>
</div>

The result

If you try to save a task with a todo you get the following result:

4	INSERT INTO `todos` (`name`) VALUES ('My todo List')
5	SELECT LAST_INSERT_ID() AS insertID
6	INSERT INTO `tasks` (`todo_id`,`name`) VALUES (135,'My first task')

As you can see, the following happens:

  • The todo gets saved
  • The insert id of the todo is retrieved from the database
  • Now the task gets saved with the todo_id that was just retrieved from the database

The result is a successfully saved model Todo with one task attached to it.

Filed Under: CakePHP, Code, English - read on

Paginating child models

Sometimes you want to use pagination on a hasmany child of a model and, of course, CakePHP can handle this.

Lets say we have a model Form that hasMany Topics.
relation graph

Models

First we set up the models (/app/models).

forum.php

<?php
class Forum extends AppModel
{
 var $name = 'Forum';

 var $hasMany = array(
     'Topic' => array(
			'dependent'		=> true,
			'order'			=> 'Topic.created ASC'
     )
 );  
}
?>

and topic.php

<?php
class Topic extends AppModel
{
 var $name = 'Topic';
 var $belongsTo = array('Forum');
}
?>

Controllers

Now if we go to the url /forums/view/1 we want to see all the topics that belong to this forum, but we also want to paginate them.
Lets set up the forums controller (/app/controllers).

forums_controller.php

<?php
class ForumsController extends AppController {
	var $name = 'Forums';
        var $helpers = array('Time' );
	var $paginate = array('limit' => 25, 'page' => 1);

	function view($id = null ) {
		if ($id && is_numeric($id)) {
			$this->set('forum', $this->Forum->findById($id));
			$this->set('topics', $this->paginate('Topic', array('Topic.forum_id'=>$id))); 
		} else {
			$this->Session->setFlash('Invalid action');
			$this->redirect($this->referer());
		}
	}
}
?>

What it does

Lets go trough the code. In the model you specify the relations to other models. A forum hasMany topics and a Topic belongsTo a forum, nothing new so far.
In the controllers there are a few things that stand out.

First we add pagination to the controller and set the number of pages and the items per page.

var $paginate = array('limit' => 25, 'page' => 1);

In the view function we set the value forum to be the result of the findById and then the magic happens.
We just call $this->paginate(’Topic’, conditions), in this case $this->paginate(’Topic’, array(’Topic.forum_id’=>$id));
Because we defined the relation in the model, Cake knows that we can also paginate Topics in the forums controller.

But we can also do a few things more.

What if you would like to order the topics the other way around, only for this action. Instead of changing the hasMany option ‘order’, you could also set it in the controller like this.

forums_controller.php

function view($id = null ) {
	if ($id && is_numeric($id)) {
		$this->set('forum', $this->Forum->findById($id));
		$this->paginate['order']='Topic.created ASC';
		$this->set('topics', $this->paginate('Topic', array('Topic.forum_id'=>$id))); 
	} else {
		$this->Session->setFlash('Invalid action');
		$this->redirect($this->referer());
	}
}

We basically add a new option to the $this->paginate array. Note that this is done in the action function. If you do this in the controller like below you get errors if you want to use pagination for the forums itself
(like in a index action $this->paginate(’Forum’);).

forums_controller.php

var $paginate = array('limit' => 25, 'page' => 1, 'order'=>'Topic.created ASC');

The view

The view is basically like any other view with pagination, but i’ll put it down here anyway ;)

view.ctp (/app/views/forums/)

<h2>Forum</h2>
<p>Welcome to the forum "<?php echo $forum['Forum']['title']; ?>".</p>
<?php if(isset($topics)) { ?>
<?php echo $html->link('Add a topic', array('controller'=>'topics', 'action'=>'add', 'id'=>$forum['Forum']['id']), null, false, false);?>
<table>
	<tr class="tr_header">
		<td class="td_header_topic">Title</td>
		<td class="td_header_long">Created</td>
	</tr>
	<?php foreach ($topics as $topic): ?>
	<tr>
		<td>
			<?php echo $topic['Topic']['title']; ?>
		</td>
		<td>
			<?php echo $time->timeAgoInWords($topic['Topic']['created']); ?>
		</td>
	</tr>
	<?php endforeach; ?>
</table>
	<?php echo $paginator->prev(); ?>
	<?php echo $paginator->numbers(); ?>
	<?php echo $paginator->next(); ?>
<?php }else{ ?>
<p>There are no topics available at the moment, come back soon!</p>
<?php } ?>
Filed Under: CakePHP, Code, English - read on