Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .docker/Dockerfile.local
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM php:8.1-cli-alpine
ENV XDG_CACHE_HOME=/tmp/.cache
ENV COMPOSER_CACHE_DIR=${XDG_CACHE_HOME}/composer
# posible could install cache directory here
RUN --mount=type=bind,from=mlocati/php-extension-installer:1.5,source=/usr/bin/install-php-extensions,target=/usr/local/bin/install-php-extensions \
install-php-extensions zip xdebug-stable
RUN addgroup -S app && adduser -S app -G app
RUN mkdir -p /app && chown -R app /app && mkdir -p /tmp && chown -R app /tmp
COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer
USER app
WORKDIR /app
4 changes: 4 additions & 0 deletions .docker/xdebug.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[xdebug]
zend_extension="xdebug.so"
xdebug.client_host=host.docker.internal
xdebug.mode=debug
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
/.github export-ignore
/phpunit.xml.dist export-ignore
/tests export-ignore
/.docker export-ignore
/docker-compose.yaml export-ignore
/Makefile export-ignore
8 changes: 5 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,23 @@ jobs:
- "lowest"
- "highest"
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4

- name: Install PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
extensions: mbstring, dom
extensions: mbstring, dom, igbinary

- name: Validate composer.json and composer.lock
run: composer validate

- name: "Composer install ${{ matrix.dependencies }} dependencies"
uses: "ramsey/composer-install@v1"
uses: ramsey/composer-install@v2
with:
dependency-versions: "${{ matrix.dependencies }}"
env:
COMPOSER_PROCESS_TIMEOUT: 6000

- name: Run test suite
run: composer run-script test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ composer.phar
*.iml
.idea/
coverage.xml
.phpunit.result.cache
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
init:
docker compose run cli composer i
test:
docker compose run cli vendor/bin/phpunit
shell:
docker compose run cli sh
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,20 @@ See [Guzzle Cache](https://www.drupal.org/project/guzzle_cache) module.
# Links that talk about the project
- [Speeding Up APIs/Apps/Smart Toasters with HTTP Response Caching](https://apisyouwonthate.com/blog/speeding-up-apis-apps-smart-toasters-with-http-response-caching)
- [Caching HTTP-Requests with Guzzle 6 and PSR-6](http://a.kabachnik.info/caching-http-requests-with-guzzle-6-and-psr-6.html)

# Development

## Docker quick start

### Initialization
```bash
make init
```
### Running test
```bash
make test
```
### Entering container shell
```bash
make shell
```
8 changes: 7 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@
"require": {
"php": ">=7.2.0",
"guzzlehttp/guzzle": "^6.0 || ^7.0",
"guzzlehttp/promises": "^1.4 || ^2.0",
"guzzlehttp/psr7": "^1.7.0 || ^2.0.0"
},
"require-dev": {
"phpunit/phpunit": "^8.5.15 || ^9.5",
"doctrine/cache": "^1.10",
"league/flysystem": "^1.0",
"league/flysystem": "^2.5",
"psr/cache": "^1.0",
"cache/array-adapter": "^0.4 || ^0.5 || ^1.0",
"illuminate/cache": "^5.0",
Expand Down Expand Up @@ -48,5 +49,10 @@
},
"scripts": {
"test": "vendor/bin/phpunit"
},
"config": {
"allow-plugins": {
"kylekatarnls/update-helper": true
}
}
}
11 changes: 11 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: "3.9"
services:
cli:
build:
dockerfile: .docker/Dockerfile.local
context: .
working_dir: /app
volumes:
- ./:/app
- ~/.composer/cache/:/tmp/cache/
- ./.docker/xdebug.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini:ro
91 changes: 61 additions & 30 deletions src/CacheEntry.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
namespace Kevinrob\GuzzleCache;

use GuzzleHttp\Psr7\PumpStream;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

class CacheEntry
class CacheEntry implements \Serializable
{
/**
* @var RequestInterface
Expand Down Expand Up @@ -256,47 +257,77 @@ public function getAge()
return time() - $this->dateCreated->getTimestamp();
}

public function __sleep()
public function __serialize(): array
{
// Stream/Resource can't be serialized... So we copy the content into an implementation of `Psr\Http\Message\StreamInterface`
if ($this->response !== null) {
$responseBody = (string)$this->response->getBody();
$this->response = $this->response->withBody(
new PumpStream(
new BodyStore($responseBody),
[
'size' => mb_strlen($responseBody),
]
)
);
return [
'request' => self::toSerializeableMessage($this->request),
'response' => $this->response !== null ? self::toSerializeableMessage($this->response) : null,
'staleAt' => $this->staleAt,
'staleIfErrorTo' => $this->staleIfErrorTo,
'staleWhileRevalidateTo' => $this->staleWhileRevalidateTo,
'dateCreated' => $this->dateCreated,
'timestampStale' => $this->timestampStale,
];
}

public function __unserialize(array $data): void
{
$prefix = '';
if (isset($data["\0*\0request"])) {
// We are unserializing a cache entry which was serialized with a version < 4.1.1
$prefix = "\0*\0";
}
$this->request = self::restoreStreamBody($data[$prefix.'request']);
$this->response = $data[$prefix.'response'] !== null ? self::restoreStreamBody($data[$prefix.'response']) : null;
$this->staleAt = $data[$prefix.'staleAt'];
$this->staleIfErrorTo = $data[$prefix.'staleIfErrorTo'];
$this->staleWhileRevalidateTo = $data[$prefix.'staleWhileRevalidateTo'];
$this->dateCreated = $data[$prefix.'dateCreated'];
$this->timestampStale = $data[$prefix.'timestampStale'];
}

/**
* Stream/Resource can't be serialized... So we copy the content into an implementation of `Psr\Http\Message\StreamInterface`
*
* @template T of MessageInterface
*
* @param T $message
* @return T
*/
private static function toSerializeableMessage(MessageInterface $message): MessageInterface
{
$bodyString = (string)$message->getBody();

$requestBody = (string)$this->request->getBody();
$this->request = $this->request->withBody(
return $message->withBody(
new PumpStream(
new BodyStore($requestBody),
new BodyStore($bodyString),
[
'size' => mb_strlen($requestBody)
'size' => mb_strlen($bodyString),
]
)
);
}

return array_keys(get_object_vars($this));
/**
* @template T of MessageInterface
*
* @param T $message
* @return T
*/
private static function restoreStreamBody(MessageInterface $message): MessageInterface
{
return $message->withBody(
\GuzzleHttp\Psr7\Utils::streamFor((string) $message->getBody())
);
}

public function __wakeup()
public function serialize()
{
// We re-create the stream of the response
if ($this->response !== null) {
$this->response = $this->response
->withBody(
\GuzzleHttp\Psr7\Utils::streamFor((string) $this->response->getBody())
);
}
$this->request = $this->request
->withBody(
\GuzzleHttp\Psr7\Utils::streamFor((string) $this->request->getBody())
);
return serialize($this->__serialize());
}

public function unserialize($data)
{
$this->__unserialize(unserialize($data));
}
}
10 changes: 4 additions & 6 deletions src/CacheMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public function getHttpMethods()
*/
public function purgeReValidation()
{
\GuzzleHttp\Promise\inspect_all($this->waitingRevalidate);
\GuzzleHttp\Promise\Utils::inspectAll($this->waitingRevalidate);
}

/**
Expand Down Expand Up @@ -237,11 +237,9 @@ function (ResponseInterface $response) use ($request, $cacheEntry) {
return static::addToCache($this->cacheStorage, $request, $response, $update);
},
function ($reason) use ($cacheEntry) {
if ($reason instanceof TransferException) {
$response = static::getStaleResponse($cacheEntry);
if ($response instanceof ResponseInterface) {
return $response;
}
$response = static::getStaleResponse($cacheEntry);
if ($response instanceof ResponseInterface) {
return $response;
}

return new RejectedPromise($reason);
Expand Down
14 changes: 7 additions & 7 deletions src/Storage/FlysystemStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
namespace Kevinrob\GuzzleCache\Storage;

use Kevinrob\GuzzleCache\CacheEntry;
use League\Flysystem\AdapterInterface;
use League\Flysystem\Filesystem;
use League\Flysystem\FileNotFoundException;
use League\Flysystem\FilesystemAdapter;
use League\Flysystem\FilesystemException;

class FlysystemStorage implements CacheStorageInterface
{
Expand All @@ -15,7 +15,7 @@ class FlysystemStorage implements CacheStorageInterface
*/
protected $filesystem;

public function __construct(AdapterInterface $adapter)
public function __construct(FilesystemAdapter $adapter)
{
$this->filesystem = new Filesystem($adapter);
}
Expand All @@ -25,7 +25,7 @@ public function __construct(AdapterInterface $adapter)
*/
public function fetch($key)
{
if ($this->filesystem->has($key)) {
if ($this->filesystem->fileExists($key)) {
// The file exist, read it!
$data = @unserialize(
$this->filesystem->read($key)
Expand All @@ -44,7 +44,7 @@ public function fetch($key)
*/
public function save($key, CacheEntry $data)
{
return $this->filesystem->put($key, serialize($data));
$this->filesystem->write($key, serialize($data));
}

/**
Expand All @@ -53,8 +53,8 @@ public function save($key, CacheEntry $data)
public function delete($key)
{
try {
return $this->filesystem->delete($key);
} catch (FileNotFoundException $ex) {
$this->filesystem->delete($key);
} catch (FilesystemException $ex) {
return true;
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/Storage/LaravelCacheStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public function __construct(Cache $cache)
public function fetch($key)
{
try {
$cache = unserialize($this->cache->get($key));
$cache = unserialize($this->cache->get($key, ''));
if ($cache instanceof CacheEntry) {
return $cache;
}
Expand Down Expand Up @@ -70,7 +70,7 @@ public function delete($key)
{
return $this->cache->forget($key);
}

protected function getLifeTime(CacheEntry $data)
{
$version = app()->version();
Expand Down
46 changes: 46 additions & 0 deletions tests/CacheEntryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,52 @@ public function testCacheEntryShouldBeSerializableWithIgBinaryWithoutWarning()
}
}

public function testSerializationShouldNotMutateCacheEntry()
{
$request = new Request(
'GET',
'test.local',
[],
'Sample body' // Always include a body in the request to be sure there is a stream in it
);
$response = new Response(
200, [
'Cache-Control' => 'max-age=60',
],
'Test content'
);
$cacheEntry = new CacheEntry($request, $response, $this->makeDateTimeOffset(10));

$originalCacheEntry = clone $cacheEntry;

serialize($cacheEntry);

$this->assertEquals($cacheEntry, $originalCacheEntry);
}

/**
* @dataProvider versionsToTestProvider
*/
public function testPreviousUnserialize($version)
{
if (version_compare(PHP_VERSION, '7.4.0') < 0) {
$this->markTestSkipped('Compat with previous version is not available with \Serializable interface');
}

$cacheEntry = unserialize(file_get_contents(__DIR__."/data/{$version}_serialized_cache_entry"));

self::assertInstanceOf(CacheEntry::class, $cacheEntry);
}

public function versionsToTestProvider()
{
return [
['v4.0.0'],
['v4.1.0'],
['v4.1.1'],
];
}

private function setResponseHeader($name, $value)
{
$this->responseHeaders[$name] = [$value];
Expand Down
Loading