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,14 @@ 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
+
199
219
public function invalidate (Request $ request ): void
200
220
{
201
221
$ cacheKey = $ this ->getCacheKey ($ request );
@@ -375,6 +395,26 @@ private function getVaryKey(array $vary, Request $request): string
375
395
return hash ('sha256 ' , $ hashData );
376
396
}
377
397
398
+ private function isResponseGzipped (Response $ response ): bool
399
+ {
400
+ return $ response ->headers ->get ('Content-Encoding ' ) === 'gzip ' ;
401
+ }
402
+
403
+ private function doesRequestSupportGzip (Request $ request ): bool
404
+ {
405
+ return \in_array ('gzip ' , $ request ->getEncodings ());
406
+ }
407
+
408
+ private function isGzipSupported (): bool
409
+ {
410
+ return $ this ->options ['gzip_level ' ] !== 0 && function_exists ('gzencode ' ) && function_exists ('gzdecode ' );
411
+ }
412
+
413
+ private function isCacheGzipped (array $ headers ): bool
414
+ {
415
+ return isset ($ headers ['content-encoding ' ][0 ]) && $ headers ['content-encoding ' ][0 ] === 'gzip ' ;
416
+ }
417
+
378
418
private function saveContentDigest (Response $ response ): void
379
419
{
380
420
if ($ response ->headers ->has ('X-Content-Digest ' )) {
@@ -391,20 +431,17 @@ private function saveContentDigest(Response $response): void
391
431
392
432
if ($ digestCacheItem ->isHit ()) {
393
433
$ 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
434
} else {
435
+ if ($ this ->isBinaryFileResponseContentDigest ($ contentDigest )) {
436
+ $ contents = $ response ->getFile ()->getPathname ();
437
+ } else {
438
+ $ this ->gzipResponse ($ response );
439
+ $ contents = $ response ->getContent ();
440
+ }
441
+
403
442
$ cacheValue = [
404
443
'expires ' => 0 , // Forces storing the new entry
405
- 'contents ' => $ this ->isBinaryFileResponseContentDigest ($ contentDigest ) ?
406
- $ response ->getFile ()->getPathname () :
407
- $ response ->getContent (),
444
+ 'contents ' => $ contents
408
445
];
409
446
}
410
447
@@ -427,6 +464,25 @@ private function saveContentDigest(Response $response): void
427
464
}
428
465
}
429
466
467
+ private function gzipResponse (Response $ response ): void
468
+ {
469
+ // Not supported or already gzipped
470
+ if ($ response instanceof BinaryFileResponse || !$ this ->isGzipSupported () || $ this ->isResponseGzipped ($ response )) {
471
+ return ;
472
+ }
473
+
474
+ $ encoded = gzencode ((string ) $ response ->getContent (), $ this ->options ['gzip_level ' ]);
475
+
476
+ // Could not gzip
477
+ if (false === $ encoded ) {
478
+ return ;
479
+ }
480
+
481
+ // Update the content and set the encoding header
482
+ $ response ->setContent ($ encoded );
483
+ $ response ->headers ->set ('Content-Encoding ' , 'gzip ' );
484
+ }
485
+
430
486
/**
431
487
* Test whether a given digest identifies a BinaryFileResponse.
432
488
*
@@ -481,13 +537,14 @@ private function saveDeferred(CacheItem $item, $data, ?int $expiresAfter = null,
481
537
*
482
538
* @param array $cacheData An array containing the cache data
483
539
*/
484
- private function restoreResponse (array $ cacheData ): ?Response
540
+ private function restoreResponse (Request $ request , array $ cacheData ): ?Response
485
541
{
486
542
// Check for content digest header
487
543
if (!isset ($ cacheData ['headers ' ]['x-content-digest ' ][0 ])) {
488
544
// No digest was generated but the content was stored inline
489
545
if (isset ($ cacheData ['content ' ])) {
490
- return new Response (
546
+ return $ this ->buildResponseFromCache (
547
+ $ request ,
491
548
$ cacheData ['content ' ],
492
549
$ cacheData ['status ' ],
493
550
$ cacheData ['headers ' ]
@@ -506,11 +563,6 @@ private function restoreResponse(array $cacheData): ?Response
506
563
507
564
$ value = $ item ->get ();
508
565
509
- // BC
510
- if (\is_string ($ value )) {
511
- $ value = ['contents ' => $ value ];
512
- }
513
-
514
566
if ($ this ->isBinaryFileResponseContentDigest ($ cacheData ['headers ' ]['x-content-digest ' ][0 ])) {
515
567
try {
516
568
$ file = new File ($ value ['contents ' ]);
@@ -525,13 +577,58 @@ private function restoreResponse(array $cacheData): ?Response
525
577
);
526
578
}
527
579
528
- return new Response (
580
+ return $ this ->buildResponseFromCache (
581
+ $ request ,
529
582
$ value ['contents ' ],
530
583
$ cacheData ['status ' ],
531
584
$ cacheData ['headers ' ]
532
585
);
533
586
}
534
587
588
+ private function buildResponseFromCache (Request $ request , string $ contents , int $ status , array $ headers ): ?Response
589
+ {
590
+ // If the cache entry is not gzipped we return the file as is.
591
+ if (!$ this ->isCacheGzipped ($ headers )) {
592
+ return new Response (
593
+ $ contents ,
594
+ $ status ,
595
+ $ headers
596
+ );
597
+ }
598
+
599
+ // Otherwise it was gzipped. Let's check if the client supports gzip, in which case we'll also return as is for
600
+ // the client to decode
601
+ if ($ this ->doesRequestSupportGzip ($ request )) {
602
+ return new Response (
603
+ $ contents ,
604
+ $ status ,
605
+ $ headers
606
+ );
607
+ }
608
+
609
+ // Otherwise we now have to decode which we can only do if our setup supports it
610
+ if ($ this ->isGzipSupported ()) {
611
+ $ decoded = gzdecode ($ contents );
612
+
613
+ if (false === $ decoded ) {
614
+ return null ;
615
+ }
616
+
617
+ // Unset the encoding header because it is now not encoded anymore
618
+ unset($ headers ['content-encoding ' ]);
619
+
620
+ return new Response (
621
+ $ decoded ,
622
+ $ status ,
623
+ $ headers
624
+ );
625
+ }
626
+
627
+ // Cache file was encoded (previously gzipping was supported and now the setup has changed but the cached entries
628
+ // are still here) but could not be decoded anymore here - we're unable to serve a response now.
629
+ return null ;
630
+ }
631
+
535
632
/**
536
633
* Build and return a default lock factory for when no explicit factory
537
634
* was specified.
0 commit comments