08. TimelineQuery

This commit is contained in:
theUniC
2022-09-05 09:41:07 +02:00
parent be8e771617
commit b30d869bd1
14 changed files with 187 additions and 62 deletions

View File

@@ -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) { }
}

View File

@@ -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);

View File

@@ -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
{

View File

@@ -9,4 +9,4 @@ interface CommandBus
{
/** @psalm-param T $command */
public function handle(Command $command): void;
}
}

View File

@@ -13,4 +13,4 @@ final class CountFollowersQueryResponse implements QueryResponse
public readonly int $totalNumberOfFollowers
) {
}
}
}

View File

@@ -15,4 +15,4 @@ interface QueryBus
* @psalm-return TQueryResponse
*/
public function askFor(Query $query): QueryResponse;
}
}

View File

@@ -7,4 +7,4 @@ namespace Cheeper\Application;
/** @psalm-immutable */
interface QueryResponse
{
}
}

View 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,
) {
}
}

View 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)
);
}
}

View 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
) {
}
}

View File

@@ -19,4 +19,4 @@ final class SymfonyMessengerCommandBus implements CommandBus
{
$this->commandBus->dispatch($command);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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)
);
}
}

View 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)
);
}
}