Skip to content

Commit

Permalink
Cache Article read model
Browse files Browse the repository at this point in the history
  • Loading branch information
LVoogd committed Feb 18, 2024
1 parent 484c44c commit 9b723b3
Show file tree
Hide file tree
Showing 10 changed files with 153 additions and 23 deletions.
2 changes: 1 addition & 1 deletion phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ parameters:
path: src/Feed/Infrastructure/Persistence/Doctrine/Article/DoctrineArticleRepository.php

-
message: "#^Method App\\\\Feed\\\\Infrastructure\\\\Persistence\\\\Doctrine\\\\Article\\\\DoctrineArticleRepository\\:\\:findLatest\\(\\) should return array\\<int, App\\\\Feed\\\\Domain\\\\Article\\\\Article\\> but returns mixed\\.$#"
message: "#^Parameter \\#1 \\$id of class App\\\\Feed\\\\Domain\\\\Article\\\\ArticleId constructor expects string, mixed given\\.$#"
count: 1
path: src/Feed/Infrastructure/Persistence/Doctrine/Article/DoctrineArticleRepository.php

Expand Down
7 changes: 5 additions & 2 deletions src-dev/Feed/Repository/InMemoryArticleRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,16 @@ public function count(): int
return count($this->entities);
}

public function findLatest(int $offset, int $numberOfArticles): array
public function findLatestIds(int $offset, int $numberOfArticles): array
{
$entities = $this->entities;

usort($entities, fn (Article $a, Article $b) => $a->getUpdated() > $b->getUpdated() ? -1 : 1);

return array_values(array_slice($entities, $offset, $numberOfArticles));
return array_map(
fn(Article $entity) => $entity->getId(),
array_values(array_slice($entities, $offset, $numberOfArticles))
);
}

public function findByUrl(string $url): ?Article
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace App\Feed\Application\Listener\Article;

use App\Common\Infrastructure\Messenger\EventBus\AsEventSubscriber;
use App\Feed\Application\Event\Article\ArticleUpdatedEvent;
use App\Feed\Infrastructure\Cache\FeedCacheKeys;
use Symfony\Contracts\Cache\CacheInterface;

final readonly class InvalidateArticleCacheListener
{
public function __construct(
private CacheInterface $cache,
) {
}

#[AsEventSubscriber]
public function onArticleUpdated(ArticleUpdatedEvent $event): void
{
$this->cache->delete(sprintf(FeedCacheKeys::ARTICLE_WITH_ARTICLE_ID->value, $event->articleId));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@
use App\Feed\Application\Query\Article\LatestUpdatedArticlesQuery;
use App\Feed\Domain\Article\Article;
use App\Feed\Domain\Article\ArticleRepository;
use App\Feed\Infrastructure\Cache\FeedCacheKeys;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;

final readonly class LatestUpdatedArticlesHandler
{
public function __construct(
private ArticleRepository $articleRepository
private ArticleRepository $articleRepository,
private CacheInterface $cache,
) {
}

Expand All @@ -19,9 +23,19 @@ public function __construct(
*/
public function __invoke(LatestUpdatedArticlesQuery $query): array
{
return array_map(
ArticleReadModel::fromArticle(...),
$this->articleRepository->findLatest($query->offset, $query->numberOfArticles),
);
$articleIds = $this->articleRepository->findLatestIds($query->offset, $query->numberOfArticles);

$articles = [];

foreach ($articleIds as $articleId) {
$articles[] = $this->cache->get(
sprintf(FeedCacheKeys::ARTICLE_WITH_ARTICLE_ID->value, $articleId),
fn() => ArticleReadModel::fromArticle(
$this->articleRepository->findOrThrow($articleId)
),
);
}

return $articles;
}
}
4 changes: 2 additions & 2 deletions src/Feed/Domain/Article/ArticleRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ public function count(): int;

/**
* @param int $numberOfArticles
* @return list<Article>
* @return list<ArticleId>
*/
public function findLatest(
public function findLatestIds(
int $offset,
int $numberOfArticles,
): array;
Expand Down
1 change: 1 addition & 0 deletions src/Feed/Infrastructure/Cache/FeedCacheKeys.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
enum FeedCacheKeys : string
{
case TOTAL_ARTICLES_COUNT = 'total_articles_count';
case ARTICLE_WITH_ARTICLE_ID = 'article_with_article_id_%s';
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,22 @@ public function count(): int

/**
* @param int $numberOfArticles
* @return List<Article>
* @return List<ArticleId>
*/
public function findLatest(
public function findLatestIds(
int $offset,
int $numberOfArticles,
): array {
return $this->createQueryBuilder('a')
->orderBy('a.updated', Criteria::DESC)
->setFirstResult($offset)
->setMaxResults($numberOfArticles)
->getQuery()
->getResult()
;
return array_map(
fn($id) => new ArticleId($id),
$this->createQueryBuilder('a')
->select('a.id')
->orderBy('a.updated', Criteria::DESC)
->setFirstResult($offset)
->setMaxResults($numberOfArticles)
->getQuery()
->getSingleColumnResult()
);
}

public function findByUrl(string $url): ?Article
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ public function it_should_return_the_latest_articles_in_a_sorted_manner(): void
$this->getDoctrine()->resetManager();

// Act
$articles = $this->repository->findLatest(0, 2);
$articles = $this->repository->findLatestIds(0, 2);

// Assert
self::assertCount(2, $articles);

self::assertEquals($article1->getId(), $articles[0]->getId());
self::assertEquals($article3->getId(), $articles[1]->getId());
self::assertEquals($article1->getId(), $articles[0]);
self::assertEquals($article3->getId(), $articles[1]);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace Unit\Feed\Application\Listener\Article;

use App\Feed\Application\Event\Article\ArticleUpdatedEvent;
use App\Feed\Application\Listener\Article\InvalidateArticleCacheListener;
use App\Feed\Infrastructure\Cache\FeedCacheKeys;
use Dev\Feed\Factory\ArticleFactory;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;

final class InvalidateArticleCacheListenerTest extends TestCase
{
private InvalidateArticleCacheListener $listener;
private ArrayAdapter $cache;

protected function setUp(): void
{
$this->listener = new InvalidateArticleCacheListener(
$this->cache = new ArrayAdapter(),
);
}

/**
* @test
*/
public function it_should_delete_the_cache_entry_when_the_article_is_updated(): void
{
// Arrange
$article = (new ArticleFactory())->create();
$cacheKey = sprintf(FeedCacheKeys::ARTICLE_WITH_ARTICLE_ID->value, $article->getId());

$this->cache->get(
$cacheKey,
fn (ItemInterface $item) => $article,
);

// Act
$this->listener->onArticleUpdated(new ArticleUpdatedEvent($article->getId()));

// Assert
self::assertFalse($this->cache->hasItem($cacheKey));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,28 @@
use App\Feed\Application\Query\Article\Handler\LatestUpdatedArticlesHandler;
use App\Feed\Application\Query\Article\LatestUpdatedArticlesQuery;
use App\Feed\Domain\Article\ArticleRepository;
use App\Feed\Infrastructure\Cache\FeedCacheKeys;
use DateTime;
use Dev\Common\Infrastructure\Cache\RecordingCache;
use Dev\Feed\Factory\ArticleFactory;
use Dev\Feed\Repository\InMemoryArticleRepository;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Cache\Adapter\ArrayAdapter;

final class LatestUpdatedArticlesHandlerTest extends TestCase
{
private LatestUpdatedArticlesHandler $handler;

private ArticleRepository $articleRepository;

private RecordingCache $cache;

public function setUp(): void
{
$this->articleRepository = new InMemoryArticleRepository();
$this->cache = new RecordingCache(new ArrayAdapter());

$this->handler = new LatestUpdatedArticlesHandler($this->articleRepository);
$this->handler = new LatestUpdatedArticlesHandler($this->articleRepository, $this->cache);
}

/**
Expand Down Expand Up @@ -119,4 +125,39 @@ public function it_should_return_subset_if_asked_articles_expands_available(): v
self::assertEquals($articles[1], ArticleReadModel::fromArticle($article1));
self::assertEquals($articles[2], ArticleReadModel::fromArticle($article2));
}

/**
* @test
*/
public function it_should_return_results_from_cache(): void
{
// Arrange
$article1 = (new ArticleFactory())->withUpdated(new DateTime('2000-05-20 8:01'))->create();
$article2 = (new ArticleFactory())->withUpdated(new DateTime('2000-05-20 8:00'))->create();

$this->articleRepository->save($article1, $article2);

$this->cache->get(
sprintf(FeedCacheKeys::ARTICLE_WITH_ARTICLE_ID->value, $article2->getId()),
fn() => ArticleReadModel::fromArticle($article2),
);

$query = new LatestUpdatedArticlesQuery(0, 2);

// Act
$articles = $this->handler->__invoke($query);

// Assert
self::assertCount(2, $articles);

self::assertFalse($this->cache->cacheIsHitForKey(
sprintf(FeedCacheKeys::ARTICLE_WITH_ARTICLE_ID->value, $article1->getId())
));
self::assertTrue($this->cache->cacheIsHitForKey(
sprintf(FeedCacheKeys::ARTICLE_WITH_ARTICLE_ID->value, $article2->getId())
));

self::assertEquals($articles[0], ArticleReadModel::fromArticle($article1));
self::assertEquals($articles[1], ArticleReadModel::fromArticle($article2));
}
}

0 comments on commit 9b723b3

Please sign in to comment.