From 22bcd49b6ab505cd819d97b51b027b1674c84f42 Mon Sep 17 00:00:00 2001 From: Cedric ANTHONY Date: Fri, 26 Sep 2025 19:23:48 +0200 Subject: [PATCH] :sparkles: (game): Add is_patched property + fix issues raised from change --- .env | 1 + .env.dist | 44 --- compose.yaml | 22 +- ...CreateGameWithCacheCheckCommandHandler.php | 19 +- .../CreateReportFromDtoCommandHandler.php | 1 + .../Query/Game/GetGameBySlugOnApiQuery.php | 12 + .../Game/GetGameBySlugOnApiQueryHandler.php | 24 ++ src/Domain/Factory/Game/GameFactory.php | 2 - src/Domain/Factory/Report/ReportFactory.php | 2 + src/Domain/Model/Game/Game.php | 13 - src/Domain/Model/Report/Report.php | 36 +- .../Resources/config/validator/Game.xml | 4 - .../Resources/config/validator/Report.xml | 4 + src/Infrastructure/Client/IgdbClient.php | 37 +- .../Migrations/Version20250925163138.php | 33 ++ .../Repository/DoctrineReportRepository.php | 4 +- .../Doctrine/config/Game/Game.orm.xml | 6 - .../Doctrine/config/Report/Report.orm.xml | 6 + .../Dto/CreateReportFormInputDto.php | 7 +- .../Form/CreateReportCommentForm.php | 1 + src/Presentation/Form/CreateReportForm.php | 10 + .../partials/_reports_comment_modal.html.twig | 8 +- .../partials/_reports_create_form.html.twig | 12 +- .../reports/partials/_reports_list.html.twig | 50 ++- .../partials/_reports_list_footer.html.twig | 3 +- .../partials/_reports_list_header.html.twig | 1 + .../partials/_reports_top_report.html.twig | 4 + .../reports/reports_for_game.html.twig | 2 +- .../Game/CreateGameCommandHandlerTest.php | 1 - ...teGameWithCacheCheckCommandHandlerTest.php | 369 +++++++++++++----- .../CreateReportFromDtoCommandHandlerTest.php | 12 +- translations/report.en.yaml | 6 +- 32 files changed, 514 insertions(+), 242 deletions(-) delete mode 100644 .env.dist create mode 100644 src/Application/Query/Game/GetGameBySlugOnApiQuery.php create mode 100644 src/Application/QueryHandler/Game/GetGameBySlugOnApiQueryHandler.php create mode 100644 src/Infrastructure/Persistence/Doctrine/Migrations/Version20250925163138.php diff --git a/.env b/.env index 9c00373..58cc648 100644 --- a/.env +++ b/.env @@ -7,6 +7,7 @@ USER_ID=1000 GROUP_ID=1000 POSTGRES_DB= +DB_PASSWORD= ###> symfony/framework-bundle ### APP_ENV=dev diff --git a/.env.dist b/.env.dist deleted file mode 100644 index c7d5dc6..0000000 --- a/.env.dist +++ /dev/null @@ -1,44 +0,0 @@ -COMPOSE_PROJECT_NAME=symfony-php -SYMFONY_VERSION=7.2.* -PHP_VERSION=8.3 - -USER_ID=1000 -GROUP_ID=1000 - -###> symfony/framework-bundle ### -APP_ENV=dev -APP_SECRET= -###< symfony/framework-bundle ### - -###> doctrine/doctrine-bundle ### -# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url -# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml -# -# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" -# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4" -# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4" -DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8" -###< doctrine/doctrine-bundle ### - -###> symfony/messenger ### -# Choose one of the transports below -# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages -# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages -MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0 -###< symfony/messenger ### - -###> lexik/jwt-authentication-bundle ### -JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem -JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem -JWT_PASSPHRASE=d65b7170cb3bad7e898e9e838e8cb78ea92e48ff7c69e8cfcfd979ef7087ab40 -###< lexik/jwt-authentication-bundle ### - -###> symfony/mercure-bundle ### -# See https://symfony.com/doc/current/mercure.html#configuration -# The URL of the Mercure hub, used by the app to publish updates (can be a local URL) -MERCURE_URL=https://example.com/.well-known/mercure -# The public URL of the Mercure hub, used by the browser to connect -MERCURE_PUBLIC_URL=https://example.com/.well-known/mercure -# The secret used to sign the JWTs -MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!" -###< symfony/mercure-bundle ### diff --git a/compose.yaml b/compose.yaml index 8b62222..144b166 100644 --- a/compose.yaml +++ b/compose.yaml @@ -22,24 +22,24 @@ services: - caddy_config:/config ports: # HTTP - - target: 80 - published: ${HTTP_PORT:-80} - protocol: tcp + - target: 80 + published: ${HTTP_PORT:-80} + protocol: tcp # HTTPS - - target: 443 - published: ${HTTPS_PORT:-443} - protocol: tcp + - target: 443 + published: ${HTTPS_PORT:-443} + protocol: tcp # HTTP/3 - - target: 443 - published: ${HTTP3_PORT:-443} - protocol: udp + - target: 443 + published: ${HTTP3_PORT:-443} + protocol: udp healthcheck: - test: ["CMD", "curl", "--fail", "http://localhost:80/"] + test: ['CMD', 'curl', '--fail', 'http://localhost:80/'] interval: 10s timeout: 5s retries: 5 start_period: 10s - + # Mercure is installed as a Caddy module, prevent the Flex recipe from installing another service ###> symfony/mercure-bundle ### ###< symfony/mercure-bundle ### diff --git a/src/Application/CommandHandler/Game/CreateGameWithCacheCheckCommandHandler.php b/src/Application/CommandHandler/Game/CreateGameWithCacheCheckCommandHandler.php index e5e7e0c..094bb23 100644 --- a/src/Application/CommandHandler/Game/CreateGameWithCacheCheckCommandHandler.php +++ b/src/Application/CommandHandler/Game/CreateGameWithCacheCheckCommandHandler.php @@ -7,6 +7,8 @@ use App\Application\Command\Game\CreateGameCommand; use App\Application\Command\Game\CreateGameWithCacheCheckCommand; use App\Application\Exception\Game\CannotCreateGameException; +use App\Application\Helper\MessageBusHelper; +use App\Application\Query\Game\GetGameBySlugOnApiQuery; use App\Domain\Model\Game\ApiGame; use Psr\Cache\InvalidArgumentException; use Psr\Log\LoggerInterface; @@ -21,6 +23,7 @@ class CreateGameWithCacheCheckCommandHandler public function __construct( private readonly CacheInterface $cache, private readonly MessageBusInterface $messageBus, + private readonly MessageBusHelper $messageBusHelper, private readonly LoggerInterface $logger, ) { } @@ -40,7 +43,19 @@ function (): ?ApiGame { ); if (null === $apiGame) { - return; + $gameEnvelope = $this->messageBus->dispatch( + new GetGameBySlugOnApiQuery($command->gameSlug), + ); + + /** @var ?ApiGame $apiGame */ + $apiGame = $this->messageBusHelper->getContentFromEnvelope( + $gameEnvelope, + 'Failed to fetch game from API', + ); + } + + if (null === $apiGame) { + throw new CannotCreateGameException('Failed to fetch game from API'); } try { @@ -53,7 +68,7 @@ function (): ?ApiGame { publisher: $apiGame->getPublisher(), developer: $apiGame->getDeveloper(), description: $apiGame->getDescription(), - ) + ), ); } catch (\Exception $e) { $this->logger->error($e->getMessage()); diff --git a/src/Application/CommandHandler/Report/CreateReportFromDtoCommandHandler.php b/src/Application/CommandHandler/Report/CreateReportFromDtoCommandHandler.php index 206869e..d263967 100644 --- a/src/Application/CommandHandler/Report/CreateReportFromDtoCommandHandler.php +++ b/src/Application/CommandHandler/Report/CreateReportFromDtoCommandHandler.php @@ -42,6 +42,7 @@ public function __invoke(CreateReportFromDtoCommand $command): void isNativeResolutionImprovedDocked: $command->formInputDto->isNativeResolutionDocked, hasImprovedLoadingTimes: $command->formInputDto->hasImprovedLoadingTimes, gameStatus: $command->formInputDto->gameStatus, + isPatched: $command->formInputDto->isPatched, ); $this->reportRepository->save($newReport); diff --git a/src/Application/Query/Game/GetGameBySlugOnApiQuery.php b/src/Application/Query/Game/GetGameBySlugOnApiQuery.php new file mode 100644 index 0000000..b6c5c1d --- /dev/null +++ b/src/Application/Query/Game/GetGameBySlugOnApiQuery.php @@ -0,0 +1,12 @@ +apiGameRepository->getGameBySlug($query->slug); + } +} diff --git a/src/Domain/Factory/Game/GameFactory.php b/src/Domain/Factory/Game/GameFactory.php index 9ae8d51..abddd34 100644 --- a/src/Domain/Factory/Game/GameFactory.php +++ b/src/Domain/Factory/Game/GameFactory.php @@ -22,7 +22,6 @@ public static function create( string $description, string $releaseDate, string $imageCover, - bool $isPatched = false, bool $isActive = true, ?Publisher $publisher = null, ?Developer $developer = null, @@ -34,7 +33,6 @@ public static function create( description: $description, releaseDate: $releaseDate, imageCover: $imageCover, - isPatched: $isPatched, isActive: $isActive, publisher: $publisher, developer: $developer, diff --git a/src/Domain/Factory/Report/ReportFactory.php b/src/Domain/Factory/Report/ReportFactory.php index 07c9a7c..06197a2 100644 --- a/src/Domain/Factory/Report/ReportFactory.php +++ b/src/Domain/Factory/Report/ReportFactory.php @@ -23,6 +23,7 @@ public static function create( bool $isNativeResolutionImprovedDocked = false, bool $hasImprovedLoadingTimes = true, ReportGameStatusEnum $gameStatus = ReportGameStatusEnum::OK, + bool $isPatched = false, int $upvoteCount = 0, bool $isVisible = true, ): Report { @@ -39,6 +40,7 @@ public static function create( hasImprovedLoadingTimes: $hasImprovedLoadingTimes, isSwitch2Edition: $isSwitch2Edition, gameStatus: $gameStatus, + isPatched: $isPatched, upvoteCount: $upvoteCount, isVisible: $isVisible, ); diff --git a/src/Domain/Model/Game/Game.php b/src/Domain/Model/Game/Game.php index 7573bb3..2cddec0 100644 --- a/src/Domain/Model/Game/Game.php +++ b/src/Domain/Model/Game/Game.php @@ -22,7 +22,6 @@ public function __construct( private ?string $description = null, private ?string $releaseDate = null, private ?string $imageCover = null, - private bool $isPatched = false, private bool $isActive = true, private ?Publisher $publisher = null, private ?Developer $developer = null, @@ -91,18 +90,6 @@ public function setImageCover(string $imageCover): self return $this; } - public function isPatched(): bool - { - return $this->isPatched; - } - - public function setIsPatched(bool $isPatched): self - { - $this->isPatched = $isPatched; - - return $this; - } - public function isActive(): bool { return $this->isActive; diff --git a/src/Domain/Model/Report/Report.php b/src/Domain/Model/Report/Report.php index d787bd2..de2b44c 100644 --- a/src/Domain/Model/Report/Report.php +++ b/src/Domain/Model/Report/Report.php @@ -6,16 +6,16 @@ use App\Domain\Model\Common\ModelInterface; use App\Domain\Model\Game\Game; +use App\Domain\Trait\IdTrait; use App\Domain\Trait\TimestampableTrait; use App\Infrastructure\Enum\ReportGameStatusEnum; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; -use Symfony\Component\Uid\Uuid; class Report implements ModelInterface { + use IdTrait; use TimestampableTrait; - private ?string $id = null; public function __construct( private Game $game, @@ -30,6 +30,7 @@ public function __construct( private bool $hasImprovedLoadingTimes = false, private bool $isSwitch2Edition = false, private ReportGameStatusEnum $gameStatus = ReportGameStatusEnum::OK, + private bool $isPatched = false, private int $upvoteCount = 0, private bool $isVisible = true, /** @var Collection $reportComments */ @@ -37,18 +38,6 @@ public function __construct( ) { } - public function getId(): ?string - { - return $this->id; - } - - public function setId(string $id): self - { - $this->id = $id; - - return $this; - } - public function getGame(): Game { return $this->game; @@ -123,6 +112,18 @@ public function setGameStatus(ReportGameStatusEnum $gameStatus): self return $this; } + public function isPatched(): bool + { + return $this->isPatched; + } + + public function setIsPatched(bool $isPatched): self + { + $this->isPatched = $isPatched; + + return $this; + } + public function isNativeResolutionPortable(): bool { return $this->isNativeResolutionPortable; @@ -249,11 +250,4 @@ public function removeReportComment(ReportComment $reportComment): self return $this; } - - public function setDefaultId(): self - { - $this->id = Uuid::v7()->toRfc4122(); - - return $this; - } } diff --git a/src/Domain/Resources/config/validator/Game.xml b/src/Domain/Resources/config/validator/Game.xml index 9893626..165ce42 100644 --- a/src/Domain/Resources/config/validator/Game.xml +++ b/src/Domain/Resources/config/validator/Game.xml @@ -19,10 +19,6 @@ - - - - diff --git a/src/Domain/Resources/config/validator/Report.xml b/src/Domain/Resources/config/validator/Report.xml index dfd10d1..6ec0b61 100644 --- a/src/Domain/Resources/config/validator/Report.xml +++ b/src/Domain/Resources/config/validator/Report.xml @@ -50,6 +50,10 @@ + + + + diff --git a/src/Infrastructure/Client/IgdbClient.php b/src/Infrastructure/Client/IgdbClient.php index be241cf..3ee8d3a 100644 --- a/src/Infrastructure/Client/IgdbClient.php +++ b/src/Infrastructure/Client/IgdbClient.php @@ -136,7 +136,7 @@ private function generateGetGameSlugCacheKey( /** * @return ApiGame[] - * * + * * @throws ExceptionInterface * @throws ClientExceptionInterface * @throws DecodingExceptionInterface @@ -251,7 +251,9 @@ private function formatResponseData(array $igdbSearchResponseDto): array if (!empty($igdbSearchResultItem->involved_companies)) { /** @var array $company */ - foreach ($igdbSearchResultItem->involved_companies as $company) { + foreach ( + $igdbSearchResultItem->involved_companies as $company + ) { if (empty($publisher->name)) { $publisher = $this->checkCompany( involvedCompany: $company, @@ -268,10 +270,7 @@ private function formatResponseData(array $igdbSearchResponseDto): array ); } - if ( - !empty($publisher->name) - && !empty($developer->name) - ) { + if (!empty($publisher->name) && !empty($developer->name)) { break; } } @@ -286,10 +285,7 @@ private function formatResponseData(array $igdbSearchResponseDto): array $imageCover = 'https:'.$coverUrl; } - if ( - empty($publisher->name) - || empty($developer->name) - ) { + if (empty($publisher->name) || empty($developer->name)) { if (empty($developer->name)) { $developer = null; } @@ -297,12 +293,16 @@ private function formatResponseData(array $igdbSearchResponseDto): array $publisher = null; } - $updatedAt = new \DateTimeImmutable()->setTimestamp($igdbSearchResultItem->updatedAt); + $updatedAt = new \DateTimeImmutable()->setTimestamp( + $igdbSearchResultItem->updatedAt, + ); $apiGames[] = ApiGameFactory::create( name: $igdbSearchResultItem->name, slug: $slug, - description: $igdbSearchResultItem->summary ? nl2br($igdbSearchResultItem->summary) : '', + description: $igdbSearchResultItem->summary + ? nl2br($igdbSearchResultItem->summary) + : '', imageCover: $imageCover, releaseDate: $releaseDate ? $releaseDate->format(DATE_ATOM) @@ -471,14 +471,19 @@ private function checkCompany( } $companyWebsite = ''; - if (isset($companyWebsites[0]['url']) && is_array($companyWebsites[0]) && is_string( - $companyWebsites[0]['url'] - )) { + if ( + isset($companyWebsites[0]['url']) + && is_array($companyWebsites[0]) + && is_string($companyWebsites[0]['url']) + ) { $companyWebsite = $companyWebsites[0]['url']; } $companyApiId = 0; - if (isset($company['id']) && (is_int($company['id']) || is_numeric($company['id']))) { + if ( + isset($company['id']) + && (is_int($company['id']) || is_numeric($company['id'])) + ) { $companyApiId = (int) $company['id']; } diff --git a/src/Infrastructure/Persistence/Doctrine/Migrations/Version20250925163138.php b/src/Infrastructure/Persistence/Doctrine/Migrations/Version20250925163138.php new file mode 100644 index 0000000..a8450a0 --- /dev/null +++ b/src/Infrastructure/Persistence/Doctrine/Migrations/Version20250925163138.php @@ -0,0 +1,33 @@ +addSql('ALTER TABLE games DROP ispatched'); + $this->addSql('ALTER TABLE reports ADD is_patched BOOLEAN DEFAULT false NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE games ADD ispatched BOOLEAN DEFAULT false NOT NULL'); + $this->addSql('ALTER TABLE reports DROP is_patched'); + } +} diff --git a/src/Infrastructure/Persistence/Doctrine/Report/Repository/DoctrineReportRepository.php b/src/Infrastructure/Persistence/Doctrine/Report/Repository/DoctrineReportRepository.php index 0dbc7b9..207bd67 100644 --- a/src/Infrastructure/Persistence/Doctrine/Report/Repository/DoctrineReportRepository.php +++ b/src/Infrastructure/Persistence/Doctrine/Report/Repository/DoctrineReportRepository.php @@ -26,7 +26,7 @@ public function getAllVisibleReportsForGame(string $gameId): array ->andWhere('r.game = :game') ->setParameter('game', $gameId) ->orderBy('r.upvoteCount', 'DESC') - ->addOrderBy('r.createdAt', 'DESC') + ->addOrderBy('r.createdAt', 'ASC') ->getQuery(); /** @var Report[] */ @@ -40,7 +40,7 @@ public function findMostUpvotedReportForGame(string $gameId): ?Report ->andWhere('r.game = :game') ->setParameter('game', $gameId) ->orderBy('r.upvoteCount', 'DESC') - ->addOrderBy('r.createdAt', 'DESC') + ->addOrderBy('r.createdAt', 'ASC') ->setMaxResults(1); /** @var Report|null */ diff --git a/src/Infrastructure/Persistence/Doctrine/config/Game/Game.orm.xml b/src/Infrastructure/Persistence/Doctrine/config/Game/Game.orm.xml index e8ca79d..99c6e59 100644 --- a/src/Infrastructure/Persistence/Doctrine/config/Game/Game.orm.xml +++ b/src/Infrastructure/Persistence/Doctrine/config/Game/Game.orm.xml @@ -24,12 +24,6 @@ - - - - - - diff --git a/src/Infrastructure/Persistence/Doctrine/config/Report/Report.orm.xml b/src/Infrastructure/Persistence/Doctrine/config/Report/Report.orm.xml index f7f2c3c..e5810a3 100644 --- a/src/Infrastructure/Persistence/Doctrine/config/Report/Report.orm.xml +++ b/src/Infrastructure/Persistence/Doctrine/config/Report/Report.orm.xml @@ -74,6 +74,12 @@ + + + + + + diff --git a/src/Presentation/Dto/CreateReportFormInputDto.php b/src/Presentation/Dto/CreateReportFormInputDto.php index ac46f13..8d21545 100644 --- a/src/Presentation/Dto/CreateReportFormInputDto.php +++ b/src/Presentation/Dto/CreateReportFormInputDto.php @@ -10,10 +10,8 @@ class CreateReportFormInputDto { public function __construct( - #[Assert\NotBlank] - public string $gameId = '', - #[Assert\NotBlank] - public string $gameSlug = '', + #[Assert\NotBlank] public string $gameId = '', + #[Assert\NotBlank] public string $gameSlug = '', public ReportGameStatusEnum $gameStatus = ReportGameStatusEnum::OK, public bool $is60FpsPortable = false, public bool $hasStableFrameratePortable = true, @@ -25,6 +23,7 @@ public function __construct( public bool $isNativeResolutionDocked = false, public bool $hasImprovedLoadingTimes = true, public bool $isSwitch2Edition = false, + public bool $isPatched = false, ) { } } diff --git a/src/Presentation/Form/CreateReportCommentForm.php b/src/Presentation/Form/CreateReportCommentForm.php index 862ed20..7a6bad8 100644 --- a/src/Presentation/Form/CreateReportCommentForm.php +++ b/src/Presentation/Form/CreateReportCommentForm.php @@ -52,6 +52,7 @@ public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => CreateReportCommentFormInputDto::class, + 'csrf_protection' => false, ]); } } diff --git a/src/Presentation/Form/CreateReportForm.php b/src/Presentation/Form/CreateReportForm.php index 354f935..3f104c1 100644 --- a/src/Presentation/Form/CreateReportForm.php +++ b/src/Presentation/Form/CreateReportForm.php @@ -140,6 +140,16 @@ public function buildForm( 'empty_data' => ReportGameStatusEnum::OK->value, 'required' => true, ]) + ->add('isPatched', CheckboxType::class, [ + 'label' => $this->translator->trans( + 'add_report.form.misc.is_patched', + [], + 'report', + ), + 'empty_data' => false, + 'data' => false, + 'required' => false, + ]) ->add('gameId', HiddenType::class, [ 'required' => true, 'constraints' => [new Assert\NotBlank()], diff --git a/src/Presentation/Template/reports/partials/_reports_comment_modal.html.twig b/src/Presentation/Template/reports/partials/_reports_comment_modal.html.twig index a395618..0fbd447 100644 --- a/src/Presentation/Template/reports/partials/_reports_comment_modal.html.twig +++ b/src/Presentation/Template/reports/partials/_reports_comment_modal.html.twig @@ -6,8 +6,9 @@ + +
+ {{ report.isPatched ? ux_icon('ph:check-square') : ux_icon('ph:x-square') }} +
+ +
{{ report.switch2Edition ? ux_icon('ph:check-square') : ux_icon('ph:x-square') }} @@ -111,13 +119,51 @@
-
- {% include '@reports/partials/_reports_comment_modal.html.twig' with { modal_index: loop.index, report: report, game: report.game } %} {% endfor %} {% include '@reports/partials/_reports_list_footer.html.twig' %} + +{% block javascripts %} + + +{% endblock %} diff --git a/src/Presentation/Template/reports/partials/_reports_list_footer.html.twig b/src/Presentation/Template/reports/partials/_reports_list_footer.html.twig index 1be6a8e..1db7f91 100644 --- a/src/Presentation/Template/reports/partials/_reports_list_footer.html.twig +++ b/src/Presentation/Template/reports/partials/_reports_list_footer.html.twig @@ -8,7 +8,7 @@ TV Portable - Misc + Misc @@ -21,5 +21,6 @@

{{ reverse_ordinal.reverse_ordinal('list.table.notice.better_resolution'|trans) }}

{{ reverse_ordinal.reverse_ordinal('list.table.notice.native_resolution'|trans) }}

{{ reverse_ordinal.reverse_ordinal('list.table.notice.switch_2_edition'|trans) }}

+

{{ reverse_ordinal.reverse_ordinal('list.table.notice.is_patched'|trans) }}

{% endif %} diff --git a/src/Presentation/Template/reports/partials/_reports_list_header.html.twig b/src/Presentation/Template/reports/partials/_reports_list_header.html.twig index fea1ca4..cfaf82e 100644 --- a/src/Presentation/Template/reports/partials/_reports_list_header.html.twig +++ b/src/Presentation/Template/reports/partials/_reports_list_header.html.twig @@ -16,6 +16,7 @@ {{ ordinal.ordinal('list.table.header.better_resolution'|trans) }} {{ ordinal.ordinal('list.table.header.native_resolution'|trans) }} {{ 'list.table.header.better_loading_times'|trans }} + {{ ordinal.ordinal('list.table.header.is_patched'|trans) }} {{ ordinal.ordinal('list.table.header.switch_2_edition'|trans) }} {{ 'list.table.header.upvoted'|trans }} {{ 'list.table.header.issue'|trans }} diff --git a/src/Presentation/Template/reports/partials/_reports_top_report.html.twig b/src/Presentation/Template/reports/partials/_reports_top_report.html.twig index 02c57cf..a6b83d9 100644 --- a/src/Presentation/Template/reports/partials/_reports_top_report.html.twig +++ b/src/Presentation/Template/reports/partials/_reports_top_report.html.twig @@ -35,6 +35,10 @@ +
+ {{ 'most_upvoted.badge.is_patched'|trans }} +
+
{{ 'most_upvoted.badge.switch_2_edition'|trans }}
diff --git a/src/Presentation/Template/reports/reports_for_game.html.twig b/src/Presentation/Template/reports/reports_for_game.html.twig index 4e73a71..b92c973 100644 --- a/src/Presentation/Template/reports/reports_for_game.html.twig +++ b/src/Presentation/Template/reports/reports_for_game.html.twig @@ -1,7 +1,7 @@ {% extends '@app/base.html.twig' %} {% block content %} - {% include '@reports/partials/_reports_game_info.html.twig' %} + {% include '@reports/partials/_reports_game_info.html.twig' with { game: game, topReport: topReport } %} {% include '@reports/partials/_reports_create_form.html.twig' %} diff --git a/tests/Unit/Application/CommandHandler/Game/CreateGameCommandHandlerTest.php b/tests/Unit/Application/CommandHandler/Game/CreateGameCommandHandlerTest.php index 8307c4f..094c76e 100644 --- a/tests/Unit/Application/CommandHandler/Game/CreateGameCommandHandlerTest.php +++ b/tests/Unit/Application/CommandHandler/Game/CreateGameCommandHandlerTest.php @@ -319,7 +319,6 @@ && $game->getDescription() === 'A rogue-like dungeon crawler' && $game->getReleaseDate() === '2020-09-17' && $game->getImageCover() === 'https://example.com/hades.jpg' - && $game->isPatched() === false && $game->isActive() === true && $game->getPublisher() === $this->createdPublisher && $game->getReports()->isEmpty(); diff --git a/tests/Unit/Application/CommandHandler/Game/CreateGameWithCacheCheckCommandHandlerTest.php b/tests/Unit/Application/CommandHandler/Game/CreateGameWithCacheCheckCommandHandlerTest.php index d4d00e0..b487a57 100644 --- a/tests/Unit/Application/CommandHandler/Game/CreateGameWithCacheCheckCommandHandlerTest.php +++ b/tests/Unit/Application/CommandHandler/Game/CreateGameWithCacheCheckCommandHandlerTest.php @@ -8,6 +8,8 @@ use App\Application\Command\Game\CreateGameWithCacheCheckCommand; use App\Application\CommandHandler\Game\CreateGameWithCacheCheckCommandHandler; use App\Application\Exception\Game\CannotCreateGameException; +use App\Application\Helper\MessageBusHelper; +use App\Application\Query\Game\GetGameBySlugOnApiQuery; use App\Domain\Model\Game\ApiGame; use App\Shared\Dto\Game\GameCompanyDataDto; use Faker\Factory; @@ -23,6 +25,7 @@ $this->cache = $this->createMock(CacheInterface::class); $this->messageBus = $this->createMock(MessageBusInterface::class); + $this->messageBusHelper = $this->createMock(MessageBusHelper::class); $this->logger = $this->createMock(LoggerInterface::class); $this->gameSlug = $this->faker->slug(); @@ -73,26 +76,31 @@ description: $this->gameDescription, ); - $envelope = new Envelope($createGameCommand, [new HandledStamp(true, 'handler.service_id')]); + $envelope = new Envelope($createGameCommand, [ + new HandledStamp(true, 'handler.service_id'), + ]); $this->messageBus ->expects($this->once()) ->method('dispatch') - ->with($this->callback(function ($command) { - return $command instanceof CreateGameCommand - && $command->getName() === $this->gameName - && $command->getSlug() === $this->gameSlug - && $command->getDescription() === $this->gameDescription - && $command->getImageCover() === $this->gameImageCover - && $command->getReleaseDate() === $this->gameReleaseDate - && $command->getPublisher() === $this->publisher - && $command->getDeveloper() === $this->developer; - })) + ->with( + $this->callback(function ($command) { + return $command instanceof CreateGameCommand && + $command->getName() === $this->gameName && + $command->getSlug() === $this->gameSlug && + $command->getDescription() === $this->gameDescription && + $command->getImageCover() === $this->gameImageCover && + $command->getReleaseDate() === $this->gameReleaseDate && + $command->getPublisher() === $this->publisher && + $command->getDeveloper() === $this->developer; + }), + ) ->willReturn($envelope); $handler = new CreateGameWithCacheCheckCommandHandler( $this->cache, $this->messageBus, + $this->messageBusHelper, $this->logger, ); @@ -105,7 +113,7 @@ expect(true)->toBeTrue(); }); -it('returns early when ApiGame is not found in cache', function () { +it('fetches from API when ApiGame is not found in cache', function () { // Given $this->cache ->expects($this->once()) @@ -113,13 +121,59 @@ ->with('api_game_' . $this->gameSlug, $this->anything()) ->willReturn(null); + $getGameBySlugQuery = new GetGameBySlugOnApiQuery($this->gameSlug); + $queryEnvelope = new Envelope($getGameBySlugQuery, [ + new HandledStamp($this->apiGame, 'handler.service_id'), + ]); + + $createGameCommand = new CreateGameCommand( + name: $this->gameName, + slug: $this->gameSlug, + releaseDate: $this->gameReleaseDate, + imageCover: $this->gameImageCover, + publisher: $this->publisher, + developer: $this->developer, + description: $this->gameDescription, + ); + + $createGameEnvelope = new Envelope($createGameCommand, [ + new HandledStamp(true, 'handler.service_id'), + ]); + $this->messageBus - ->expects($this->never()) - ->method('dispatch'); + ->expects($this->exactly(2)) + ->method('dispatch') + ->willReturnCallback(function ($dispatchedCommand) use ( + $queryEnvelope, + $createGameEnvelope, + $getGameBySlugQuery, + $createGameCommand, + ) { + if ($dispatchedCommand === $getGameBySlugQuery) { + return $queryEnvelope; + } + if ($dispatchedCommand == $createGameCommand) { + return $createGameEnvelope; + } + return new Envelope($dispatchedCommand); + }); + + $this->messageBusHelper + ->expects($this->once()) + ->method('getContentFromEnvelope') + ->with( + $this->callback(function ($envelope) { + return $envelope instanceof Envelope && + $envelope->getMessage() instanceof GetGameBySlugOnApiQuery; + }), + 'Failed to fetch game from API', + ) + ->willReturn($this->apiGame); $handler = new CreateGameWithCacheCheckCommandHandler( $this->cache, $this->messageBus, + $this->messageBusHelper, $this->logger, ); @@ -132,24 +186,76 @@ expect(true)->toBeTrue(); }); -it('handles exception during message bus dispatch', function () { +it( + 'throws CannotCreateGameException when both cache and API return null', + function () { + // Given + $this->cache + ->expects($this->once()) + ->method('get') + ->with('api_game_' . $this->gameSlug, $this->anything()) + ->willReturn(null); + + $getGameBySlugQuery = new GetGameBySlugOnApiQuery($this->gameSlug); + $queryEnvelope = new Envelope($getGameBySlugQuery, [ + new HandledStamp(null, 'handler.service_id'), + ]); + + $this->messageBus + ->expects($this->once()) + ->method('dispatch') + ->with($this->isInstanceOf(GetGameBySlugOnApiQuery::class)) + ->willReturn($queryEnvelope); + + $this->messageBusHelper + ->expects($this->once()) + ->method('getContentFromEnvelope') + ->with( + $this->callback(function ($envelope) { + return $envelope instanceof Envelope && + $envelope->getMessage() instanceof + GetGameBySlugOnApiQuery; + }), + 'Failed to fetch game from API', + ) + ->willReturn(null); + + $handler = new CreateGameWithCacheCheckCommandHandler( + $this->cache, + $this->messageBus, + $this->messageBusHelper, + $this->logger, + ); + + $command = new CreateGameWithCacheCheckCommand($this->gameSlug); + + // When & Then + expect(function () use ($handler, $command) { + $handler->__invoke($command); + })->toThrow(CannotCreateGameException::class); + }, +); + +it('handles exception during GetGameBySlugOnApiQuery dispatch', function () { // Given $this->cache ->expects($this->once()) ->method('get') ->with('api_game_' . $this->gameSlug, $this->anything()) - ->willReturn($this->apiGame); + ->willReturn(null); - $exception = new \Exception('Message bus error'); + $exception = new \Exception('API query error'); $this->messageBus ->expects($this->once()) ->method('dispatch') + ->with($this->isInstanceOf(GetGameBySlugOnApiQuery::class)) ->willThrowException($exception); $handler = new CreateGameWithCacheCheckCommandHandler( $this->cache, $this->messageBus, + $this->messageBusHelper, $this->logger, ); @@ -158,10 +264,10 @@ // When & Then expect(function () use ($handler, $command) { $handler->__invoke($command); - })->toThrow(CannotCreateGameException::class); + })->toThrow(\Exception::class); }); -it('handles different types of exceptions during message bus dispatch', function () { +it('handles exception during CreateGameCommand dispatch', function () { // Given $this->cache ->expects($this->once()) @@ -169,16 +275,18 @@ ->with('api_game_' . $this->gameSlug, $this->anything()) ->willReturn($this->apiGame); - $exception = new \RuntimeException('Runtime error occurred'); + $exception = new \Exception('Message bus error'); $this->messageBus ->expects($this->once()) ->method('dispatch') + ->with($this->isInstanceOf(CreateGameCommand::class)) ->willThrowException($exception); $handler = new CreateGameWithCacheCheckCommandHandler( $this->cache, $this->messageBus, + $this->messageBusHelper, $this->logger, ); @@ -190,78 +298,125 @@ })->toThrow(CannotCreateGameException::class); }); -it('creates CreateGameCommand with correct parameters from ApiGame', function () { - // Given - $customPublisher = new GameCompanyDataDto( - name: 'Custom Publisher', - website: 'https://custompublisher.com', - apiId: 123456, - ); - - $customDeveloper = new GameCompanyDataDto( - name: 'Custom Developer', - website: 'https://customdeveloper.com', - apiId: 789012, - ); - - $customApiGame = new ApiGame( - name: 'Test Game Name', - slug: 'test-game-slug', - description: 'Test game description', - imageCover: 'https://example.com/image.jpg', - releaseDate: '2024-01-01', - updatedAt: new \DateTimeImmutable(), - publisher: $customPublisher, - developer: $customDeveloper, - ); - - $this->cache - ->expects($this->once()) - ->method('get') - ->with('api_game_test-game-slug', $this->anything()) - ->willReturn($customApiGame); - - $createGameCommand = new CreateGameCommand( - name: 'Test Game Name', - slug: 'test-game-slug', - releaseDate: '2024-01-01', - imageCover: 'https://example.com/image.jpg', - publisher: $customPublisher, - developer: $customDeveloper, - description: 'Test game description', - ); - - $envelope = new Envelope($createGameCommand, [new HandledStamp(true, 'handler.service_id')]); - - $this->messageBus - ->expects($this->once()) - ->method('dispatch') - ->with($this->callback(function ($command) use ($customPublisher, $customDeveloper) { - return $command instanceof CreateGameCommand - && $command->getName() === 'Test Game Name' - && $command->getSlug() === 'test-game-slug' - && $command->getDescription() === 'Test game description' - && $command->getImageCover() === 'https://example.com/image.jpg' - && $command->getReleaseDate() === '2024-01-01' - && $command->getPublisher() === $customPublisher - && $command->getDeveloper() === $customDeveloper; - })) - ->willReturn($envelope); - - $handler = new CreateGameWithCacheCheckCommandHandler( - $this->cache, - $this->messageBus, - $this->logger, - ); - - $command = new CreateGameWithCacheCheckCommand('test-game-slug'); - - // When - $handler->__invoke($command); +it( + 'handles different types of exceptions during CreateGameCommand dispatch', + function () { + // Given + $this->cache + ->expects($this->once()) + ->method('get') + ->with('api_game_' . $this->gameSlug, $this->anything()) + ->willReturn($this->apiGame); + + $exception = new \RuntimeException('Runtime error occurred'); + + $this->messageBus + ->expects($this->once()) + ->method('dispatch') + ->with($this->isInstanceOf(CreateGameCommand::class)) + ->willThrowException($exception); + + $handler = new CreateGameWithCacheCheckCommandHandler( + $this->cache, + $this->messageBus, + $this->messageBusHelper, + $this->logger, + ); + + $command = new CreateGameWithCacheCheckCommand($this->gameSlug); + + // When & Then + expect(function () use ($handler, $command) { + $handler->__invoke($command); + })->toThrow(CannotCreateGameException::class); + }, +); + +it( + 'creates CreateGameCommand with correct parameters from ApiGame', + function () { + // Given + $customPublisher = new GameCompanyDataDto( + name: 'Custom Publisher', + website: 'https://custompublisher.com', + apiId: 123456, + ); + + $customDeveloper = new GameCompanyDataDto( + name: 'Custom Developer', + website: 'https://customdeveloper.com', + apiId: 789012, + ); + + $customApiGame = new ApiGame( + name: 'Test Game Name', + slug: 'test-game-slug', + description: 'Test game description', + imageCover: 'https://example.com/image.jpg', + releaseDate: '2024-01-01', + updatedAt: new \DateTimeImmutable(), + publisher: $customPublisher, + developer: $customDeveloper, + ); + + $this->cache + ->expects($this->once()) + ->method('get') + ->with('api_game_test-game-slug', $this->anything()) + ->willReturn($customApiGame); + + $createGameCommand = new CreateGameCommand( + name: 'Test Game Name', + slug: 'test-game-slug', + releaseDate: '2024-01-01', + imageCover: 'https://example.com/image.jpg', + publisher: $customPublisher, + developer: $customDeveloper, + description: 'Test game description', + ); + + $envelope = new Envelope($createGameCommand, [ + new HandledStamp(true, 'handler.service_id'), + ]); + + $this->messageBus + ->expects($this->once()) + ->method('dispatch') + ->with( + $this->callback(function ($command) use ( + $customPublisher, + $customDeveloper, + ) { + return $command instanceof CreateGameCommand && + $command->getName() === 'Test Game Name' && + $command->getSlug() === 'test-game-slug' && + $command->getDescription() === + 'Test game description' && + $command->getImageCover() === + 'https://example.com/image.jpg' && + $command->getReleaseDate() === '2024-01-01' && + $command->getPublisher() === $customPublisher && + $command->getDeveloper() === $customDeveloper; + }), + ) + ->willReturn($envelope); + + $handler = new CreateGameWithCacheCheckCommandHandler( + $this->cache, + $this->messageBus, + $this->messageBusHelper, + $this->logger, + ); + + $command = new CreateGameWithCacheCheckCommand('test-game-slug'); + + // When + $handler->__invoke($command); - // Then - expect(true)->toBeTrue(); -}); + // Then + expect(true)->toBeTrue(); + }, +); it('handles ApiGame with null publisher and developer', function () { // Given @@ -292,26 +447,32 @@ description: 'An indie game', ); - $envelope = new Envelope($createGameCommand, [new HandledStamp(true, 'handler.service_id')]); + $envelope = new Envelope($createGameCommand, [ + new HandledStamp(true, 'handler.service_id'), + ]); $this->messageBus ->expects($this->once()) ->method('dispatch') - ->with($this->callback(function ($command) { - return $command instanceof CreateGameCommand - && $command->getName() === 'Indie Game' - && $command->getSlug() === 'indie-game-slug' - && $command->getDescription() === 'An indie game' - && $command->getImageCover() === 'https://example.com/indie.jpg' - && $command->getReleaseDate() === '2023-12-31' - && $command->getPublisher() === null - && $command->getDeveloper() === null; - })) + ->with( + $this->callback(function ($command) { + return $command instanceof CreateGameCommand && + $command->getName() === 'Indie Game' && + $command->getSlug() === 'indie-game-slug' && + $command->getDescription() === 'An indie game' && + $command->getImageCover() === + 'https://example.com/indie.jpg' && + $command->getReleaseDate() === '2023-12-31' && + $command->getPublisher() === null && + $command->getDeveloper() === null; + }), + ) ->willReturn($envelope); $handler = new CreateGameWithCacheCheckCommandHandler( $this->cache, $this->messageBus, + $this->messageBusHelper, $this->logger, ); @@ -324,7 +485,7 @@ expect(true)->toBeTrue(); }); -it('handles ExceptionInterface during message bus dispatch', function () { +it('handles ExceptionInterface during CreateGameCommand dispatch', function () { // Given $this->cache ->expects($this->once()) @@ -337,11 +498,13 @@ $this->messageBus ->expects($this->once()) ->method('dispatch') + ->with($this->isInstanceOf(CreateGameCommand::class)) ->willThrowException($exception); $handler = new CreateGameWithCacheCheckCommandHandler( $this->cache, $this->messageBus, + $this->messageBusHelper, $this->logger, ); diff --git a/tests/Unit/Application/CommandHandler/Report/CreateReportFromDtoCommandHandlerTest.php b/tests/Unit/Application/CommandHandler/Report/CreateReportFromDtoCommandHandlerTest.php index f7fe605..fe60e16 100644 --- a/tests/Unit/Application/CommandHandler/Report/CreateReportFromDtoCommandHandlerTest.php +++ b/tests/Unit/Application/CommandHandler/Report/CreateReportFromDtoCommandHandlerTest.php @@ -40,6 +40,7 @@ isNativeResolutionDocked: true, hasImprovedLoadingTimes: true, isSwitch2Edition: false, + isPatched: false, ); }); @@ -66,7 +67,8 @@ && $report->isNativeResolutionDocked() === $this->createReportDto->isNativeResolutionDocked && $report->isHasImprovedLoadingTimes() === $this->createReportDto->hasImprovedLoadingTimes && $report->isSwitch2Edition() === $this->createReportDto->isSwitch2Edition - && $report->getGameStatus() === $this->createReportDto->gameStatus; + && $report->getGameStatus() === $this->createReportDto->gameStatus + && $report->isPatched() === $this->createReportDto->isPatched; })); $handler = new CreateReportFromDtoCommandHandler( @@ -152,6 +154,7 @@ isNativeResolutionDocked: false, hasImprovedLoadingTimes: false, isSwitch2Edition: true, + isPatched: false, ); $this->gameRepository @@ -175,7 +178,8 @@ && $report->isNativeResolutionDocked() === $customDto->isNativeResolutionDocked && $report->isHasImprovedLoadingTimes() === $customDto->hasImprovedLoadingTimes && $report->isSwitch2Edition() === $customDto->isSwitch2Edition - && $report->getGameStatus() === $customDto->gameStatus; + && $report->getGameStatus() === $customDto->gameStatus + && $report->isPatched() === $customDto->isPatched; })); $handler = new CreateReportFromDtoCommandHandler( @@ -208,6 +212,7 @@ isNativeResolutionDocked: false, hasImprovedLoadingTimes: false, isSwitch2Edition: false, + isPatched: false, ); $this->gameRepository @@ -231,7 +236,8 @@ && $report->isNativeResolutionDocked() === $dtoWithBadStatus->isNativeResolutionDocked && $report->isHasImprovedLoadingTimes() === $dtoWithBadStatus->hasImprovedLoadingTimes && $report->isSwitch2Edition() === $dtoWithBadStatus->isSwitch2Edition - && $report->getGameStatus() === $dtoWithBadStatus->gameStatus; + && $report->getGameStatus() === $dtoWithBadStatus->gameStatus + && $report->isPatched() === $dtoWithBadStatus->isPatched; })); $handler = new CreateReportFromDtoCommandHandler( diff --git a/translations/report.en.yaml b/translations/report.en.yaml index e8b4379..2e9a09d 100644 --- a/translations/report.en.yaml +++ b/translations/report.en.yaml @@ -24,6 +24,7 @@ add_report: has_improved_loading_times: 'Improved Loading Times?' game_status: 'How the game run on Switch 2 system?' is_switch_2_editon: 'Is this a "Switch 2 edition"?' + is_patched: 'Patched version for the Switch 2?' notice: stable_framerate: '*Mostly keeping maximum framerate (30 or 60 fps)' resolution_improved: '**Is the resolution higher than on Switch 1 system?' @@ -46,7 +47,8 @@ list: better_resolution: 'Better Res3' native_resolution: 'Native Res4' better_loading_times: 'Better loading' - switch_2_edition: 'S2E5' + switch_2_edition: 'S2E?5' + is_patched: 'Patched?6' issue: 'Issue?' upvoted: 'Upvoted' portable_label: 'Portable' @@ -58,6 +60,7 @@ list: better_resolution: '3: Has a better resolution than on Nintendo Switch 1 system in related mode' native_resolution: '4: Can hit 1080p in portable mode and/or 4K in TV mode on Nintendo Switch 2 system' switch_2_edition: '5: Is a "Switch 2 Edition" or not' + is_patched: '6: Has the game been patched for the Nintendo Switch 2 system?' most_upvoted: title: 'Top report' @@ -72,3 +75,4 @@ most_upvoted: native_resolution_tv: 'Native Resolution on TV' better_loading_times: 'Better loading times' switch_2_edition: 'Switch 2 Edition' + is_patched: 'Patched'