Serializeable behavior for CakePHP

For a project i’m working on i needed to serialize a number of arrays before they could be saved and since i don’t like to serialize and un-serialize all those arrays in each controller action, i wrote a behavior to take care of this.

It serializes all array data before it is saved into the database and when you do a find it will unserialize all data(including related model data).

Download serializeable behavior

Filed Under: CakePHP, Code, English - read on

A central hub for cakePHP

Last week i was in the #CakePHP channel on irc.freenode.net when i noticed a discussion about someone who had created a component, but didn’t want it posted on the bakery. He thought that his code wasn’t up to par with everything else on the bakery. So he thought about posting it on his blog.

I can see why he didn’t want to post it on the bakery. You can get a lot of critics there, but those are only there to help improve the thing you posted and can help you to write better code.

It got me thinking how scattered the information about Cake really is. There is, of course, the bakery, but there are also the saved bookmarks on del.icio.us, the custom search engine, the superfeed and a gazillion blog posts about cake. All containing really good components, helpers and tutorials.

But what i really want is a central hub for all of these great contributions to Cake. Right now i’m feeling that i only get to see a part of all that’s good about cake and still miss out on a lot of good stuff. Shure if i need something i google it first before i create it myself, but there must be a better way to stay up-to-date about all that is cake.

The bakery is a good start, but there are a lot of people who don’t want to post their stuff there, because they think it isn’t good enough and there is a list on the CakePHP home page, but it only lists a few blogs and there are so much more.

I think it would help the community as a whole and give more attention to all the great CakePHP stuff out there.

What do you think? Does CakePHP need a central place for all stuff cake, or do you think it’s fine the way it is. Or maybe you have a great idea to get people to post more of their content to the bakery?
Let me know in the comments!

Filed Under: CakePHP, 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

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

SaveAll with CakePHP

SaveAll in Cakephp

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).

Lets make a simple todo list to demonstrate.

First we need a table named tasks.
It has an id, int(11) and a name, varchar(255).

Models

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

<?php
class Task extends AppModel
{
 var $name = 'Task';
}
?>

Controller

Next we set up the 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 Tasks');?></legend>
	<?php
		echo $form->input('Task.1.name');
                echo $form->input('Task.2.name');
                // and so on...
	?>
	</fieldset>
<?php echo $form->end('Submit');?>
</div>

Note the task.1.name, this is to let the model know that there could be more tasks.
And that’s basically it!

Tomorrow i’ll make a post about the other feature of saveAll, saving related models.

Filed Under: CakePHP, Code, English - read on