The widely unused and powerful setAction()

It has been quite a while since I last developed in Cake, but earlier today I spent a good 5+ hours working on some new controller internals for one of my apps. While working on this app, I thought I would give the not so used method, setAction() a spin. If you are unfamiliar with setAction(), it's a method that will internally forward one action to another. This is incredibly useful as it allows you to switch actions without having to do another HTTP request, nor having to reload all the controller and related objects; could be a huge saver on overhead.

Here's a quick example of a regular action:

public function profile($id) {
	$user = $this->User->findById($id);
	if (empty($user)) {
		$this->redirect(array('action' => 'listing'));
	}
	$this->set('user', $user);
}

What the action is doing is relatively simple. First it grabs a user based on an ID, checks to see if the result is empty, if so redirects to the users listing page, else sets the data. You could either redirect the action like I have, or you can simply throw an error message in the view saying something like "No user was found with that ID". I personally prefer redirecting in most cases, but it's a matter of preference. Lets move forward and try setAction() now.

public function profile($id) {
	$user = $this->User->findById($id);
	if (empty($user)) {
		return $this->setAction('listing');
	}
	$this->set('user', $user);
}

As you can see, I simply switched the redirect() with setAction(). Now if we visit a profile with an non-existent user, the listing action will be rendered without having to do another HTTP request. Also, do notice the use of return; this is extremely important because if you do not use return, all the logic after the setAction() will be ran and processed, causing unneeded overhead and unexpected results.

One downside that I have ran into with this approach is that the URL in the address bar does not change. But 99% of the time it won't matter as the page you render in its place will contain links to the correct pages or the user won't be copy and pasting the URL anyways.

AJAX calls in CakePHP: The JSON Response

The controller and HTTP response are now working correctly, so it is time to display the response message to the client. We will achieve this by utilizing jQuery to output our JSON. The plan is to show a success message if the JSON passed or display a list of errors if it has failed. To display the messages we need to create our HTML elements that will hold the responses.

<div id="responseSuccess" class="responseBox" style="display: none"></div>
<ul id="responseError" class="responseBox" style="display: none"></ul>

Before we can write our Javascript to handle the JSON, we need to first inspect the JSON response. Our JSON "data" message can come in 3 possible formats: a simple string, an array containing the error messages, or an object containing the error messages / post data with their key values. Below are a couple examples of how our JSON response could be formatted:

// Single string error
{"success":false,"data":"No username/password","code":-1}
// An array containing the errors
{"success":false,"data":["No username","No password"],"code":-1}
// Errors / Post data and their key
{"success":false,"data":{"username":"No username","password":"No password"},"code":-1}
{"success":true,"data":{"username":"Miles","password":"p4ssw0rd"}}

Now this poses a significant problem. If our response data can be sent as 3 possible formats, we need to write our Javascript to be able to handle all possible variations. We can do this by creating a loop for the arrays/objects and store the values in a custom array, and for the single string just return the message. The following code block can do just that, and all we need to do is place it within the "success" parameter of our AJAX call.

function login() {
	var data = $("#UserAddForm").serialize();
	$.ajax({
		type: "post",
		url: "/ajax/login/",
		data: data,
		dataType: "json",
		success: function(response, status) {
			// Response was a success
			if (response.success) {
				$("#responseSuccess").html(response.data).slideDown();
			// Response contains errors
			} else {
				var errors = [];
				if (typeof(response.data) == ("object" || "array")) {
					$.each(response.data, function(key, value) {
						var text = isNaN(key) ? key + ": " + value : value;
						errors.push("<li>"+ text +"</li>");
					});
				} else {
					errors.push("<li>"+ response.data +"</li>");
				}
				errors = errors.join("\n");
				$("#responseError").html(errors).slideDown();
			}
		}
	});
	return false;
}

Pretty simple right? We can improve on this code by adding a setTimeout() that will remove the response box after 5 seconds. Additionally we can add the AJAX error argument that is called if the AJAX mechanism fails as a whole.

// Remove box after 5 seconds
setTimeout(function() {
	$(".responseBox").slideUp();
}, 5000);
error: function(XMLHttpRequest, textStatus, errorThrown) {
	$("#responseError").html("<li>An unexpected error has occurred.</li>").slideDown();
}

Our Javascript is now complete, but we could improve on it yet again. Say you have multiple AJAX calls that all utilize the same type of callback mechanism. It would be a major pain and waste of time to write all that code over and over again. What we can do is separate the code into 3 separate functions, all handling certain aspects of the logic. Our final code may look something like the following.

/**
 * Fire an AJAX call to login the user
 */
function login() {
	var data = $("#UserAddForm").serialize();
	$.ajax({
		type: "post",
		url: "/ajax/login/",
		data: data,
		dataType: "json",
		success: function(response, status) {
			handleCallback(response, status);
		},
		error: function(XMLHttpRequest, textStatus, errorThrown) {
			handleError(XMLHttpRequest, textStatus, errorThrown);
		}
	});
	return false;
}
/**
 * Handle the AJAX callbacks
 */
function handleCallback(response, status) {
	// Response was a success
	if (response.success) {
		$("#responseSuccess").html(response.data).slideDown();
	// Response contains errors
	} else {
		var errors = [];
		if (typeof(response.data) == ("object" || "array")) {
			$.each(response.data, function(key, value) {
				var text = isNaN(key) ? key + ": " + value : value;
				errors.push("<li>"+ text +"</li>");
			});
		} else {
			errors.push("<li>"+ response.data +"</li>");
		}
		errors = errors.join("\n");
		$("#responseError").html(errors).slideDown();
	}
	// Remove box after 5 seconds
	setTimeout(function() {
		$(".responseBox").slideUp();
	}, 5000);
	return false;
}
/**
 * Handle an AJAX failure
 */
function handleError(XMLHttpRequest, textStatus, errorThrown) {
	$("#responseError").html("<li>An unexpected error has occurred.</li>").slideDown();
}

Now there are multiple ways for your to handle a JSON response, but the approach above is a pretty straight forward implementation of displaying a success or failure message. Be sure that when you write your AJAX requests, that the $.ajax() function has a dataType of json, or your JSON response will completely fail.

AJAX calls in CakePHP: The Controller

Now that you have setup the Javascript and view to properly fire off an AJAX call, it's time to process the call within the Controller. The AJAX call was firing off to AjaxController::login(), so lets begin by creating the action. What we need to do is check to see if the username and password is set (validation), then attempt to login the user by using the AuthComponent. If the login is successful, or even if it fails, we will return a JSON response.

Below would be a basic setup to accomplish what we need to do. However, in your actual application you would apply more strict validation and sanitization, but for the sake of the tutorial I will not.

function login() {
	$this->layout = 'ajax'; // Or $this->RequestHandler->ajaxLayout, Only use for HTML
	$this->autoLayout = false;
	$this->autoRender = false;
	$response = array('success' => false);
	if (!empty($this->data['User']['username']) && !empty($this->data['User']['password'])) {
		if ($this->Auth->login($this->data)) {
			$response['success'] = true;
			$response['data'] = $this->data['User'];
		} else {
			$response['data'] = 'Username/password combo incorrect';
			$response['code'] = 0;
		}
	} else {
		$response['data'] = 'No username/password';
		$response['code'] = -1;
	}
	$this->header('Content-Type: application/json');
	echo json_encode($response);
	return;
}

As an added bonus, I will supply an example of how to achieve the same result above using my AjaxHandlerComponent. The component does all the necessary preparations and output leaving you to write less code.

function login() {
	if ($this->AjaxHandler->valid($this->data['username'], true) && $this->AjaxHandler->valid($this->data['password'], true)) {
		if ($this->Auth->login($this->data)) {
			$this->AjaxHandler->response(true, $this->data);
		} else {
			$this->AjaxHandler->response(false, 'Username/password combo incorrect', 0);
		}
	} else {
		$this->AjaxHandler->response(false, 'No username/password', -1);
	}
	$this->AjaxHandler->respond();
	return;
}

Basically what we need to do, to get a proper JSON response is to build the response array. We create an array with a success variable that would be a true or false. If the success is true that means the core logic that we want completed was a success, else the AJAX logic failed and the user should try again. We also have a data index that would house the error/success message(s), an array of data to use, or anything else you might need. Lastly we have a code index which could be used to differentiate the levels of errors and their importance. Once we have our response array complete, we set the correct headers and respond it back while encoding it with json_encode().

