CakePHP 2.0 change error layout for admin

Most sites must have a separate admin area to the main site and many will be using a different layout for the admin area. CakePHP will render errors using the default template so your admin area can look inconsistent and messy if an error has to be raised.

The solution to this follows. I can’t take claim for coming up with this but I’ve adapted from a variety of sources and updated for CakePHP 2.0.

Firstly you need to add some code to /app/Controller/AppController.php, if you don’t have one of these then copy it over from /lib/Cake/Controller/AppController.php. The beforeRender method will check if there is an error then checks if you are using Admin routing. If that is the case the layout is changed:

class AppController extends Controller {
 
	public $helpers = array('Ulc', 'Html', 'Form', 'Js', 'Session', 'Text');
	public $components = array('Auth', 'Session');
 
	public function beforeFilter() {
		$this->Auth->allow('*');
	}
 
	public function beforeRender() {
		$this->_configureErrorLayout();
	}
 
	public function _configureErrorLayout() {
		if ($this->name == 'CakeError') {
			if ($this->_isAdminMode()) {
				$this->layout = 'admin';
			} else {
				$this->layout = 'default';
			}
		}
	}
 
	public function _isAdminMode() {
		$adminRoute = Configure::read('Routing.prefixes');
		if (isset($this->params['prefix']) && in_array($this->params['prefix'], $adminRoute)) {
			return true;
		}
		return false;
	}
}

Now in production mode (debug = 0 in core.php) CakePHP will tend to use 2 different error views so it may be worth creating your own customised views.
Copy error400.ctp and error500.ctp from lib/Cake/Views/Errors into app/Views/Errors.
If there is anything you want to display differently in the admin version you can use:

if ($this->layout == 'admin') {}

to detect that you are in the admin area and then hide unnecessary markup that may need to be shown to regular site visitors.

With help from: http://nuts-and-bolts-of-cakephp.com/2009/04/30/give-all-of-your-error-messages-a-different-layout/ and http://bin.cakephp.org/saved/50047

CakePHP 2.0 paginate on related model

Just a quick note on this one as it took a while to discover how to it.

Say you have a relationship between two tables and you want to be able to use the built in pagination helper to generate sort links on the content in the related table.
One way of doing this is to add virtual fields to bring the related column(s) into the Model.

In your controller:

$this->Post->virtualFields['user_name'] = 'User.name';

then in your view you can generate a sort link for this field with:

echo $this->Paginator->sort('user_name');

CakePHP validation on a Search Form

I wanted to validate the fields on a search form (e.g. date, price) like the add/edit forms work when you bake or use the scaffolding.

Why is this a problem.

CakePHP has great built in validation but it is tied into saving data.
If you have a search form on your website, nothing needs to be saved and there probably isn’t even a suitable Model anyway.
Another issue is that I wanted it to work where my index view contains the search form and this posts to the results action. The results action processes the search and the view displays the search results. If there’s a validation error you need to go back to the index view.

I did consider just using jQuery to validate the form contents before it went up to the server – but I’m sure you all know that it is poor practice to just use client side validation.

To validate like the add or edit scaffolding the view needs to submit to its own action just like they do.
However that would mean the search results would also be displayed within the same view.

  1. I could run the query and then render a different view (but then the URL won’t change).
  2. I could store the search params in a session and redirect to the results view, then retrieve the search params, run the search and display it.

I began to worry how well pagination would work with either of these options.

I decided to try this with the way I wanted it to work – the search view submitting to a different action which displays the results, then if invalid I just needed a way of displaying the search form again and highlighting the errors.

The trick is to store validation messages and posted form data in Sessions and redirect back to the search form.

First I considered reading the Session in the view and injecting into CakePHP variables.
The code below shows how to add validation information within a view:

<?php
$this->validationErrors['Category']['name'][0] = 'Validation message text here';
$this->validationErrors['Category']['name'][0] = 'Price should be < 100';
$this->request->data['Category']['name'] = 'Form field data in here';
$this->request->data['Category']['price'] = 123;
?>

In practice it is easier to inject those messages in the Controller.
Many thanks for this blog article which showed me how to do it:
http://www.jamesfairhurst.co.uk/posts/view/validating_cakephp_data_from_another_model/

In the controller action which receives form input you do the following:

$post = $this->request->data['Category'];
if (empty($post['search_text'])) {
	$this->Category->validationErrors['search_text'][0] = 'You must provide search text.';
}
if (empty($post['price'])) {
	$this->Category->validationErrors['resort_id'][0] = 'You must provide a price.';
}
 
if (count($this->Category->validationErrors) > 0) {
	$this->Session->write('Category', $this->data);
	$this->Session->write('CategoryErrors', $this->Category->validationErrors);
	$this->redirect(array('action' => 'index'));
}

This checks for errors, stores the errors and data in sessions and redirects back to the search form.

And in the controller action which displays the form:

