Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like CacheMiddleware often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use CacheMiddleware, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
19 | class CacheMiddleware |
||
20 | { |
||
21 | const HEADER_RE_VALIDATION = 'X-Kevinrob-GuzzleCache-ReValidation'; |
||
22 | const HEADER_INVALIDATION = 'X-Kevinrob-GuzzleCache-Invalidation'; |
||
23 | const HEADER_CACHE_INFO = 'X-Kevinrob-Cache'; |
||
24 | const HEADER_CACHE_HIT = 'HIT'; |
||
25 | const HEADER_CACHE_MISS = 'MISS'; |
||
26 | const HEADER_CACHE_STALE = 'STALE'; |
||
27 | |||
28 | /** |
||
29 | * @var array of Promise |
||
30 | */ |
||
31 | protected $waitingRevalidate = []; |
||
32 | |||
33 | /** |
||
34 | * @var Client |
||
35 | */ |
||
36 | protected $client; |
||
37 | |||
38 | /** |
||
39 | * @var CacheStrategyInterface |
||
40 | */ |
||
41 | protected $cacheStorage; |
||
42 | |||
43 | /** |
||
44 | * List of allowed HTTP methods to cache |
||
45 | * Key = method name (upscaling) |
||
46 | * Value = true. |
||
47 | * |
||
48 | * @var array |
||
49 | */ |
||
50 | protected $httpMethods = ['GET' => true]; |
||
51 | |||
52 | /** |
||
53 | * @param CacheStrategyInterface|null $cacheStrategy |
||
54 | */ |
||
55 | 44 | public function __construct(CacheStrategyInterface $cacheStrategy = null) |
|
61 | |||
62 | /** |
||
63 | * @param Client $client |
||
64 | */ |
||
65 | 3 | public function setClient(Client $client) |
|
69 | |||
70 | /** |
||
71 | * @param CacheStrategyInterface $cacheStorage |
||
72 | */ |
||
73 | public function setCacheStorage(CacheStrategyInterface $cacheStorage) |
||
77 | |||
78 | /** |
||
79 | * @return CacheStrategyInterface |
||
80 | */ |
||
81 | public function getCacheStorage() |
||
85 | |||
86 | /** |
||
87 | * @param array $methods |
||
88 | */ |
||
89 | public function setHttpMethods(array $methods) |
||
93 | |||
94 | public function getHttpMethods() |
||
98 | |||
99 | /** |
||
100 | * Will be called at the end of the script. |
||
101 | */ |
||
102 | 1 | public function purgeReValidation() |
|
106 | |||
107 | /** |
||
108 | * @param callable $handler |
||
109 | * |
||
110 | * @return callable |
||
111 | */ |
||
112 | 43 | public function __invoke(callable $handler) |
|
113 | { |
||
114 | return function (RequestInterface $request, array $options) use (&$handler) { |
||
115 | 43 | if (!isset($this->httpMethods[strtoupper($request->getMethod())])) { |
|
116 | // No caching for this method allowed |
||
117 | |||
118 | 3 | return $handler($request, $options)->then( |
|
119 | function (ResponseInterface $response) use ($request) { |
||
120 | // Invalidate cache after a call of non-safe method on the same URI |
||
121 | 3 | $response = $this->invalidateCache($request, $response); |
|
122 | |||
123 | 3 | return $response->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_MISS); |
|
124 | } |
||
125 | 3 | ); |
|
126 | } |
||
127 | |||
128 | 41 | if ($request->hasHeader(self::HEADER_RE_VALIDATION)) { |
|
129 | // It's a re-validation request, so bypass the cache! |
||
130 | 1 | return $handler($request->withoutHeader(self::HEADER_RE_VALIDATION), $options); |
|
131 | } |
||
132 | |||
133 | // Retrieve information from request (Cache-Control) |
||
134 | 41 | $reqCacheControl = new KeyValueHttpHeader($request->getHeader('Cache-Control')); |
|
135 | 41 | $onlyFromCache = $reqCacheControl->has('only-if-cached'); |
|
136 | 41 | $staleResponse = $reqCacheControl->has('max-stale') |
|
137 | 41 | && $reqCacheControl->get('max-stale') === ''; |
|
138 | 41 | $maxStaleCache = $reqCacheControl->get('max-stale', null); |
|
139 | 41 | $minFreshCache = $reqCacheControl->get('min-fresh', null); |
|
140 | |||
141 | // If cache => return new FulfilledPromise(...) with response |
||
142 | 41 | $cacheEntry = $this->cacheStorage->fetch($request); |
|
143 | 41 | if ($cacheEntry instanceof CacheEntry) { |
|
144 | 34 | $body = $cacheEntry->getResponse()->getBody(); |
|
145 | 34 | if ($body->tell() > 0) { |
|
146 | 2 | $body->rewind(); |
|
147 | 2 | } |
|
148 | |||
149 | 34 | if ($cacheEntry->isFresh() |
|
150 | 34 | && ($minFreshCache === null || $cacheEntry->getStaleAge() + (int)$minFreshCache <= 0) |
|
151 | 34 | ) { |
|
152 | // Cache HIT! |
||
153 | 28 | return new FulfilledPromise( |
|
154 | 28 | $cacheEntry->getResponse()->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_HIT) |
|
155 | 28 | ); |
|
156 | } elseif ($staleResponse |
||
157 | 15 | || ($maxStaleCache !== null && $cacheEntry->getStaleAge() <= $maxStaleCache) |
|
158 | 15 | ) { |
|
159 | // Staled cache! |
||
160 | 1 | return new FulfilledPromise( |
|
161 | 1 | $cacheEntry->getResponse()->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_HIT) |
|
162 | 1 | ); |
|
163 | 15 | } elseif ($cacheEntry->hasValidationInformation() && !$onlyFromCache) { |
|
164 | // Re-validation header |
||
165 | 4 | $request = static::getRequestWithReValidationHeader($request, $cacheEntry); |
|
166 | |||
167 | 4 | if ($cacheEntry->staleWhileValidate()) { |
|
168 | 1 | static::addReValidationRequest($request, $this->cacheStorage, $cacheEntry); |
|
169 | |||
170 | 1 | return new FulfilledPromise( |
|
171 | 1 | $cacheEntry->getResponse() |
|
172 | 1 | ->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_STALE) |
|
173 | 1 | ); |
|
174 | } |
||
175 | 3 | } |
|
176 | 14 | } else { |
|
177 | 41 | $cacheEntry = null; |
|
178 | } |
||
179 | |||
180 | 41 | if ($cacheEntry === null && $onlyFromCache) { |
|
181 | // Explicit asking of a cached response => 504 |
||
182 | 1 | return new FulfilledPromise( |
|
183 | 1 | new Response(504) |
|
184 | 1 | ); |
|
185 | } |
||
186 | |||
187 | /** @var Promise $promise */ |
||
188 | 41 | $promise = $handler($request, $options); |
|
189 | |||
190 | 41 | return $promise->then( |
|
191 | function (ResponseInterface $response) use ($request, $cacheEntry) { |
||
192 | // Check if error and looking for a staled content |
||
193 | 41 | if ($response->getStatusCode() >= 500) { |
|
194 | 1 | $responseStale = static::getStaleResponse($cacheEntry); |
|
195 | 1 | if ($responseStale instanceof ResponseInterface) { |
|
196 | 1 | return $responseStale; |
|
197 | } |
||
198 | } |
||
199 | |||
200 | 41 | $update = false; |
|
201 | |||
202 | 41 | if ($response->getStatusCode() == 304 && $cacheEntry instanceof CacheEntry) { |
|
203 | // Not modified => cache entry is re-validate |
||
204 | /** @var ResponseInterface $response */ |
||
205 | $response = $response |
||
206 | 2 | ->withStatus($cacheEntry->getResponse()->getStatusCode()) |
|
207 | 2 | ->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_HIT); |
|
208 | 2 | $response = $response->withBody($cacheEntry->getResponse()->getBody()); |
|
209 | |||
210 | // Merge headers of the "304 Not Modified" and the cache entry |
||
211 | /** |
||
212 | * @var string $headerName |
||
213 | * @var string[] $headerValue |
||
214 | */ |
||
215 | 2 | View Code Duplication | foreach ($cacheEntry->getOriginalResponse()->getHeaders() as $headerName => $headerValue) { |
|
|||
216 | 2 | if (!$response->hasHeader($headerName) && $headerName !== self::HEADER_CACHE_INFO) { |
|
217 | 2 | $response = $response->withHeader($headerName, $headerValue); |
|
218 | 2 | } |
|
219 | 2 | } |
|
220 | |||
221 | 2 | $update = true; |
|
222 | 2 | } else { |
|
223 | 41 | $response = $response->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_MISS); |
|
224 | } |
||
225 | |||
226 | 41 | return static::addToCache($this->cacheStorage, $request, $response, $update); |
|
227 | 41 | }, |
|
228 | function ($reason) use ($cacheEntry) { |
||
229 | if ($reason instanceof TransferException) { |
||
230 | $response = static::getStaleResponse($cacheEntry); |
||
231 | if ($response instanceof ResponseInterface) { |
||
232 | return $response; |
||
233 | } |
||
234 | } |
||
235 | |||
236 | return new RejectedPromise($reason); |
||
237 | } |
||
238 | 41 | ); |
|
239 | 43 | }; |
|
240 | } |
||
241 | |||
242 | /** |
||
243 | * @param CacheStrategyInterface $cache |
||
244 | * @param RequestInterface $request |
||
245 | * @param ResponseInterface $response |
||
246 | * @param bool $update cache |
||
247 | * @return ResponseInterface |
||
248 | */ |
||
249 | 41 | protected static function addToCache( |
|
270 | |||
271 | /** |
||
272 | * @param RequestInterface $request |
||
273 | * @param CacheStrategyInterface $cacheStorage |
||
274 | * @param CacheEntry $cacheEntry |
||
275 | * |
||
276 | * @return bool if added |
||
277 | */ |
||
278 | 1 | protected function addReValidationRequest( |
|
316 | |||
317 | /** |
||
318 | * @param CacheEntry|null $cacheEntry |
||
319 | * |
||
320 | * @return null|ResponseInterface |
||
321 | */ |
||
322 | 1 | protected static function getStaleResponse(CacheEntry $cacheEntry = null) |
|
332 | |||
333 | /** |
||
334 | * @param RequestInterface $request |
||
335 | * @param CacheEntry $cacheEntry |
||
336 | * |
||
337 | * @return RequestInterface |
||
338 | */ |
||
339 | 4 | protected static function getRequestWithReValidationHeader(RequestInterface $request, CacheEntry $cacheEntry) |
|
356 | |||
357 | /** |
||
358 | * @param CacheStrategyInterface|null $cacheStorage |
||
359 | * |
||
360 | * @return CacheMiddleware the Middleware for Guzzle HandlerStack |
||
361 | * |
||
362 | * @deprecated Use constructor => `new CacheMiddleware()` |
||
363 | */ |
||
364 | public static function getMiddleware(CacheStrategyInterface $cacheStorage = null) |
||
368 | |||
369 | /** |
||
370 | * @param RequestInterface $request |
||
371 | * |
||
372 | * @param ResponseInterface $response |
||
373 | * |
||
374 | * @return ResponseInterface |
||
375 | */ |
||
376 | 3 | private function invalidateCache(RequestInterface $request, ResponseInterface $response) |
|
382 | } |
||
383 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.