SaveAll for CakePHP (part 3)

Recently i needed to save multiple entries for the same model and i thought saveAll should do the trick.
After trying some different approaches i found the right one. This approach can be used for both new entries and to update entries.

Controller

(/app/controller/tasks_controller.php)

New entry:

function add() {
	if (!empty($this->data)) {
		$this->Task->create();
		if ($this->Task->saveAll($this->data)) {
			$this->Session->setFlash('Tasks saved');
			$this->redirect(array('action'=>'index'), null, true);
		} else {
			$this->Session->setFlash('Tasks could not be saved');
			$this->redirect(array('action'=>'index'), null, true);
		}
	}		
}

Edit entry:

function edit($todo_id=null){
	if (!empty($this->data)) {
		if ($this->Task->saveAll($this->data['Task'])) {
			$this->Session->setFlash('Tasks saved');
			$this->redirect(array('action'=>'index'), null, true);
		} else {
			$this->Session->setFlash('Tasks could not be saved');
			$this->redirect(array('action'=>'index'), null, true);
		}
	} else {
		// Find ten tasks to edit
		$tasks = $this->Task->findAll(null, null, null, 10);
		$this->set('tasks', $tasks);
	}
}

View

For a new entry:

<?php echo $form->create('Task');?>
	<fieldset>
 		<legend><?php __('New Tasks');?></legend>
	<?php
		// Lets generate 10 task input fields
		for(i=0;i<10;i++){
			echo $form->input($i.'.name');
		}
	?>
	</fieldset>
<?php echo $form->end('Submit');?>

To edit entries:

<?php echo $form->create('Task', array('url'=>array('action'=>'edit')));?>
	<fieldset>
 		<legend><?php __('Edit Tasks');?></legend>
	<?php
	
		// Loop trough the ten tasks and create form fields. We need at least the ID to update a task.
		$count = 0;
		foreach($tasks as $task){	
		echo $form->input($count.'.id', array('value'=>$task['Task']['id']));
		echo $form->input($count.'.name', array('value'=>$task['Task']['name']));
		$count++;
	}
	
	?>
	</fieldset>
<?php echo $form->end('Submit');?>

How it works

It’s not much different from the previous saveAll parts, the only really big change is that the form field names are a bit different and you need to specify the right model/array to save in the controller ( the $this->Task->saveAll($this->data['Task']); part).

Filed Under: CakePHP, Code, English - read on

Validate single fields trough AJAX with CakePHP

Today i was busy making a form for a signup procedure and i tought: “Lets make it a bit easier for the customer by adding some real-time validation”. Of course this is a piece of cake with.. Cake!

Enabling JavaScript

First we need to load the script.aculo.us libaries.
Download them from script.aculo.us, extract the files and copy prototype.js in /lib to your cake js folder (app/webroot/js) and all the files in /bin to the cake js folder.

Then add the following to your default.ctp (app/views/layouts/default.ctp (if it’s not there create one))

<?php
if(isset($javascript)) 
{ 
    echo $javascript->link('prototype'); 
    echo $javascript->link('scriptaculous.js'); 
} 
?>

And in your controller (app/controllers/customers_controller.php) load JavaScript (note: you could also put this in app_controller to make it available in every view instad of putting it in every single controller).

var $helpers = array('Html', 'Javascript', 'Form', 'Ajax');

Enabling RequestHanlder

Now we need to make shure we serve the right content with the right request (return xml if it’s requested).
Add the following to your routes file (app/config/routes.php)

Router::parseExtensions();

Now open your controller (app/controllers/customers_controller.php) and add the following line:

var $components = array('RequestHandler');

We can now use the power of requesthandler \0/

Model

Lets make some basic validation rules: (app/models/customer.php)

var $validate = array(
 		'initials' 	=>	array('length' => array('rule' => array('minLength', '1'), 'message'=>'This field cannot be empty')),
		'lastname' 	=>	array('length' => array('rule' => array('minLength', '3'), 'message'=>'This field cannot be empty'))
	);

Now lets make some actions!

Controller

The add action in the controller (app/controllers/customers_controller.php)

function add() {
	// Ajax validation of single fields, check if xml is asked
	if ($this->RequestHandler->ext =='xml') {
		// If we have data, process it. If not send back an error.
		if(!empty($this->data['Customer'])){
			$this->cleanUpFields();
			// Validate the customer, if it's ok, show no errors. If not ok, show errors
			if ($this->Customer->create($this->data['Customer']) && $this->Customer->validates()) {
                $this->set('error', '0');
				$this->set('message', '');
            } else {
                $errorMessages = $this->validateErrors($this->Customer);
				$this->set('error', '1');
                $this->set('message', array_shift($errorMessages));
            }
		} else {
			$this->set('error', '1');
			$this->set('message', 'No data sent');
		}
	} else {
		// Normal validation and save
		if(!empty($this->data)) {
			$this->cleanUpFields();
			$this->Customer->create($this->data);
			if($this->Customer->save($this->data)) {
				$this->redirect(array('action'=>'thankyou'));
			} else {
				$this->Session->setFlash('Woot, something went wrong, please check the red fields!');
			}			
		}
	}
}

What we did is check if a xml file was requested by the client (this is what we use in the ajax request), if so, check only a single field and return if there is an error or not.

The XML views

In your customers view folder (app/views/customers) create a new folder named xml. In there create a new file called add.ctp.
This is the file the controller uses when it wants to send back a xml view.