On top of building the response we need to configure the controller a bit. We need to disable the view and layout from rendering or we will receive the "Missing View" errors within Cake, and we also need to remove the layout so that our response is not surrounded by HTML (unless you want to respond with HTML, but we are using JSON). We do this by setting autoLayout and autoRender to false -- furthermore we set the layout to ajax which would be required for HTML responses.

You must be thinking to yourself, "Wow that was easy, we are now done!", but you are wrong! We still have one final step, validating that the request is a legitimate AJAX call. To achieve this we will use the RequestHandler::isAjax() method. Since all of our AJAX calls are housed in the AjaxController, we can place this code in the beforeFilter() so it applies to all actions. If your AJAX actions are within your primary controllers, you will need to add a bit more logic.

I will be using the SecurityComponent in my examples, but you can use die() or something similar in place of blackHole(), like Controller::redirect().

// Must be disabled or AJAX calls fail
$this->Security->validatePost = false;
if (!$this->RequestHandler->isAjax()) {
	$this->Security->blackHole($this, 'You are not authorized to process this request!');
}
// Use the following code in place of blackHole()
// $this->redirect(null, 401, true);

The previous code should blackhole any request that does not contain the XMLHttpRequest header. To further build on this code, we can check to make sure the AJAX request is coming from the current domain, if not then kill it. Our final beforeFilter() should look something like the following.

function beforeFilter() {
	parent::beforeFilter();
	// Must be disabled or AJAX calls fail
	$this->Security->validatePost = false;
	if (!$this->RequestHandler->isAjax()) {
		$this->Security->blackHole($this, 'You are not authorized to process this request!');
	} else {
		if (strpos(env('HTTP_REFERER'), trim(env('HTTP_HOST'), '/')) === false) {
			$this->Security->blackHole($this, 'Invalid referrer detected for this request!');
		}
	}
}

The controller code should now be complete and ready for your amazing AJAX use. We could however improve it a little more by utilizing the RequestHandlerComponent and some of its methods, but ill leave that up to you to adventure into. In the next chapter we will go over the response and how to interact with the JSON.

Using and understanding setFlash()

When I first started using CakePHP, I never used the Session->setFlash() method to display success or error messages. I never used it because I was under the impression that flash messages were automatically redirected if debug is turned off, but this only applies to the controllers flash(). So I started to use and understand setFlash(), and here are my findings.

<?php // Controller action
public function add($id) {
	$pageMessage = '';
	if (!empty($this->data)) {
		if ($this->Post->save($this->data)) {
			$pageMessage = 'Your post has been added!';
			$this->data = '';
		}
	}
	$this->pageTitle = 'Add Post';
	$this->set('pageMessage', $pageMessage);
}
?>

Now there isn't anything entirely wrong with the code above, it simply adds a bit of unnecessary logic. To do it correctly using Cake, you would remove all my instances of $pageMessage and instead use $this->Session->setFlash() (SessionComponent must be active). This method saves the success message to the session, and within the view you would use $session->flash() to output the message.

By default the $session->flash() method will generate a layout for the flash message. You could use your own layout by creating a view in the app/views/layout/ folder. For example, if we created a layout called success.ctp, the code may look something like the following:

<div id="success">
Success!<br />
<?php echo $content_for_layout; ?>
</div>

The $content_for_layout variable will be replaced with the flash message. And now to use this layout you would pass a second parameter with the name of the layout, like so:

$this->Session->setFlash('Your post has been added!', 'success');

You could use this technique and create as many flash layouts as you please, but there are other alternatives. The setFlash() can take a third parameter being an array of options/variables that can be used in the flash view. One of the default options is the class. You could set the class option to "success" and in the auto-generated view it will have the class success; you could then just create a .success class in your CSS.

What I presented is only a small way that you could use setFlash(). You could find more information on the setFlash() by visiting the link below. And with my closing statement, I will also throw up a quick example for using this method.

http://book.cakephp.org/view/400/setFlash

<?php // controllers/posts_controller.php
public function add($id) {
	if (!empty($this->data)) {
		if ($this->Post->save($this->data)) {
			$this->Session->setFlash('Your post has been added!', 'success');
			$this->data = '';
		}
	}
	$this->pageTitle = 'Add Post';
}
// views/posts/add.ctp (somewhere in the code)
$session->flash();
// views/layouts/success.ctp
<div id="success">
	Success!<br />
	<?php echo $content_for_layout; ?>
</div>
?>