Skip to content

Commit 9042180

Browse files
committed
Add CachePlugin to cache responses
1 parent b17fc8a commit 9042180

File tree

3 files changed

+292
-3
lines changed

3 files changed

+292
-3
lines changed

src/Http/Plugin/CachePlugin.php

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of SolidWorx/SimpleHttp project.
7+
*
8+
* Copyright (c) Pierre du Plessis <[email protected]>
9+
*
10+
* This source file is subject to the MIT license that is bundled
11+
* with this source code in the file LICENSE.
12+
*/
13+
14+
namespace SolidWorx\SimpleHttp\Http\Plugin;
15+
16+
use Http\Client\Common\Plugin;
17+
use Http\Promise\FulfilledPromise;
18+
use Http\Promise\Promise;
19+
use Psr\Cache\CacheItemPoolInterface;
20+
use Psr\Cache\InvalidArgumentException;
21+
use Psr\Http\Message\RequestInterface;
22+
23+
final class CachePlugin implements Plugin
24+
{
25+
private CacheItemPoolInterface $cacheItemPool;
26+
private int $ttl;
27+
28+
public function __construct(CacheItemPoolInterface $cacheItemPool, int $ttl = 0)
29+
{
30+
$this->cacheItemPool = $cacheItemPool;
31+
$this->ttl = $ttl;
32+
}
33+
34+
public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise
35+
{
36+
$cacheKey = $this->getCacheKey($request);
37+
38+
try {
39+
$cacheItem = $this->cacheItemPool->getItem($cacheKey);
40+
41+
if ($cacheItem->isHit()) {
42+
return new FulfilledPromise($cacheItem->get());
43+
}
44+
} catch (InvalidArgumentException $e) {
45+
}
46+
47+
$promise = $next($request);
48+
49+
if (isset($cacheItem)) {
50+
$promise->then(function ($response) use ($cacheItem) {
51+
$cacheItem->set($response);
52+
53+
if ($this->ttl > 0) {
54+
$cacheItem->expiresAfter($this->ttl);
55+
}
56+
57+
$this->cacheItemPool->save($cacheItem);
58+
});
59+
}
60+
61+
return $promise;
62+
}
63+
64+
private function getCacheKey(RequestInterface $request): string
65+
{
66+
return md5($request->getMethod().$request->getUri());
67+
}
68+
}

src/RequestBuilder.php

+13-3
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@
1515

1616
use Http\Client\Common\PluginClient;
1717
use Http\Discovery\Psr17FactoryDiscovery;
18+
use Psr\Cache\CacheItemPoolInterface;
1819
use Psr\Http\Client\ClientInterface;
1920
use Psr\Http\Message\RequestInterface;
2021
use Psr\Http\Message\UriInterface;
2122
use SolidWorx\SimpleHttp\Discovery\HttpAsyncClientDiscovery;
2223
use SolidWorx\SimpleHttp\Exception\MissingUrlException;
24+
use SolidWorx\SimpleHttp\Http\Plugin\CachePlugin;
2325
use SolidWorx\SimpleHttp\Traits\HttpMethodsTrait;
2426
use SolidWorx\SimpleHttp\Traits\HttpOptionsTrait;
2527
use Throwable;
@@ -58,9 +60,9 @@ public function path(string $path): self
5860
{
5961
$request = clone $this;
6062

61-
if (!$request->url instanceof UriInterface) {
62-
throw new MissingUrlException();
63-
}
63+
if (!$request->url instanceof UriInterface) {
64+
throw new MissingUrlException();
65+
}
6466

6567
$request->url = $request->url->withPath($path);
6668

@@ -79,6 +81,14 @@ public function request(): Response
7981
return new Response($pluginClient->sendAsyncRequest($this->build()));
8082
}
8183

84+
public function cacheResponse(CacheItemPoolInterface $cachePool, int $ttl = 0): self
85+
{
86+
$request = clone $this;
87+
$request->plugins[] = new CachePlugin($cachePool, $ttl);
88+
89+
return $request;
90+
}
91+
8292
private function build(): RequestInterface
8393
{
8494
if (null === $this->url) {

tests/Http/Plugin/CachePluginTest.php

+211
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of SolidWorx/SimpleHttp project.
7+
*
8+
* Copyright (c) Pierre du Plessis <[email protected]>
9+
*
10+
* This source file is subject to the MIT license that is bundled
11+
* with this source code in the file LICENSE.
12+
*/
13+
14+
namespace SolidWorx\SimpleHttp\Tests\Http\Plugin;
15+
16+
use Http\Promise\FulfilledPromise;
17+
use Http\Promise\Promise;
18+
use Nyholm\Psr7\Request;
19+
use Nyholm\Psr7\Response;
20+
use PHPUnit\Framework\TestCase;
21+
use Psr\Cache\CacheItemInterface;
22+
use Psr\Cache\CacheItemPoolInterface;
23+
use Psr\Cache\InvalidArgumentException;
24+
use Psr\Http\Message\RequestInterface;
25+
use Psr\Http\Message\ResponseInterface;
26+
use SolidWorx\SimpleHttp\Http\Plugin\CachePlugin;
27+
28+
/**
29+
* @coversDefaultClass \SolidWorx\SimpleHttp\Http\Plugin\CachePlugin
30+
*/
31+
final class CachePluginTest extends TestCase
32+
{
33+
public function testCachePlugin(): void
34+
{
35+
$cacheItemPool = $this->createMock(CacheItemPoolInterface::class);
36+
$cacheItem = $this->createMock(CacheItemInterface::class);
37+
$request = new Request('GET', 'https://example.com');
38+
$response = new Response(200, [], 'Hello World');
39+
40+
$cacheItemPool->expects(self::once())
41+
->method('getItem')
42+
->with(md5('GEThttps://example.com'))
43+
->willReturn($cacheItem);
44+
45+
$cacheItemPool->expects(self::once())
46+
->method('save')
47+
->with($cacheItem);
48+
49+
$cacheItem->expects(self::once())
50+
->method('isHit')
51+
->willReturn(false);
52+
53+
$cacheItem->expects(self::never())
54+
->method('expiresAfter');
55+
56+
$cacheItem->expects(self::once())
57+
->method('set')
58+
->with($response);
59+
60+
$plugin = new CachePlugin($cacheItemPool);
61+
62+
$pluginResponse = $plugin->handleRequest(
63+
$request,
64+
function (RequestInterface $nextRequest) use ($request, $response): Promise {
65+
self::assertSame($request, $nextRequest);
66+
67+
return new FulfilledPromise($response);
68+
},
69+
fn (RequestInterface $nextRequest): Promise => new FulfilledPromise($response)
70+
);
71+
72+
$pluginResponse->then(
73+
fn (ResponseInterface $nextResponse) => self::assertSame($response, $nextResponse)
74+
)->wait();
75+
}
76+
77+
public function testCachePluginWithTtl(): void
78+
{
79+
$cacheItemPool = $this->createMock(CacheItemPoolInterface::class);
80+
$cacheItem = $this->createMock(CacheItemInterface::class);
81+
$request = new Request('GET', 'https://example.com');
82+
$response = new Response(200, [], 'Hello World');
83+
84+
$cacheItemPool->expects(self::once())
85+
->method('getItem')
86+
->with(md5('GEThttps://example.com'))
87+
->willReturn($cacheItem);
88+
89+
$cacheItemPool->expects(self::once())
90+
->method('save')
91+
->with($cacheItem);
92+
93+
$cacheItem->expects(self::once())
94+
->method('isHit')
95+
->willReturn(false);
96+
97+
$cacheItem->expects(self::once())
98+
->method('expiresAfter')
99+
->with(100);
100+
101+
$cacheItem->expects(self::once())
102+
->method('set')
103+
->with($response);
104+
105+
$plugin = new CachePlugin($cacheItemPool, 100);
106+
107+
$pluginResponse = $plugin->handleRequest(
108+
$request,
109+
function (RequestInterface $nextRequest) use ($request, $response): Promise {
110+
self::assertSame($request, $nextRequest);
111+
112+
return new FulfilledPromise($response);
113+
},
114+
fn (RequestInterface $nextRequest): Promise => new FulfilledPromise($response)
115+
);
116+
117+
$pluginResponse->then(
118+
fn (ResponseInterface $nextResponse) => self::assertSame($response, $nextResponse)
119+
)->wait();
120+
}
121+
122+
public function testCachePluginWithCacheHit(): void
123+
{
124+
$cacheItemPool = $this->createMock(CacheItemPoolInterface::class);
125+
$cacheItem = $this->createMock(CacheItemInterface::class);
126+
$request = new Request('GET', 'https://example.com');
127+
$response = new Response(200, [], 'Hello World');
128+
129+
$cacheItemPool->expects(self::once())
130+
->method('getItem')
131+
->with(md5('GEThttps://example.com'))
132+
->willReturn($cacheItem);
133+
134+
$cacheItemPool->expects(self::never())
135+
->method('save');
136+
137+
$cacheItem->expects(self::once())
138+
->method('isHit')
139+
->willReturn(true);
140+
141+
$cacheItem->expects(self::never())
142+
->method('expiresAfter');
143+
144+
$cacheItem->expects(self::never())
145+
->method('set');
146+
147+
$cacheItem->expects(self::once())
148+
->method('get')
149+
->willReturn($response);
150+
151+
$plugin = new CachePlugin($cacheItemPool, 100);
152+
153+
$pluginResponse = $plugin->handleRequest(
154+
$request,
155+
function (RequestInterface $nextRequest): Promise {
156+
self::fail('Request should not be called when cache hit');
157+
},
158+
function (RequestInterface $nextRequest): Promise {
159+
self::fail('Request should not be called when cache hit');
160+
},
161+
);
162+
163+
$pluginResponse->then(
164+
fn (ResponseInterface $nextResponse) => self::assertSame($response, $nextResponse)
165+
)->wait();
166+
}
167+
168+
public function testCachePluginWithCacheException(): void
169+
{
170+
$cacheItemPool = $this->createMock(CacheItemPoolInterface::class);
171+
$cacheItem = $this->createMock(CacheItemInterface::class);
172+
$request = new Request('GET', 'https://example.com');
173+
$response = new Response(200, [], 'Hello World');
174+
175+
$cacheItemPool->expects(self::once())
176+
->method('getItem')
177+
->with(md5('GEThttps://example.com'))
178+
->willThrowException(new class extends \Exception implements InvalidArgumentException{});
179+
180+
$cacheItemPool->expects(self::never())
181+
->method('save');
182+
183+
$cacheItem->expects(self::never())
184+
->method('isHit');
185+
186+
$cacheItem->expects(self::never())
187+
->method('expiresAfter');
188+
189+
$cacheItem->expects(self::never())
190+
->method('set');
191+
192+
$cacheItem->expects(self::never())
193+
->method('get');
194+
195+
$plugin = new CachePlugin($cacheItemPool, 100);
196+
197+
$pluginResponse = $plugin->handleRequest(
198+
$request,
199+
function (RequestInterface $nextRequest) use ($request, $response): Promise {
200+
self::assertSame($request, $nextRequest);
201+
202+
return new FulfilledPromise($response);
203+
},
204+
fn (RequestInterface $nextRequest): Promise => new FulfilledPromise($response)
205+
);
206+
207+
$pluginResponse->then(
208+
fn (ResponseInterface $nextResponse) => self::assertSame($response, $nextResponse)
209+
)->wait();
210+
}
211+
}

0 commit comments

Comments
 (0)