Skip to content

Instantly share code, notes, and snippets.

@Zizaco
Last active May 31, 2023 10:04
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save Zizaco/1777fe7566f334dc04d83b17f7832007 to your computer and use it in GitHub Desktop.
Save Zizaco/1777fe7566f334dc04d83b17f7832007 to your computer and use it in GitHub Desktop.
<?php
namespace App\Http\Middleware;
use Closure;
use App\Services\Cache\ActionCacheManager;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
/**
* Middleware that will cache controller action responses
*/
class ActionCache
{
protected $cacheManager;
/**
* Injects the class dependencies
* @param ActionCacheManager $cacheManager To manage the cached actions.
*/
public function __construct(ActionCacheManager $cacheManager)
{
$this->cacheManager = $cacheManager;
}
/**
* Run the request middleware.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
*/
public function handle($request, Closure $next)
{
$path = $request->path();
if ($this->cacheManager->isCached($path) && $this->canBeCached($request)) {
return $this->cacheManager->get($path);
}
return $next($request);
}
/**
* Sometimes a middleware may need to do some work after the HTTP response
* has already been sent to the browser. The ActionCache middleware will
* store the response using the cacheManager for it to be used when
* handling the next request.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Http\Response $response
*/
public function terminate($request, $response)
{
$path = $request->path();
if ($this->cacheManager->isCached($path) || !$this->canBeCached($request)) {
return;
}
$this->cleanCookies($response);
$this->cacheManager->cache($path, $response);
}
/**
* Tells if a request can be cached
*
* @param \Illuminate\Http\Request $request
*
* @return boolean
*/
protected function canBeCached($request)
{
return ! $request->get('preview');
}
/**
* Clean the cookies of the given response
*
* @param \Illuminate\Http\Response $response
*/
protected function cleanCookies($response)
{
foreach ($response->headers->getCookies(ResponseHeaderBag::COOKIES_ARRAY) as $path) {
foreach ($path as $cookies) {
foreach ($cookies as $cookie) {
$response->headers->removeCookie($cookie->getName(), $cookie->getPath(), $cookie->getDomain());
}
}
}
}
}
<?php
namespace App\Services\Cache;
/**
* Manages how controller actions are cached
*/
class ActionCacheManager extends BaseManager
{
/**
* Generates a cache key for the given url. It's here that you
* should differentiate the cache key for each request. Parameters
* such as "page" should be considered in order not to overlap the
* cache keys.
*
* @param string $key String that identifies a resource.
*
* @return string Cache key for the given $key.
*/
protected function getCacheKeyFor(string $key)
{
// Avoid "cache conflict" between different methods like, GET, POST, etc
$method = request()->method();
// Avoid "cache conflict" from ajax routes. If a controller answers differently when json is wanted
if (request()->ajax() || request('json')) {
$method = $method . "_ajax";
}
// Avoid "cache conflict" between domains.
$domainHash = substr(md5(request()->root()), 0, 5);
// Avoid "cache conflict" for other stuff, "page", "language", etc.
return sprintf(
'%s:%s:%s:%s',
static::KEY_PREFIX,
$domainHash,
$key,
strtolower($method)
);
}
}
<?php
namespace App\Services\Cache;
use Illuminate\Contracts\Cache\Repository;
/**
* Base class for specific purpose cache managers
*/
abstract class BaseManager
{
const KEY_PREFIX = "managedcache";
protected $cacheRepo;
/**
* Injects the dependencies of the class
*
* @param Repository $cacheRepo Cache repository being used by the manager.
*/
public function __construct(Repository $cacheRepo)
{
$this->cacheRepo = $cacheRepo;
}
/**
* Checks if the given identificator is cached
*
* @param string $key String that identifies a value that may be cached.
*
* @return boolean Is the resource of the given key is cached?
*/
public function isCached(string $key)
{
return $this->cacheRepo->has($this->getCacheKeyFor($key));
}
/**
* Caches the given $value within the $key
*
* @param string $key String that identifies the value being cached.
* @param mixed $value Value being cached.
* @param integer $minutes For how long will the cache be stored.
*
* @return void
*/
public function cache(string $key, $value, int $minutes = 10)
{
$this->cacheRepo->put(
$this->getCacheKeyFor($key),
$value,
$minutes
);
}
/**
* Retrieves a cached value
*
* @param string $key String that identifies a value.
*
* @return mixed
*/
public function get(string $key)
{
return $this->cacheRepo->get($this->getCacheKeyFor($key));
}
/**
* Generates a cache key for the given key
*
* @param string $key String that identifies a resource.
*
* @return string Cache key for the given $key.
*/
abstract protected function getCacheKeyFor(string $key);
}
<?php
if (! function_exists('cached_include')) {
/**
* Mimics the blade include function but caches the rendered for $time
* minutes in key 'cache:partials:<view_name>:<context>_<$cache_key>'
* in order to avoid conflicts and to be able to flush the view using
* patterns.
* Ex: Cache::clear('cache:partials:myview_*');
*
* @param string $view The name of the view that are going to be included.
* @param array $vars Variables that are being passed to the view.
* @param integer $time The amount of minutes that the result of the view is going to be stored.
*
* @return string Html code of the included view
*/
function cached_include($view, $vars = null, $time = 60)
{
$cacheKey = $view . md5(serialize($vars));
return Cache::remember(
$cacheKey,
$time,
function () use ($view) {
return view(
$view,
is_array($vars) ? $vars : array_except(get_defined_vars(), ['__data', '__path'])
)->render();
}
);
}
/**
* Registers the cached_include as blade's @cached_include in order to be able
* to be used in the template engine.
*/
Blade::extend(
function ($view) {
$pattern = '/@cached_include\((.*)\)/';
return preg_replace($pattern, "<?php echo cached_include($1); ?>", $view);
}
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment