RSS
 
09. Jan. 2014

AJAX and CakePHP

09 Jan

I don’t know why I didn’t post about AJAX earlier. It is probably one of the topics that most don’t really know how to approach. The docs don’t say much about it, and so many try to rely on (outdated) blog posts out there.

Why AJAX

We shouldn’t use stuff just because it sounds fun. So sometimes AJAX is abused or used where it shouldn’t. I use it mainly where the user cannot or should not reload the whole page just to get more information or an updated piece of content which will be loaded after the page completed rendering (asynchronous). This can be part of a (sub) form, a search result or detailed information on some piece of content in a popover like window.

Basic concept

I always recommend to use the proper extension when dealing with AJAX. So if we deal with basic HTML, we can simply go for /controller/action/. See the AJAX Pagination example on how to use AJAX here as sugar on top of the basic pagination (that then serves as a fallback for non-JS-browsing or search engines). In this case it is fine to serve AJAX extension-less.

In most cases (and in all where fallbacks are not necessary) it is better to use JSON as a return type, though. this allows you to deal with the returned content in a more flexible way. That also means that is is not extension-less anymore now. Thus, you should target /controller/action.json and use the RequestHandler to get all the CakePHP magic going here (setting the correct response header application/json and switching/disabling the layout/views.

This is especially important when throwing exceptions (404 = NotFoundException, 403 = NotAllowedException). All those would then automatically be a proper JSON response message right away.

So for a basic JSON response via AJAX, all you then need is:

/**
 * Ajax action to get favorites
 */
public function favorites() {
    $this->autoRender = false; // We don't render a view in this example
    $this->request->onlyAllow('ajax'); // No direct access via browser URL
 
    $data = array(
        'content' => ...,
        'error' => null,
    );
    return json_encode($data));
}

This will not use a view or layout file and simply return a json encoded string.

But to me that looks kind of wrong. Manually using json_encode() in the controller is not something you should aim for. The view classes are responsible for the actual translation into the target representation format. So such a view class should contain that call. We only make sure that this view class gets the data it needs.

That said you could use the JsonView to make it cleaner and cut down the code by a few lines:

public function favorites() {
    $this->request->onlyAllow('ajax'); // No direct access via browser URL - Note for Cake2.5: allowMethod()
 
    $data = array(
        'content' => ...,
        'error' => null,
    );
    $this->set('_serialize', 'data');
}

Also make sure to check out the JsonView docs on how to use a view (if needed). It will then automatically use a subfolder “json” and its View/ControllerName/json/favorites.ctp template here.

In the view we can then use this action via JS and the URL /controller_name/favorites.json:

$.ajax({
    type: 'get',
    url: '<?php echo $this->Html->url(array('action' => 'favorites', 'ext' => 'json')); ?>',
    beforeSend: function(xhr) {
        xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
    },
    success: function(response) {
        if (response.error) {
            alert(response.error);
            console.log(response.error);
        }
        if (response.content) {
            $('#target').html(response.content);
        }
    },
    error: function(e) {
        alert("An error occurred: " + e.responseText.message);
        console.log(e);
    }
});

Note that we can first check on the error string to abort modification of the DOM if there is some error (similar to validation errors or flash message errors) occuring. And if there is some content we can then easily output that.

AjaxView

Now that is all nice, but for me that is not yet it. In this particular case we don’t just pass down model data. We render the view just as we would for normal GET access. Thus I would like it to be as close to it as possible. I want to avoid the serialize call and manual json_encode part (both in controller and view layer), just as in JsonView, but with all the basic View rendering still intact. So I created the AjaxView.

It takes care of mapping the content into the above JSON structure, and automatically tries to render via “ajax” subfolder (View/ControllerName/ajax/favorites.ctp). It will also not use any layout, just the simple HTML template code in the ctp.

A typical use case:

public function favorites() {
    $this->request->onlyAllow('ajax');
    $this->viewClass = 'Tools.Ajax';
}

The result can be of the following structure:

{
    "content": [Result of our rendered favorites.ctp],
    "error": ''
}

See the test cases for it on more cool stuff – e.g. adding some more data to the response object via _serialize.

Full blown example: Chained dropdowns

This has been one of the examples around the usage of AJAX. When you have two dropdowns and select something from the first, the second dropdown should contain updated content relevant for that selection. In our example, we select countries, and upon selection you can then chose from country provinces (if applicable).

Our controller fetches the lists of countries and country provinces:

public function chained_dropdowns() {
        $countries = $this->Controller->Country->getList();
        $countryProvinces = array();
        foreach ($countries as $key => $value) {
            $countryProvinces = $this->Controller->CountryProvince->getListByCountry($key);
            break;
        }
        $this->set(compact('countries', 'countryProvinces'));
    }

The CountryProvinceHelper component (from my Data plugin) which I usually use here basically does some more, but for a simple example this suffices. It will simply pass down all countries and the country provinces for the first country (which will be auto selected right away).

The view then contains the form:

<?php echo $this->Form->create('User');?>
    <fieldset>
        <legend><?php echo __('Countries and Country Provinces');?></legend>
    <?php
        $url = $this->Html->url(array('plugin' => 'sandbox', 'controller' => 'ajax_examples', 'action' => 'country_provinces_ajax', 'ext' => 'json'));
        $empty = count($countryProvinces) > 0 ? __('pleaseSelect') : array('0' => __('noOptionAvailable'));
 
        echo $this->Form->input('country_id', array('id' => 'countries', 'rel' => $url));
        echo $this->Form->input('country_province_id', array('id' => 'provinces', 'empty' => $empty));
    ?>
    </fieldset>
 
    The province list is updated each time the country is switched. It also has a basic fallback for POST data (will auto-remember the previous selection).
    <br /><br />
 
<?php echo $this->Form->end(__('Submit'));?>
</div>

The jQuery code can either be added to the page or stored in a separate js file:

$(function() {
 
    $('#countries').change(function() {
        var selectedValue = $(this).val();
 
        var targeturl = $(this).attr('rel') + '?id=' + selectedValue;
        $.ajax({
            type: 'get',
            url: targeturl,
            beforeSend: function(xhr) {
                xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
            },
            success: function(response) {
                if (response.content) {
                    $('#provinces').html(response.content);
                }
            },
            error: function(e) {
                alert("An error occurred: " + e.responseText.message);
                console.log(e);
            }
        });
    });
 
});

Then all we need is the AJAX action

public function country_provinces_ajax() {
        $this->request->onlyAllow('ajax');
        $id = $this->request->query('id');
        if (!$id) {
            throw new NotFoundException();
        }
 
        $this->viewClass = 'Tools.Ajax';
 
        $this->loadModel('Data.CountryProvince');
        $countryProvinces = $this->CountryProvince->getListByCountry($id);
 
        $this->set(compact('countryProvinces'));
    }

and the AJAX view for this action:

<?php
if (!empty($countryProvinces)) {
    echo '<option value="">' . __('pleaseSelect') . '</option>';
    foreach ($countryProvinces as $k => $v) {
        echo '<option value="' . $k . '">' . h($v) . '</option>';
    }
} else {
    echo '<option value="0">' . __('noOptionAvailable') . '</option>';
}

To see the full code in action take a look at the live example in my Sandbox or the source code of it.

Error out

If some error occurs in your action you could either throw an exception and handle that in the frontend or pass down an error code/string to the AjaxView, disabling rendering and returning only the error to the frontend (in response.error):

$this->set('error', 'No record found');

For simple stuff it’s probably easier to use the exception free approach (using HTTP code 200). The resulting JSON would be something like:

{
    "error": "No Record found",
    "content": "",
}

But in general try to stick to exception based error handling. That would return a response with an error code of 4xx or 5xx usually (depending on the type of exception) and the following JSON object:

{
    "code": 404,
    "message": "Not Found", // Note that in Cake < 2.5 this might be "name"
    "url": "...",
}

In this example it’s a NotFoundException.

Your JS code needs to handle this gracefully. There could always be some database exception or alike thrown. So better account for that scenario.

Further improvement

You could write some generic JS code or even a generic jquery plugin to handle the AJAX response. This would cut down the lines for the query code drastically and keep it DRY.

If you want to automatically make all ajax requests respond with this view class, you could use something like this in your AppController’s beforeFilter callback:

if ($this->request->is('ajax') {
    $this->viewClass = 'Tools.Ajax'
}

It would cut down on that one line your actions.

I would not recommend that, though. as the JSON view is usually a good default. In my experience you usually need that more often, e.g. when just returning data (multiple records in particular) from the model without rendering any view.

But in general you could try to leverage a component to take care of all that and on top add even more cool things like redirect handling (output it along with the rest of the JSON data instead of actually redirecting) or flash message handling. That together could make it a real useful bundle and cut down your controller code a lot. No need to add a lot of switchs and if statements, if the code is exactly the same for both AJAX and normal requests. See my AjaxComponent which is still “beta” and could be the answer to this.

Last tips

Don’t forget to include the RequestHandler component and set up Router::parseExtensions in your routes:

Router::parseExtensions();
Router::setExtensions(array('json', ...));

This will tell CakePHP that all .json URLS are automatically served as JSON (which the appropriate headers).

Remember to not cache ajax actions to avoid old content being delivered:

if ($this->request->is('ajax')) {
    $this->disableCache();
}

I actually use that statement for all my actions/content that contain dynamically build content.

If you plan on updating the DB, never use “get”, always use type: 'post' and also assert that your action only accepts POST. “get” is only appropriate if the DB is not changed by the AJAX request, so if you only use find().

Note the difference between <option value=""> and <option value="0">. Using validation and the numeric rule we assert that the latter passes validation (when there is nothing to select) whereas the first would trigger a “You need to select a province” error of some sort when trying to save.

Using var targeturl = $(this).attr('rel') + '?id=' + selectedValue; or tricks like data('url') and data-url etc keeps the URL handling and other dynamic PHP based content out of the JS files and makes it easier to store them separately and without direct dependencies. It also avoids shortcut solutions like hardcoding URLs and such.

The xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); part is quite useful as some browsers might not properly work without it (at least for me it used to).

Final note

I should mention that the AjaxView is not THE way to go. It was more an idea over night to overcome those manual render and “echo json_encode” calls and to unify some of the redundant code in my app. For your app it might be a different AjaxView. Go for it. This post, besides the AJAX topic itself, can also just show how easy it is in 2.x to extend the View class to something you can leverage to make your code DRY and consistent throughout your app.

AJAX and CakePHP
3 votes, 4.67 avg. rating (92% score)
 
3 Comments

Posted by Mark in CakePHP

 

Tags: , , ,

Leave a Reply

Tip:
If you need to post a piece of code use {code type=php}...{/code}.
Allowed types are "php", "mysql", "html", "js", "css".

Please do not escape your post (leave all ", <, > and & as they are!). If you have encoded characters and need to reverse ("decode") it, you can do that here!
 

 
  1. Anand Ghaywankar

    January 9, 2014 at 06:49

    Hi Mark,

    Thank you so much for this blog. I have been looking for something like this since long. It will help me to explain more about Ajax in Cake to my team mates. A small query..

    is this good idea to start action and view name with prefix as "ajax_"? I have recently started doing this which helps me to wrok with ACL and permission related stuff, because I can easily find the Ajax action inside controller.

    Please give me your valuable feedback on this.

    Thanks

     
  2. Charles Bueche

    January 10, 2014 at 14:39

    Excellent post, thanks a lot. I will put this in use in my two drop-downs.

     
  3. flashios09

    January 20, 2014 at 08:24

    Hi Mark,

    sorry for my bad english :(

    this post helps me a lot ;)

    before i have to use "Mustache" (https://github.com/janl/mustache.js) to "format" the response "data" and append it to the DOM.
    Now with your "ajaxView" i can "format" the data directly without using a "javascript template engine"

    i made some changes to your ajaxView, the purpose is to send also the "validationErrors"(if exist) and the "message Flash"(setFlash) + the "content" var:

    public function render($view = null, $layout = null) {
    		$response = array(
    			'errors' => null,
    			'content' => null,
    			'message' => null,
    			'id' => null
    		);
     
    		/*if (!empty($this->viewVars['errors'])) {
    			$view = false;
    		}*/
     
    		if ($view !== false && $this->_getViewFileName($view)) {
    			$response['content'] = parent::render($view, $layout);
    			if(isset($this->viewVars['errors'])){
    				$response['errors'] = $this->viewVars['errors'];
    			}
    			if(isset($this->viewVars['message'])){
    				$response['message'] = $this->viewVars['message'];
    			}
    			if(isset($this->viewVars['id'])){
    				$response['id'] = $this->viewVars['id'];
    			}
     
    		}
    		if (isset($this->viewVars['_serialize'])) {
    			$response = $this->_serialize($response, $this->viewVars['_serialize']);
    		}
    		return json_encode($response);
    	}

    i want to take a look at your "AJAX Component" but i didn"t find a documentation (how to use this component)

    please give me the doc link

    thanks
    flashios09