08. TimelineQuery
This commit is contained in:
@@ -19,7 +19,7 @@ namespace Symfony\Component\Messenger
|
||||
/**
|
||||
* @template T
|
||||
* @psalm-param object|Envelope $message
|
||||
* @psalm-return T|Envelope<HandledStamp>
|
||||
* @psalm-return T
|
||||
*/
|
||||
private function handle($message) { }
|
||||
}
|
||||
|
||||
@@ -6,11 +6,15 @@ namespace App\Controller;
|
||||
|
||||
use App\Dto\CheepDto;
|
||||
use Cheeper\Application\CheepApplicationService;
|
||||
use Cheeper\Application\QueryBus;
|
||||
use Cheeper\Application\Timeline\TimelineQuery;
|
||||
use Cheeper\Application\Timeline\TimelineQueryResponse;
|
||||
use Cheeper\DomainModel\Author\AuthorDoesNotExist;
|
||||
use Cheeper\DomainModel\Cheep\Cheep;
|
||||
use Nelmio\ApiDocBundle\Annotation\Model;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@@ -22,7 +26,8 @@ final class GetTimelineController extends AbstractController
|
||||
private const DEFAULT_TIMELINE_CHUNK_SIZE = 10;
|
||||
|
||||
public function __construct(
|
||||
private readonly CheepApplicationService $cheepApplicationService
|
||||
private readonly CheepApplicationService $cheepApplicationService,
|
||||
private readonly QueryBus $queryBus,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -64,18 +69,30 @@ final class GetTimelineController extends AbstractController
|
||||
$this->assertNotEmptyString($id, "Author ID");
|
||||
$this->assertValidUuid($id);
|
||||
|
||||
$offset = $request->query->getInt('offset');
|
||||
$size = $request->query->getInt('size', self::DEFAULT_TIMELINE_CHUNK_SIZE);
|
||||
$offset = $request->query->getInt('offset');
|
||||
|
||||
if ($offset < 0) {
|
||||
throw new BadRequestException("Offset should be 0 or greater");
|
||||
}
|
||||
|
||||
$size = $request->query->getInt('size', self::DEFAULT_TIMELINE_CHUNK_SIZE);
|
||||
|
||||
if ($size <= 0) {
|
||||
throw new BadRequestException("Size should be greater than 0");
|
||||
}
|
||||
|
||||
try {
|
||||
$timeline = $this->cheepApplicationService->timelineFrom($id, $offset, $size);
|
||||
$timeline = $this->queryBus->askFor(
|
||||
new TimelineQuery($id, $offset, $size)
|
||||
);
|
||||
assert($timeline instanceof TimelineQueryResponse);
|
||||
} catch (AuthorDoesNotExist) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
$cheeps = array_map(
|
||||
static fn (Cheep $c) => CheepDto::assembleFrom($c),
|
||||
$timeline
|
||||
$timeline->timeline
|
||||
);
|
||||
|
||||
return $this->json($cheeps);
|
||||
|
||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||
namespace Cheeper\Application;
|
||||
|
||||
use Cheeper\DomainModel\Author\AuthorDoesNotExist;
|
||||
use Cheeper\DomainModel\Author\AuthorId;
|
||||
use Cheeper\DomainModel\Author\AuthorRepository;
|
||||
use Cheeper\DomainModel\Author\UserName;
|
||||
use Cheeper\DomainModel\Cheep\Cheep;
|
||||
@@ -45,20 +44,6 @@ final class CheepApplicationService
|
||||
return $cheep;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param non-empty-string $authorId
|
||||
*/
|
||||
public function timelineFrom(string $authorId, int $offset, int $size): array
|
||||
{
|
||||
$author = $this->authorRepository->ofId(AuthorId::fromString($authorId));
|
||||
|
||||
if (null === $author) {
|
||||
throw AuthorDoesNotExist::withAuthorIdOf(AuthorId::fromString($authorId));
|
||||
}
|
||||
|
||||
return $this->cheepRepository->ofFollowingPeopleOf($author, $offset, $size);
|
||||
}
|
||||
|
||||
/** @psalm-param non-empty-string $id */
|
||||
public function getCheep(string $id): Cheep|null
|
||||
{
|
||||
|
||||
@@ -9,4 +9,4 @@ interface CommandBus
|
||||
{
|
||||
/** @psalm-param T $command */
|
||||
public function handle(Command $command): void;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,4 +13,4 @@ final class CountFollowersQueryResponse implements QueryResponse
|
||||
public readonly int $totalNumberOfFollowers
|
||||
) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,4 +15,4 @@ interface QueryBus
|
||||
* @psalm-return TQueryResponse
|
||||
*/
|
||||
public function askFor(Query $query): QueryResponse;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,4 +7,4 @@ namespace Cheeper\Application;
|
||||
/** @psalm-immutable */
|
||||
interface QueryResponse
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
23
src/Cheeper/Application/Timeline/TimelineQuery.php
Normal file
23
src/Cheeper/Application/Timeline/TimelineQuery.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cheeper\Application\Timeline;
|
||||
|
||||
use Cheeper\Application\Query;
|
||||
|
||||
/** @psalm-immutable */
|
||||
final class TimelineQuery implements Query
|
||||
{
|
||||
/**
|
||||
* @psalm-param non-empty-string $authorId
|
||||
* @psalm-param positive-int|0 $offste
|
||||
* @psalm-param positive-int $size
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $authorId,
|
||||
public readonly int $offset,
|
||||
public readonly int $size,
|
||||
) {
|
||||
}
|
||||
}
|
||||
35
src/Cheeper/Application/Timeline/TimelineQueryHandler.php
Normal file
35
src/Cheeper/Application/Timeline/TimelineQueryHandler.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cheeper\Application\Timeline;
|
||||
|
||||
use Cheeper\DomainModel\Author\AuthorDoesNotExist;
|
||||
use Cheeper\DomainModel\Author\AuthorId;
|
||||
use Cheeper\DomainModel\Author\AuthorRepository;
|
||||
use Cheeper\DomainModel\Cheep\CheepRepository;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler]
|
||||
final class TimelineQueryHandler
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AuthorRepository $authorRepository,
|
||||
private readonly CheepRepository $cheepRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(TimelineQuery $query): TimelineQueryResponse
|
||||
{
|
||||
$authorId = AuthorId::fromString($query->authorId);
|
||||
$author = $this->authorRepository->ofId($authorId);
|
||||
|
||||
if (null === $author) {
|
||||
throw AuthorDoesNotExist::withAuthorIdOf($authorId);
|
||||
}
|
||||
|
||||
return new TimelineQueryResponse(
|
||||
$this->cheepRepository->ofFollowingPeopleOf($author, $query->offset, $query->size)
|
||||
);
|
||||
}
|
||||
}
|
||||
21
src/Cheeper/Application/Timeline/TimelineQueryResponse.php
Normal file
21
src/Cheeper/Application/Timeline/TimelineQueryResponse.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cheeper\Application\Timeline;
|
||||
|
||||
use Cheeper\Application\QueryResponse;
|
||||
use Cheeper\DomainModel\Cheep\Cheep;
|
||||
|
||||
/** @psalm-immutable */
|
||||
final class TimelineQueryResponse implements QueryResponse
|
||||
{
|
||||
/**
|
||||
* @psalm-param list<Cheep> $timeline
|
||||
* @param Cheep[] $timeline
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly array $timeline
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -19,4 +19,4 @@ final class SymfonyMessengerCommandBus implements CommandBus
|
||||
{
|
||||
$this->commandBus->dispatch($command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,8 @@ namespace Cheeper\Infrastructure\Application;
|
||||
use Cheeper\Application\Query;
|
||||
use Cheeper\Application\QueryBus;
|
||||
use Cheeper\Application\QueryResponse;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Exception\LogicException;
|
||||
use Symfony\Component\Messenger\HandleTrait;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Messenger\Stamp\HandledStamp;
|
||||
|
||||
final class SymfonyMessengerQueryBus implements QueryBus
|
||||
{
|
||||
@@ -30,4 +27,4 @@ final class SymfonyMessengerQueryBus implements QueryBus
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,15 +18,13 @@ use Psl\Iter;
|
||||
|
||||
final class CheepApplicationServiceTest extends TestCase
|
||||
{
|
||||
private CheepRepository $cheepRepository;
|
||||
private AuthorRepository $authorRepository;
|
||||
private CheepApplicationService $cheepService;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->cheepRepository = new InMemoryCheepRepository();
|
||||
$this->authorRepository = new InMemoryAuthorRepository();
|
||||
$this->cheepService = new CheepApplicationService($this->authorRepository, $this->cheepRepository);
|
||||
$this->cheepService = new CheepApplicationService($this->authorRepository, new InMemoryCheepRepository());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
@@ -53,33 +51,4 @@ final class CheepApplicationServiceTest extends TestCase
|
||||
$this->assertNotNull($cheep->authorId());
|
||||
$this->assertEquals('message', $cheep->cheepMessage()->message);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function givenATimelineRequestWhenTheAuthorDoesNotExistThenAnExceptionShouldBeThrown(): void
|
||||
{
|
||||
$this->expectException(AuthorDoesNotExist::class);
|
||||
|
||||
$this->cheepService->timelineFrom(AuthorTestDataBuilder::anAuthorIdentity()->id, 0, 1);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function givenATimelineRequestWhenExecutionGoesWellThenAListOfCheepsShouldBeReturned(): void
|
||||
{
|
||||
$author = AuthorTestDataBuilder::anAuthor()->build();
|
||||
|
||||
$this->authorRepository->add($author);
|
||||
|
||||
$cheeps = [
|
||||
CheepTestDataBuilder::aCheep()->withAMessage("test1")->build(),
|
||||
CheepTestDataBuilder::aCheep()->withAMessage("test2")->build(),
|
||||
CheepTestDataBuilder::aCheep()->withAMessage("test3")->build(),
|
||||
];
|
||||
|
||||
Iter\apply($cheeps, fn(Cheep $c) => $this->cheepRepository->add($c));
|
||||
|
||||
$this->assertCount(
|
||||
count($cheeps),
|
||||
$this->cheepService->timelineFrom($author->authorId()->id, 0, 10)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
78
tests/Cheeper/Tests/Application/TimelineQueryHandlerTest.php
Normal file
78
tests/Cheeper/Tests/Application/TimelineQueryHandlerTest.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cheeper\Tests\Application;
|
||||
|
||||
use Cheeper\Application\CheepApplicationService;
|
||||
use Cheeper\Application\Timeline\TimelineQuery;
|
||||
use Cheeper\Application\Timeline\TimelineQueryHandler;
|
||||
use Cheeper\Application\Timeline\TimelineQueryResponse;
|
||||
use Cheeper\DomainModel\Author\AuthorDoesNotExist;
|
||||
use Cheeper\DomainModel\Author\AuthorRepository;
|
||||
use Cheeper\DomainModel\Cheep\Cheep;
|
||||
use Cheeper\DomainModel\Cheep\CheepRepository;
|
||||
use Cheeper\Infrastructure\Persistence\InMemoryAuthorRepository;
|
||||
use Cheeper\Infrastructure\Persistence\InMemoryCheepRepository;
|
||||
use Cheeper\Tests\DomainModel\Author\AuthorTestDataBuilder;
|
||||
use Cheeper\Tests\DomainModel\Cheep\CheepTestDataBuilder;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psl\Iter;
|
||||
|
||||
final class TimelineQueryHandlerTest extends TestCase
|
||||
{
|
||||
private CheepRepository $cheepRepository;
|
||||
private AuthorRepository $authorRepository;
|
||||
private CheepApplicationService $cheepService;
|
||||
private TimelineQueryHandler $timelineQueryHandler;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->cheepRepository = new InMemoryCheepRepository();
|
||||
$this->authorRepository = new InMemoryAuthorRepository();
|
||||
$this->cheepService = new CheepApplicationService($this->authorRepository, $this->cheepRepository);
|
||||
$this->timelineQueryHandler = new TimelineQueryHandler($this->authorRepository, $this->cheepRepository);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function givenATimelineRequestWhenTheAuthorDoesNotExistThenAnExceptionShouldBeThrown(): void
|
||||
{
|
||||
$this->expectException(AuthorDoesNotExist::class);
|
||||
|
||||
$this->timelineFrom(AuthorTestDataBuilder::anAuthorIdentity()->id, 0, 1);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function givenATimelineRequestWhenExecutionGoesWellThenAListOfCheepsShouldBeReturned(): void
|
||||
{
|
||||
$author = AuthorTestDataBuilder::anAuthor()->build();
|
||||
|
||||
$this->authorRepository->add($author);
|
||||
|
||||
$cheeps = [
|
||||
CheepTestDataBuilder::aCheep()->withAMessage("test1")->build(),
|
||||
CheepTestDataBuilder::aCheep()->withAMessage("test2")->build(),
|
||||
CheepTestDataBuilder::aCheep()->withAMessage("test3")->build(),
|
||||
];
|
||||
|
||||
Iter\apply($cheeps, fn(Cheep $c) => $this->cheepRepository->add($c));
|
||||
|
||||
$this->assertCount(
|
||||
count($cheeps),
|
||||
$this->timelineFrom($author->authorId()->id, 0, 10)->timeline
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $authorId
|
||||
* @param positive-int|0 $offset
|
||||
* @param positive-int $size
|
||||
* @return TimelineQueryResponse
|
||||
*/
|
||||
private function timelineFrom(string $authorId, int $offset, int $size): TimelineQueryResponse
|
||||
{
|
||||
return ($this->timelineQueryHandler)(
|
||||
new TimelineQuery($authorId, $offset, $size)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user