zen of coding

Dynamic menus without requestAction() in CakePHP 1.2

You’ve probably heard time and time again (especially in a few recent and popular blog posts) that using requestAction() is, generally speaking, considered to be a “bad practice”, “last resort”, “hackish” way of doing things in cake.

Let’s consider one common use to see if we can achieve our goals without using requestAction()…

We have a Company model and our goal is to build a navigational menu consisting of all the companies in our table. We would create an element called ‘company_nav.ctp’ and somehow load the relevant data from our Company model.
The first, obvious, approach is to use requsestAction() in an element, to get the required data. This, however, is something we would like to avoid.

The approach I offer below relies on cache, or cached model’s data to be precise…

We can agree that our menu or navigation would only change if a company has been added, modified or deleted. Therefore we use our Company model’s callback methods such as afterSave() and afterDelete() to properly cache the required data for the menu.

So, assuming we have a Company model, we can add the following to handle the data caching:

function afterSave() {
   $this->_cacheNav();
}

function afterDelete() {
   $this->_cacheNav();
}

function _cacheNav() {
$companies = $this->find('list');

Cache::config(null, array('engine'=>'File', 'path'=>CACHE));
Cache::write('companies', $companies, array('duration'=>7200, 'config'=>null));
}

The code is pretty simple, but let me explain it just a little…

We rely on afterSave() and afterDelete() to call our custom method _cacheNav(), which handles the actual caching of the Company data.

In this case we only need Company.name and Company.id for our menu, so using find(‘list’) works perfectly well for that.
We then call Cache::config() to ensure that we write to the correct location (the constant CACHE is defined for us by cake core) and that we use the correct storage engine (I use File, but you can use any supported engine). I found that specifying the ‘path’ is a good idea, since Cache can write to different locations depending on the context, therefore it’s best to be explicit about it.

Alright, now that our data is cached, we can easily build our element (company_nav.ctp):

<?php

Cache::config(null, array('engine'=>'File', 'path'=>CACHE));

$companyData = Cache::read('companies');

if(!empty($companyData)) {
   foreach($companyData as $key => $value) {
      echo '<div>'.$html->link($value, array('action'=>'view', $key)).'</div>';
   }
}

?>

I think the above should be pretty much self-explanatory, we are simply reading the data we’ve cached earlier in our model and building a menu of links.
Again, I prefer to specify the ‘path’ to ensure we attempting to read from the correct cache path.

Before we finish up, I wanted to point out a few important issues:

1. The duration of the cache is limited
Of course you can increase it to some unrealistically large value to cache forever, or you could come up with some fall-back mechanism (i.e. the good ol’ requestAction()) if cache data is not available.
At any rate, this is just something to be aware of and it can be easily fine-tuned to fit your specific needs.

