6

I am just starting to get my head into caching as a whole. I have a simple indexAction() that fetches all given Datasets. My approach is:

  • check for existing key 'controllername-index-index'
  • if existing: return the value of the key
  • if not existing, do the normal action and add the key

The value inside the key should be the ViewModel that will be generated and populated with my data.

Here's what i have done so far:

<?php
public function indexAction()
{
    $sl = $this->getServiceLocator();
//  $cache = $sl->get('cache');
//  $key = 'kennzahlen-index-index';
//
//  if ($cache->hasItem($key)) {
//      return $cache->getItem($key);
//  }

    $viewModel = new ViewModel();
    $viewModel->setTemplate('kennzahlen/index/index');
    $entityService = $sl->get('kennzahlen_referenzwert_service');
    $viewModel->setVariable('entities', $entityService->findAll());

//  $cache->setItem($key, $viewModel);

    return $viewModel;
}

The Caching parts are commented out for testing purposes, but basically this is all that i am doing. The Caching config/service looks like the following:

<?php
'cache' => function () {
    return \Zend\Cache\StorageFactory::factory(array(
        'adapter' => array(
            'name' => 'filesystem',
            'options' => array(
                'cache_dir' => __DIR__ . '/../../data/cache',
                'ttl' => 100
            ),
        ),
        'plugins' => array(
            array(
                'name' => 'serializer',
                'options' => array(

                )
            )
        )
    ));
},

The serialization and caching works quite well, but i am surprised by the missing results. Going by what the ZendDevelopersToolbar tells me, the times WITHOUT caching range between 1.8s to 2.5s. Having the caching parts uncommented (enabled) doesn't really improve the loading time of my page at all.

So my question is: Is this approach completely wrong? Are there different, more speedy parts, that can be saved with some neat configuration tricks?

I Feel that a 2 second load time of a page is DEFINITELY too slow. 1s to me is the maximum given a huge amount of data, but certainly not anything more than that :S

All help/hints/suggestions will be greatly appreciated. Thanks in advance!

janw
  • 6,672
  • 6
  • 26
  • 45
Sam
  • 16,435
  • 6
  • 55
  • 89
  • Are you sure this specific part is causing you having this trouble? My ZF2 pages usually run under 100ms, so there should be another cause for this. Also I would prefer to cache specific query results, no view models. If you want to cache anything view related, cache the view rendering so you skip the parsing of .phtml files as well. Otherwise, cache the result of the service. – Jurian Sluiman Dec 07 '12 at 22:22
  • The question would be, where to inject the code so i can cache the view rendering already? Though apparently I'd be wise suggested to see whats making my script so slow. Don't really have many modules enabled, zfcuser, bjyauthorize, zenddeveloperstoolbar and the doctrine-stuff + my module that uses doctrine to get some db entities :S – Sam Dec 08 '12 at 13:44
  • I answered a possible cache option where your html result is cached. Install Xdebug and perform some profiling actions to see what the real bottleneck of your application is. – Jurian Sluiman Dec 08 '12 at 15:46
  • Maybe the query is fast so the cache is not beneficial. Beside that I recommend using APC for caching. It will be much faster then using the filesystem. – michaelbn Dec 09 '12 at 07:45
  • Well known, problem is our server guys won't give us APC (or MC) for some non-relevant reasonings :S So Filesystem is my only choice... – Sam Dec 09 '12 at 09:37

1 Answers1

17

One option would be to cache the complete output of your page, for example based on the route match. You need to listen between routing and dispatching which route has been found as match and then act accordingly:

namespace MyModule;

use Zend\Mvc\MvcEvent;

