Skip to content

Commit 827b0ce

Browse files
authored
Merge pull request #11 from carsdotcom/log-in-cache
feat: On cache hit, the request class has access to the log file when the cache was written
2 parents c7488fd + 9bc9975 commit 827b0ce

File tree

3 files changed

+107
-52
lines changed

3 files changed

+107
-52
lines changed

app/AbstractRequest.php

Lines changed: 53 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -323,12 +323,15 @@ protected function writeResponseToCache(): void
323323
Cache::tags($this->cacheTags)->put(
324324
$this->cacheKey(),
325325
[
326-
// matches the argument order for Response constructor
327-
$this->response->getStatusCode(),
328-
$this->response->getHeaders(),
329-
(string) $this->response->getBody(),
330-
$this->response->getProtocolVersion(),
331-
$this->response->getReasonPhrase(),
326+
'logs' => $this->sentLogs,
327+
'response' => [
328+
// matches the argument order for Response constructor
329+
$this->response->getStatusCode(),
330+
$this->response->getHeaders(),
331+
(string) $this->response->getBody(),
332+
$this->response->getProtocolVersion(),
333+
$this->response->getReasonPhrase(),
334+
]
332335
],
333336
$this->cacheExpiresTime(),
334337
);
@@ -341,7 +344,38 @@ protected function responseFromCache(): ?Response
341344
}
342345

343346
$fromCache = Cache::tags($this->cacheTags)->get($this->cacheKey());
344-
return $fromCache ? new Response(...$fromCache) : null;
347+
if ($fromCache) {
348+
$this->sentLogs = $fromCache['logs'];
349+
return new Response(...$fromCache['response']);
350+
}
351+
return null;
352+
}
353+
354+
/**
355+
* Return cache key string based on this class, URL, and request body
356+
* @return string
357+
*/
358+
public function cacheKey(): string
359+
{
360+
return hash(
361+
'sha256',
362+
json_encode([
363+
self::class,
364+
$this->getURL(),
365+
$this->encodeBody(),
366+
config('api-request.cache_key_seed', 'v2024.8.6'),
367+
]),
368+
);
369+
}
370+
371+
public function canBeFulfilledByCache(): bool
372+
{
373+
return Cache::tags($this->cacheTags)->has($this->cacheKey());
374+
}
375+
376+
public function isFromCache(): bool
377+
{
378+
return $this->responseIsFromCache;
345379
}
346380

347381
/**
@@ -387,11 +421,21 @@ public function log($outcome): void
387421
* @throws DomainException if this instance has never logged (could mean never run, or $shouldLog is false)
388422
*/
389423
public function getLastLogContents(): string
424+
{
425+
return LogFile::disk()->get($this->getLastLogFile());
426+
}
427+
428+
/**
429+
* Get the filename for the last run of this instance of this request.
430+
* If the last sync or async was a cache hit, this will return the original log of the request that was cached
431+
* @throws DomainException if this instance has never logged (could mean never run, or $shouldLog is false)
432+
*/
433+
public function getLastLogFile(): string
390434
{
391435
if (!$this->sentLogs) {
392436
throw new DomainException('No log files have been saved by this instance.');
393437
}
394-
return LogFile::disk()->get($this->getLastLogFile());
438+
return Str::finish($this->getLogFolder(), '/') . Arr::last($this->sentLogs);
395439
}
396440

