├── .github ├── dependabot.yml └── workflows │ └── tests.yml ├── LICENSE.md ├── composer.json └── src ├── Cache.php └── CacheProvider.php /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: composer 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | tests: 7 | name: Tests PHP ${{ matrix.php }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | php: [7.4, 8.0, 8.1, 8.2, 8.3] 13 | include: 14 | - php: 8.2 15 | analysis: true 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Set up PHP ${{ matrix.php }} 22 | uses: shivammathur/setup-php@v2 23 | with: 24 | php-version: ${{ matrix.php }} 25 | coverage: xdebug 26 | 27 | - name: Install dependencies 28 | run: composer install --prefer-dist --no-progress 29 | 30 | - name: Coding standards 31 | if: matrix.analysis 32 | run: vendor/bin/phpcs 33 | 34 | - name: Tests 35 | run: vendor/bin/phpunit --coverage-clover clover.xml 36 | 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2015 Josh Lockhart 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slim/http-cache", 3 | "type": "library", 4 | "description": "Slim Framework HTTP cache middleware and service provider", 5 | "keywords": [ 6 | "slim", 7 | "framework", 8 | "middleware", 9 | "cache" 10 | ], 11 | "homepage": "https://www.slimframework.com", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Josh Lockhart", 16 | "email": "hello@joshlockhart.com", 17 | "homepage": "http://joshlockhart.com" 18 | } 19 | ], 20 | "require": { 21 | "php": "^7.2 || ^8.0", 22 | "psr/http-message": "^1.1 || ^2.0", 23 | "psr/http-server-middleware": "^1.0" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": "^8.5.13 || ^9.3.8", 27 | "slim/psr7": "^1.1", 28 | "squizlabs/php_codesniffer": "^3.5", 29 | "phpstan/phpstan": "^1.3" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "Slim\\HttpCache\\": "src" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "Slim\\HttpCache\\Tests\\": "tests" 39 | } 40 | }, 41 | "scripts": { 42 | "test": [ 43 | "@phpcs", 44 | "@phpstan", 45 | "@phpunit" 46 | ], 47 | "phpcs": "phpcs", 48 | "phpstan": "phpstan analyse src --memory-limit=-1", 49 | "phpunit": "phpunit" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Cache.php: -------------------------------------------------------------------------------- 1 | type = $type; 58 | $this->maxAge = $maxAge; 59 | $this->mustRevalidate = $mustRevalidate; 60 | } 61 | 62 | /** 63 | * {@inheritDoc} 64 | */ 65 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 66 | { 67 | $response = $handler->handle($request); 68 | 69 | // Cache-Control header 70 | if (!$response->hasHeader('Cache-Control')) { 71 | if ($this->maxAge === 0) { 72 | $response = $response->withHeader( 73 | 'Cache-Control', 74 | sprintf( 75 | '%s, no-cache%s', 76 | $this->type, 77 | $this->mustRevalidate ? ', must-revalidate' : '' 78 | ) 79 | ); 80 | } else { 81 | $response = $response->withHeader( 82 | 'Cache-Control', 83 | sprintf( 84 | '%s, max-age=%s%s', 85 | $this->type, 86 | $this->maxAge, 87 | $this->mustRevalidate ? ', must-revalidate' : '' 88 | ) 89 | ); 90 | } 91 | } 92 | 93 | 94 | // ETag header and conditional GET check 95 | $etag = $response->getHeader('ETag'); 96 | $etag = reset($etag); 97 | 98 | if ($etag) { 99 | $ifNoneMatch = $request->getHeaderLine('If-None-Match'); 100 | 101 | if ($ifNoneMatch) { 102 | $etagList = preg_split('@\s*,\s*@', $ifNoneMatch); 103 | if (is_array($etagList) && (in_array($etag, $etagList) || in_array('*', $etagList))) { 104 | return $response->withStatus(304); 105 | } 106 | } 107 | } 108 | 109 | 110 | // Last-Modified header and conditional GET check 111 | $lastModified = $response->getHeaderLine('Last-Modified'); 112 | 113 | if ($lastModified) { 114 | if (!is_numeric($lastModified)) { 115 | $lastModified = strtotime($lastModified); 116 | } 117 | 118 | $ifModifiedSince = $request->getHeaderLine('If-Modified-Since'); 119 | 120 | if ($ifModifiedSince && $lastModified <= strtotime($ifModifiedSince)) { 121 | return $response->withStatus(304); 122 | } 123 | } 124 | 125 | return $response; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/CacheProvider.php: -------------------------------------------------------------------------------- 1 | withHeader('Cache-Control', $headerValue); 56 | } 57 | 58 | /** 59 | * Disable client-side HTTP caching 60 | * 61 | * @param ResponseInterface $response PSR7 response object 62 | * 63 | * @return ResponseInterface A new PSR7 response object with `Cache-Control` header 64 | */ 65 | public function denyCache(ResponseInterface $response): ResponseInterface 66 | { 67 | return $response->withHeader('Cache-Control', 'no-store,no-cache'); 68 | } 69 | 70 | /** 71 | * Add `Expires` header to PSR7 response object 72 | * 73 | * @param ResponseInterface $response A PSR7 response object 74 | * @param int|string $time A UNIX timestamp or a valid `strtotime()` string 75 | * 76 | * @return ResponseInterface A new PSR7 response object with `Expires` header 77 | * @throws InvalidArgumentException if the expiration date cannot be parsed 78 | */ 79 | public function withExpires(ResponseInterface $response, $time): ResponseInterface 80 | { 81 | if (!is_integer($time)) { 82 | $time = strtotime($time); 83 | if ($time === false) { 84 | throw new InvalidArgumentException('Expiration value could not be parsed with `strtotime()`.'); 85 | } 86 | } 87 | 88 | return $response->withHeader('Expires', gmdate('D, d M Y H:i:s T', $time)); 89 | } 90 | 91 | /** 92 | * Add `ETag` header to PSR7 response object 93 | * 94 | * @param ResponseInterface $response A PSR7 response object 95 | * @param string $value The ETag value 96 | * @param string $type ETag type: "strong" or "weak" 97 | * 98 | * @return ResponseInterface A new PSR7 response object with `ETag` header 99 | * @throws InvalidArgumentException if the etag type is invalid 100 | */ 101 | public function withEtag(ResponseInterface $response, string $value, string $type = 'strong'): ResponseInterface 102 | { 103 | if (!in_array($type, ['strong', 'weak'])) { 104 | throw new InvalidArgumentException('Invalid etag type. Must be "strong" or "weak".'); 105 | } 106 | $value = '"'.$value.'"'; 107 | if ($type === 'weak') { 108 | $value = 'W/'.$value; 109 | } 110 | 111 | return $response->withHeader('ETag', $value); 112 | } 113 | 114 | /** 115 | * Add `Last-Modified` header to PSR7 response object 116 | * 117 | * @param ResponseInterface $response A PSR7 response object 118 | * @param int|string $time A UNIX timestamp or a valid `strtotime()` string 119 | * 120 | * @return ResponseInterface A new PSR7 response object with `Last-Modified` header 121 | * @throws InvalidArgumentException if the last modified date cannot be parsed 122 | */ 123 | public function withLastModified(ResponseInterface $response, $time): ResponseInterface 124 | { 125 | if (!is_integer($time)) { 126 | $time = strtotime($time); 127 | if ($time === false) { 128 | throw new InvalidArgumentException('Last Modified value could not be parsed with `strtotime()`.'); 129 | } 130 | } 131 | 132 | return $response->withHeader('Last-Modified', gmdate('D, d M Y H:i:s T', $time)); 133 | } 134 | } 135 | --------------------------------------------------------------------------------