<validation>
	<error><?php echo $error; ?></error>
	<message><?php echo $message;?></message>
</validation>

We also need to create a layout for the xml view, in the layouts folder there should be a folder named xml (app/views/layouts/xml)
Open it and create a file called default.ctp with the following content

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

The add view

Now lets make a form (app/views/customers/add.ctp)

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

<fieldset>
<legend>Name</legend>
	<span>
		<?php echo $form->input('initials', array('label'=>'Initials', 'maxlength'=>'8', 'size'=>'8', 'class'=>'req', 'div'=>false)); ?>	
	</span>
	<span>
		<?php echo $form->input('lastname', array('label'=>'Last name', 'size'=>'20', 'class'=>'req', 'div'=>false)); ?>
	</span>
</fieldset>
<?php echo $form->end('Submit'); ?>

This is basically a simple form, but note the ‘class’=>’req’, we are going to use this to let javascript know wich fields to check (this might be handy if you don’t want to validate very single field in the form)

JavaScript

Now the javascript code (place it at the end of app/views/customers/add.ctp)

<script type="text/javascript">
	// Look for all req fields and put them in an array
	requiredFields = document.getElementsByClassName('req');
	
	// Loop the array and place observers on the req fields
     for(i=0; i<requiredFields.length;i++){
		new Form.Element.Observer(requiredFields[i], 2, liveActionCallback);	
	}

	// Callback if something happens in one of the req fields
	function liveActionCallback( element, value ) {
		
		// Funcion for handling the response XML file wich the Ajax.Request below gets for us
		var handlerFunc = function(t) {
	    	var xmlDoc = t.responseXML.documentElement;
			// Check if there is an error
			if(xmlDoc.getElementsByTagName('error')[0].firstChild.nodeValue == '1'){
				// You could do all kinds of stuff here, i'm just adding a classname to the form field wich makes it red
			 	$(element).addClassName('form-error');
			} else {
				// if there is no error, remove the req class 
				$(element).removeClassName('req');
			}
		}
		
		
		// Create a new ajax request, note the .xml added to the url, it makes shure we get an xml back from the controller.
		
		new Ajax.Request('/regwiz/customer.xml', {
					asynchronous:true, 
					evalScripts:true, 
					parameters:Form.Element.serialize(element), 
					onSuccess:handlerFunc
				}
			);
	}
</script>

CSS

As you can see i’m not doing much if there is an error (like not using the error message, but you could of course append a div with the error message, or alert it or… well whatever you want ;)).

To make it complete, here’s the CSS file is used.

/* ----- REQUIRED ----- */

form .req{
	border-right: 2px solid red;
}

form .form-error{
	display:block !important;
	background-color: #FFDFDF !important;
	border:1px dotted red;
}

Example

A few images with the different states: (note labels are in Dutch ;))
pre

The field with the .req class (it creates a red right border, that disappears when the field is valid)

A valid field:
valid field

And an invalid field:
invalid

And the ajax request (from Bugzilla)
Ajax request

And that’s basically it. If you don’t understand everything, please read the comments or try changing stuff :)

Filed Under: CakePHP, Code, English - read on

CakePHP Countercache

As i’ve seen a few questions about the countercache behavior in the irc channel (irc://irc.freenode.net/#cakephp) i’ve decided to make a quick tutorial.

What is countercache

CounterCache is used in Ruby on Rails to count the number of children a model has. Say we have a model called Project and this model hasMany Documents. It would be nice to know the number of Documents for a Project without doing a Count. Well countercache does exactly this.

The database

Lets use this table for projects:

-- 
-- Table structure for table `projects`
-- 

CREATE TABLE `projects` (
  `id` int(11) NOT NULL auto_increment,
  `name` varchar(255) NOT NULL default '',
  `description` text NOT NULL,
  `created` datetime default NULL,
  `updated` datetime default NULL,
  `documents_count` int(11) NOT NULL default '0',
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;

Note the documents_count column in the table Projects, this is where the behavior saves the count of the current number of documents.

The models

Project.php (/app/models/)

<?php
// Project model
class Project extends AppModel
{

 var $name = 'Project';
 var $hasMany = array('Document'=>array('dependent'=>true));
}
?>

and the document:

document.php (/app/models/)

// Docment model
<?php
class Document extends AppModel
{

 var $name = 'Document';
 var $belongsTo = array('Project');
 var $actsAs = array('CounterCache');
}

?>

How it works

Note that the $actsAs is in the child model (in this case documents). This is because the behavior basically does a count on each beforeSave and beforeDelete and stores it in the parent’s model as _count, in this case documents_count. Note that the model name is pluralized, so it becomes documents_count instead of document_count.

Where can i find the behavior?

Since this feature is not implemented yet (note this post was written on december 15, 2007). You can get the behavior at the bakery on: http://bakery.cakephp.org/articles/view/countercache-or-counter_cache-behavior.

When the feature does get implemented, you could just use it like this:

var $belongsTo = array(
                        'Project' =>
                            array('className'   => 'Project',
                                'conditions'        => '',
                                'order'             => '',
                                'foreignKey'        => 'project_id',
                                'counterCache'      => 'projects_count' 
                                )
                        );
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

Sexy CSS Buttons

Alex Griffioen has written a nice tutorial on how to make sexy css buttons.

Filed Under: Snippets - read on