Implementing a Data Caching Layer in Laravel 5

Hi, I'm Steven Maguire

  • I've been building software since 2004.
  • Contribute to open source.
  • Author courses for Pluralsight.com.
  • Build product development teams in Chicago.
  • Tweet from @stevenmaguire.

The Plan

  • Review variety of caching "types"
  • Explain caching with
    abstraction "layers"
  • Add abstraction layer to
    Laravel 5 project
  • Add caching to abstraction layer
  • Demo and Code sample
    (Product Service)

Preface

Caching is a big topic. These thoughts are my own and not academic in nature.

Review variety of caching "types"

Caching Types

  • Output caching
  • Data caching

Output Caching

  • Browser

  • Content Delivery
    Network
    (Akamai, Instart Logic, Cloudflare, Cloudfront)
  • Web Server 
    (NGINX, Apache, Varnish, Squid)
  • Applications
    (Laravel View Caching, WP Super Cache,
    ASP.NET OutputCache)

Static Content, HTML, JSON, XML, etc.

Output Caching

Static Content, HTML, JSON, XML

Why? Save trips upstream for content; save computation cycles.

Output Caching

Static Content, HTML, JSON, XML

Data Caching

  • Applications
    (File system, APC, Memcached, Redis)

Serialized representations of data

Data Caching

Serialized representations of data

Data Caching

Serialized representations of data

Why? Save trips to database; save computation cycles.

Data Caching

Serialized representations of data

The sole topic of
this conversation.

Explain caching with
abstraction "layers"

Basic

Basic

Controllers depend on Models for data resolution.

Service Layer

Service Layer

Controllers depend on Service who depend on Models for data resolution.

Service Layer with Caching

Service Layer with Caching

Controllers depend on Service who depend on Models for data resolution when cache is unavailable or expired.

That's more complex!

Yes it is more complex, and it's worth it!

Implementing cache policy logic can become noisy as requirements grow and that's not the job of a controller.

How noisy?

<?php namespace App\Http\Controllers;

use App\User;

class UserController
{
    public function getUsers()
    {
        return User::all();
    }
}

We need to list all users.

Easy!

<?php namespace App\Http\Controllers;

use App\User;
use Illuminate\Support\Facades\Cache;

class UserController
{
    public function getUsers()
    {
        return Cache::remember('users', 10, function() {
            return User::all();
        });
    }
}

We need to add caching.

laravel.com/docs/5.1/cache

That's not that bad.

Until reality sets in...

<?php namespace App\Http\Controllers;

use App\User;
use Illuminate\Support\Facades\Cache;

class UserController
{
    public function getUsers()
    {
        return Cache::remember('users', 10, function() {
            return User::paginate();
        });
    }
}

We have too many users for "all."

<?php namespace App\Http\Controllers;

use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;

class UserController
{
    public function getUsers(Request $request)
    {
        $page = $request->input('page', 1);

        return Cache::remember('users.page('.$page.')', 10, function() {
            return User::paginate();
        });
    }
}

"getUsers" always returns page 1.

<?php namespace App\Http\Controllers;

use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;

class UserController
{
    public function getUsers(Request $request)
    {
        $take = $request->input('take', 15);
        $page = $request->input('page', 1);

        return Cache::remember(
            'users.page('.$page.').take('.$take.')', 
            10, 
            function() use ($take) {
                return User::paginate($take);
            }
        );
    }
}

We need to support dynamic records per page.

<?php namespace App\Http\Controllers;

use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;

class UserController
{
    public function getUsers(Request $request)
    {
        $take = $request->input('take', 15);
        $page = $request->input('page', 1);

        return Cache::remember('users.page('.$page.').take('.$take.')', 10, function() use ($take) {
            return User::paginate($take);
        });
    }

    public function addUser(Request $request)
    {
        $user = User::create($request->all());

        Cache::flush();

        return $user;
    }
}

New users don't appear in "getUsers" for 10 minutes.

<?php namespace App\Http\Controllers;

use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;

class UserController
{
    public function getUsers(Request $request)
    {
        $take = $request->input('take', 15);
        $page = $request->input('page', 1);

        return Cache::remember('users.page('.$page.').take('.$take.')', 10, function() use ($take) {
            return User::paginate($take);
        });
    }

    public function addUser(Request $request)
    {
        $user = User::create($request->all());

        // Something that will only clear cache 
        // keys that start with "users"?

        return $user;
    }
}

We can't clear all application cache when a user is added.

We can do it!

Add abstraction layer

The plan

  • Create a service class
  • Inject service object into controller
  • Update controller methods
<?php namespace App\Services;

use App\Contracts\UserServiceable;
use App\User;

class UserService implements UserServiceable
{
    public function getPaginatedUsers($perPage = 15, $page = 1)
    {
        return User::paginate($perPage);
    }

    public function createUser($attributes = [])
    {
        return User::create($attributes);
    }
}

Create a service class

<?php namespace App\Contracts;

interface UserServiceable
{
    public function getPaginatedUsers($perPage = 15, $page = 1);
    public function createUser($attributes = []);
}
<?php namespace App\Http\Controllers;

use App\Contracts\UserServiceable;
use Illuminate\Http\Request;

class UserController
{
    public function __construct(UserServiceable $user)
    {
        $this->user = $user;
    }

    public function getUsers(Request $request)
    {
        //
    }

    public function addUser(Request $request)
    {
        //
    }
}

Inject service object into controller

<?php namespace App\Http\Controllers;

use App\Contracts\UserServiceable;
use Illuminate\Http\Request;

class UserController
{
    public function __construct(UserServiceable $user)
    {
        $this->user = $user;
    }

    public function getUsers(Request $request)
    {
        $take = $request->input('take', 15);
        $page = $request->input('page', 1);

        return $this->user->getPaginatedUsers($take, $page);
    }

    public function addUser(Request $request)
    {
        return $this->user->createUser($request->all());
    }
}

Update controller methods

Not comprehensive.
We're ignoring:

  • Interface/Class service
    container binding
  • Validation
  • Middleware
  • Other legitimate
    controller responsibilities

Clean controllers!

Add caching to abstraction layer

<?php namespace App\Services;

use App\Contracts\UserServiceable;
use App\User;
use Illuminate\Support\Facades\Cache;

class UserService implements UserServiceable
{
    public function getPaginatedUsers($perPage = 15, $page = 1)
    {
        return Cache::remember('users.page('.$page.').take('.$perPage.')', 10, function() use ($perPage) {
            return User::paginate($perPage);
        });
    }

    public function createUser($attributes = [])
    {
        $user = User::create($attributes);

        Cache::flush();

        return $user;
    }
}

Add DIY caching to service class

There is a package for that.

I wrote it.

laravel-cache

The package provides

  • A "cache" method to
    enforce cache policy
  • No responsibility for query
    construction; only execution
  • Per service cache
    policy configuration
  • Cache key indexing
  • Regular expression based
    selective cache flushing
$ composer require stevenmaguire/laravel-cache

Install with Composer

github.com/stevenmaguire/laravel-cache

<?php namespace App\Services;

use App\Contracts\UserServiceable;
use App\User;
use Illuminate\Support\Facades\Cache;
use Stevenmaguire\Laravel\Services\EloquentCacheTrait;

class UserService implements UserServiceable
{
    use EloquentCacheTrait;
    // Service methods
}

Include the base service

<?php namespace App\Services;

use App\Contracts\UserServiceable;
use App\User;
use Illuminate\Support\Facades\Cache;
use Stevenmaguire\Laravel\Services\EloquentCache;

class UserService extends EloquentCache implements UserServiceable
{
    // Service methods
}

as a trait

as a parent class

Build queries using Eloquent and request cache object

<?php namespace App\Services;

use App\Contracts\UserServiceable;
use App\User;
use Illuminate\Support\Facades\Cache;
use Stevenmaguire\Laravel\Services\EloquentCache;

class UserService extends EloquentCache implements UserServiceable
{
    public function __construct(User $user)
    {
        $this->user = $user;
    }    

    public function getPaginatedUsers($perPage = 15, $page = 1)
    {
        $query = $this->user->query();
        $key = 'users.page('.$page.').take('.$perPage.')';
        $verb = 'paginate:'.$perPage;

        return $this->cache($key, $query, $verb);        
    }

    public function createUser($attributes = [])
    {
        $user = User::create($attributes);

        $this->flushCache();

        return $user;
    }
}

'cache' method

Takes three parameters:

  • The unique key associated with the method's intentions
  • The query Builder object for the Eloquent query
  • The optional verb, get, first, list, paginate etc; get by default

'cache' method

If the method associated with the optional verb takes parameters, like paginate, the parameters can be expressed as a comma separated list following the verb and a colon. If a parameter expects an array of literal values, these may be expressed as a pipe delimited sting.

'cache' method

public function getPaginatedUsers($perPage = 15, $page = 1)
{
    $query = $this->user->query();
    $key = 'users.page('.$page.').take('.$perPage.')';
    $verb = 'paginate:'.$perPage;

    return $this->cache($key, $query, $verb);    
    // $query->paginate(15);    
}

Configure cache policy

<?php namespace App\Services;

use App\Contracts\UserServiceable;
use App\User;
use Illuminate\Support\Facades\Cache;
use Stevenmaguire\Laravel\Services\EloquentCache;

class UserService extends EloquentCache implements UserServiceable
{
    // Cache duration in minutes, default 15
    protected $cacheForMinutes = 15;

    // Enable caching, default true
    protected $enableCaching = false;

    // Enable logging, default true
    protected $enableLogging = false;

    // Service methods
}

Flush all cache for service

<?php namespace App\Services;

use App\Contracts\UserServiceable;
use App\User;
use Illuminate\Support\Facades\Cache;
use Stevenmaguire\Laravel\Services\EloquentCache;

class UserService extends EloquentCache implements UserServiceable
{
    // Service methods

    public function flushServiceCache()
    {
        $this->flushCache();
    }
}

Flush cache with regex pattern matching

<?php namespace App\Services;

use App\Contracts\UserServiceable;
use App\User;
use Illuminate\Support\Facades\Cache;
use Stevenmaguire\Laravel\Services\EloquentCache;

class UserService extends EloquentCache implements UserServiceable
{
    public function getPaginatedUsers($perPage = 15, $page = 1)
    {
        $query = $this->user->query();
        $key = 'users.page('.$page.').take('.$perPage.')';
        $verb = 'paginate:'.$perPage;
    
        return $this->cache($key, $query, $verb);    
    }

    public function flushUserCache()
    {
        $this->flushCache('^users\.');
    }
}

Demo

github.com/stevenmaguire/bydreco-service

(bit.ly/laravel-caching-layer-demo)

Questions?

Thank you!

@stevenmaguire
on twitter

stevenmaguire@gmail.com
on electronic mail

stevenmaguire
on github

Implementing a Data Caching Layer in Laravel 5

By Steven Maguire

Implementing a Data Caching Layer in Laravel 5

Deck originally created for a presentation to a gathering of the Chicago Laravel Meetup group - bit.ly/laravel-caching-layer

  • 5,497