A Practical 3-Step Cache Implementation in Laravel

Implementing Laravel’s built-in cache is a great way to speed up your application when dealing with a large data set. This brief tutorial defines a practical way to significantly speed up the TTFB (Time to First Byte) for HTTP requests to your app.

The following method implements the traditional Laravel Blade templating process, but forgoes using @include directives and instead echoes cached partial views binded to the main view instance called by the controller. This provides a nice alternative to caching full html responses when portions of the page update frequently.

The following code implements Laravel’s file cache driver, hence the improvised tagging system since tags aren’t supported by that driver. Let’s dive in!!

1. Setup the controller


namespace BagelPrinterApp\Http\Controllers;
use Illuminate\Support\Collection;
use BagelPrinterApp\Traits\ConsumesBagelCache as ConsumesBagelCache;

class BagelController extends Controller
{
  use ConsumesBagelCache;
  //admins see all bagels, regular users view a customized bagel feed
  $cacheTag = auth()->user()->isAdmin() ? 'admin' : auth()->user()->id;
  $userSpecificBagelFeedPartial = 'partials.bagelFeed#' . $cacheTag;
  
  //$partials array: {partialName} => [{collectionNameUsedInPartial},{Closure returning data in Collection}]
  //Closure passed so query not run unless needed for blade view
  //instead of binding collections to view, we're binding HTML
  $partialsCollection = $this->getOrSetCachedPartialsAndReturnHtml(
    [
      'partials.trending_bagels' => [
        'boundVarName' => 'trendingBagels',
        'query' => function(){
          return $bagel->getTrending();
        }
      ],
      $userSpecificBagelFeedPartial => [
        'boundVarName' => 'bagels',
        'query' => function(){
          return auth()->user()->myBagels()->sortBy('name');
        }
      ]
    ]
  );
  return view('bagels.index')
    ->with('bagels', $partialsCollection[$userSpecificBagelFeedPartial])
    ->with('trendingBagels', $partialsCollection['partials.trending_bagels']);
      
  
}

Basically what we’re doing here is sending an array (keyed by our blade subview paths) to getOrSetCachedPartialsAndReturnHtml and receiving back a collection of HTML chunks rendered by our blade subviews. The boundVarName and Closure callback attributes we send are only used when the subview path keys don’t match any key in the filesystem cache. Either way we get html back and the use of closures prevents any queries running unless needed. Let’s have a look at the trait, here’s where the heavy lifting occurs.

2. Get or set cache and return HTML

namespace BagelPrinterApp\Traits;
use Illuminate\Support\Collection;

trait ConsumesBagelCache {

  private function getOrSetCachedPartialsAndReturnHtml(array $partialsArray)
  {
    $partialsCollection = collect($partialsArray)
      ->map(function ($partial, $key) {  

        //partials like the myBagelsFeed have a hash identifier per user id. index 1 in $matches holds the pattern match
        $keyBeforeHash = preg_match('/(.+)#/',$key, $matches);
        $partial = $matches[1] ?? $key;
        if ($this->isCached($key)){
          return $this->getCachedPartial($key); 
        } 
        $value = view( $partial, [$collectionName => $callback()] )->render();
        Cache::forever( $key, $value );
        return $value;    
    });
    return $partialsCollection;
  }

  private function isCached($key)
  {
    return Cache::has($key);
  }
  private function getCachedPartial($key)
  {
    return Cache::get($key);
  }
}

3. Echo HTML in Blade view

Finally let’s look at the switcheroo we make in the blade template.

{{--  @include('partials.trending_bagels')  --}}
{!! $trendingBagels !!}

Note the curly brace syntax here in order to work with unescaped data, it comes in handy but be careful with those! Well that about wraps things up. The next step would be to implement a cache buster that’s triggered by CRUD actions involving our bagel resources. Thanks for reading!

About

Charlie
Charlie's passion for graphic and web design is, quite remarkably, an exact 50-50 split. He otherwise enjoys long walks and documentary films. Connect with him on Linkedin.