2. Breaking of MVC (?)
In theory the view (element) should not access the data directly, and I’m not 100% sure if using Model’s cached data is somehow an exception to this rule. Looking at the benefits, however, I feel that this approach is justified. Cache is accessible to all of our application objects, therefore we could at least say that we are not breaking the rules, but rather bending them slightly.

  • This is, BY FAR, the best way I´ve read. Teknoid, as always, you show that you have God´s cellphone. Thanks!

  • teknoid

    @Martin Bavio

    Heh, never heard that saying before ;)

    Thanks.

  • Very cunning! :-)

  • Just a thought: You might want to include afterEdit too in case a user changes a company name.

  • afterEdit doesn’t exist.

    @teknoid you might want to consider putting your call to _cacheNav, or the logic directly in _clearCache – which is automatically called.

    i.e. complete replacement for first snippet:

    function _clearCache() {
    $companies = $this->find(‘list’);

    Cache::config(null, array(‘engine’=>’File’, ‘path’=>CACHE));
    Cache::write(‘companies’, $companies, array(‘duration’=>7200, ‘config’=>null));
    }

  • Great solution to a common problem. I looked over the other answers offered on blogs lately and this makes a lot more sense to me and doesn’t seem to hack around the problem.

    Thanks for coming up with this :)

  • Really nice solution! I like the use of cache. I did this in the app_controller beforeRender() and assign a template var and use an element. But this is also nice.

  • FWIW – accessing data directly in views is not breaking MVC

  • teknoid

    @Richard@Home

    Thanks.

    re: afterEdit()… that’s actually handled by afterSave() as well.

    @primeminister

    That’s a good solution, the only issue is that when you need the menu on some pages and not the others, it seems like you need to add a bit of logic to figure out when to make the call and when to not.

  • teknoid

    @AD7six

    Thanks, good to know.

  • teknoid

    @Tarique Sani

    Well, I wasn’t quite sure, but now I feel even better about it :)

    @David Boyer

    You’re welcome

  • Pingback: links for 2008-08-22 « Richard@Home()

  • I think the use of caching sets this approach apart from the others that have been suggested. That and you have kept the logic in the model which I am a fan of.

    You mention backing up the cache with a requestAction() just in case the cache expires. If you are trying to avoid requestAction() wouldn’t it more prudent to make the cache last much longer? Like forever? As you mentioned you only need to refresh it whenever the companies change. So caching forever is an option.

  • teknoid

    @Mark Story

    Thanks, good points.
    I kinda just threw out requsestAction() fallback as food for thought… also I wasn’t sure if cake has some built-in setting to cache something forever.

  • in proposed task better to use requestAction in view and caching of menu element.
    In cacheNav you need to remove element cache file.
    This will give you
    1. better speed during menu rendering. You will execute code slow first time when requestAction called, but immediatelly in all other requests.
    2. your solution has problems if cashe file will deleted. This will not happen if you will use requestAction solution

  • teknoid

    @Skiedr

    Using requestAction() is always slow and never recommended, so the whole point of this article was to look at alternatives to requestAction().

    As I mention you should have some fall-back in place, if cache is not available.

  • jacknirons

    eh, simply mov ethe cache-reading-code to the controller layer (controller or component), and you don’t need to worry about breaking the mvc-pattern!? ;)

  • teknoid

    @jacknirons

    Well, the code has to be in an element, since we don’t know when we need to read that data (i.e. in which controller/action). That being said, I think reading cache is fine, since it is available to my view as well.

  • WalkingSoul

    Howdy folks!

    After a few tries I did not get the above method to work. For no apparent reason the cache got deleted since the duration was not set properly in the cache file.

    Instead I tried to use another method which worked fine:

    My solution:

    I have moved the duration to the Cache::config call and named it.

    ‘File’, ‘path’=>CACHE, ‘duration’=> ‘1 week’));
    Cache::write(‘newsFrontpage’, $this->getFrontpage(), array(‘config’=> ‘newsCache’));
    }
    ?>

    Hope if it helps anyone.

    Regards.

  • WalkingSoul

    Somehow my code did not come through as supposed.

    Another try:

    function _cacheNews () {
    Cache::config(‘newsCache’, array(‘engine’=>’File’, ‘path’=>CACHE, ‘duration’=> ‘1 week’));
    Cache::write(‘newsFrontpage’, $this->getFrontpage(), array(‘config’=> ‘newsCache’));
    }

  • WalkingSoul

    My bad tho, forgot to add something:

    You should also have to use the same Cache::config in your view.

    Cache::config(’newsCache’, array(’engine’=>’File’, ‘path’=>CACHE, ‘duration’=> ‘1 week’));

  • @WalkingSoul

    Well, I’m glad you got it sorted out ;)

  • Correct me if I’m wrong, but if you update _cacheNav to return $companies, you could override the find method like this:

    // Get the cached version of the full list whenever possible
    function find($findType, $arguments = NULL) {
    // Ideally, use the cached version of the website contents list
    if ($findType == ‘list’ && $arguments == NULL) {
    if ($list = Cache::read(‘website_contents_list’)) {
    return $list; // Read from the cached list
    } else {
    return $this->_cacheNav(); // Write to the cached list
    }
    }

    // Fall back on doing it the proper way
    return parent::find($findType, $arguments);
    }

  • @Zoe Blade

    Definitely a sound improvement, thank you for sharing it.

  • Mohd Amjed

    Hi,
    This may sound silly, but still,how do you set the cache, first time ??

  • Zoe

    Mohd:
    Cache::write(‘companies’, $companies); is the line that writes the contents of the variable $companies to the cache. Because it’s called from within the afterSave() and afterDelete() methods, the cache is automatically set whenever you change the table in the database at all.

  • @Zoe, Mohd Amjed

    Thanks for putting it a lot more eloquently than I would’ve.
    (Excellent playlist, btw… but I’m surpised you don’t have scsi-9 (no, not the computer stuff, the producer) ;)

  • @Teknoid

    I was relatively new to CakePHP so I was accomplishing this task setting a layout variable (~_for_loyout) at AppController::beforeFilter() and reading it in my dynamic element. I changed it to the method that you are describing, but I have a question. I added a piece of code in my AppController::beforeFilter() to check if the cache variable is set and if it’s not set then call Category->_cacheNav()… It works perfectly, but Is this a good approach? or am I doing something wrong?

    Thanks a lot for this nice post!

  • @Jorge Pedret

    Sounds good to me.

    Also check out the the comments by Zoe, I think it’s a really nice approach to override the find() and include caching right there.
    Also, I have a recent post about using memcached to accomplish a similar situation, with a bit more “elegance”, I guess…

    At any rate, this was a simple exercise to try and avoid requestAction() with cached elements, but I must digress there really isn’t anything wrong with using it in that specific scenario.

    Just trying to bake the cake up-side-down a little ;)

  • Chris

    Just found your blog, great stuff, thanks!

    I don’t know how reasonable this is, but thought it was kind of interesting. Using Zoe Blade’s idea above, I thought of this:

    function find ($findType, $arguments = NULL) {
        $str = "my_model_name".$findType.json_encode($arguments);
        $space = "-";
        $string = substr($str,0,100);
        $string = preg_replace("/[^a-zA-Z0-9 -]/", "", $string);
        $string = strtolower($string);
        $string = str_replace(" ", $space, $string);
        $string = $string;
        if ($list = Cache::read($string)) {
          return $list; // Read from the cached list
        } else {
          $get = parent::find($findType, $arguments);
          Cache::config(null, array('engine'=>'File', 'path'=>CACHE));
          Cache::write($string, $get, array('duration'=>7200, 'config'=>null));
          return $get;
        }
      }

    Thus, ANY find query made to the model is cached, using a json string, then stripping out any special characters and such in the cache filename.

    Cache filenames end up looking like:

    cake_triviagamesall
    cake_triviagamesallconditionspublished1dailygamedatenull
    cake_triviagamesallconditionsuserid201published1limit5sortupdateddesc
    cake_triviagamescountconditionsuserid201published0

    etc…

    Of course this probably wouldn’t not be ideal with frequently updated data, would have to figure out how to re-build the cache with afterSave and afterDelete, but thought it was kinda interesting anyways

  • Roger Kaplan

    Can I be the party pooper?
    This approach totally breaks MVC and uses the cache as a global memory pointer, which is much worse design IMO.

  • Roger Kaplan

    Sorry let me continue, I hit post too fast

    What you’ve got here is the view (an element) accessing model data directly. The fact that it’s using the cache is an implementation detail. The approach is neither extensible nor scalable.

    The core problem is that Cake forces controller -> Model -> Render workflow, and we don’t know that we need model data until the Render phase.

    My personal preference (I have a college degree in CSCI from a prestigious university and have been programming professionally for 25 years, and am an architect at a major investment bank) is as follows

    Use requestaction from elements.

    If this really bugs you, then give the controller knowledge of which elements are to be rendered in the view (this violates MVC but that’s because MVC is insufficient) and have the controller load all the model data. You can come up with some clever mechanisms where the controller logic for an element is in a component, so that you’re not rewriting code. An IOC approach (think Spring) may help here

    What I’d **REALLY** like to do is to allow the element to access the model directly. Think about it. If you go with thick model approach (which I highly recommend btw), then your key business logic is nicely contained. In this case, the controller ends up being the main callback for the browser/web server, only needing to be called once. The view can then be free to include elements, which can pull the data they need. Cache that data if you need, or not. In my site I use a lot of elements and they can’t be cached.

  • @Roger Kaplan

    Thanks for bringing up some interesting points.

    As mentioned in #2 above, this approach does potentially break MVC. However, considering the suggestion, where you say that an element could access model data directly, why not have the view (or element) access the cache? Other than semantics, what is the true difference between the two proposed methods? I am hoping you could expand on this a little more…

    The fall-back of using

    requestAction()

    , if no cached data is available, is always there. Also, the whole point of this “experimental” approach (if you wish), was to look at some other possibilities of accessing the data. Also, you mention that logic should be contained within the model, which I completely agree with and this is exactly what is shown here. No controller involvement at all.

    Well, other points have already been covered in the comments above quite well, so I won’t reiterate them here.

    • Roger Kaplan

      Re your question- a true cache should be totally transparent to the caller. The consumer of the data makes the query. If the query can be fulfilled by cache, it is. if not, it goes back to disk. The consumer should be ignorant of the actual source. The element is accessing the model, period. Whether the bytes come from memory or disk should be transparent. I say it’s not extensible or scalable because you’re reproducing your cache logic for each element.

      Been doing some thinking about this (I’m currently between jobs so I’ve been revisiting a CakePHP hobby site I put together awhile back)

      The sequence of events in a Cake transaction is really bugging me.

      In a simplistic textbook Cake app, the controller uses the model to gather data (note that the location of the business logic is ambiguous here!) and passes it to the view. It’s a one-way street. Once the controller starts rendering the view, no more data can be gathered or business logic triggered.

      But with a self-contained element, that’s exactly what you need – the element doesn’t make the request for data until the view rendering phase, at which point it’s too late to go back for more data.

      That’s why the requestAction() needs to start up another controller call, and all the baggage that entails.

      So I can think of 2 ways to solve this:
      1. Accept the above and have the element call the model somehow. I’ve seen several brute-force ways of accomplishing this (your approach being one), coupled with a “but this breaks MVC” comment. As I alluded to, I think the controller’s responsibility is being overloaded in this regard. Controllers should coordinate rendering pages, not necessarily be linked to entities.
      2. Force the controller to have knowledge of the elements in the view during the action phase, and make sure the necessary data is in the view variable space when the action starts rendering the view, so the data is waiting for the element.

      The more I think about it, I like (2) better. A controller should indeed know what data its view needs, whether in the template, the main view file, or an element. Like I said, controller actions should be responsible for rendering a page. But since elements are shareable across web pages, there needs to be a generic way to do this.

      One solution to this is the mini-controller approach at ad7six.com but that solution is IMO an enhanced caching solution. My particular site has elements changing with most clicks so caching is less important to me. But I think the theme is correct.

      So what I’ve been thinking about is building on the mini-controller approach, whereby components (in the controller space) are paired with elements (in the view space). So an “element package” would have a component and an element authored together. For an element to appear in a view, its component would need to be mentioned (somehow) in the controller’s action. (It should be at the action level not the controller level). The action needs to know enough about the view as well, to know if the element will be invoked.

      The component, as a mini-controller, would have access to the model, and be able to pull the data needed and inject it into the view space (maybe in some sort of namespace). Then only one action needs to happen to feed all the widgets on the page.

      So I’ve been thinking a bit about the similarity between Cake’s convention-based automation and Spring’s IOC functionality. You declare what you need, and the framework takes care of creating it and handing it to you when you need it. So I’d like the same thing to happen here.

      Ideally the controller would pre-parse the view and determine which elements were referenced, then automagically instantiate and call the correct components (with the same base-name as the element of course!). I’m not sure how feasible this is without hacking the core. And in an ideal world, I’d create a new class, not Component, which is really for other things.

      A less elegant but simpler method would be to list the elements in the action, and have either an event handler or some subclassed hook do the work. This is sorta the way mini-controller works. The _continue() requirement speaks to its shortcomings however.

      One thing I’m chewing over a bit is context. Elements should be as standalone as possible but often they need some sort of hint about what else is happening on the page. I suspect some sort of context dictionary (maybe lodged in the Session) would work. With appropriate app-level conventions it would provide enough decoupling; ie the action would have to define/maintain some well-understood globals (logged-in user, nickname, current page, etc etc) for elements to work correctly, without direct knowledge of each other.

  • @Roger Kaplan

    Thanks for your response.
    I have a feeling that you are “over-engineering” a seemingly simple solution :)

    Elements (especially if you cache them) with

    requestAction()

    work perfectly well, and Mark Story did a benchmark a little while since the writing of this post showing that

    requestAction()

    has no impact on the performance:
    http://cakedc.com/eng/mark_story/2009/02/27/benchmarking-requestaction
    http://mark-story.com/posts/view/how-using-requestaction-increased-performance-on-my-site

    The main problem with

    requestAction()

    , is that people misuse it, and wind up with fat controllers, rather than abstracting the logic into the model layer.

    Personally, I still don’t consider this approach as breaking of MVC, not anymore than an element accessing the model directly in any way.

    ….

    “I say it’s not extensible or scalable because you’re reproducing your cache logic for each element. ”

    Sure, it’s just an example here, but it is extremely simple to make this approach a lot more modular to keep things DRY.

  • I can’t hear anything over the sound of how awesome this ariltce is.

%d bloggers like this: