13
13
14
14
namespace Toflar \Psr6HttpCacheStore ;
15
15
16
+ use Psr \Cache \CacheItemInterface ;
16
17
use Psr \Cache \InvalidArgumentException as CacheInvalidArgumentException ;
17
18
use Symfony \Component \Cache \Adapter \AdapterInterface ;
18
19
use Symfony \Component \Cache \Adapter \FilesystemTagAwareAdapter ;
@@ -79,6 +80,16 @@ public function __construct(array $options = [])
79
80
$ resolver ->setDefault ('generate_content_digests ' , true )
80
81
->setAllowedTypes ('generate_content_digests ' , 'boolean ' );
81
82
83
+ $ resolver ->setDefault ('gzip_level ' , 9 )
84
+ ->setAllowedTypes ('gzip_level ' , 'int ' )
85
+ ->setNormalizer ('gzip_level ' , function (Options $ options , int $ value ): int {
86
+ if ($ value < 0 || $ value > 9 ) {
87
+ throw new \InvalidArgumentException ('The gzip_level has to be between 0 (disabled) and 9. ' );
88
+ }
89
+
90
+ return $ value ;
91
+ });
92
+
82
93
$ resolver ->setDefault ('cache ' , function (Options $ options ) {
83
94
if (!isset ($ options ['cache_directory ' ])) {
84
95
throw new MissingOptionsException ('The cache_directory option is required unless you set the cache explicitly ' );
@@ -119,7 +130,7 @@ public function lookup(Request $request): ?Response
119
130
foreach ($ entries as $ varyKeyResponse => $ responseData ) {
120
131
// This can only happen if one entry only
121
132
if (self ::NON_VARYING_KEY === $ varyKeyResponse ) {
122
- return $ this ->restoreResponse ($ responseData );
133
+ return $ this ->restoreResponse ($ request , $ responseData );
123
134
}
124
135
125
136
// Otherwise we have to see if Vary headers match
@@ -129,7 +140,7 @@ public function lookup(Request $request): ?Response
129
140
);
130
141
131
142
if ($ varyKeyRequest === $ varyKeyResponse ) {
132
- return $ this ->restoreResponse ($ responseData );
143
+ return $ this ->restoreResponse ($ request , $ responseData );
133
144
}
134
145
}
135
146
@@ -146,8 +157,6 @@ public function write(Request $request, Response $response): string
146
157
$ this ->saveContentDigest ($ response );
147
158
148
159
$ cacheKey = $ this ->getCacheKey ($ request );
149
- $ headers = $ response ->headers ->all ();
150
- unset($ headers ['age ' ]);
151
160
152
161
/** @var CacheItem $item */
153
162
$ item = $ this ->cache ->getItem ($ cacheKey );
@@ -162,16 +171,19 @@ public function write(Request $request, Response $response): string
162
171
$ varyKey = $ this ->getVaryKey ($ response ->getVary (), $ request );
163
172
$ entries [$ varyKey ] = [
164
173
'vary ' => $ response ->getVary (),
165
- 'headers ' => $ headers ,
166
174
'status ' => $ response ->getStatusCode (),
167
175
'uri ' => $ request ->getUri (), // For debugging purposes
168
176
];
169
177
170
178
// Add content if content digests are disabled
171
179
if (!$ this ->options ['generate_content_digests ' ]) {
180
+ $ this ->gzipResponse ($ response );
172
181
$ entries [$ varyKey ]['content ' ] = $ response ->getContent ();
173
182
}
174
183
184
+ // Set headers (after potentially gzipping the response)
185
+ $ entries [$ varyKey ]['headers ' ] = $ this ->getHeadersForCache ($ response );
186
+
175
187
// If the response has a Vary header we remove the non-varying entry
176
188
if ($ response ->hasVary ()) {
177
189
unset($ entries [self ::NON_VARYING_KEY ]);
@@ -196,6 +208,19 @@ public function write(Request $request, Response $response): string
196
208
return $ cacheKey ;
197
209
}
198
210
211
+ private function getHeadersForCache (Response $ response ): array
212
+ {
213
+ $ headers = $ response ->headers ->all ();
214
+ unset($ headers ['age ' ]);
215
+
216
+ return $ headers ;
217
+ }
218
+
219
+ /**
220
+ * Invalidates all cache entries that match the request.
221
+ *
222
+ * @param Request $request A Request instance
223
+ */
199
224
public function invalidate (Request $ request ): void
200
225
{
201
226
$ cacheKey = $ this ->getCacheKey ($ request );
@@ -375,6 +400,26 @@ private function getVaryKey(array $vary, Request $request): string
375
400
return hash ('sha256 ' , $ hashData );
376
401
}
377
402
403
+ private function isResponseGzipped (Response $ response ): bool
404
+ {
405
+ return $ response ->headers ->get ('Content-Encoding ' ) === 'gzip ' ;
406
+ }
407
+
408
+ private function doesRequestSupportGzip (Request $ request ): bool
409
+ {
410
+ return \in_array ('gzip ' , $ request ->getEncodings ());
411
+ }
412
+
413
+ private function isGzipSupported (): bool
414
+ {
415
+ return $ this ->options ['gzip_level ' ] !== 0 && function_exists ('gzencode ' ) && function_exists ('gzdecode ' );
416
+ }
417
+
418
+ private function isCacheGzipped (array $ headers ): bool
419
+ {
420
+ return isset ($ headers ['content-encoding ' ][0 ]) && $ headers ['content-encoding ' ][0 ] === 'gzip ' ;
421
+ }
422
+
378
423
private function saveContentDigest (Response $ response ): void
379
424
{
380
425
if ($ response ->headers ->has ('X-Content-Digest ' )) {
@@ -391,20 +436,17 @@ private function saveContentDigest(Response $response): void
391
436
392
437
if ($ digestCacheItem ->isHit ()) {
393
438
$ cacheValue = $ digestCacheItem ->get ();
394
-
395
- // BC
396
- if (\is_string ($ cacheValue )) {
397
- $ cacheValue = [
398
- 'expires ' => 0 , // Forces update to the new format
399
- 'contents ' => $ cacheValue ,
400
- ];
401
- }
402
439
} else {
440
+ if ($ this ->isBinaryFileResponseContentDigest ($ contentDigest )) {
441
+ $ contents = $ response ->getFile ()->getPathname ();
442
+ } else {
443
+ $ this ->gzipResponse ($ response );
444
+ $ contents = $ response ->getContent ();
445
+ }
446
+
403
447
$ cacheValue = [
404
448
'expires ' => 0 , // Forces storing the new entry
405
- 'contents ' => $ this ->isBinaryFileResponseContentDigest ($ contentDigest ) ?
406
- $ response ->getFile ()->getPathname () :
407
- $ response ->getContent (),
449
+ 'contents ' => $ contents
408
450
];
409
451
}
410
452
@@ -427,6 +469,25 @@ private function saveContentDigest(Response $response): void
427
469
}
428
470
}
429
471
472
+ private function gzipResponse (Response $ response ): void
473
+ {
474
+ // Not supported or already gzipped
475
+ if ($ response instanceof BinaryFileResponse || !$ this ->isGzipSupported () || $ this ->isResponseGzipped ($ response )) {
476
+ return ;
477
+ }
478
+
479
+ $ encoded = gzencode ((string ) $ response ->getContent (), $ this ->options ['gzip_level ' ]);
480
+
481
+ // Could not gzip
482
+ if (false === $ encoded ) {
483
+ return ;
484
+ }
485
+
486
+ // Update the content and set the encoding header
487
+ $ response ->setContent ($ encoded );
488
+ $ response ->headers ->set ('Content-Encoding ' , 'gzip ' );
489
+ }
490
+
430
491
/**
431
492
* Test whether a given digest identifies a BinaryFileResponse.
432
493
*
@@ -481,13 +542,14 @@ private function saveDeferred(CacheItem $item, $data, ?int $expiresAfter = null,
481
542
*
482
543
* @param array $cacheData An array containing the cache data
483
544
*/
484
- private function restoreResponse (array $ cacheData ): ?Response
545
+ private function restoreResponse (Request $ request , array $ cacheData ): ?Response
485
546
{
486
547
// Check for content digest header
487
548
if (!isset ($ cacheData ['headers ' ]['x-content-digest ' ][0 ])) {
488
549
// No digest was generated but the content was stored inline
489
550
if (isset ($ cacheData ['content ' ])) {
490
- return new Response (
551
+ return $ this ->buildResponseFromCache (
552
+ $ request ,
491
553
$ cacheData ['content ' ],
492
554
$ cacheData ['status ' ],
493
555
$ cacheData ['headers ' ]
@@ -506,11 +568,6 @@ private function restoreResponse(array $cacheData): ?Response
506
568
507
569
$ value = $ item ->get ();
508
570
509
- // BC
510
- if (\is_string ($ value )) {
511
- $ value = ['contents ' => $ value ];
512
- }
513
-
514
571
if ($ this ->isBinaryFileResponseContentDigest ($ cacheData ['headers ' ]['x-content-digest ' ][0 ])) {
515
572
try {
516
573
$ file = new File ($ value ['contents ' ]);
@@ -525,13 +582,58 @@ private function restoreResponse(array $cacheData): ?Response
525
582
);
526
583
}
527
584
528
- return new Response (
585
+ return $ this ->buildResponseFromCache (
586
+ $ request ,
529
587
$ value ['contents ' ],
530
588
$ cacheData ['status ' ],
531
589
$ cacheData ['headers ' ]
532
590
);
533
591
}
534
592
593
+ private function buildResponseFromCache (Request $ request , string $ contents , int $ status , array $ headers ): ?Response
594
+ {
595
+ // If the cache entry is not gzipped we return the file as is.
596
+ if (!$ this ->isCacheGzipped ($ headers )) {
597
+ return new Response (
598
+ $ contents ,
599
+ $ status ,
600
+ $ headers
601
+ );
602
+ }
603
+
604
+ // Otherwise it was gzipped. Let's check if the client supports gzip, in which case we'll also return as is for
605
+ // the client to decode
606
+ if ($ this ->doesRequestSupportGzip ($ request )) {
607
+ return new Response (
608
+ $ contents ,
609
+ $ status ,
610
+ $ headers
611
+ );
612
+ }
613
+
614
+ // Otherwise we now have to decode which we can only do if our setup supports it
615
+ if ($ this ->isGzipSupported ()) {
616
+ $ decoded = gzdecode ($ contents );
617
+
618
+ if (false === $ decoded ) {
619
+ return null ;
620
+ }
621
+
622
+ // Unset the encoding header because it is now not encoded anymore
623
+ unset($ headers ['content-encoding ' ]);
624
+
625
+ return new Response (
626
+ $ decoded ,
627
+ $ status ,
628
+ $ headers
629
+ );
630
+ }
631
+
632
+ // Cache file was encoded (previously gzipping was supported and now the setup has changed but the cached entries
633
+ // are still here) but could not be decoded anymore here - we're unable to serve a response now.
634
+ return null ;
635
+ }
636
+
535
637
/**
536
638
* Build and return a default lock factory for when no explicit factory
537
639
* was specified.
0 commit comments