1: <?php
2:
3: namespace Charcoal\App\Middleware;
4:
5:
6: use Psr\Cache\CacheItemPoolInterface;
7:
8:
9: use \Psr\Http\Message\RequestInterface;
10: use \Psr\Http\Message\ResponseInterface;
11:
12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25:
26: class CacheMiddleware
27: {
28: 29: 30: 31:
32: private $cachePool;
33:
34: 35: 36:
37: private $cacheTtl;
38:
39: 40: 41:
42: private $includedPath;
43:
44: 45: 46:
47: private $excludedPath;
48:
49: 50: 51:
52: private $methods;
53:
54: 55: 56:
57: private $statusCode;
58:
59: 60: 61:
62: private $includedQuery;
63:
64: 65: 66:
67: private $excludedQuery;
68:
69: 70: 71:
72: private $ignoredQuery;
73:
74: 75: 76:
77: private $headers;
78:
79: 80: 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: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 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: 178: 179: 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: 194: 195: 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: 210: 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: 235: 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: 260: 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: 278: 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: 295: 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: