AJAX and CakePHP

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. It 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' => '',
    );
    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' => '',
    );
    $this->set(compact('data')); // Pass $data to the view
    $this->set('_serialize', 'data'); // Let the JsonView class know what variable to use
}

Note that by using _serialize here we tell the JsonView class that we don’t need a view template to be rendered here. It will skip all the rendering and just use what
you set() to the view class.
Make sure to check out the JsonView docs on how to use a view template if you really need it. though.
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).

The example can be seen live in the sandbox, as well.

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.

Note that an alternative to this could be a ("_serialize") JSON response containing the list of country provinces and let the JS render the HTML. But I prefer to have as much of HTML generation in (Cake)PHP itself and let JS only handle displaying the response data.

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

CakeFest Talk 2014 Madrid

I had a talk about this topic at the CakePHP Conference this year, check out the slides @ slideshare.net or speakerdeck.com.

CakePHP 3.x

With 3.x being beta and all, it might be worth noting that not much will change here for the new major framework version.
The parseExtensions() method is gone, so this simplifies it a bit:

Router::extensions(['json']);

And in case you need to build the URL for this action, it is now _ext instead of ext as key:

$this->Html->link('Title', 
    ['controller' => 'MyController', 'action' => 'myAction', '_ext' => 'json']);

Mind your casing: The above example assumes you are using the recommended DashedRoute class for URL generation.

All the rest of the code above is the same πŸ™‚

2015-01 CakePHP 3

The code has been extracted into its own Ajax plugin. Check it out!

The live examples are to be found in the upgraded sandbox.

4.50 avg. rating (89% score) - 18 votes

28 Comments

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

  3. Thanks for your clear instructions here!

    Trying to get any kind of useful information from the CakePHP docs hurts my brain – you’ve managed to answer questions I had from reading the docs…

    Thanks alot!

  4. Great tutorial, what about $this-&gt;disableCache(); ? This results in an error.

    From the Cake 3 tutorial I found $this-&gt;response-&gt;disableCache();, which results in the error: ‘Controller action can only return an instance of Response’

    Still looking how to replace this Cake 2 stuff in Cake 3:
    $this-&gt;Js-&gt;writeBuffer(array(‘cache’ =&gt; FALSE, ‘onDomReady’ =&gt; TRUE));

  5. "I should mention that the AjaxView is not THE way to go."

    Well, as a final notice its a bit frustrating to read this line without a hint to "THE way".

  6. Franz, I added the word "necessarily" – as it CAN be THE way to go, at least it is for me right now πŸ™‚

  7. can any one explain the sequence of controller and action ?
    i have a Country Controller and States Controller.I did not understand the where to write which action
    Thanks.

  8. Mark

    This helped me a lot – however when I combine this with AJAX pagination I am able to get the first page fine. The second page is a heavily escaped text that is not readable. I am trying to figure out what could be the problem but have you come across this issue before?

  9. i tried to do work in this way, but it did not work for me, may be some issue with sandbox plugin , then i found another way to solve my problem, but still i’m interested in it, if anyone can help me, it will be appreciated.

  10. Hm, thx for tutor. I have a little problem with response, it’s empty πŸ™ When i set $this-&gt;autoRender = true, i get html response with header and footer, but without content :-/

  11. Mark no doubt you’re a great developer. And you go to great lengths to document your work for others to re-use, both which must be appreciated.

    However, am sad to say that I always have trouble using your plugins because your documentation style usually is from a point of view that already understands whats going on. I usually have trouble figuring out where to place what code and so on.

    Could you please consider documenting in the CakePHP style, as in put this code here, and that code there, then see that it works. After which we (adopters) can fine tune the solution to work in our context.

    Thanks.

    PS. You could use the Bookmarker examples so that even newborn CakePHP users can benefit.

  12. I agree that I tend to expect users to know at least the basics. It is not always easy to know how much can be expected – and it feels like just repeating the CakePHP docs which already exist for that reason. And IMO without at least some basic knowledge it is often very difficult using specific plugin solutions.
    But in open source it is always possible to help make the documentation better and more suitable for everyone. So please help out and provide your feedback on how to do that – or directly make a PR to change these things for other beginners.
    Thank you.

  13. Well because its asked here and I needed also some time to figure it out…here is how I did in Cake3:

    if($this->request->is('ajax')){
                $this->RequestHandler->renderAs($this, 'json');
                $this->response->disableCache();
                $this->set(compact('myVar'));
                $this->set('_serialize', ['myVar']);
            }

    another Thing: there must be a bug in Cake 3.1.0 where this code tries to render a view… well or I did something wrong – anyway this code works with Cake 3.1.2 without a view.

    Have fun πŸ™‚ (open for comments on that)

  14. $this-&gt;viewClass = ‘Tools.Ajax’;
    is not working. I’m using cake 3.2.7

  15. Please see the new 3.2 syntax of method invocation here in the CakePHP documention – on how to switch view classes.

  16. Mark,

    still unclear how to set a response in a controller to use with jQuery Ajax. How to get "e.responseText" in the jQuery Ajax error function, when response code in controller is set to 500 and appropriate error message. Any ideas?

  17. first of all thanks a lot for providing excellent tutorial what i am searching for the cake php with ajax tutorial

  18. Sry, the link to the pagination example was just outdated, with CakePHP 3.x the URLs are now dasherized. I fixed up the link. Thanks for reported.

  19. Hi,

    I have installed the plugin, but no matter what I do I am not able to get the JSON response. I get empty response. I am using Cake 3.4. I am trying to build a chained dropdown. Here is my action:-

    public function getRegions() {
    $this-&gt;autoRender = false ;
    $this-&gt;request-&gt;allowMethod(‘ajax’);
    $id = $this-&gt;request-&gt;getQuery(‘id’);;
    if (!$id) {
    throw new NotFoundException();
    }
    $this-&gt;viewBuilder()-&gt;setClassName(‘Ajax.Ajax’);

    	$regions = $this-&amp;gt;Weins-&amp;gt;Regions-&amp;gt;find(&#039;all&#039;,[&#039;conditions&#039;=&amp;gt;[&#039;land_id&#039;=&amp;gt;$id]])-&amp;gt;toArray();
        
        $this-&amp;gt;set(compact(&#039;regions&#039;));
        $this-&amp;gt;set(&#039;_serialize&#039;, true);
        
    }
    

    Can you suggest what is that I may be doing wrong. My call are reaching the action but I just dont get back any response.

    Regards

  20. Please ignore my ignorance. Please ignore my last message, I was able to resolve the issue. Great work on the plugin.

    Regards

  21. The last paragraph contains all the 3.x code and docs you need to get it run and also to see the live examples.

  22. How to make the dropdown work in cakephp4? Been searching for a solution but can’t find a good example for this…

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.