//see if there were any validation problems
if ($this->Session->check('Category')) {   
	// get data the user posted   
	$chalet = $this->Session->read('Category');   
	// get the errors   
	$errors = $this->Session->read('CategoryErrors');   
	// set their data for view   
	$this->request->data['Category'] = $chalet['Category'];   
	// set validation errors for view   
	$this-> Category->validationErrors = $errors;   
	// delete the session data   
	$this->Session->delete('Category');   
	$this->Session->delete('CategoryErrors');   
}

This breaks CakePHP’s principles of Fat Models and Skinny Controllers (maybe I could move some of this code into Model functions) but I’d be very interested to see what other ways there are of doing this.

Dynamic select box with CakePHP 2.0

I wanted some auto populating Select boxes in a site I was creating so that when I changed a Category the Subcategories would automatically update. CakePHP can do this pretty easily but it is let down by the documentation as there are no examples.

Initially I came up with a version that wrote JSON into a JavaScript variable in the page and then used jQuery to achieve the updating of select elements but I wanted to do this the Cake way which uses AJAX and as little code as possible.

Here is a simplified solution to demonstrate how it can be done.

This assumes a new CakePHP site already configured with a database connection. I used CakePHP 2.0.3 but this may also work with 1.3 (?)

1. Models

We need 3 associated tables

CREATE TABLE `categories` (
  `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(60) DEFAULT NULL,
  PRIMARY KEY (`id`)
);
 
INSERT INTO `categories` (`id`, `name`)
VALUES
	(1,'books'),
	(2,'music'),
	(3,'electronics');
 
CREATE TABLE `posts` (
  `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  `title` VARCHAR(50) DEFAULT NULL,
  `subcategory_id` INT(10) UNSIGNED DEFAULT NULL,
  PRIMARY KEY (`id`)
);
 
INSERT INTO `posts` (`id`, `title`, `subcategory_id`)
VALUES
	(1,'The title',1),
	(2,'A title once again',4),
	(3,'Title strikes back',7);
 
CREATE TABLE `subcategories` (
  `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  `category_id` INT(10) UNSIGNED DEFAULT NULL,
  `name` VARCHAR(60) DEFAULT NULL,
  PRIMARY KEY (`id`)
);
 
INSERT INTO `subcategories` (`id`, `category_id`, `name`)
VALUES
	(1,1,'fiction'),
	(2,1,'biography'),
	(3,1,'children'),
	(4,2,'classical'),
	(5,2,'rock'),
	(6,2,'jazz'),
	(7,3,'camera'),
	(8,3,'audio'),
	(9,3,'tv');

Bake the 3 Models for these and allow CakePHP to define model associations for you.

2. Controllers

Bake a Controller for the Post Model with the default CRUD actions.

While you are at it you will also need to bake the CRUD Views for the Post Controller.

If you browse to the posts index page now you can view the data.
In this example I will add the Category and Subcategory select lists to the Add view, so now we need to change this so that the selection in the second list changes according to the selection in the first list.
This will be done via the Js Helper so make it available at the top of the Posts controller (after the Class declaration) with:

public $helpers = array('Js');

You need a Subcategories Controller with a single action to provide the data via AJAX:

<?php
App::uses('AppController', 'Controller');
 
class SubcategoriesController extends AppController {
 
	public function getByCategory() {
		$category_id = $this->request->data['Post']['category_id'];
 
		$subcategories = $this->Subcategory->find('list', array(
			'conditions' => array('Subcategory.category_id' => $category_id),
			'recursive' => -1
			));
 
		$this->set('subcategories',$subcategories);
		$this->layout = 'ajax';
	}
}

3. Views

The view is a very simple AJAX view that renders the option tags that go within the select tag.

<!-- file path View/Subcategories/get_by_category.ctp -->
<?php foreach ($subcategories as $key => $value): ?>
<option value="<?php echo $key; ?>"><?php echo $value; ?></option>
<?php endforeach; ?>

4. Putting it all together

In the Post Add view, file path: View/Posts/add.ctp we can finally add the Js methods that make the dynamic updating happen. This is the cryptic bit that I struggled with for a few hours as although the CakePHP documentation outlines all the options there are not any complete examples.
Firstly add a categories select element to the form (and change the order of the elements):

<?php
	echo $this->Form->input('category_id');
	echo $this->Form->input('subcategory_id');
	echo $this->Form->input('title');
?>

Add the following code to the bottom of the view, this uses the Js Helper to create the necessary jQuery to perform the updating:

<?php
$this->Js->get('#PostCategoryId')->event('change', 
	$this->Js->request(array(
		'controller'=>'subcategories',
		'action'=>'getByCategory'
		), array(
		'update'=>'#PostSubcategoryId',
		'async' => true,
		'method' => 'post',
		'dataExpression'=>true,
		'data'=> $this->Js->serializeForm(array(
			'isForm' => true,
			'inline' => true
			))
		))
	);
?>

This is saying – watch the HTML element with Id PostCategoryId for a change. When it changes update the HTML element with Id PostSubcategoryId with the response from subcategories/GetByCategory, the data option is used to send the current value from the initial select element.

Before you leap to test this you need to make changes to the default layout to include jQuery and provide a place for your scripts to be written out.

<?php
// file path View/Layouts/default.ctp
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
	<?php echo $this->Html->charset(); ?>
	<title>
		<?php echo $title_for_layout; ?>
	</title>
	<?php
		echo $this->Html->meta('icon');
 
		echo $this->Html->css('cake.generic');
	?>
</head>
<body>
	<div id="container">
		<div id="header">
			<h1>Dynamic Select Box Demonstration</h1>
		</div>
		<div id="content">
 
			<?php echo $this->Session->flash(); ?>
 
			<?php echo $content_for_layout; ?>
 
		</div>
		<div id="footer">
			footer
		</div>
	</div>
	<?php echo $this->element('sql_dump'); ?>
	<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
	<!-- scripts_for_layout -->
	<?php echo $scripts_for_layout; ?>
	<!-- Js writeBuffer -->
	<?php
	if (class_exists('JsHelper') && method_exists($this->Js, 'writeBuffer')) echo $this->Js->writeBuffer();
	// Writes cached scripts
	?>
</body>
</html>

Now you can test the application. On changing the category select list, the subcategory one will automatically update to show the relevant options.

CakePHP 2.0 AJAX sortable list that updates to databases

It is easy to create an AJAX sortable list with CakePHP 2.0, using jQuery that automatically updates the database, however it is hard to find documentation explaining how to do it.

Here is my explanation – it assumes a basic knowledge of CakePHP.

Step 1: Data

Lets use a simple categories table which includes a column for sort_order. This will be used to define the order the records are displayed on the page.

CREATE TABLE `categories` (
  `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(60) DEFAULT NULL,
  `sort_order` INT(10) UNSIGNED DEFAULT NULL,
  PRIMARY KEY (`id`)
);

Step 2: CakePHP

Bake a model, controller and views for this table so that we have some files to work with.
You should be able to go onto your website at http://yoururl/categories and see the default index view.

Layout

In order to use the sortable feature, you will need to edit the default layout. If you don’t already have your own layout you can copy the CakePHP default one (which is used if your own one isn’t found) from /lib/Cake/View/Layouts/default.ctp and place it in app/View/Layouts/default.ctp.
Edit this so that the writeBuffer method is one of the final lines, just before the close body tag:

<?php echo $this->Js->writeBuffer();?>
</body>
</html>

This will create the HTML and JavaScript generated by the Js helper in the view.

Controller

At the top of the CategoriesController make the Js helper available to the controller.

public $helpers = array('Js');

I am going to add 2 actions, one to display the sorted categories and one to handle the AJAX requests to update the database.
The first action is called sort, all this does is retrieve all the categories sorted by the sort_order column and send them off to the view.

public function sort() {
	$this->set('categories', $this->Category->find('all',array(
		'order' => 'sort_order ASC',
		)
	));
}

The second action is called reorder.
This will receive the AJAX post and update the categories table. I’ve commented out a line that can be used for debugging. If uncommented this will write to app/tmp/logs/error.log and is a really good way to see what is happening when you are dealing with AJAX requests.

public function reorder() {
	foreach ($this->data['Category'] as $key => $value) {
		$this->Category->id = $value;
		$this->Category->saveField("sort_order",$key + 1);
	}
	//$this->log(print_r($this->data,true));
	exit();
}

The code here is just looping through the posted data which will look like this:

Array
(
    [Category] => Array
        (
            [0] => 4
            [1] => 5
            [2] => 3
            [3] => 6
            [4] => 2
            [5] => 1
        )
)

The index is the order of elements after they have been sorted and the value is the id of each element. When saving the data I am adding 1 to the index as the one coming from jQuery is zero based.

View

For the sake of simplicity I’ve included the links to the jQuery and jQuery UI libraries here but in practice you would probably want to put them in the head of your layout.
In the view I have a simple unordered list tag containing the data.
The id of each element is important as this is used by jQuery UI to format the data sent back to the server.

<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/jquery-ui.min.js"></script>
<ul id="my-list">
	<?php foreach ($categories as $category): ?>
	<li id='Category_<?php echo $category['Category']['id']?>'><?php echo $category['Category']['name']; ?></li>
	<?php endforeach; ?>
</ul>
<?php
$this->Js->get('#my-list');
$this->Js->sortable(array(
	'complete' => '$.post("/categories/reorder", $("#my-list").sortable("serialize"))',
	));
?>

And that is all there is to it.
You can simply drag and drop any item in the list and behind the scenes the database will be updated with the new sort order.