Overview

Namespaces

  • Charcoal
    • App
      • Action
      • Config
      • Handler
      • Middleware
      • Module
      • Route
      • Script
      • ServiceProvider
      • Template

Classes

  • Charcoal\App\Action\AbstractAction
  • Charcoal\App\App
  • Charcoal\App\AppConfig
  • Charcoal\App\AppContainer
  • Charcoal\App\Config\CacheConfig
  • Charcoal\App\Config\DatabaseConfig
  • Charcoal\App\Config\FilesystemConfig
  • Charcoal\App\Config\LoggerConfig
  • Charcoal\App\Config\MemcacheCacheConfig
  • Charcoal\App\Config\MemcacheCacheServerConfig
  • Charcoal\App\Handler\AbstractHandler
  • Charcoal\App\Handler\Error
  • Charcoal\App\Handler\HandlerConfig
  • Charcoal\App\Handler\NotAllowed
  • Charcoal\App\Handler\NotFound
  • Charcoal\App\Handler\PhpError
  • Charcoal\App\Handler\Shutdown
  • Charcoal\App\Middleware\CacheMiddleware
  • Charcoal\App\Module\AbstractModule
  • Charcoal\App\Module\ModuleConfig
  • Charcoal\App\Module\ModuleManager
  • Charcoal\App\Route\ActionRoute
  • Charcoal\App\Route\ActionRouteConfig
  • Charcoal\App\Route\RouteConfig
  • Charcoal\App\Route\RouteManager
  • Charcoal\App\Route\ScriptRoute
  • Charcoal\App\Route\ScriptRouteConfig
  • Charcoal\App\Route\TemplateRoute
  • Charcoal\App\Route\TemplateRouteConfig
  • Charcoal\App\Script\AbstractScript
  • Charcoal\App\ServiceProvider\AppServiceProvider
  • Charcoal\App\ServiceProvider\CacheServiceProvider
  • Charcoal\App\ServiceProvider\DatabaseServiceProvider
  • Charcoal\App\ServiceProvider\FilesystemServiceProvider
  • Charcoal\App\ServiceProvider\LoggerServiceProvider
  • Charcoal\App\ServiceProvider\ViewServiceProvider
  • Charcoal\App\Template\AbstractTemplate
  • Charcoal\App\Template\AbstractWidget
  • Charcoal\App\Template\WidgetBuilder

Interfaces

  • Charcoal\App\Action\ActionInterface
  • Charcoal\App\AppAwareInterface
  • Charcoal\App\Handler\HandlerInterface
  • Charcoal\App\Module\ModuleInterface
  • Charcoal\App\Route\RouteInterface
  • Charcoal\App\Script\CronScriptInterface
  • Charcoal\App\Script\ScriptInterface
  • Charcoal\App\Template\TemplateInterface
  • Charcoal\App\Template\WidgetInterface

Traits

  • Charcoal\App\AppAwareTrait
  • Charcoal\App\CallableResolverAwareTrait
  • Charcoal\App\Script\ArgScriptTrait
  • Charcoal\App\Script\CronScriptTrait
  • Charcoal\App\Script\PathScriptTrait
  • Overview
  • Namespace
  • Class
  1: <?php
  2: 
  3: namespace Charcoal\App\Middleware;
  4: 
  5: // Dependencies from 'PSR-6' (Caching)
  6: use Psr\Cache\CacheItemPoolInterface;
  7: 
  8: // Dependencies from 'PSR-7' (HTTP Messaging)
  9: use \Psr\Http\Message\RequestInterface;
 10: use \Psr\Http\Message\ResponseInterface;
 11: 
 12: /**
 13:  * The cache loader middleware attempts to load cache from the request's path (route).
 14:  *
 15:  * It should be run as the first middleware of the stack, in most cases.
 16:  * (With Slim, this means adding it last)
 17:  *
 18:  * There is absolutely no extra configuration or dependencies to this middleware.
 19:  * If the cache key exists, then it will be injected in the response body and returned.
 20:  *
 21:  * Note that if the cache is hit, the response is directly returned; meaning the rest
 22:  * of the middilewares in the stack will be ignored
 23:  *
 24:  * It is up to other means, such as the provided `CacheGeneratorMiddleware`, to set this cache entry.
 25:  */
 26: class CacheMiddleware
 27: {
 28:     /**
 29:      * PSR-6 cache item pool.
 30:      * @var CacheItemPool
 31:      */
 32:     private $cachePool;
 33: 
 34:     /**
 35:      * @var integer
 36:      */
 37:     private $cacheTtl;
 38: 
 39:     /**
 40:      * @var string
 41:      */
 42:     private $includedPath;
 43: 
 44:     /**
 45:      * @var string
 46:      */
 47:     private $excludedPath;
 48: 
 49:     /**
 50:      * @var string[]
 51:      */
 52:     private $methods;
 53: 
 54:     /**
 55:      * @var int[]
 56:      */
 57:     private $statusCode;
 58: 
 59:     /**
 60:      * @var array|string|null
 61:      */
 62:     private $includedQuery;
 63: 
 64:     /**
 65:      * @var array|string|null
 66:      */
 67:     private $excludedQuery;
 68: 
 69:     /**
 70:      * @var array|string|null
 71:      */
 72:     private $ignoredQuery;
 73: 
 74:     /**
 75:      * @var boolean
 76:      */
 77:     private $headers;
 78: 
 79:     /**
 80:      * @param array $data Constructor dependencies and options.
 81:      */
 82:     public function __construct(array $data)
 83:     {
 84:         $defaults = [
 85:             'included_path'  => null,
 86:             'excluded_path'  => null,
 87:             'methods'        => [
 88:                 'GET'
 89:             ],
 90:             'status_codes'   => [
 91:                 200
 92:             ],
 93:             'ttl'            => 864000,
 94:             'included_query' => null,
 95:             'excluded_query' => null,
 96:             'ignored_query'  => null,
 97:             'headers'        => true
 98:         ];
 99:         $data = array_merge($defaults, $data);
100: 
101:         $this->cachePool = $data['cache'];
102:         $this->cacheTtl = $data['ttl'];
103: 
104:         $this->includedPath = $data['included_path'];
105:         $this->excludedPath = $data['excluded_path'];
106: 
107:         $this->methods = $data['methods'];
108:         $this->statusCodes = $data['status_codes'];
109: 
110:         $this->includedQuery = $data['included_query'];
111:         $this->excludedQuery = $data['excluded_query'];
112:         $this->ignoredQuery = $data['ignored_query'];
113: 
114:         $this->headers = $data['headers'];
115:     }
116: 
117:     /**
118:      * Load a route content from path's cache.
119:      *
120:      * This method is as dumb / simple as possible.
121:      * It does not rely on any sort of settings / configuration.
122:      * Simply: if the cache for the route exists, it will be used to display the page.
123:      * The `$next` callback will not be called, therefore stopping the middleware stack.
124:      *
125:      * To generate the cache used in this middleware, @see \Charcoal\App\Middleware\CacheGeneratorMiddleware.
126:      *
127:      * @param RequestInterface  $request  The PSR-7 HTTP request.
128:      * @param ResponseInterface $response The PSR-7 HTTP response.
129:      * @param callable          $next     The next middleware callable in the stack.
130:      * @return ResponseInterface
131:      */
132:     public function __invoke(RequestInterface $request, ResponseInterface $response, callable $next)
133:     {
134:         $path = $request->getUri()->getPath();
135:         $queryParams = $request->getQueryParams();
136:         $cacheKey  = $this->cacheKey($path, $queryParams);
137: 
138:         if ($this->cachePool->hasItem($cacheKey)) {
139:             $cacheItem = $this->cachePool->getItem($cacheKey);
140:             $cached = $cacheItem->get();
141:             $response->getBody()->write($cached);
142:             return $response;
143:         } else {
144:             $included = $this->isQueryIncluded($queryParams);
145:             $response = $next($request, $response);
146: 
147:             if ($this->isActive($request, $response) === false) {
148:                 return $response;
149:             }
150:             if ($this->isPathIncluded($path) === false) {
151:                 return $response;
152:             }
153:             if ($this->isPathExcluded($path) === true) {
154:                 return $response;
155:             }
156: 
157:             if ($this->isQueryIncluded($queryParams) === false) {
158:                 $keyParams = $this->parseIgnoredParams($queryParams);
159:                 if (!empty($keyParams)) {
160:                     return $response;
161:                 }
162:             }
163: 
164:             if ($this->isQueryExcluded($queryParams) === true) {
165:                 return $response;
166:             }
167: 
168:             $cacheItem = $this->cachePool->getItem($cacheKey);
169:             $cacheItem->expiresAfter($this->cacheTtl);
170:             $this->cachePool->save($cacheItem->set((string)$response->getBody()));
171: 
172:             return $response;
173:         }
174:     }
175: 
176:     /**
177:      * @param string $path        The query path (route).
178:      * @param array  $queryParams The query parameters.
179:      * @return string
180:      */
181:     private function cacheKey($path, array $queryParams)
182:     {
183:         $cacheKey  = 'request/'.str_replace('/', '.', $path);
184:         if (!empty($queryParams)) {
185:             $keyParams = $this->parseIgnoredParams($queryParams);
186:             $cacheKey .= '.'.md5(json_encode($keyParams));
187:         }
188:         return $cacheKey;
189:     }
190: 
191: 
192:     /**
193:      * @param RequestInterface  $request  The PSR-7 HTTP request.
194:      * @param ResponseInterface $response The PSR-7 HTTP response.
195:      * @return boolean
196:      */
197:     private function isActive(RequestInterface $request, ResponseInterface $response)
198:     {
199:         if (!in_array($response->getStatusCode(), $this->statusCodes)) {
200:             return false;
201:         }
202:         if (!in_array($request->getMethod(), $this->methods)) {
203:             return false;
204:         }
205:         return true;
206:     }
207: 
208:     /**
209:      * @param string $path The request path (route) to verify.
210:      * @return boolean
211:      */
212:     private function isPathIncluded($path)
213:     {
214:         if ($this->includedPath == '*') {
215:             return true;
216:         }
217:         if (!$this->includedPath || empty($this->includedPath)) {
218:             return false;
219:         }
220:         if (is_string($this->includedPath)) {
221:             return !!(preg_match('@'.$this->includedPath.'@', $path));
222:         }
223:         if (is_array($this->includedPath)) {
224:             foreach ($this->includedPath as $included) {
225:                 if (preg_match('@'.$included.'@', $path)) {
226:                     return true;
227:                 }
228:             }
229:             return false;
230:         }
231:     }
232: 
233:     /**
234:      * @param string $path The request path (route) to verify.
235:      * @return boolean
236:      */
237:     private function isPathExcluded($path)
238:     {
239:         if ($this->excludedPath == '*') {
240:             return true;
241:         }
242:         if (!$this->excludedPath || empty($this->excludedPath)) {
243:             return false;
244:         }
245:         if (is_string($this->excludedPath)) {
246:             return !!(preg_match('@'.$this->excludedPath.'@', $path));
247:         }
248:         if (is_array($this->excludedPath)) {
249:             foreach ($this->excludedPath as $excluded) {
250:                 if (preg_match('@'.$excluded.'@', $path)) {
251:                     return true;
252:                 }
253:             }
254:             return false;
255:         }
256:     }
257: 
258:     /**
259:      * @param array $queryParams The query parameters.
260:      * @return boolean
261:      */
262:     private function isQueryIncluded(array $queryParams)
263:     {
264:         if (empty($queryParams)) {
265:             return true;
266:         }
267:         if ($this->includedQuery == '*') {
268:             return true;
269:         }
270:         if (!is_array($this->includedQuery) || empty($this->includedQuery)) {
271:             return false;
272:         }
273:         return (count(array_intersect_key($queryParams, $this->includedQuery)) > 0);
274:     }
275: 
276:     /**
277:      * @param array $queryParams The query parameters.
278:      * @return boolean
279:      */
280:     private function isQueryExcluded(array $queryParams)
281:     {
282:         if ($this->excludedQuery == '*') {
283:             return true;
284:         }
285:         if (!is_array($this->excludedQuery) || empty($this->excludedQuery)) {
286:             return false;
287:         }
288:         if (count(array_intersect_key($queryParams, array_flip($this->excludedQuery))) > 0) {
289:             return true;
290:         }
291:     }
292: 
293:     /**
294:      * @param array $queryParams The query parameters.
295:      * @return array
296:      */
297:     private function parseIgnoredParams(array $queryParams)
298:     {
299:         if ($this->ignoredQuery == '*') {
300:             $ret = [];
301:             if (is_array($this->includedQuery)) {
302:                 foreach ($queryParams as $k => $v) {
303:                     if (in_array($k, $this->includedQuery)) {
304:                         $ret[$k] = $v;
305:                     }
306:                 }
307:             }
308:             return $ret;
309:         }
310:         if (!is_array($this->ignoredQuery) || empty($this->ignoredQuery)) {
311:             return $queryParams;
312:         }
313:         $ret = [];
314:         foreach ($queryParams as $k => $v) {
315:             if (!in_array($k, $this->ignoredQuery)) {
316:                 $ret[$k] = $v;
317:             }
318:         }
319:         return $queryParams;
320:     }
321: }
322: 
API documentation generated by ApiGen