1 <?php 2 3 namespace Kevinrob\GuzzleCache\Strategy; 4 5 use Kevinrob\GuzzleCache\CacheEntry; 6 use Kevinrob\GuzzleCache\KeyValueHttpHeader; 7 use Kevinrob\GuzzleCache\Storage\CacheStorageInterface; 8 use Kevinrob\GuzzleCache\Storage\VolatileRuntimeStorage; 9 use Psr\Http\Message\RequestInterface; 10 use Psr\Http\Message\ResponseInterface; 11 12 /** 13 * This strategy represents a "private" HTTP client. 14 * Pay attention to share storage between application with caution! 15 * 16 * For example, a response with cache-control header "private, max-age=60" 17 * will be cached by this strategy. 18 * 19 * The rules applied are from RFC 7234. 20 * 21 * @see https://tools.ietf.org/html/rfc7234 22 */ 23 class PrivateCacheStrategy implements CacheStrategyInterface 24 { 25 /** 26 * @var CacheStorageInterface 27 */ 28 protected $storage; 29 30 /** 31 * @var int[] 32 */ 33 protected $statusAccepted = [ 34 200 => 200, 35 203 => 203, 36 204 => 204, 37 300 => 300, 38 301 => 301, 39 404 => 404, 40 405 => 405, 41 410 => 410, 42 414 => 414, 43 418 => 418, 44 501 => 501, 45 ]; 46 47 /** 48 * @var string[] 49 */ 50 protected $ageKey = [ 51 'max-age', 52 ]; 53 54 public function __construct(CacheStorageInterface $cache = null) 55 { 56 $this->storage = $cache !== null ? $cache : new VolatileRuntimeStorage(); 57 } 58 59 /** 60 * @param RequestInterface $request 61 * @param ResponseInterface $response 62 * @return CacheEntry|null entry to save, null if can't cache it 63 */ 64 protected function getCacheObject(RequestInterface $request, ResponseInterface $response) 65 { 66 if (!isset($this->statusAccepted[$response->getStatusCode()])) { 67 // Don't cache it 68 return; 69 } 70 71 $cacheControl = new KeyValueHttpHeader($response->getHeader('Cache-Control')); 72 $varyHeader = new KeyValueHttpHeader($response->getHeader('Vary')); 73 74 if ($varyHeader->has('*')) { 75 // This will never match with a request 76 return; 77 } 78 79 if ($cacheControl->has('no-store')) { 80 // No store allowed (maybe some sensitives data...) 81 return; 82 } 83 84 if ($cacheControl->has('no-cache')) { 85 // Stale response see RFC7234 section 5.2.1.4 86 $entry = new CacheEntry($request, $response, new \DateTime('-1 seconds')); 87 88 return $entry->hasValidationInformation() ? $entry : null; 89 } 90 91 foreach ($this->ageKey as $key) { 92 if ($cacheControl->has($key)) { 93 return new CacheEntry( 94 $request, 95 $response, 96 new \DateTime('+'.(int) $cacheControl->get($key).'seconds') 97 ); 98 } 99 } 100 101 if ($response->hasHeader('Expires')) { 102 $expireAt = \DateTime::createFromFormat(\DateTime::RFC1123, $response->getHeaderLine('Expires')); 103 if ($expireAt !== false) { 104 return new CacheEntry( 105 $request, 106 $response, 107 $expireAt 108 ); 109 } 110 } 111 112 return new CacheEntry($request, $response, new \DateTime('-1 seconds')); 113 } 114 115 /** 116 * Generate a key for the response cache. 117 * 118 * @param RequestInterface $request 119 * @param null|KeyValueHttpHeader $varyHeaders The vary headers which should be honoured by the cache (optional) 120 * 121 * @return string 122 */ 123 protected function getCacheKey(RequestInterface $request, KeyValueHttpHeader $varyHeaders = null) 124 { 125 if (!$varyHeaders) { 126 return hash('sha256', $request->getMethod().$request->getUri()); 127 } 128 129 $cacheHeaders = []; 130 131 foreach ($varyHeaders as $key => $value) { 132 if ($request->hasHeader($key)) { 133 $cacheHeaders[$key] = $request->getHeader($key); 134 } 135 } 136 137 return hash('sha256', $request->getMethod().$request->getUri().json_encode($cacheHeaders)); 138 } 139 140 /** 141 * Return a CacheEntry or null if no cache. 142 * 143 * @param RequestInterface $request 144 * 145 * @return CacheEntry|null 146 */ 147 public function fetch(RequestInterface $request) 148 { 149 /** @var int|null $maxAge */ 150 $maxAge = null; 151 152 if ($request->hasHeader('Cache-Control')) { 153 $reqCacheControl = new KeyValueHttpHeader($request->getHeader('Cache-Control')); 154 if ($reqCacheControl->has('no-cache')) { 155 // Can't return cache 156 return null; 157 } 158 159 $maxAge = $reqCacheControl->get('max-age', null); 160 } elseif ($request->hasHeader('Pragma')) { 161 $pragma = new KeyValueHttpHeader($request->getHeader('Pragma')); 162 if ($pragma->has('no-cache')) { 163 // Can't return cache 164 return null; 165 } 166 } 167 168 $cache = $this->storage->fetch($this->getCacheKey($request)); 169 if ($cache !== null) { 170 $varyHeaders = $cache->getVaryHeaders(); 171 172 // vary headers exist from a previous response, check if we have a cache that matches those headers 173 if (!$varyHeaders->isEmpty()) { 174 $cache = $this->storage->fetch($this->getCacheKey($request, $varyHeaders)); 175 176 if (!$cache) { 177 return null; 178 } 179 } 180 181 if ((string)$cache->getOriginalRequest()->getUri() !== (string)$request->getUri()) { 182 return null; 183 } 184 185 if ($maxAge !== null) { 186 if ($cache->getAge() > $maxAge) { 187 // Cache entry is too old for the request requirements! 188 return null; 189 } 190 } 191 192 if (!$cache->isVaryEquals($request)) { 193 return null; 194 } 195 } 196 197 return $cache; 198 } 199 200 /** 201 * @param RequestInterface $request 202 * @param ResponseInterface $response 203 * 204 * @return bool true if success 205 */ 206 public function cache(RequestInterface $request, ResponseInterface $response) 207 { 208 $reqCacheControl = new KeyValueHttpHeader($request->getHeader('Cache-Control')); 209 if ($reqCacheControl->has('no-store')) { 210 // No caching allowed 211 return false; 212 } 213 214 $cacheObject = $this->getCacheObject($request, $response); 215 if ($cacheObject !== null) { 216 // store the cache against the URI-only key 217 $success = $this->storage->save( 218 $this->getCacheKey($request), 219 $cacheObject 220 ); 221 222 $varyHeaders = $cacheObject->getVaryHeaders(); 223 224 if (!$varyHeaders->isEmpty()) { 225 // also store the cache against the vary headers based key 226 $success = $this->storage->save( 227 $this->getCacheKey($request, $varyHeaders), 228 $cacheObject 229 ); 230 } 231 232 return $success; 233 } 234 235 return false; 236 } 237 238 /** 239 * @param RequestInterface $request 240 * @param ResponseInterface $response 241 * 242 * @return bool true if success 243 */ 244 public function update(RequestInterface $request, ResponseInterface $response) 245 { 246 return $this->cache($request, $response); 247 } 248 249 /** 250 * {@inheritdoc} 251 */ 252 public function delete(RequestInterface $request) 253 { 254 return $this->storage->delete($this->getCacheKey($request)); 255 } 256 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body