05. Change CountFollowersQuery to make use a Read Model based on Redis to improve performance 🚀 👉 Projection is computed directly on the event handler.
This commit is contained in:
6
.env
6
.env
@@ -27,6 +27,7 @@ APP_SECRET=24d45e8c51d769273b29ee9236ba01e4
|
||||
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
|
||||
DATABASE_URL=mysql://user:pass@127.0.0.1:3306/db?serverVersion=mariadb-10.9.2
|
||||
###< doctrine/doctrine-bundle ###
|
||||
|
||||
###> nelmio/cors-bundle ###
|
||||
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
||||
###< nelmio/cors-bundle ###
|
||||
@@ -37,3 +38,8 @@ CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
||||
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
|
||||
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
|
||||
###< symfony/messenger ###
|
||||
|
||||
###> snc/redis-bundle ###
|
||||
# passwords that contain special characters (@, %, :, +) must be urlencoded
|
||||
REDIS_URL=redis://localhost
|
||||
###< snc/redis-bundle ###
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
APP_ENV=dev
|
||||
APP_SECRET=24d45e8c51d769273b29ee9236ba01e4
|
||||
DATABASE_URL=mysql://user:pass@mysql:3306/db?serverVersion=mariadb-10.9.2
|
||||
REDIS_URL=redis://redis
|
||||
7
.psalm-stubs/phpredis.phpstub
Normal file
7
.psalm-stubs/phpredis.phpstub
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
class Redis
|
||||
{
|
||||
/** @return false|string */
|
||||
public function get(string $key);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ RUN apt-get update \
|
||||
libicu-dev \
|
||||
libzip-dev \
|
||||
unzip \
|
||||
redis \
|
||||
$PHPIZE_DEPS \
|
||||
&& docker-php-ext-install pdo_mysql mysqli pcntl bcmath intl zip \
|
||||
&& pecl install redis \
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"phpstan/phpdoc-parser": "^1.6",
|
||||
"ramsey/uuid": "^4.0",
|
||||
"ramsey/uuid-doctrine": "^1.6",
|
||||
"snc/redis-bundle": "^4.3",
|
||||
"symfony/apache-pack": "^1.0",
|
||||
"symfony/asset": "6.1.*",
|
||||
"symfony/console": "6.1.*",
|
||||
@@ -54,6 +55,8 @@
|
||||
"keyvanakbary/mimic": "^1.0",
|
||||
"mockery/mockery": "^1.5",
|
||||
"php-standard-library/psalm-plugin": "^2.0",
|
||||
"phpspec/prophecy": "^1.15",
|
||||
"phpspec/prophecy-phpunit": "^2.0",
|
||||
"phpunit/phpunit": "^9.0",
|
||||
"psalm/plugin-mockery": "^0.9.1",
|
||||
"psalm/plugin-phpunit": "^0.17.0",
|
||||
@@ -69,7 +72,7 @@
|
||||
"symfony/phpunit-bridge": "^6.1",
|
||||
"symfony/stopwatch": "6.1.*",
|
||||
"symfony/web-profiler-bundle": "6.1.*",
|
||||
"theofidry/psysh-bundle": "^4.3",
|
||||
"theofidry/psysh-bundle": "^4.5",
|
||||
"vimeo/psalm": "^4.26",
|
||||
"weirdan/doctrine-psalm-plugin": "^2.3"
|
||||
},
|
||||
|
||||
211
composer.lock
generated
211
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "af06f3081839152655c23b4db1073ec2",
|
||||
"content-hash": "c63441fd0cbff30a03c0eb01d4de0708",
|
||||
"packages": [
|
||||
{
|
||||
"name": "azjezz/psl",
|
||||
@@ -2610,6 +2610,96 @@
|
||||
},
|
||||
"time": "2022-08-01T03:01:07+00:00"
|
||||
},
|
||||
{
|
||||
"name": "snc/redis-bundle",
|
||||
"version": "4.3.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/snc/SncRedisBundle.git",
|
||||
"reference": "5ac15ab0824a4a4448ba27edd2e2773c159c373d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/snc/SncRedisBundle/zipball/5ac15ab0824a4a4448ba27edd2e2773c159c373d",
|
||||
"reference": "5ac15ab0824a4a4448ba27edd2e2773c159c373d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0",
|
||||
"symfony/framework-bundle": "^4.4 || ^5.3 || ^6.0",
|
||||
"symfony/http-foundation": "^4.4 || ^5.3 || ^6.0",
|
||||
"symfony/var-dumper": "^4.4 || ^5.3 || ^6.0"
|
||||
},
|
||||
"conflict": {
|
||||
"ext-redis": "<5.3",
|
||||
"predis/predis": "<1.1,>=2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/annotations": "^1.13",
|
||||
"doctrine/coding-standard": "^9.0",
|
||||
"ext-pdo_sqlite": "*",
|
||||
"ext-redis": "*",
|
||||
"friendsofphp/proxy-manager-lts": "^1.0.6",
|
||||
"monolog/monolog": "*",
|
||||
"phpunit/phpunit": "^8.5 || ^9.5",
|
||||
"predis/predis": ">=1.1",
|
||||
"symfony/browser-kit": "^4.4 || ^5.3 || ^6.0",
|
||||
"symfony/cache": "^4.4 || ^5.3 || ^6.0",
|
||||
"symfony/console": "^4.4 || ^5.3 || ^6.0",
|
||||
"symfony/dom-crawler": "^4.4 || ^5.3 || ^6.0",
|
||||
"symfony/filesystem": "^4.4 || ^5.3 || ^6.0",
|
||||
"symfony/phpunit-bridge": "^6.0",
|
||||
"symfony/profiler-pack": "^1.0",
|
||||
"symfony/proxy-manager-bridge": "^4.4 || ^5.3 || ^6.0",
|
||||
"symfony/stopwatch": "^4.4 || ^5.3 || ^6.0",
|
||||
"symfony/twig-bundle": "^4.4 || ^5.3 || ^6.0",
|
||||
"symfony/yaml": "^4.4 || ^5.3 || ^6.0",
|
||||
"vimeo/psalm": "^4.13"
|
||||
},
|
||||
"suggest": {
|
||||
"monolog/monolog": "If you want to use the monolog redis handler.",
|
||||
"predis/predis": "If you want to use predis.",
|
||||
"symfony/console": "If you want to use commands to interact with the redis database",
|
||||
"symfony/proxy-manager-bridge": "If you want to lazy-load some services"
|
||||
},
|
||||
"type": "symfony-bundle",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Snc\\RedisBundle\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Henrik Westphal",
|
||||
"email": "henrik.westphal@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Community contributors",
|
||||
"homepage": "https://github.com/snc/SncRedisBundle/contributors"
|
||||
}
|
||||
],
|
||||
"description": "A Redis bundle for Symfony",
|
||||
"homepage": "https://github.com/snc/SncRedisBundle",
|
||||
"keywords": [
|
||||
"nosql",
|
||||
"redis",
|
||||
"symfony"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/snc/SncRedisBundle/issues",
|
||||
"source": "https://github.com/snc/SncRedisBundle/tree/4.3.0"
|
||||
},
|
||||
"time": "2022-07-13T20:58:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/apache-pack",
|
||||
"version": "v1.0.1",
|
||||
@@ -9838,6 +9928,125 @@
|
||||
},
|
||||
"time": "2022-02-27T20:08:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpspec/prophecy",
|
||||
"version": "v1.15.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpspec/prophecy.git",
|
||||
"reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/bbcd7380b0ebf3961ee21409db7b38bc31d69a13",
|
||||
"reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"doctrine/instantiator": "^1.2",
|
||||
"php": "^7.2 || ~8.0, <8.2",
|
||||
"phpdocumentor/reflection-docblock": "^5.2",
|
||||
"sebastian/comparator": "^3.0 || ^4.0",
|
||||
"sebastian/recursion-context": "^3.0 || ^4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpspec/phpspec": "^6.0 || ^7.0",
|
||||
"phpunit/phpunit": "^8.0 || ^9.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Prophecy\\": "src/Prophecy"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Konstantin Kudryashov",
|
||||
"email": "ever.zet@gmail.com",
|
||||
"homepage": "http://everzet.com"
|
||||
},
|
||||
{
|
||||
"name": "Marcello Duarte",
|
||||
"email": "marcello.duarte@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Highly opinionated mocking framework for PHP 5.3+",
|
||||
"homepage": "https://github.com/phpspec/prophecy",
|
||||
"keywords": [
|
||||
"Double",
|
||||
"Dummy",
|
||||
"fake",
|
||||
"mock",
|
||||
"spy",
|
||||
"stub"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/phpspec/prophecy/issues",
|
||||
"source": "https://github.com/phpspec/prophecy/tree/v1.15.0"
|
||||
},
|
||||
"time": "2021-12-08T12:19:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpspec/prophecy-phpunit",
|
||||
"version": "v2.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpspec/prophecy-phpunit.git",
|
||||
"reference": "2d7a9df55f257d2cba9b1d0c0963a54960657177"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpspec/prophecy-phpunit/zipball/2d7a9df55f257d2cba9b1d0c0963a54960657177",
|
||||
"reference": "2d7a9df55f257d2cba9b1d0c0963a54960657177",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.3 || ^8",
|
||||
"phpspec/prophecy": "^1.3",
|
||||
"phpunit/phpunit": "^9.1"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Prophecy\\PhpUnit\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christophe Coevoet",
|
||||
"email": "stof@notk.org"
|
||||
}
|
||||
],
|
||||
"description": "Integrating the Prophecy mocking library in PHPUnit test cases",
|
||||
"homepage": "http://phpspec.net",
|
||||
"keywords": [
|
||||
"phpunit",
|
||||
"prophecy"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/phpspec/prophecy-phpunit/issues",
|
||||
"source": "https://github.com/phpspec/prophecy-phpunit/tree/v2.0.1"
|
||||
},
|
||||
"time": "2020-07-09T08:33:42+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
"version": "9.2.17",
|
||||
|
||||
@@ -17,4 +17,5 @@ return [
|
||||
Nelmio\ApiDocBundle\NelmioApiDocBundle::class => ['all' => true],
|
||||
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
|
||||
ApiPlatform\Core\Bridge\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
|
||||
Snc\RedisBundle\SncRedisBundle::class => ['all' => true],
|
||||
];
|
||||
|
||||
16
config/packages/snc_redis.yaml
Normal file
16
config/packages/snc_redis.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
snc_redis:
|
||||
clients:
|
||||
default:
|
||||
type: phpredis
|
||||
alias: default
|
||||
dsn: "%env(REDIS_URL)%"
|
||||
|
||||
# Define your clients here. The example below connects to database 0 of the default Redis server.
|
||||
#
|
||||
# See https://github.com/snc/SncRedisBundle/blob/master/docs/README.md for instructions on
|
||||
# how to configure the bundle.
|
||||
#
|
||||
# default:
|
||||
# type: phpredis
|
||||
# alias: default
|
||||
# dsn: "%env(REDIS_URL)%"
|
||||
@@ -13,6 +13,7 @@ services:
|
||||
Cheeper\DomainModel\Cheep\CheepRepository: '@Cheeper\Infrastructure\Persistence\DoctrineOrmCheepRepository'
|
||||
Cheeper\DomainModel\Follow\FollowRepository: '@Cheeper\Infrastructure\Persistence\DoctrineOrmFollowRepository'
|
||||
Cheeper\Application\EventBus: '@Cheeper\Infrastructure\Application\SymfonyMessengerEventBus'
|
||||
Redis: '@snc_redis.default'
|
||||
|
||||
# makes classes in src/ available to be used as services
|
||||
# this creates a service per class whose id is the fully-qualified class name
|
||||
|
||||
@@ -13,13 +13,13 @@ services:
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
context: .
|
||||
target: dev
|
||||
args:
|
||||
- UID=${UID:-}
|
||||
- GID=${GID:-}
|
||||
target: local
|
||||
init: true
|
||||
env_file: .env.docker.dist
|
||||
ports:
|
||||
- "8000:80"
|
||||
volumes:
|
||||
- ./:/var/www/html
|
||||
- ./:/var/www/html
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
@@ -15,6 +15,10 @@
|
||||
</ignoreFiles>
|
||||
</projectFiles>
|
||||
|
||||
<stubs>
|
||||
<file name=".psalm-stubs/phpredis.phpstub"/>
|
||||
</stubs>
|
||||
|
||||
<plugins>
|
||||
<pluginClass class="Psalm\SymfonyPsalmPlugin\Plugin"/>
|
||||
<pluginClass class="Psalm\PhpUnitPlugin\Plugin"/>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cheeper\Application\AuthorWasFollowed;
|
||||
|
||||
use Cheeper\DomainModel\Author\AuthorWasFollowed;
|
||||
use Redis;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler]
|
||||
final class AuthorWasFollowedEventHandler
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Redis $redis,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(AuthorWasFollowed $event): void
|
||||
{
|
||||
$this->redis->incr("followers_of:" . $event->toAuthorId);
|
||||
}
|
||||
}
|
||||
@@ -7,4 +7,4 @@ namespace Cheeper\Application;
|
||||
/** @psalm-immutable */
|
||||
interface Command
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,4 +14,4 @@ final class CountFollowersQuery implements Query
|
||||
public readonly string $authorId
|
||||
) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,43 +4,17 @@ declare(strict_types=1);
|
||||
|
||||
namespace Cheeper\Application\CountFollowers;
|
||||
|
||||
use Cheeper\DomainModel\Author\Author;
|
||||
use Cheeper\DomainModel\Author\AuthorDoesNotExist;
|
||||
use Cheeper\DomainModel\Author\AuthorId;
|
||||
use Cheeper\DomainModel\Author\AuthorRepository;
|
||||
use Cheeper\DomainModel\Follow\FollowRepository;
|
||||
|
||||
final class CountFollowersQueryHandler
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AuthorRepository $authorRepository,
|
||||
private readonly FollowRepository $followRepository,
|
||||
private readonly \Redis $redis,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(CountFollowersQuery $query): int
|
||||
{
|
||||
$this->tryToFindAuthor($query->authorId);
|
||||
$totalFollowers = $this->redis->get("followers_of:" . $query->authorId);
|
||||
|
||||
return $this->followRepository->numberOfFollowersFor(
|
||||
AuthorId::fromString($query->authorId)
|
||||
);
|
||||
return false === $totalFollowers ? 0 : (int)$totalFollowers;
|
||||
}
|
||||
|
||||
/** @psalm-param non-empty-string $authorId */
|
||||
private function tryToFindAuthor(string $authorId): void
|
||||
{
|
||||
$id = AuthorId::fromString($authorId);
|
||||
$author = $this->authorRepository->ofId($id);
|
||||
|
||||
$this->assertAuthorIsNotNull($author, $id);
|
||||
}
|
||||
|
||||
/** @psalm-assert Author $author */
|
||||
private function assertAuthorIsNotNull(Author|null $author, AuthorId $id): void
|
||||
{
|
||||
if (null === $author) {
|
||||
throw AuthorDoesNotExist::withAuthorIdOf($id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,4 +13,4 @@ interface EventBus
|
||||
* @param DomainEvent[] $events
|
||||
*/
|
||||
public function publishAll(array $events): void;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,4 +18,4 @@ final class FollowCommand implements Command
|
||||
public readonly string $toAuthorId,
|
||||
) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,7 @@ final class FollowCommandHandler
|
||||
private readonly AuthorRepository $authorRepository,
|
||||
private readonly FollowRepository $followRepository,
|
||||
private readonly EventBus $eventBus,
|
||||
)
|
||||
{
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(FollowCommand $command): void
|
||||
@@ -54,4 +53,4 @@ final class FollowCommandHandler
|
||||
throw AuthorDoesNotExist::withAuthorIdOf($id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,4 +7,4 @@ namespace Cheeper\Application;
|
||||
/** @psalm-immutable */
|
||||
interface Query
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,21 +4,23 @@ declare(strict_types=1);
|
||||
|
||||
namespace Cheeper\DomainModel;
|
||||
|
||||
trait RecordsEvents
|
||||
/** @template T of DomainEvent */
|
||||
abstract class AggregateRoot
|
||||
{
|
||||
/**
|
||||
* @psalm-var list<DomainEvent>
|
||||
* @psalm-var list<T>
|
||||
* @var DomainEvent[]
|
||||
*/
|
||||
private array $events = [];
|
||||
|
||||
private function recordThat(DomainEvent $eventHappened): void
|
||||
/** @psalm-param T $eventHappened */
|
||||
protected function recordThat(DomainEvent $eventHappened): void
|
||||
{
|
||||
$this->events[] = $eventHappened;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-return list<DomainEvent>
|
||||
* @psalm-return list<T>
|
||||
* @return DomainEvent[]
|
||||
*/
|
||||
public function pullEvents(): array
|
||||
@@ -28,4 +30,4 @@ trait RecordsEvents
|
||||
|
||||
return $events;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,4 +15,4 @@ final class AuthorWasFollowed implements DomainEvent
|
||||
public readonly \DateTimeImmutable $occurredOn,
|
||||
) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,4 +7,4 @@ namespace Cheeper\DomainModel;
|
||||
/** @psalm-immutable */
|
||||
interface DomainEvent
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,17 @@ declare(strict_types=1);
|
||||
|
||||
namespace Cheeper\DomainModel\Follow;
|
||||
|
||||
use Cheeper\DomainModel\AggregateRoot;
|
||||
use Cheeper\DomainModel\Author\AuthorId;
|
||||
use Cheeper\DomainModel\Author\AuthorWasFollowed;
|
||||
use Cheeper\DomainModel\Clock\Clock;
|
||||
use Cheeper\DomainModel\RecordsEvents;
|
||||
|
||||
/** @final */
|
||||
class Follow
|
||||
/**
|
||||
* @final
|
||||
* @extends AggregateRoot<AuthorWasFollowed>
|
||||
*/
|
||||
class Follow extends AggregateRoot
|
||||
{
|
||||
use RecordsEvents;
|
||||
|
||||
/**
|
||||
* @psalm-param non-empty-string $followId
|
||||
* @psalm-param non-empty-string $fromAuthorId
|
||||
|
||||
@@ -16,6 +16,7 @@ final class InMemoryEventBus implements EventBus
|
||||
*/
|
||||
private array $events = [];
|
||||
|
||||
/** @psalm-param list<DomainEvent> $events */
|
||||
public function publishAll(array $events): void
|
||||
{
|
||||
$this->events = Vec\concat($this->events, $events);
|
||||
@@ -29,4 +30,4 @@ final class InMemoryEventBus implements EventBus
|
||||
{
|
||||
return $this->events;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ declare(strict_types=1);
|
||||
namespace Cheeper\Infrastructure\Application;
|
||||
|
||||
use Cheeper\Application\EventBus;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Psl\Iter;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
final class SymfonyMessengerEventBus implements EventBus
|
||||
{
|
||||
@@ -19,4 +19,4 @@ final class SymfonyMessengerEventBus implements EventBus
|
||||
{
|
||||
Iter\apply($events, $this->messageBus->dispatch(...));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
symfony.lock
12
symfony.lock
@@ -335,6 +335,18 @@
|
||||
"sebastian/version": {
|
||||
"version": "3.0.0"
|
||||
},
|
||||
"snc/redis-bundle": {
|
||||
"version": "4.3",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes-contrib",
|
||||
"branch": "main",
|
||||
"version": "2.0",
|
||||
"ref": "36b3d9ab65be62de4e085a25e6ca899efa96b1f3"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/snc_redis.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/apache-pack": {
|
||||
"version": "1.0",
|
||||
"recipe": {
|
||||
|
||||
@@ -24,11 +24,10 @@ final class GetFollowersCountControllerTest extends ApiTestCase
|
||||
|
||||
// Make first follow second author
|
||||
$this->makeFollow($client, $fromAuthor['id'], $toAuthor['id']);
|
||||
sleep(2);
|
||||
|
||||
$totalFollowers = $this->getFollowersCount($client, $toAuthor['id']);
|
||||
|
||||
$this->assertSame(1, $totalFollowers);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ final class PostFollowersControllerTest extends ApiTestCase
|
||||
|
||||
$this->makeFollow($client, $firstAuthor['id'], $secondAuthor['id']);
|
||||
|
||||
sleep(3);
|
||||
|
||||
$this->assertSame(0, $this->getFollowersCount($client, $firstAuthor['id']));
|
||||
$this->assertSame(1, $this->getFollowersCount($client, $secondAuthor['id']));
|
||||
}
|
||||
|
||||
@@ -4,43 +4,47 @@ declare(strict_types=1);
|
||||
|
||||
namespace Cheeper\Tests\Application;
|
||||
|
||||
use Cheeper\Application\AuthorWasFollowed\AuthorWasFollowedEventHandler;
|
||||
use Cheeper\Application\CountFollowers\CountFollowersQuery;
|
||||
use Cheeper\Application\CountFollowers\CountFollowersQueryHandler;
|
||||
use Cheeper\Application\Follow\FollowCommand;
|
||||
use Cheeper\Application\Follow\FollowCommandHandler;
|
||||
use Cheeper\DomainModel\Author\AuthorDoesNotExist;
|
||||
use Cheeper\DomainModel\Author\AuthorRepository;
|
||||
use Cheeper\DomainModel\Author\AuthorWasFollowed;
|
||||
use Cheeper\DomainModel\Follow\FollowRepository;
|
||||
use Cheeper\Infrastructure\Application\InMemoryEventBus;
|
||||
use Cheeper\Infrastructure\Persistence\InMemoryAuthorRepository;
|
||||
use Cheeper\Infrastructure\Persistence\InMemoryFollowRepository;
|
||||
use Cheeper\Tests\DomainModel\Author\AuthorTestDataBuilder;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psl\Iter;
|
||||
use Psl\Type;
|
||||
use Redis;
|
||||
|
||||
final class CountFollowersQueryHandlerTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
/** @var ObjectProphecy<Redis> */
|
||||
private ObjectProphecy $redis;
|
||||
private AuthorRepository $authorRepository;
|
||||
private FollowRepository $followRepository;
|
||||
private CountFollowersQueryHandler $countFollowersQueryHandler;
|
||||
private AuthorWasFollowedEventHandler $authorWasFollowedEventHandler;
|
||||
private int $followers = 0;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->redis = $this->prophesize(\Redis::class);
|
||||
$this->authorRepository = new InMemoryAuthorRepository();
|
||||
$this->followRepository = new InMemoryFollowRepository();
|
||||
|
||||
$this->countFollowersQueryHandler = new CountFollowersQueryHandler($this->authorRepository, $this->followRepository);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function givenFollowersCountForANonExistingAuthorWhenCountIsRequestedThenAnExceptionShouldBeThrown(): void
|
||||
{
|
||||
$this->expectException(AuthorDoesNotExist::class);
|
||||
|
||||
$authorId = AuthorTestDataBuilder::anAuthorIdentity()->id;
|
||||
|
||||
($this->countFollowersQueryHandler)(
|
||||
new CountFollowersQuery($authorId)
|
||||
);
|
||||
$redis = $this->redis->reveal();
|
||||
$this->countFollowersQueryHandler = new CountFollowersQueryHandler($redis);
|
||||
$this->authorWasFollowedEventHandler = new AuthorWasFollowedEventHandler($redis);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
@@ -59,11 +63,41 @@ final class CountFollowersQueryHandlerTest extends TestCase
|
||||
|
||||
$authorId = $author->authorId()->id;
|
||||
|
||||
$followCommandHandler = new FollowCommandHandler($this->authorRepository, $this->followRepository, new InMemoryEventBus());
|
||||
$eventBus = new InMemoryEventBus();
|
||||
|
||||
$followCommandHandler = new FollowCommandHandler($this->authorRepository, $this->followRepository, $eventBus);
|
||||
($followCommandHandler)(new FollowCommand($follower1->authorId()->id, $authorId));
|
||||
($followCommandHandler)(new FollowCommand($follower2->authorId()->id, $authorId));
|
||||
($followCommandHandler)(new FollowCommand($follower3->authorId()->id, $authorId));
|
||||
|
||||
$followersCounter = new class() {
|
||||
private int $count = 0;
|
||||
|
||||
public function increment(): void
|
||||
{
|
||||
++$this->count;
|
||||
}
|
||||
|
||||
public function getCount(): int
|
||||
{
|
||||
return $this->count;
|
||||
}
|
||||
};
|
||||
|
||||
$this->redis->incr(Argument::type('string'))->will(function () use($followersCounter): int {
|
||||
$followersCounter->increment();
|
||||
return $followersCounter->getCount();
|
||||
});
|
||||
|
||||
$this->redis->get(Argument::type('string'))->will(static fn() => $followersCounter->getCount());
|
||||
$this->redis->exists(Argument::type('string'))->willReturn(true);
|
||||
|
||||
$events = Type\vec(
|
||||
Type\instance_of(AuthorWasFollowed::class)
|
||||
)->coerce($eventBus->getEvents());
|
||||
|
||||
Iter\apply($events, ($this->authorWasFollowedEventHandler)(...));
|
||||
|
||||
$totalNumberOfFollowers = ($this->countFollowersQueryHandler)(
|
||||
new CountFollowersQuery($authorId)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user