397441
/**
@@ -438,36 +482,6 @@ public function getResponse(): ?Response
438482
return $this->response;
439483
}
440484

441-
/**
442-
* Return cache key string based on this class, URL, and request body
443-
* @return string
444-
*/
445-
public function cacheKey(): string
446-
{
447-
return hash(
448-
'sha256',
449-
json_encode([
450-
self::class,
451-
$this->getURL(),
452-
$this->encodeBody(),
453-
config('api-request.cache_key_seed', 'v2022.4.12.0'),
454-
]),
455-
);
456-
}
457-
458-
public function canBeFulfilledByCache(): bool
459-
{
460-
return Cache::tags($this->cacheTags)->has($this->cacheKey());
461-
}
462-
463-
/**
464-
* @return string
465-
*/
466-
public function getLastLogFile(): string
467-
{
468-
return Str::finish($this->getLogFolder(), '/') . Arr::last($this->sentLogs);
469-
}
470-
471485
/**
472486
* Change the timeout of this request. This is the preferred way to override the default,
473487
* even in the constructor of a custom class I think this is much more expressive than a numeric constant
@@ -477,4 +491,5 @@ public function setTimeout(CarbonInterval $interval): void
477491
{
478492
$this->guzzleOptions[RequestOptions::TIMEOUT] = $interval->totalSeconds;
479493
}
494+
480495
}

tests/BaseTestCase.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ protected static function mockRequestCachedResponse(
3939
array $headers = [],
4040
) {
4141
$tags = getProperty($request, 'cacheTags');
42-
Cache::tags($tags)->put($request->cacheKey(), [$status, $headers, $body]);
42+
Cache::tags($tags)->put($request->cacheKey(), ['logs' => [], 'response' => [$status, $headers, $body]]);
4343
}
4444

4545
/**
@@ -54,4 +54,4 @@ protected static function requestCacheBodyContains(
5454
self::assertNotNull($cached, 'Cache should not be empty for request.');
5555
self::assertStringContainsString($substring, (string) $cached->getBody(), $message);
5656
}
57-
}
57+
}

tests/Feature/AbstractRequestTest.php

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,37 +31,36 @@ class AbstractRequestTest extends BaseTestCase
3131
{
3232
use MocksGuzzleInstance;
3333

34+
protected function setUp(): void
35+
{
36+
parent::setUp();
37+
Storage::fake('api-logs');
38+
}
39+
3440
protected function mockRequestWithLog()
3541
{
3642
return new class extends ConcreteRequest {
3743
use ParseResponseJSON;
3844

3945
protected bool $shouldLog = true;
4046

41-
public $logged;
42-
public function log($outcome): void
43-
{
44-
$this->logged = $outcome;
45-
}
46-
47-
public function isFromCache(): bool
47+
public function getLogFolder(): string
4848
{
49-
return $this->responseIsFromCache;
49+
return 'one/two';
5050
}
5151
};
5252
}
5353

5454
public function testCacheMissUsesGuzzle(): void
5555
{
5656
$this->mockGuzzleWithTapper();
57-
$expectedResponse = new Response(200, [], '{"awesome":"sauce"}');
58-
$this->tapper->addMatch('POST', '/.*?/', $expectedResponse);
57+
$this->tapper->addMatchBody('POST', '/.*?/', '{"awesome":"sauce"}');
5958

6059
$requestClass = $this->mockRequestWithLog();
6160

6261
$result = $requestClass->sync();
6362

64-
self::assertSame($expectedResponse, $requestClass->logged);
63+
self::assertStringContainsString('"sauce"', $requestClass->getLastLogContents());
6564
$this->expectTotalRequestCount(1);
6665
$this->assertTapperRequestLike('POST', '/.*?/', 1);
6766

@@ -103,6 +102,38 @@ public function testCacheHitAfterFirstRequest(): void
103102
self::assertTrue($secondRequest->canBeFulfilledByCache());
104103
}
105104

105+
public function testCacheHitHasAccessToOriginalLog(): void
106+
{
107+
Storage::fake('api-logs');
108+
$tapper = $this->mockGuzzleWithTapper();
109+
$tapper->addMatchBody('POST', '/awesome/', '{"awesome":"sauce"}');
110+
111+
$firstLogTime = '2018-01-01T00:00:00.000000+00:00';
112+
Carbon::setTestNow($firstLogTime);
113+
$firstRequest = $this->mockRequestWithLog();
114+
$firstRequest->sync();
115+
self::assertTrue($firstRequest->canBeFulfilledByCache());
116+
self::assertSame($firstRequest->getLastLogFile(), "one/two/{$firstLogTime}");
117+
self::assertSame(1, $tapper->getCountLike('POST', '/awesome/'));
118+
119+
// Regenerate the request
120+
$secondLogTime = '2018-01-01T00:02:02.000000+00:00';
121+
Carbon::setTestNow($secondLogTime);
122+
$secondRequest = $this->mockRequestWithLog();
123+
self::assertTrue($secondRequest->canBeFulfilledByCache());
124+
// Both requests have same key
125+
self::assertSame($firstRequest->cacheKey(), $secondRequest->cacheKey());
126+
127+
$secondRequest->sync();
128+
self::assertSame(1, $tapper->getCountLike('POST', '/awesome/'));
129+
// Result was in cache
130+
self::assertTrue($secondRequest->isFromCache());
131+
// Second request can retrieve log from original time
132+
self::assertSame($secondRequest->getLastLogFile(), "one/two/{$firstLogTime}");
133+
// Second request did not log to disk, only the first request
134+
self::assertSame(["one/two/{$firstLogTime}"], Storage::disk('api-logs')->files('one/two'));
135+
}
136+
106137
public function testDontCacheErrorStatus(): void
107138
{
108139
$firstRequest = $this->mockRequestWithPostProcessor();
@@ -320,6 +351,7 @@ public function testMissingGetLogFolderImplementation(): void
320351
$tapper->addMatchBody('POST', '/awesome/', 'true');
321352

322353
$this->expectException(ToDoException::class);
354+
$this->expectExceptionMessageMatches('/To enable request logging, .+ will have to implement the method getLogFolder/');
323355
$request->sync();
324356
}
325357

@@ -366,6 +398,12 @@ public function getLogFolder(): string
366398
} catch (\DomainException $exception) {
367399
self::assertSame('No log files have been saved by this instance.', $exception->getMessage());
368400
}
401+
try {
402+
$request->getLastLogFile();
403+
self::fail('Should have thrown');
404+
} catch (\DomainException $exception) {
405+
self::assertSame('No log files have been saved by this instance.', $exception->getMessage());
406+
}
369407

370408
$tapper = $this->mockGuzzleWithTapper();
371409
$tapper->addMatchBody('POST', '/awesome/', 'true');
@@ -374,6 +412,7 @@ public function getLogFolder(): string
374412
Carbon::setTestNow($firstLogTime);
375413
$request->sync();
376414
Storage::disk('api-logs')->assertExists("one/two/{$firstLogTime}");
415+
self::assertSame($request->getLastLogFile(), "one/two/{$firstLogTime}");
377416
self::assertSame($request->getLastLogContents(), Storage::disk('api-logs')->get("one/two/{$firstLogTime}"));
378417

379418
// Make a second log, see that it's now returned
@@ -383,6 +422,7 @@ public function getLogFolder(): string
383422
Carbon::setTestNow($secondLogTime);
384423
$request->sync();
385424
Storage::disk('api-logs')->assertExists("one/two/{$secondLogTime}");
425+
self::assertSame($request->getLastLogFile(), "one/two/{$secondLogTime}");
386426
self::assertSame($request->getLastLogContents(), Storage::disk('api-logs')->get("one/two/{$secondLogTime}"));
387427
}
388428

@@ -553,7 +593,7 @@ public function testResponseIsFromCachePreventsWritesToCache(): void
553593
Cache::shouldReceive('get')
554594
->once()
555595
->with($request->cacheKey())
556-
->andReturn([200, [], '42']);
596+
->andReturn(['logs' => [], 'response' => [200, [], '42']]);
557597
// It is NOT written back to cache
558598
Cache::shouldReceive('put')->never();
559599

0 commit comments

Comments
 (0)