Skip to content

Commit 68528d1

Browse files
committed
First commit
0 parents  commit 68528d1

File tree

11 files changed

+474
-0
lines changed

11 files changed

+474
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
vendor
2+
composer.lock

CONTRIBUTING.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# How to Contribute
2+
3+
## Pull Requests
4+
5+
1. Fork this repository
6+
2. Create a new branch for each feature or improvement
7+
3. Send a pull request from each feature branch
8+
9+
It is very important to separate new features or improvements into separate feature branches, and to send a pull request for each branch. This allows me to review and pull in new features or improvements individually.
10+
11+
## Style Guide
12+
13+
All pull requests must adhere to the [PSR-2 standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md).
14+
15+
## Unit Testing
16+
17+
All pull requests must be accompanied by passing unit tests and complete code coverage. The Slim Framework uses phpunit for testing.
18+
19+
[Learn about PHPUnit](https://github.com/sebastianbergmann/phpunit/)

LICENSE.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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.

README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Slim Framework HTTP Cache
2+
3+
This repository contains a Slim Framework HTTP cache middleware and service provider.
4+
5+
## Install
6+
7+
Via Composer
8+
9+
``` bash
10+
$ composer require slim/httpcache
11+
```
12+
13+
Requires Slim 3.0.0 or newer.
14+
15+
## Usage
16+
17+
```php
18+
$app = new \Slim\App();
19+
20+
// Register middleware
21+
$app->add(new \Slim\HttpCache\Cache('public', 86400));
22+
23+
// Register service provider
24+
$app->register(new \Slim\HttpCache\CacheProvider);
25+
26+
// Example route with ETag header
27+
$app->get('/foo', function ($req, $res, $args) {
28+
$resWithEtag = $this['cache']->withEtag($res, 'abc');
29+
30+
return $resWithEtag;
31+
});
32+
33+
$app->run();
34+
```
35+
36+
## Testing
37+
38+
``` bash
39+
$ phpunit
40+
```
41+
42+
## Contributing
43+
44+
Please see [CONTRIBUTING](CONTRIBUTING.md) for details.
45+
46+
## Security
47+
48+
If you discover any security related issues, please email [email protected] instead of using the issue tracker.
49+
50+
## Credits
51+
52+
- [Josh Lockhart](https://github.com/codeguy)
53+
- [All Contributors](../../contributors)
54+
55+
## License
56+
57+
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

composer.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "slim/http-cache",
3+
"type": "library",
4+
"description": "Slim Framework HTTP cache middleware and service provider",
5+
"keywords": ["slim","framework","middleware","cache"],
6+
"homepage": "http://slimframework.com",
7+
"license": "MIT",
8+
"authors": [
9+
{
10+
"name": "Josh Lockhart",
11+
"email": "[email protected]",
12+
"homepage": "http://joshlockhart.com"
13+
}
14+
],
15+
"require": {
16+
"php": ">=5.4.0",
17+
"pimple/pimple": "~3.0",
18+
"psr/http-message": "~0.9"
19+
},
20+
"require-dev": {
21+
"slim/slim": "dev-develop"
22+
},
23+
"autoload": {
24+
"psr-4": {
25+
"Slim\\HttpCache\\": "src"
26+
}
27+
},
28+
"autoload-dev": {
29+
"psr-4": {
30+
"Slim\\HttpCache\\Tests\\": "tests"
31+
}
32+
}
33+
}

phpunit.xml.dist

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
3+
<phpunit backupGlobals="false"
4+
backupStaticAttributes="false"
5+
colors="true"
6+
convertErrorsToExceptions="true"
7+
convertNoticesToExceptions="true"
8+
convertWarningsToExceptions="true"
9+
processIsolation="false"
10+
stopOnFailure="false"
11+
syntaxCheck="false"
12+
bootstrap="tests/bootstrap.php"
13+
>
14+
<testsuites>
15+
<testsuite name="Slim Test Suite">
16+
<directory>tests/</directory>
17+
</testsuite>
18+
</testsuites>
19+
20+
<filter>
21+
<whitelist>
22+
<directory>src/</directory>
23+
</whitelist>
24+
</filter>
25+
</phpunit>

src/Cache.php

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
namespace Slim\HttpCache;
3+
4+
use Pimple\Container;
5+
use Pimple\ServiceProviderInterface;
6+
use Psr\Http\Message\RequestInterface;
7+
use Psr\Http\Message\ResponseInterface;
8+
9+
class Cache
10+
{
11+
/**
12+
* Cache-Control type (public or private)
13+
*
14+
* @var string
15+
*/
16+
protected $type;
17+
18+
/**
19+
* Cache-Control max age in seconds
20+
*
21+
* @var int
22+
*/
23+
protected $maxAge;
24+
25+
/**
26+
* Create new HTTP cache
27+
*
28+
* @param string $type The cache type: "public" or "private"
29+
* @param int $maxAge The maximum age of client-side cache
30+
*/
31+
public function __construct($type = 'private', $maxAge = 86400)
32+
{
33+
$this->type = $type;
34+
$this->maxAge = $maxAge;
35+
}
36+
37+
/**
38+
* Invoke cache middleware
39+
*
40+
* @param RequestInterface $request A PSR7 request object
41+
* @param ResponseInterface $response A PSR7 response object
42+
* @param callable $next The next middleware callable
43+
*
44+
* @return ResponseInterface A PSR7 response object
45+
*/
46+
public function __invoke(RequestInterface $request, ResponseInterface $response, callable $next)
47+
{
48+
$response = $next($request, $response);
49+
50+
// Cache-Control header
51+
$response = $response->withHeader('Cache-Control', sprintf(
52+
'%s, max-age=%s',
53+
$this->type,
54+
$this->maxAge
55+
));
56+
57+
// Last-Modified header and conditional GET check
58+
$lastModified = $response->getHeader('Last-Modified');
59+
if ($lastModified) {
60+
if (!is_integer($lastModified)) {
61+
$lastModified = strtotime($lastModified);
62+
}
63+
$ifModifiedSince = $request->getHeader('If-Modified-Since');
64+
if ($ifModifiedSince && $lastModified === strtotime($ifModifiedSince)) {
65+
return $response->withStatus(304);
66+
}
67+
}
68+
69+
// ETag header and conditional GET check
70+
$etag = $response->getHeader('ETag');
71+
if ($etag) {
72+
$ifNoneMatch = $request->getHeader('If-None-Match');
73+
if ($ifNoneMatch) {
74+
$etagList = preg_split('@\s*,\s*@', $ifNoneMatch);
75+
if (in_array($etag, $etagList) || in_array('*', $etagList)) {
76+
return $response->withStatus(304);
77+
}
78+
}
79+
}
80+
81+
return $response;
82+
}
83+
}

src/CacheProvider.php

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
namespace Slim\HttpCache;
3+
4+
use Pimple\Container;
5+
use Pimple\ServiceProviderInterface;
6+
use Psr\Http\Message\ResponseInterface;
7+
8+
class CacheProvider implements ServiceProviderInterface
9+
{
10+
/**
11+
* Register this cache provider with a Pimple container
12+
*
13+
* @param Container $container
14+
*/
15+
public function register(Container $container)
16+
{
17+
$container['cache'] = $this;
18+
}
19+
20+
/**
21+
* Add `Expires` header to PSR7 response object
22+
*
23+
* @param ResponseInterface $response A PSR7 response object
24+
* @param int|string $time A UNIX timestamp or a valid `strtotime()` string
25+
*
26+
* @return ResponseInterface A new PSR7 response object with `Expires` header
27+
*/
28+
public function withExpires(ResponseInterface $response, $time)
29+
{
30+
if (!is_integer($time)) {
31+
$time = strtotime($time);
32+
if ($time === false) {
33+
throw new \InvalidArgumentException('Expiration value could not be parsed with `strtotime()`.');
34+
}
35+
}
36+
37+
return $response->withHeader('Expires', gmdate('D, d M Y H:i:s T', $time));
38+
}
39+
40+
/**
41+
* Add `ETag` header to PSR7 response object
42+
*
43+
* @param ResponseInterface $response A PSR7 response object
44+
* @param string $value The ETag value
45+
* @param string $type ETag type: "strong" or "weak"
46+
*
47+
* @return ResponseInterface A new PSR7 response object with `ETag` header
48+
*/
49+
public function withEtag(ResponseInterface $response, $value, $type = 'strong')
50+
{
51+
if (!in_array($type, ['strong', 'weak'])) {
52+
throw new \InvalidArgumentException('Invalid etag type. Must be "strong" or "weak".');
53+
}
54+
$value = '"' . $value . '"';
55+
if ($type === 'weak') {
56+
$value = 'W/' . $value;
57+
}
58+
59+
return $response->withHeader('ETag', $value);
60+
}
61+
62+
/**
63+
* Add `Last-Modified` header to PSR7 response object
64+
*
65+
* @param ResponseInterface $response A PSR7 response object
66+
* @param int|string $time A UNIX timestamp or a valid `strtotime()` string
67+
*
68+
* @return ResponseInterface A new PSR7 response object with `Last-Modified` header
69+
*/
70+
public function withLastModified(ResponseInterface $response, $time)
71+
{
72+
if (!is_integer($time)) {
73+
$time = strtotime($time);
74+
if ($time === false) {
75+
throw new \InvalidArgumentException('Last Modified value could not be parsed with `strtotime()`.');
76+
}
77+
}
78+
79+
return $response->withHeader('Last-Modified', gmdate('D, d M Y H:i:s T', $time));
80+
}
81+
}

tests/CacheProviderTest.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
namespace Slim\HttpCache\Tests;
3+
4+
use Slim\HttpCache\CacheProvider;
5+
use Slim\Http\Response;
6+
7+
class CacheProviderTest extends \PHPUnit_Framework_TestCase
8+
{
9+
public function testWithExpires()
10+
{
11+
$now = time();
12+
$cacheProvider = new CacheProvider();
13+
$res = $cacheProvider->withExpires(new Response(), $now);
14+
15+
$this->assertEquals(gmdate('D, d M Y H:i:s T', $now), $res->getHeader('Expires'));
16+
}
17+
18+
public function testWithETag()
19+
{
20+
$etag = 'abc';
21+
$cacheProvider = new CacheProvider();
22+
$res = $cacheProvider->withEtag(new Response(), $etag);
23+
24+
$this->assertEquals('"' . $etag . '"', $res->getHeader('ETag'));
25+
}
26+
27+
public function testWithETagWeak()
28+
{
29+
$etag = 'abc';
30+
$cacheProvider = new CacheProvider();
31+
$res = $cacheProvider->withEtag(new Response(), $etag, 'weak');
32+
33+
$this->assertEquals('W/"' . $etag . '"', $res->getHeader('ETag'));
34+
}
35+
36+
/**
37+
* @expectedException \InvalidArgumentException
38+
*/
39+
public function testWithETagInvalidType()
40+
{
41+
$etag = 'abc';
42+
$cacheProvider = new CacheProvider();
43+
$cacheProvider->withEtag(new Response(), $etag, 'bork');
44+
}
45+
46+
public function testWithLastModified()
47+
{
48+
$now = time();
49+
$cacheProvider = new CacheProvider();
50+
$res = $cacheProvider->withLastModified(new Response(), $now);
51+
52+
$this->assertEquals(gmdate('D, d M Y H:i:s T', $now), $res->getHeader('Last-Modified'));
53+
}
54+
}

0 commit comments

Comments
 (0)