class Module
{
    public function onBootstrap(MvcEvent $e)
    {
        // A list of routes to be cached
        $routes = array('foo/bar', 'foo/baz');

        $app = $e->getApplication();
        $em  = $app->getEventManager();
        $sm  = $app->getServiceManager();

        $em->attach(MvcEvent::EVENT_ROUTE, function($e) use ($sm) {
            $route = $e->getRouteMatch()->getMatchedRouteName();
            $cache = $sm->get('cache-service');
            $key   = 'route-cache-' . $route;

            if ($cache->hasItem($key)) {
                // Handle response
                $content  = $cache->getItem($key);

                $response = $e->getResponse();
                $response->setContent($content);

                return $response;
            }
        }, -1000); // Low, then routing has happened

        $em->attach(MvcEvent::EVENT_RENDER, function($e) use ($sm, $routes) {
            $route = $e->getRouteMatch()->getMatchedRouteName();
            if (!in_array($route, $routes)) {
                return;
            }

            $response = $e->getResponse();
            $content  = $response->getContent();

            $cache = $sm->get('cache-service');
            $key   = 'route-cache-' . $route;
            $cache->setItem($key, $content);
        }, -1000); // Late, then rendering has happened
    }
}

The second listener checks at the render event. If that happens, the result of the response will be cached.

This system (perhaps not with 100% copy/paste, but the concept) works because if you return a Response during the route or dispatch event, the application will short circuit the application flow and stop further triggering listeners. It will then serve this response as it is.

Bear in mind it will be the complete page (including layout). If you don't want that (only the controller), move the logic to the controller. The first event (now route) will be dispatch of the controller. Listen to that early, so the normal execution of the action will be omitted. To cache the result, check the render event for the view layer to listen to.

/update: I wrote a small module to use this DRY in your app: SlmCache

Jurian Sluiman
  • 13,498
  • 3
  • 67
  • 99
  • Thank you for these examples. XDebug will indeed be the next thing to do. This example however - to me - acts as a nice reference for the EventManager usage, which is something i haven't quite figured out yet :) Thanks for that, will report prolly on monday ;) – Sam Dec 08 '12 at 18:34
  • I will accept this answer, since it has general purpose. My error though, after some xdebugging, appears to be my DB connection, which takes 1s on a local xampp environment, so gotta figure that out somehow ;) Thanks for your help! – Sam Dec 10 '12 at 14:02
  • I think I read somewhere that `pre` and `post` events are great usage for caching things. Pre for checking if in cache (then returning if so) and Post for adding to cache after doing Bootstrap/whatever. – creamcheese Jan 20 '13 at 02:56
  • 1
    @DominicWatson pre/post are typical hooks for ZF1, this question is about ZF2 – Jurian Sluiman Jan 20 '13 at 09:21
  • That's what I'm talking about. Using ZF2 EventManager for caching. http://akrabat.com/zend-framework-2/an-introduction-to-zendeventmanager/ Akrabat talks about using Pre/Post Events to "ShortCircuit". Bootstrap being an event, surely you can use that? pre: Check if in cache and return if so. bootstrap: as normal. post: add to cache – creamcheese Jan 20 '13 at 13:23
  • 1
    Ah @DominicWatson you talk with `pre` and `post` about *custom defined* events. My answer uses the event manager (check the code I posted) and I short-circuit with returning a response. If you say pre as in "early" that is exactly what `route` does. If you say post as in "late" that is exactly what `render` does. Any caching solution uses a early return on hit and late listener for storage. In general such patterns could be names `pre` and `post` but for specifically Zend Framework 2's application process, I'd use the `route` and `render` events. – Jurian Sluiman Jan 20 '13 at 14:19
  • ah, thanks for that (I don't know too much yet). I thought you could just do Bootstrap.pre and Bootstrap.post. I guess my URL would at least help anyone who wants to know about caching other pieces :) – creamcheese Jan 20 '13 at 14:28
  • I really liked your solution and I came cross a very similar solution as well. This is something I learned from the Zend Framework 2 advance training course. It's good to keep caching out of the controller actions is because it's good separation of the concerns (imagine you have to think about caching each time you add a new controller action...). I did a proof of concept and uploaded the source code to Github and explained it in very detail. Hope this can be useful. The whole article is on my blog: http://www.shenghua.co.uk/example-of-caching-mvc-response-using-filesystem-cache-in-zf2/ – RHE Aug 16 '14 at 23:37
  • Great answer! Just a small note - for ZF2.3 I had to change priority of event handler to 10001 or otherwise it was triggering before the default renderer took place http://framework.zend.com/manual/2.1/en/modules/zend.mvc.mvc-event.html#mvcevent-event-render – Brock Oct 15 '14 at 22:15