diff --git a/composer.json b/composer.json index 30de67a..ffde4d4 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ "symfony/framework-bundle": "8.1.*", "symfony/messenger": "8.1.*", "symfony/runtime": "8.1.*", + "symfony/serializer": "8.1.*", "symfony/yaml": "8.1.*" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 86d237a..c2b480c 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "9dfce523e0ec35de78a25ca7e6e7277a", + "content-hash": "7222937e91cfe8d0b3a7b8c3fe9b1074", "packages": [ { "name": "doctrine/collections", @@ -3602,6 +3602,105 @@ ], "time": "2026-05-29T05:06:50+00:00" }, + { + "name": "symfony/serializer", + "version": "v8.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/serializer.git", + "reference": "f911b744bc24658f435ea30439cfe536f0173a3a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/serializer/zipball/f911b744bc24658f435ea30439cfe536f0173a3a", + "reference": "f911b744bc24658f435ea30439cfe536f0173a3a", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/property-access": "<8.1", + "symfony/property-info": "<7.4", + "symfony/type-info": "<7.4" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "seld/jsonlint": "^1.10", + "symfony/cache": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/filesystem": "^7.4|^8.0", + "symfony/form": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/property-access": "^8.1", + "symfony/property-info": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/type-info": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0", + "symfony/var-exporter": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Serializer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/serializer/tree/v8.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-27T09:05:56+00:00" + }, { "name": "symfony/service-contracts", "version": "v3.7.1", diff --git a/config/reference.php b/config/reference.php index fdde492..1a38853 100644 --- a/config/reference.php +++ b/config/reference.php @@ -352,7 +352,7 @@ * }>, * }, * serializer?: bool|array{ // Serializer configuration - * enabled?: bool|Param, // Default: false + * enabled?: bool|Param, // Default: true * enable_attributes?: bool|Param, // Default: true * name_converter?: scalar|Param|null, * circular_reference_handler?: scalar|Param|null, diff --git a/config/services.yaml b/config/services.yaml index 79b8ce2..76d706b 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -1,23 +1,20 @@ -# yaml-language-server: $schema=../vendor/symfony/dependency-injection/Loader/schema/services.schema.json - -# This file is the entry point to configure your own services. -# Files in the packages/ subdirectory configure your dependencies. -# See also https://symfony.com/doc/current/service_container/import.html - -# Put parameters here that don't need to change on each machine where the app is deployed -# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration parameters: services: - # default configuration for services in *this* file _defaults: - autowire: true # Automatically injects dependencies in your services. - autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. + autowire: true + autoconfigure: true - # makes classes in src/ available to be used as services - # this creates a service per class whose id is the fully-qualified class name App\: resource: '../src/' + exclude: + - '../src/Chess/' + + Chess\: + resource: '../src/Chess/' - # add more service definitions when explicit configuration is needed - # please note that last definitions always *replace* previous ones + Chess\Infrastructure\Http\Controller\GameController: + arguments: + $commandBus: '@command.bus' + $queryBus: '@query.bus' + tags: ['controller.service_arguments'] \ No newline at end of file diff --git a/src/Chess/Domain/Model/Game.php b/src/Chess/Domain/Model/Game.php index f274c13..56ae045 100644 --- a/src/Chess/Domain/Model/Game.php +++ b/src/Chess/Domain/Model/Game.php @@ -28,7 +28,7 @@ final class Game #[ORM\Column(type: 'json')] private array $boardState = []; - private Board $board; + private ?Board $board = null; private Color $currentTurnEnum; public function __construct( @@ -50,20 +50,13 @@ public static function start(): self ); } - #[ORM\PostLoad] - public function postLoad(): void - { - $this->currentTurnEnum = Color::{$this->currentTurn}; - $this->board = Board::fromArray($this->boardState); - } - public function play(Move $move, MoveValidator $validator): void { if ($this->isOver) { throw new \DomainException('Cannot play a move on a finished game.'); } - $piece = $this->board->pieceAt($move->from()); + $piece = $this->getBoard()->pieceAt($move->from()); if (null === $piece) { throw new \DomainException(\sprintf('No piece on square "%s".', $move->from()->toString())); @@ -73,8 +66,8 @@ public function play(Move $move, MoveValidator $validator): void throw new \DomainException(\sprintf('It is %s\'s turn.', $this->currentTurnEnum->name)); } - $validator->validate($move, $this->board); - $this->board->movePiece($move->from(), $move->to()); + $validator->validate($move, $this->getBoard()); + $this->getBoard()->movePiece($move->from(), $move->to()); $this->currentTurnEnum = $this->currentTurnEnum->opposite(); $this->currentTurn = $this->currentTurnEnum->name; $this->syncBoardState(); @@ -97,11 +90,21 @@ public function id(): GameId public function board(): Board { + return $this->getBoard(); + } + + private function getBoard(): Board + { + if (null === $this->board) { + $this->currentTurnEnum = Color::{$this->currentTurn}; + $this->board = Board::fromArray($this->boardState); + } + return $this->board; } private function syncBoardState(): void { - $this->boardState = $this->board->toArray(); + $this->boardState = $this->getBoard()->toArray(); } } diff --git a/src/Chess/Infrastructure/Http/Controller/GameController.php b/src/Chess/Infrastructure/Http/Controller/GameController.php new file mode 100644 index 0000000..f6806ef --- /dev/null +++ b/src/Chess/Infrastructure/Http/Controller/GameController.php @@ -0,0 +1,65 @@ +commandBus->dispatch(new StartGameCommand()); + $gameId = $envelope->last(HandledStamp::class)?->getResult(); + + return new JsonResponse(['gameId' => $gameId->toString()], Response::HTTP_CREATED); + } + + #[Route('/games/{id}', methods: ['GET'])] + public function getGameState(string $id): JsonResponse + { + $envelope = $this->queryBus->dispatch(new GetGameStateQuery($id)); + $dto = $envelope->last(HandledStamp::class)?->getResult(); + + return new JsonResponse([ + 'gameId' => $dto->gameId, + 'currentTurn' => $dto->currentTurn, + 'isOver' => $dto->isOver, + 'pieces' => $dto->pieces, + ]); + } + + #[Route('/games/{id}/moves', methods: ['POST'])] + public function playMove(string $id, Request $request): JsonResponse + { + $data = json_decode($request->getContent(), true); + + if (!isset($data['from'], $data['to'])) { + return new JsonResponse( + ['error' => 'Missing "from" or "to" fields.'], + Response::HTTP_BAD_REQUEST + ); + } + + $this->commandBus->dispatch(new PlayMoveCommand($id, $data['from'], $data['to'])); + + return new JsonResponse(null, Response::HTTP_NO_CONTENT); + } +}