Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Caching View Output in Laravel 4

I know that Blade already caches the compiled PHP for all blade views, but I would like to take this a step further. A website that I'm working on is modularized into component views and then pieced together in the default controller. Each of the "widgets" has its own view, which rarely changes content (with the exception of a few frequently updating ones). So, I'd like to cache the HTML output of these rarely-changing views to prevent them from being evaluated on every page load.

In Laravel 3 we could do something like so (credit Laravel forums):

Event::listen(View::loader, function($bundle, $view)
{
  return Cache::get($bundle.'::'.$view, View::file($bundle, $view, 
                                                  Bundle::path($bundle).'view'));
});

Unfortunately, View::loader has entirely disappeared in Laravel 4. When digging through \Illuminate\View\View and \Illuminate\View\Environment, I discovered that each view dispatches an event named "composing: {view_name}". Listening for this event provides the view name and data being passed to it on each view render, however returning from the callback does not have the same effect as it did in Laravel 3:

Event::listen('composing: *', function($view) {
  if(!in_array($view->getName(), Config::get('view.alwaysFresh'))) {
    // Hacky way of removing data that we didn't pass in
    // that have nasty cyclic references (like __env, app, and errors)
    $passedData = array_diff_key($view->getData(), $view->getEnvironment()
                                                                  ->getShared());

    return Cache::forever($view->getName() . json_encode($passedData), function() {
      return 'test view data -- this should appear in the browser';
    });
}, 99);

The above does not circumvent the normal view including and rendering process.

So how can you circumvent normal view rendering and return cached content from this composing event? Is it possible currently in Laravel without some ugly hackery?

like image 632
Bailey Parker Avatar asked Jul 04 '13 00:07

Bailey Parker


2 Answers

Quick and Dirty

Well, one option, as I'm sure you know, is to cache the items inside of controllers as the View is being rendered. I suspect you don't want to do that, as it's less maintainable in the long-run.

More maintainable(?) method

However, if the View loader/renderer doesn't fire an event where you want, you can create one. Because every package/library in Laravel 4 is set in the App container, you can actually replace the View library with your own.

The steps I would take is:

  1. Create a library/package. The goal is to create a class which extends Laravel's view logic. After taking a look, you might want to extend this one - This is the View facade
  2. If you extended the View facade with your own (aka if my assumption on the file in step 1 is correct), you'll then just need to replace the alias for View in app/config/app.php with your own.

Edit- I played with this a bit. Although I don't necessarily agree with caching a View result, vs caching sql queries or the "heavier lifts", here is how I'd go about doing this in Laravel 4:

The View rendering in Laravel 4 doesn't fire an event that let's us cache the result of a view. Here's how I've added in that functionality to cache a view's result.

You may want to consider the ramifications of caching a view's result. For instance, this doesn't get around doing the hard work of talking to a datbase to get the data needed for the view. In any case, this gives a good overview on extending or replacing core items.

First, create a package and set up its autoloading. I'll use the namespace Fideloper\View. It's autoloading in composer.json will looks like this:

"autoload": {
    "classmap": [
        "app/commands",
        "app/controllers",
        "app/models",
        "app/database/migrations",
        "app/database/seeds",
        "app/tests/TestCase.php"
    ],
    "psr-0": {
        "Fideloper": "app/"
    }
},

Next, create a class to replace the View facade. In our case, that means we'll be extending Illuminate\View\Environment.

In this class, we'll take the result of the View being rendered and add some logic to cache (or not cache) it. Here is Fideloper/View/Environment.php:

<?php namespace Fideloper\View;

use Illuminate\View\Environment as BaseEnvironment;
use Illuminate\View\View;

class Environment extends BaseEnvironment {

    /**
     * Get a evaluated view contents for the given view.
     *
     * @param  string  $view
     * @param  array   $data
     * @param  array   $mergeData
     * @return \Illuminate\View\View
     */
    public function make($view, $data = array(), $mergeData = array())
    {
        $path = $this->finder->find($view);

        $data = array_merge($mergeData, $this->parseData($data));

        $newView = new View($this, $this->getEngineFromPath($path), $view, $path, $data);

        // Cache Logic Here

        return $newView;
    }

}

So, that's where the bulk of your work will be - filling out that // Cache Logic Here. However, we have some plumbing left to do.

Next, we need to set up our new Environment class to work as a Facade. I have a blog post about creating Laravel facades. Here's how to accomplish that in this case:

Create the facade for our new Environment. We'll name it fideloper.view in code.

<?php namespace Fideloper\View;

use Illuminate\Support\Facades\Facade;

class ViewFacade extends Facade {

    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor() { return 'fideloper.view'; }

}

Then, create the Service Provider which will tell Laravel what to create when fideloper.view is called. Note that this needs to mimic functionality of the Illuminate\View\ViewServiceProvider for creating the extended Environment class.

<?php namespace Fideloper\View;

use Illuminate\Support\ServiceProvider;

class ViewServiceProvider extends ServiceProvider {

    public function register()
    {
        $this->app['fideloper.view'] = $this->app->share(function($app)
        {
            // Next we need to grab the engine resolver instance that will be used by the
            // environment. The resolver will be used by an environment to get each of
            // the various engine implementations such as plain PHP or Blade engine.
            $resolver = $app['view.engine.resolver'];

            $finder = $app['view.finder'];

            $env = new Environment($resolver, $finder, $app['events']);

            // We will also set the container instance on this view environment since the
            // view composers may be classes registered in the container, which allows
            // for great testable, flexible composers for the application developer.
            $env->setContainer($app);

            $env->share('app', $app);

            return $env;
        });
    }

}

Lastly, we need to hook this all together and tell Laravel to load our Service Provider and replace Illuminate's View facade with our own. Edit app/config/app.php:

Add the Service Provider:

'providers' => array(

    // Other providers

    'Fideloper\View\ViewServiceProvider',

),

Replace the View facade with our own:

'aliases' => array(

    // Other Aliases

    //'View'            => 'Illuminate\Support\Facades\View',
    'View'            => 'Fideloper\View\ViewFacade',

),

You'll then be able to use whatever logic you wish in the View::make() method!

Finally

It's worth noting that there are some patterns to load in multiple "requests" per web request. Symfony, for instance, let's you define controllers as servers. Zend has (had?) a concept of Action Stacks, which let you

... effectively help you create a queue of [controller] actions to execute during the request.

Perhaps you'd like to explore that possibility within Laravel, and cache the results of those "actions" (vs caching a view directly).

Just a thought, not a recommendation.

like image 197
fideloper Avatar answered Oct 10 '22 16:10

fideloper


There is a library for caching views/parts in Laravel(and not only) - Flatten.

It is a powerful cache system for caching pages at runtime. What it does is quite simple : you tell him which page are to be cached, when the cache is to be flushed, and from there Flatten handles it all. It will quietly flatten your pages to plain HTML and store them. That whay if an user visit a page that has already been flattened, all the PHP is highjacked to instead display a simple HTML page. This will provide an essential boost to your application's speed, as your page's cache only gets refreshed when a change is made to the data it displays.

To cache all authorized pages in your application via the artisan flatten:build command. It will crawl your application and go from page to page, caching all the pages you allowed him to.

Flushing

Sometimes you may want to flush a specific page or pattern. If per example you cache your users's profiles, you may want to flush those when the user edit its informations. You can do so via the following methods :

// Manual flushing
Flatten::flushAll();
Flatten::flushPattern('users/.+');
Flatten::flushUrl('http://localhost/users/taylorotwell');

// Flushing via an UrlGenerator
Flatten::flushRoute('user', 'taylorotwell');
Flatten::flushAction('UsersController@user', 'taylorotwell');

// Flushing template sections (see below)
Flatten::flushSection('articles');

link to - https://github.com/Anahkiasen/flatten

like image 32
Yaroslav Avatar answered Oct 10 '22 15:10

Yaroslav