├── .dockerignore ├── .env ├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── assets ├── css │ └── app.css ├── img │ └── logo.png └── js │ ├── Chat │ ├── ChatService.js │ └── Widget.js │ ├── Common │ ├── ConfirmationButton.js │ ├── EventSource.js │ ├── EventSourceStatus.js │ ├── HttpClient.js │ └── NotificationList.js │ ├── ConnectFour │ ├── AbortButton.js │ ├── Game.js │ ├── GameList.js │ ├── GameService.js │ ├── Model │ │ └── Game.js │ ├── Players.js │ ├── Redirect.js │ ├── ResignButton.js │ └── RunningGames.js │ ├── app.js │ └── pe.js ├── bin ├── chat │ └── onEntrypoint ├── connect-four │ └── onEntrypoint ├── console ├── identity │ └── onEntrypoint └── restartOnChange ├── codeception.dist.yml ├── composer.json ├── composer.lock ├── config ├── chat │ ├── config.yml │ ├── importmap.php │ ├── migrations.yml │ └── services │ │ ├── chat.yml │ │ ├── command_bus.yml │ │ ├── console.yml │ │ ├── consumer.yml │ │ ├── normalizer.yml │ │ ├── persistence.yml │ │ ├── query_bus.yml │ │ └── subscriber.yml ├── config.yml ├── config_dev.yml ├── config_prod.yml ├── connect-four │ ├── config.yml │ ├── importmap.php │ ├── migrations.yml │ ├── routing.yml │ └── services │ │ ├── command_bus.yml │ │ ├── console.yml │ │ ├── consumer.yml │ │ ├── controller.yml │ │ ├── game.yml │ │ ├── game_migrating_normalizer.yml │ │ ├── lock.yml │ │ ├── normalizer.yml │ │ ├── persistence.yml │ │ ├── query_bus.yml │ │ └── subscriber.yml ├── identity │ ├── config.yml │ ├── migrations.yml │ └── services │ │ ├── command_bus.yml │ │ ├── console.yml │ │ ├── normalizer.yml │ │ ├── persistence.yml │ │ ├── query_bus.yml │ │ ├── subscriber.yml │ │ └── user.yml ├── importmap.php ├── preload.php ├── routing.yml ├── routing_dev.yml └── web-interface │ ├── config.yml │ ├── routing.yml │ └── services │ ├── controller.yml │ ├── persistence.yml │ └── security.yml ├── deploy ├── load-test │ ├── scenario │ │ ├── go-to-page.js │ │ └── play-connect-four.js │ └── stack │ │ ├── app.env │ │ ├── chat │ │ ├── app.yml │ │ ├── mysql.yml │ │ └── redis.yml │ │ ├── connect-four │ │ ├── app-follow-event-store-1.yml │ │ ├── app-follow-event-store-2.yml │ │ ├── app-follow-event-store-3.yml │ │ ├── app-follow-event-store-4.yml │ │ ├── app-follow-event-store-5.yml │ │ ├── app.yml │ │ ├── mysql-1.yml │ │ ├── mysql-2.yml │ │ ├── mysql-3.yml │ │ ├── mysql-4.yml │ │ ├── mysql-5.yml │ │ └── redis.yml │ │ ├── identity │ │ ├── app.yml │ │ └── mysql.yml │ │ ├── observability.yml │ │ ├── proxysql-sidecar.yml │ │ ├── rabbitmq.yml │ │ ├── traefik.yml │ │ └── web-interface │ │ ├── app.yml │ │ ├── nchan.yml │ │ └── redis.yml └── single-server │ └── docker-compose.yml ├── docker-compose.ci.yml ├── docker-compose.yml ├── docker ├── Dockerfile ├── composer-install-after-code-copy.sh ├── composer-install.sh ├── development.ini ├── entrypoint.sh ├── production.ini └── warmup.sh ├── phpcs.xml.dist ├── phpstan.neon.dist ├── project ├── public ├── assets │ └── .gitignore ├── icon │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── safari-pinned-tab.svg │ └── site.webmanifest └── index.php ├── src ├── Chat │ ├── Application │ │ ├── ChatGateway.php │ │ ├── ChatId.php │ │ ├── ChatService.php │ │ ├── Command │ │ │ ├── InitiateChatCommand.php │ │ │ └── WriteMessageCommand.php │ │ ├── Event │ │ │ ├── ChatInitiated.php │ │ │ └── MessageWritten.php │ │ ├── Exception │ │ │ ├── AuthorNotAllowedException.php │ │ │ ├── ChatAlreadyExistsException.php │ │ │ ├── ChatException.php │ │ │ ├── ChatNotFoundException.php │ │ │ └── EmptyMessageException.php │ │ └── Query │ │ │ └── MessagesQuery.php │ ├── Infrastructure │ │ ├── DoctrineChatGateway.php │ │ ├── Jms │ │ │ └── Common.Domain.DomainEvent.yml │ │ ├── Messaging │ │ │ ├── PublishDomainEventsToMessageBrokerSubscriber.php │ │ │ └── PublishMessageBrokerEventsToBrowserMessageHandler.php │ │ └── Migration │ │ │ ├── Version20170608203652.php │ │ │ ├── Version20170608203825.php │ │ │ ├── Version20170609213426.php │ │ │ └── Version20220331223826.php │ └── Presentation │ │ └── Messaging │ │ └── CommandMessageHandler.php ├── Common │ ├── BrowserNotifier │ │ ├── BrowserNotifier.php │ │ └── Integration │ │ │ └── NchanBrowserNotifier.php │ ├── Bus │ │ ├── Bus.php │ │ ├── Exception │ │ │ ├── ApplicationException.php │ │ │ └── BusException.php │ │ ├── HandlerDiscovery.php │ │ ├── Integration │ │ │ ├── ApplicationExceptionToJsonListener.php │ │ │ ├── DoctrineTransactionalBus.php │ │ │ ├── FormViolationMapper.php │ │ │ ├── GamingPlatformBusBundle.php │ │ │ └── SymfonyValidatorBus.php │ │ ├── PsrCallableRoutingBus.php │ │ ├── PsrCompiledRoutingBus.php │ │ ├── PsrRuntimeRoutingBus.php │ │ ├── Request.php │ │ ├── RetryBus.php │ │ ├── TestContainer.php │ │ ├── Violation.php │ │ └── ViolationParameter.php │ ├── DoctrineHeartbeatMiddleware │ │ ├── ConnectionHeartbeatHandler.php │ │ ├── ScheduleHeartbeatOnConnectDriver.php │ │ ├── SchedulePeriodicHeartbeatMiddleware.php │ │ └── TrackActivityConnection.php │ ├── Domain │ │ ├── AggregateRoot.php │ │ ├── DomainEvent.php │ │ ├── Exception │ │ │ └── ConcurrencyException.php │ │ └── IsAggregateRoot.php │ ├── EventStore │ │ ├── CleanableEventStore.php │ │ ├── CollectsDomainEvents.php │ │ ├── CompositeDomainEventSubscriber.php │ │ ├── CompositeStoredEventSubscriber.php │ │ ├── ContentSerializer.php │ │ ├── DomainEvent.php │ │ ├── DomainEventSubscriber.php │ │ ├── DomainEvents.php │ │ ├── Event │ │ │ ├── EventsCommitted.php │ │ │ └── EventsFetched.php │ │ ├── EventListener │ │ │ ├── CleanUpEventStore.php │ │ │ └── DebugEvents.php │ │ ├── EventStore.php │ │ ├── EventStorePointer.php │ │ ├── EventStorePointerFactory.php │ │ ├── Exception │ │ │ ├── DuplicateVersionInStreamException.php │ │ │ ├── EventStoreException.php │ │ │ ├── FailedRetrieveMostRecentPublishedStoredEventIdException.php │ │ │ └── FailedTrackMostRecentPublishedStoredEventIdException.php │ │ ├── FollowEventStoreDispatcher.php │ │ ├── GapDetection.php │ │ ├── InMemoryCacheEventStorePointer.php │ │ ├── InMemoryEventStore.php │ │ ├── Integration │ │ │ ├── Doctrine │ │ │ │ ├── DoctrineEventStore.php │ │ │ │ ├── DoctrineEventStorePointerSchema.php │ │ │ │ ├── DoctrineEventStoreSchema.php │ │ │ │ ├── DoctrineMysqlEventStorePointer.php │ │ │ │ ├── DoctrineMysqlEventStorePointerFactory.php │ │ │ │ ├── EventListener │ │ │ │ │ ├── AppendDomainEvents.php │ │ │ │ │ └── PublishDomainEvents.php │ │ │ │ ├── IndexOption.php │ │ │ │ └── SkipDebugMatchingSqlPatternLogger.php │ │ │ ├── ForkPool │ │ │ │ ├── ForwardToChannelStoredEventSubscriber.php │ │ │ │ ├── Publisher.php │ │ │ │ └── Worker.php │ │ │ ├── JmsSerializer │ │ │ │ └── JmsContentSerializer.php │ │ │ ├── Predis │ │ │ │ ├── PredisEventStorePointer.php │ │ │ │ └── PredisEventStorePointerFactory.php │ │ │ └── Symfony │ │ │ │ ├── CleanUpEventStoreCommand.php │ │ │ │ ├── FollowEventStoreCommand.php │ │ │ │ └── ResetServicesListener.php │ │ ├── NoCommit.php │ │ ├── PollableEventStore.php │ │ ├── StoredEvent.php │ │ ├── StoredEventFilters.php │ │ └── StoredEventSubscriber.php │ ├── ExceptionHandling │ │ └── GamingExceptionListener.php │ ├── ForkPool │ │ ├── Channel │ │ │ ├── Channel.php │ │ │ ├── ChannelPair.php │ │ │ ├── ChannelPairFactory.php │ │ │ ├── NullChannelPairFactory.php │ │ │ ├── StreamChannel.php │ │ │ └── StreamChannelPairFactory.php │ │ ├── Exception │ │ │ └── ForkPoolException.php │ │ ├── ForkPool.php │ │ ├── Process.php │ │ ├── Processes.php │ │ ├── SessionLeaderTask.php │ │ ├── Signal.php │ │ ├── Task.php │ │ └── Wait.php │ ├── IdempotentStorage │ │ ├── IdempotentStorage.php │ │ ├── InMemoryIdempotentStorage.php │ │ └── PredisIdempotentStorage.php │ ├── JmsSerializer │ │ └── JmsSerializerFactory.php │ ├── MessageBroker │ │ ├── Consumer.php │ │ ├── Context.php │ │ ├── Event │ │ │ ├── MessageFailed.php │ │ │ ├── MessageHandled.php │ │ │ ├── MessageReceived.php │ │ │ ├── MessageReturned.php │ │ │ ├── MessageSent.php │ │ │ ├── MessagesFlushed.php │ │ │ ├── ReplySent.php │ │ │ └── RequestSent.php │ │ ├── EventListener │ │ │ ├── DebugEvents.php │ │ │ ├── ThrowWhenMessageFailed.php │ │ │ └── ThrowWhenMessageReturned.php │ │ ├── Exception │ │ │ └── MessageBrokerException.php │ │ ├── Integration │ │ │ ├── AmqpLib │ │ │ │ ├── AmqpConsumer.php │ │ │ │ ├── AmqpConsumerFactory.php │ │ │ │ ├── AmqpContext.php │ │ │ │ ├── AmqpPublisher.php │ │ │ │ ├── ConnectionFactory │ │ │ │ │ ├── AmqpStreamConnectionFactory.php │ │ │ │ │ ├── ConnectionFactory.php │ │ │ │ │ ├── DeclareTopologyConnectionFactory.php │ │ │ │ │ └── SchedulePeriodicHeartbeatConnectionFactory.php │ │ │ │ ├── MessageRouter │ │ │ │ │ ├── MessageRouter.php │ │ │ │ │ ├── Route.php │ │ │ │ │ └── RouteMessagesToExchange.php │ │ │ │ ├── MessageTranslator │ │ │ │ │ ├── ConfigurableMessageTranslator.php │ │ │ │ │ └── MessageTranslator.php │ │ │ │ ├── QueueConsumer │ │ │ │ │ ├── CallbackFactory.php │ │ │ │ │ ├── ConsumeQueueWithLeastConsumers.php │ │ │ │ │ ├── ConsumeQueues.php │ │ │ │ │ └── QueueConsumer.php │ │ │ │ └── Topology │ │ │ │ │ ├── CompositeTopology.php │ │ │ │ │ ├── DefinesQueues.php │ │ │ │ │ ├── ExchangeTopology.php │ │ │ │ │ ├── HashExchangeTopology.php │ │ │ │ │ ├── QueueTopology.php │ │ │ │ │ └── Topology.php │ │ │ ├── ForkPool │ │ │ │ ├── ConsumerTask.php │ │ │ │ └── ForkPoolConsumer.php │ │ │ └── Symfony │ │ │ │ ├── ConsumeMessagesCommand.php │ │ │ │ └── ResetServicesListener.php │ │ ├── Message.php │ │ ├── MessageHandler.php │ │ └── Publisher.php │ ├── Normalizer │ │ ├── Exception │ │ │ └── NormalizerException.php │ │ ├── Integration │ │ │ └── JmsSerializerNormalizer.php │ │ ├── MigratingNormalizer.php │ │ ├── Migration.php │ │ ├── Migrations.php │ │ ├── Normalizer.php │ │ └── Test │ │ │ ├── TestMigration.php │ │ │ └── TestNormalizer.php │ ├── Scheduler │ │ ├── CollectJobsScheduler.php │ │ ├── Handler.php │ │ ├── Job.php │ │ ├── Jobs.php │ │ ├── NullHandler.php │ │ ├── PcntlScheduler.php │ │ ├── Scheduler.php │ │ └── TestScheduler.php │ └── Sharding │ │ ├── Exception │ │ └── ShardingException.php │ │ ├── Integration │ │ ├── Crc32ModShards.php │ │ └── DoctrineMySqlSchemaShards.php │ │ └── Shards.php ├── ConnectFour │ ├── Application │ │ └── Game │ │ │ ├── Command │ │ │ ├── AbortCommand.php │ │ │ ├── AbortHandler.php │ │ │ ├── AssignChatCommand.php │ │ │ ├── AssignChatHandler.php │ │ │ ├── JoinCommand.php │ │ │ ├── JoinHandler.php │ │ │ ├── MoveCommand.php │ │ │ ├── MoveHandler.php │ │ │ ├── OpenCommand.php │ │ │ ├── OpenHandler.php │ │ │ ├── ResignCommand.php │ │ │ └── ResignHandler.php │ │ │ └── Query │ │ │ ├── GameHandler.php │ │ │ ├── GameQuery.php │ │ │ ├── GamesByPlayerHandler.php │ │ │ ├── GamesByPlayerQuery.php │ │ │ ├── Model │ │ │ ├── Game │ │ │ │ ├── Game.php │ │ │ │ ├── GameFinder.php │ │ │ │ ├── GameStore.php │ │ │ │ └── Move.php │ │ │ ├── GamesByPlayer │ │ │ │ ├── GamesByPlayer.php │ │ │ │ ├── GamesByPlayerStore.php │ │ │ │ └── State.php │ │ │ ├── OpenGames │ │ │ │ ├── OpenGame.php │ │ │ │ ├── OpenGameStore.php │ │ │ │ └── OpenGames.php │ │ │ └── RunningGames │ │ │ │ ├── RunningGameStore.php │ │ │ │ └── RunningGames.php │ │ │ ├── OpenGamesHandler.php │ │ │ ├── OpenGamesQuery.php │ │ │ ├── PlayerSearchStatistics │ │ │ ├── PlayerSearchStatisticsHandler.php │ │ │ ├── PlayerSearchStatisticsQuery.php │ │ │ └── PlayerSearchStatisticsResponse.php │ │ │ ├── RunningGamesHandler.php │ │ │ └── RunningGamesQuery.php │ ├── Domain │ │ └── Game │ │ │ ├── Board │ │ │ ├── Board.php │ │ │ ├── Field.php │ │ │ ├── Point.php │ │ │ ├── Size.php │ │ │ └── Stone.php │ │ │ ├── Configuration.php │ │ │ ├── Event │ │ │ ├── ChatAssigned.php │ │ │ ├── GameAborted.php │ │ │ ├── GameDrawn.php │ │ │ ├── GameOpened.php │ │ │ ├── GameResigned.php │ │ │ ├── GameWon.php │ │ │ ├── PlayerJoined.php │ │ │ └── PlayerMoved.php │ │ │ ├── Exception │ │ │ ├── ColumnAlreadyFilledException.php │ │ │ ├── GameException.php │ │ │ ├── GameFinishedException.php │ │ │ ├── GameNotFoundException.php │ │ │ ├── GameNotRunningException.php │ │ │ ├── GameRunningException.php │ │ │ ├── InvalidSizeException.php │ │ │ ├── OutOfSizeException.php │ │ │ ├── PlayerHasInvalidStoneException.php │ │ │ ├── PlayerNotOwnerException.php │ │ │ ├── PlayersNotUniqueException.php │ │ │ ├── UnexpectedPlayerException.php │ │ │ └── WinningSequenceLengthTooShortException.php │ │ │ ├── Game.php │ │ │ ├── GameId.php │ │ │ ├── Games.php │ │ │ ├── Player.php │ │ │ ├── Players.php │ │ │ ├── State │ │ │ ├── Aborted.php │ │ │ ├── Drawn.php │ │ │ ├── Open.php │ │ │ ├── Resigned.php │ │ │ ├── Running.php │ │ │ ├── State.php │ │ │ ├── Transition.php │ │ │ └── Won.php │ │ │ └── WinningRule │ │ │ ├── DiagonalWinningRule.php │ │ │ ├── HorizontalWinningRule.php │ │ │ ├── SequenceBasedWinningRule.php │ │ │ ├── VerticalWinningRule.php │ │ │ ├── WinningRule.php │ │ │ ├── WinningRules.php │ │ │ └── WinningSequence.php │ └── Port │ │ └── Adapter │ │ ├── Console │ │ └── PublishRunningGamesCountToNchanCommand.php │ │ ├── Http │ │ ├── ChallengeController.php │ │ ├── Form │ │ │ └── OpenType.php │ │ ├── FragmentController.php │ │ └── View │ │ │ ├── challenge.html.twig │ │ │ ├── open-games.html.twig │ │ │ ├── open.html.twig │ │ │ ├── player-search-filter.html.twig │ │ │ └── statistics.html.twig │ │ ├── Messaging │ │ ├── PublishDomainEventsToMessageBrokerSubscriber.php │ │ ├── PublishMessageBrokerEventsToBrowserMessageHandler.php │ │ └── RefereeMessageHandler.php │ │ └── Persistence │ │ ├── Jms │ │ ├── Common.Domain.DomainEvent.yml │ │ ├── ConnectFour.Domain.Game.Game.yml │ │ ├── ConnectFour.Domain.Game.State.State.yml │ │ ├── ConnectFour.Domain.Game.WinningRule.WinningRule.yml │ │ ├── FieldSubscriber.php │ │ ├── GameIdSubscriber.php │ │ └── StoneSubscriber.php │ │ ├── MigratingNormalizer │ │ └── StoneAsScalarMigration.php │ │ ├── Migration │ │ ├── Version20160903094031.php │ │ └── Version20160904024032.php │ │ ├── Projection │ │ ├── GameProjection.php │ │ ├── GamesByPlayerProjection.php │ │ ├── OpenGamesProjection.php │ │ └── RunningGamesProjection.php │ │ └── Repository │ │ ├── DoctrineJsonGameRepository.php │ │ ├── InMemoryCacheGameStore.php │ │ ├── PredisGameStore.php │ │ ├── PredisGamesByPlayerStore.php │ │ ├── PredisOpenGameStore.php │ │ └── PredisRunningGameStore.php ├── Identity │ ├── Application │ │ └── User │ │ │ ├── Command │ │ │ ├── ArriveCommand.php │ │ │ └── SignUpCommand.php │ │ │ ├── Query │ │ │ ├── User.php │ │ │ ├── UserByEmailQuery.php │ │ │ └── UserQuery.php │ │ │ └── UserService.php │ ├── Domain │ │ └── Model │ │ │ └── User │ │ │ ├── Event │ │ │ ├── UserArrived.php │ │ │ └── UserSignedUp.php │ │ │ ├── Exception │ │ │ ├── EmailAlreadyExistsException.php │ │ │ ├── UserAlreadySignedUpException.php │ │ │ ├── UserException.php │ │ │ ├── UserNotFoundException.php │ │ │ └── UsernameAlreadyExistsException.php │ │ │ ├── User.php │ │ │ ├── UserId.php │ │ │ └── Users.php │ └── Port │ │ └── Adapter │ │ ├── Messaging │ │ └── PublishDomainEventsToMessageBrokerSubscriber.php │ │ ├── Persistence │ │ ├── Jms │ │ │ └── Common.Domain.DomainEvent.yml │ │ ├── Mapping │ │ │ └── User │ │ │ │ ├── User.orm.xml │ │ │ │ └── UserId.orm.xml │ │ ├── Migration │ │ │ ├── Version20170526204325.php │ │ │ ├── Version20170923230032.php │ │ │ └── Version20220401224331.php │ │ └── Repository │ │ │ └── DoctrineUserRepository.php │ │ └── Validation │ │ ├── EmailRequirements.php │ │ └── validation.yml ├── Kernel.php ├── Memory │ └── Domain │ │ └── Model │ │ └── Game │ │ ├── Dealer │ │ ├── Dealer.php │ │ ├── LazyDealer.php │ │ └── ShuffleDealer.php │ │ ├── Event │ │ ├── GameClosed.php │ │ ├── GameOpened.php │ │ ├── GameStarted.php │ │ ├── PlayerJoined.php │ │ └── PlayerLeft.php │ │ ├── Exception │ │ ├── GameException.php │ │ ├── GameNotFoundException.php │ │ ├── GameNotOpenException.php │ │ ├── PlayerAlreadyJoinedException.php │ │ ├── PlayerNotAllowedToStartGameException.php │ │ ├── PlayerNotJoinedException.php │ │ └── PlayerPoolIsEmptyException.php │ │ ├── Game.php │ │ ├── GameId.php │ │ ├── Player.php │ │ └── PlayerPool.php └── WebInterface │ ├── Infrastructure │ ├── Security │ │ ├── Security.php │ │ ├── User.php │ │ └── UserProvider.php │ └── Symfony │ │ ├── HandleFragmentExceptions.php │ │ ├── NotifyBrowserAboutLogin.php │ │ └── ReplaceSymfonyToolbar.php │ └── Presentation │ └── Http │ ├── ChatController.php │ ├── ConnectFourController.php │ ├── Form │ ├── LoginType.php │ └── SignupType.php │ ├── LoginController.php │ ├── PageController.php │ ├── SignupController.php │ └── View │ ├── game.html.twig │ ├── lobby.html.twig │ ├── login │ ├── check-inbox.html.twig │ ├── index.html.twig │ └── layout │ │ └── base.html.twig │ ├── profile.html.twig │ └── signup │ ├── confirm.html.twig │ ├── index.html.twig │ ├── layout │ └── base.html.twig │ └── verify-email.html.twig ├── templates ├── bundles │ └── TwigBundle │ │ └── Exception │ │ ├── error.html.twig │ │ └── error404.html.twig ├── include │ └── pagination.html.twig ├── layout │ ├── base.html.twig │ ├── center-tight.html.twig │ ├── condensed.html.twig │ └── partial │ │ └── footer-links.html.twig ├── macro │ ├── alert.html.twig │ └── icons.html.twig └── tabler_form_layout.html.twig ├── tests ├── _data │ └── .gitignore ├── _output │ └── .gitignore ├── _support │ ├── AcceptanceTester.php │ ├── Helper │ │ ├── Acceptance.php │ │ └── Unit.php │ ├── UnitTester.php │ └── _generated │ │ └── .gitignore ├── acceptance.suite.dist.yml ├── acceptance │ └── GameCest.php ├── integration.suite.dist.yml ├── integration │ └── Common │ │ └── DoctrineHeartbeatMiddleware │ │ └── SchedulePeriodicHeartbeatMiddlewareTest.php ├── unit.suite.dist.yml └── unit │ ├── Chat │ └── Application │ │ ├── ChatIdTest.php │ │ └── ChatServiceTest.php │ ├── Common │ ├── Bus │ │ ├── Fixture │ │ │ ├── EmptyHandler.php │ │ │ ├── FirstRequest.php │ │ │ ├── SecondRequest.php │ │ │ ├── ThirdRequest.php │ │ │ └── UniversalHandler.php │ │ ├── Integration │ │ │ ├── FormViolationMapperTest.php │ │ │ └── SymfonyValidatorBusTest.php │ │ ├── PsrCallableRoutingBusTest.php │ │ ├── PsrCompiledRoutingBusTest.php │ │ ├── PsrRuntimeRoutingBusTest.php │ │ └── RetryBusTest.php │ ├── Normalizer │ │ └── MigratingNormalizerTest.php │ └── Scheduler │ │ └── PcntlSchedulerTest.php │ ├── ConnectFour │ ├── Application │ │ └── Game │ │ │ └── Query │ │ │ └── Model │ │ │ └── Game │ │ │ └── GameTest.php │ └── Domain │ │ └── Game │ │ ├── Board │ │ ├── BoardTest.php │ │ ├── FieldTest.php │ │ ├── PointTest.php │ │ └── SizeTest.php │ │ ├── GameIdTest.php │ │ ├── GameTest.php │ │ ├── PlayerTest.php │ │ ├── PlayersTest.php │ │ └── WinningRule │ │ ├── DiagonalWinningRuleTest.php │ │ ├── HorizontalWinningRuleTest.php │ │ ├── VerticalWinningRuleTest.php │ │ └── WinningRulesTest.php │ ├── Identity │ └── Domain │ │ └── Model │ │ └── User │ │ ├── UserIdTest.php │ │ └── UserTest.php │ └── Memory │ └── Domain │ └── Model │ └── Game │ ├── Dealer │ ├── LazyDealerTest.php │ └── ShuffleDealerTest.php │ ├── GameIdTest.php │ ├── GameTest.php │ ├── PlayerPoolTest.php │ └── PlayerTest.php └── var └── .gitignore /.dockerignore: -------------------------------------------------------------------------------- 1 | /assets/vendor 2 | /var 3 | /vendor 4 | /public/assets 5 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: master 6 | pull_request: 7 | 8 | jobs: 9 | pipeline: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | 16 | - name: Prepare 17 | run: ./project buildProductionImages 18 | 19 | - name: Testsuite 20 | run: ./project tests 21 | 22 | - name: Deploy 23 | if: github.ref == 'refs/heads/master' 24 | run: ./project pushProductionImages 25 | env: 26 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} 27 | DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /assets/vendor 2 | /codeception.yml 3 | /phpcs.xml 4 | /phpstan.neon 5 | /tests/acceptance.suite.yml 6 | /tests/unit.suite.yml 7 | /vendor 8 | -------------------------------------------------------------------------------- /assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marein/php-gaming-website/5e4fdb3962958142bc80e082bd3f8cd15c344b01/assets/img/logo.png -------------------------------------------------------------------------------- /assets/js/Chat/ChatService.js: -------------------------------------------------------------------------------- 1 | import {client} from '../Common/HttpClient.js' 2 | 3 | class ChatService { 4 | /** 5 | * @param {HttpClient} httpClient 6 | */ 7 | constructor(httpClient) { 8 | this.httpClient = httpClient; 9 | } 10 | 11 | /** 12 | * @param {String} chatId 13 | * @param {String} message 14 | * @returns {Promise} 15 | */ 16 | writeMessage(chatId, message) { 17 | return this.httpClient.post( 18 | '/api/chat/chats/' + chatId + '/write-message', 19 | {message} 20 | ); 21 | } 22 | 23 | /** 24 | * @param {String} chatId 25 | * @returns {Promise} 26 | */ 27 | messages(chatId) { 28 | return this.httpClient.get( 29 | '/api/chat/chats/' + chatId + '/messages' 30 | ); 31 | } 32 | } 33 | 34 | export const service = new ChatService(client); 35 | -------------------------------------------------------------------------------- /assets/js/ConnectFour/Redirect.js: -------------------------------------------------------------------------------- 1 | import {service} from './GameService.js' 2 | import * as sse from '../Common/EventSource.js' 3 | 4 | customElements.define('connect-four-redirect', class extends HTMLElement { 5 | connectedCallback() { 6 | this._sseAbortController = new AbortController(); 7 | 8 | sse.subscribe(`connect-four-${this.getAttribute('game-id')}`, { 9 | 'ConnectFour.PlayerJoined': e => service.redirectTo(e.detail.gameId) 10 | }, this._sseAbortController.signal); 11 | } 12 | 13 | disconnectedCallback() { 14 | this._sseAbortController.abort(); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /assets/js/ConnectFour/RunningGames.js: -------------------------------------------------------------------------------- 1 | import * as sse from '../Common/EventSource.js' 2 | 3 | customElements.define('connect-four-running-games', class extends HTMLElement { 4 | connectedCallback() { 5 | this._sseAbortController = new AbortController(); 6 | 7 | sse.subscribe('lobby', { 8 | 'ConnectFour.RunningGamesUpdated': e => this.innerText = e.detail.count 9 | }, this._sseAbortController.signal); 10 | } 11 | 12 | disconnectedCallback() { 13 | this._sseAbortController.abort(); 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /bin/chat/onEntrypoint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | if [ "${APP_CHAT_RUN_MIGRATIONS}" = "1" ] || [ "${APP_RUN_MIGRATIONS}" = "1" ] 6 | then 7 | bin/console doctrine:database:create \ 8 | --connection=chat \ 9 | --if-not-exists 10 | bin/console doctrine:migrations:migrate \ 11 | --configuration=config/chat/migrations.yml \ 12 | --conn=chat \ 13 | --allow-no-migration \ 14 | --no-interaction 15 | fi 16 | -------------------------------------------------------------------------------- /bin/connect-four/onEntrypoint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | if [ "${APP_CONNECT_FOUR_RUN_MIGRATIONS}" = "1" ] || [ "${APP_RUN_MIGRATIONS}" = "1" ] 6 | then 7 | IFS=, read -ra values <<< "${APP_CONNECT_FOUR_DOCTRINE_DBAL_SHARDS}" 8 | for value in "${values[@]}" 9 | do 10 | APP_CONNECT_FOUR_DOCTRINE_DBAL_DATABASE=${value} bin/console doctrine:database:create \ 11 | --connection=connect_four \ 12 | --if-not-exists 13 | APP_CONNECT_FOUR_DOCTRINE_DBAL_DATABASE=${value} bin/console doctrine:migrations:migrate \ 14 | --configuration=config/connect-four/migrations.yml \ 15 | --conn=connect_four \ 16 | --allow-no-migration \ 17 | --no-interaction 18 | done 19 | fi 20 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | ['path' => 'js/Chat/Widget.js'] 7 | ]; 8 | -------------------------------------------------------------------------------- /config/chat/migrations.yml: -------------------------------------------------------------------------------- 1 | migrations_paths: 2 | 'Gaming\Chat\Infrastructure\Migration': '../../src/Chat/Infrastructure/Migration' 3 | table_storage: 4 | table_name: migration_versions 5 | connection: chat 6 | -------------------------------------------------------------------------------- /config/chat/services/chat.yml: -------------------------------------------------------------------------------- 1 | services: 2 | chat.chat-gateway: 3 | class: Gaming\Chat\Infrastructure\DoctrineChatGateway 4 | arguments: ['@chat.doctrine-dbal', 'chat', 'message'] 5 | 6 | chat.idempotent-chat-id-storage: 7 | class: Gaming\Common\IdempotentStorage\PredisIdempotentStorage 8 | arguments: ['@chat.predis', 86400] 9 | 10 | Gaming\Chat\Application\ChatService: 11 | arguments: ['@chat.chat-gateway', '@chat.event-store', '@clock', '@chat.idempotent-chat-id-storage'] 12 | tags: 13 | - { name: 'gaming_platform_bus.handler', bus: 'chat_query', match: '/\\Query\\/' } 14 | - { name: 'gaming_platform_bus.handler', bus: 'chat_command', match: '/\\Command\\/' } 15 | -------------------------------------------------------------------------------- /config/chat/services/command_bus.yml: -------------------------------------------------------------------------------- 1 | services: 2 | chat.command-bus: 3 | alias: gaming_platform_bus.chat_command 4 | 5 | chat.transactional-command-bus: 6 | class: Gaming\Common\Bus\Integration\DoctrineTransactionalBus 7 | decorates: 'chat.command-bus' 8 | arguments: ['@.inner', '@chat.doctrine-dbal'] 9 | 10 | chat.validating-command-bus: 11 | class: Gaming\Common\Bus\Integration\SymfonyValidatorBus 12 | decorates: 'chat.command-bus' 13 | arguments: ['@.inner', '@validator'] 14 | -------------------------------------------------------------------------------- /config/chat/services/console.yml: -------------------------------------------------------------------------------- 1 | services: 2 | chat.follow-event-store-command: 3 | class: Gaming\Common\EventStore\Integration\Symfony\FollowEventStoreCommand 4 | arguments: 5 | - '@chat.event-store' 6 | - !service 7 | class: Gaming\Common\EventStore\Integration\Doctrine\DoctrineMysqlEventStorePointerFactory 8 | arguments: ['@chat.doctrine-dbal', 'event_store_pointer'] 9 | - !tagged_locator { tag: 'chat.stored-event-subscriber', index_by: 'key' } 10 | - '@event_dispatcher' 11 | tags: 12 | - name: console.command 13 | command: chat:follow-event-store 14 | description: 'Publish events to subscribers.' 15 | -------------------------------------------------------------------------------- /config/chat/services/normalizer.yml: -------------------------------------------------------------------------------- 1 | services: 2 | chat.jms: 3 | class: JMS\Serializer\Serializer 4 | factory: ['Gaming\Common\JmsSerializer\JmsSerializerFactory', 'create'] 5 | arguments: 6 | - '%kernel.debug%' 7 | - '%kernel.cache_dir%/chat/jms' 8 | - { 'Gaming': '%kernel.project_dir%/src/Chat/Infrastructure/Jms' } 9 | - !tagged_iterator chat.jms.subscriber 10 | 11 | chat.normalizer: 12 | class: Gaming\Common\Normalizer\Integration\JmsSerializerNormalizer 13 | arguments: ['@chat.jms'] 14 | -------------------------------------------------------------------------------- /config/chat/services/persistence.yml: -------------------------------------------------------------------------------- 1 | services: 2 | chat.predis: 3 | class: Predis\Client 4 | arguments: ['%env(APP_CHAT_PREDIS_CLIENT_URL)%', { prefix: 'chat:' }] 5 | 6 | chat.doctrine-dbal: 7 | alias: 'doctrine.dbal.chat_connection' 8 | 9 | chat.event-store: 10 | class: Gaming\Common\EventStore\Integration\Doctrine\DoctrineEventStore 11 | arguments: 12 | - '@chat.doctrine-dbal' 13 | - 'event_store' 14 | - !service 15 | class: Gaming\Common\EventStore\Integration\JmsSerializer\JmsContentSerializer 16 | arguments: ['@chat.jms', 'Gaming\Common\Domain\DomainEvent'] 17 | -------------------------------------------------------------------------------- /config/chat/services/query_bus.yml: -------------------------------------------------------------------------------- 1 | services: 2 | chat.query-bus: 3 | alias: gaming_platform_bus.chat_query 4 | 5 | chat.validating-query-bus: 6 | class: Gaming\Common\Bus\Integration\SymfonyValidatorBus 7 | decorates: 'chat.query-bus' 8 | arguments: ['@.inner', '@validator'] 9 | -------------------------------------------------------------------------------- /config/chat/services/subscriber.yml: -------------------------------------------------------------------------------- 1 | services: 2 | chat.publish-stored-events-subscriber: 3 | class: Gaming\Chat\Infrastructure\Messaging\PublishDomainEventsToMessageBrokerSubscriber 4 | arguments: ['@gaming.message-broker.gaming-exchange-publisher'] 5 | tags: [{ name: 'chat.stored-event-subscriber', key: 'publish-to-message-broker' }] 6 | -------------------------------------------------------------------------------- /config/config_prod.yml: -------------------------------------------------------------------------------- 1 | imports: [{ resource: config.yml }] 2 | -------------------------------------------------------------------------------- /config/connect-four/config.yml: -------------------------------------------------------------------------------- 1 | imports: [{ resource: services/ }] 2 | 3 | gaming_platform_bus: 4 | buses: 5 | connect_four_command: ~ 6 | connect_four_query: ~ 7 | 8 | twig: 9 | paths: { '%kernel.project_dir%/src/ConnectFour/Port/Adapter/Http/View': connect-four } 10 | 11 | doctrine: 12 | dbal: 13 | connections: 14 | connect-four: 15 | url: '%env(resolve:APP_CONNECT_FOUR_DOCTRINE_DBAL_URL)%' 16 | server_version: '8.2' 17 | charset: utf8mb4 18 | default_table_options: 19 | charset: utf8mb4 20 | collate: utf8mb4_unicode_ci 21 | -------------------------------------------------------------------------------- /config/connect-four/importmap.php: -------------------------------------------------------------------------------- 1 | ['path' => 'js/ConnectFour/Redirect.js'], 7 | 'connect-four-running-games' => ['path' => 'js/ConnectFour/RunningGames.js'], 8 | 'connect-four-game-list' => ['path' => 'js/ConnectFour/GameList.js'], 9 | 'connect-four-game' => ['path' => 'js/ConnectFour/Game.js'], 10 | 'connect-four-players' => ['path' => 'js/ConnectFour/Players.js'], 11 | 'connect-four-abort-button' => ['path' => 'js/ConnectFour/AbortButton.js'], 12 | 'connect-four-resign-button' => ['path' => 'js/ConnectFour/ResignButton.js'] 13 | ]; 14 | -------------------------------------------------------------------------------- /config/connect-four/migrations.yml: -------------------------------------------------------------------------------- 1 | migrations_paths: 2 | 'Gaming\ConnectFour\Port\Adapter\Persistence\Migration': '../../src/ConnectFour/Port/Adapter/Persistence/Migration' 3 | table_storage: 4 | table_name: migration_versions 5 | connection: connect_four 6 | -------------------------------------------------------------------------------- /config/connect-four/routing.yml: -------------------------------------------------------------------------------- 1 | connect_four_open: 2 | path: /api/connect-four/games/open 3 | methods: [POST] 4 | controller: connect-four.challenge-controller::openAction 5 | 6 | connect_four_abort_challenge: 7 | path: /challenge/{id}/abort 8 | methods: [POST] 9 | controller: connect-four.challenge-controller::abortChallengeAction 10 | 11 | connect_four_accept_challenge: 12 | path: /challenge/{id}/accept 13 | methods: [POST] 14 | controller: connect-four.challenge-controller::acceptChallengeAction 15 | 16 | connect_four_challenge: 17 | path: /challenge/{id} 18 | methods: [GET] 19 | controller: connect-four.challenge-controller::showAction 20 | -------------------------------------------------------------------------------- /config/connect-four/services/controller.yml: -------------------------------------------------------------------------------- 1 | services: 2 | connect-four.fragment-controller: 3 | class: Gaming\ConnectFour\Port\Adapter\Http\FragmentController 4 | arguments: ['@connect-four.query-bus'] 5 | calls: [[setContainer, ['@Psr\Container\ContainerInterface']]] 6 | tags: ['controller.service_arguments', 'container.service_subscriber'] 7 | 8 | connect-four.challenge-controller: 9 | class: Gaming\ConnectFour\Port\Adapter\Http\ChallengeController 10 | arguments: ['@connect-four.command-bus', '@connect-four.query-bus', '@web-interface.security'] 11 | calls: [[setContainer, ['@Psr\Container\ContainerInterface']]] 12 | tags: ['controller.service_arguments', 'container.service_subscriber'] 13 | -------------------------------------------------------------------------------- /config/connect-four/services/game_migrating_normalizer.yml: -------------------------------------------------------------------------------- 1 | services: 2 | connect-four.migrating-normalizer.game-migrations: 3 | class: Gaming\Common\Normalizer\Migrations 4 | arguments: ['schemaVersion', !tagged_iterator connect-four.migrating-normalizer.game-migration] 5 | tags: [{ name: 'connect-four.migrating-normalizer.migrations', key: 'Gaming\ConnectFour\Domain\Game\Game' }] 6 | 7 | connect-four.migrating-normalizer.game-migration.stone-as-scalar: 8 | class: Gaming\ConnectFour\Port\Adapter\Persistence\MigratingNormalizer\StoneAsScalarMigration 9 | tags: [{ name: 'connect-four.migrating-normalizer.game-migration' }] 10 | -------------------------------------------------------------------------------- /config/connect-four/services/lock.yml: -------------------------------------------------------------------------------- 1 | services: 2 | connect-four.lock-factory: 3 | class: Symfony\Component\Lock\LockFactory 4 | arguments: 5 | - !service 6 | class: Symfony\Component\Lock\Store\RedisStore 7 | arguments: ['@connect-four.predis'] 8 | -------------------------------------------------------------------------------- /config/identity/config.yml: -------------------------------------------------------------------------------- 1 | imports: [{ resource: services/ }] 2 | 3 | framework: 4 | validation: 5 | mapping: 6 | paths: ['%kernel.project_dir%/src/Identity/Port/Adapter/Validation/'] 7 | 8 | gaming_platform_bus: 9 | buses: 10 | identity_command: ~ 11 | identity_query: ~ 12 | 13 | doctrine: 14 | dbal: 15 | connections: 16 | identity: 17 | url: '%env(APP_IDENTITY_DOCTRINE_DBAL_URL)%' 18 | server_version: '8.2' 19 | charset: utf8mb4 20 | default_table_options: 21 | charset: utf8mb4 22 | collate: utf8mb4_unicode_ci 23 | orm: 24 | entity_managers: 25 | identity: 26 | connection: identity 27 | mappings: 28 | user: 29 | type: xml 30 | dir: '%kernel.project_dir%/src/Identity/Port/Adapter/Persistence/Mapping/User' 31 | prefix: Gaming\Identity\Domain\Model\User 32 | -------------------------------------------------------------------------------- /config/identity/migrations.yml: -------------------------------------------------------------------------------- 1 | migrations_paths: 2 | 'Gaming\Identity\Port\Adapter\Persistence\Migration': '../../src/Identity/Port/Adapter/Persistence/Migration' 3 | table_storage: 4 | table_name: migration_versions 5 | connection: identity 6 | -------------------------------------------------------------------------------- /config/identity/services/command_bus.yml: -------------------------------------------------------------------------------- 1 | services: 2 | identity.command-bus: 3 | alias: gaming_platform_bus.identity_command 4 | 5 | identity.transactional-command-bus: 6 | class: Gaming\Common\Bus\Integration\DoctrineTransactionalBus 7 | decorates: 'identity.command-bus' 8 | arguments: ['@.inner', '@identity.doctrine-dbal'] 9 | 10 | identity.retry-command-bus: 11 | class: Gaming\Common\Bus\RetryBus 12 | decorates: 'identity.command-bus' 13 | arguments: ['@.inner', 3, 'Gaming\Common\Domain\Exception\ConcurrencyException'] 14 | 15 | identity.validating-command-bus: 16 | class: Gaming\Common\Bus\Integration\SymfonyValidatorBus 17 | decorates: 'identity.command-bus' 18 | arguments: ['@.inner', '@validator'] 19 | -------------------------------------------------------------------------------- /config/identity/services/console.yml: -------------------------------------------------------------------------------- 1 | services: 2 | identity.follow-event-store-command: 3 | class: Gaming\Common\EventStore\Integration\Symfony\FollowEventStoreCommand 4 | arguments: 5 | - '@identity.event-store' 6 | - !service 7 | class: Gaming\Common\EventStore\Integration\Doctrine\DoctrineMysqlEventStorePointerFactory 8 | arguments: ['@identity.doctrine-dbal', 'event_store_pointer'] 9 | - !tagged_locator { tag: 'identity.stored-event-subscriber', index_by: 'key' } 10 | - '@event_dispatcher' 11 | tags: 12 | - name: console.command 13 | command: identity:follow-event-store 14 | description: 'Publish events to subscribers.' 15 | -------------------------------------------------------------------------------- /config/identity/services/normalizer.yml: -------------------------------------------------------------------------------- 1 | services: 2 | identity.jms: 3 | class: JMS\Serializer\Serializer 4 | factory: ['Gaming\Common\JmsSerializer\JmsSerializerFactory', 'create'] 5 | arguments: 6 | - '%kernel.debug%' 7 | - '%kernel.cache_dir%/identity/jms' 8 | - { 'Gaming': '%kernel.project_dir%/src/Identity/Port/Adapter/Persistence/Jms' } 9 | - !tagged_iterator identity.jms.subscriber 10 | 11 | identity.normalizer: 12 | class: Gaming\Common\Normalizer\Integration\JmsSerializerNormalizer 13 | arguments: ['@identity.jms'] 14 | -------------------------------------------------------------------------------- /config/identity/services/query_bus.yml: -------------------------------------------------------------------------------- 1 | services: 2 | identity.query-bus: 3 | alias: gaming_platform_bus.identity_query 4 | 5 | identity.validating-query-bus: 6 | class: Gaming\Common\Bus\Integration\SymfonyValidatorBus 7 | decorates: identity.query-bus 8 | arguments: ['@.inner', '@validator'] 9 | -------------------------------------------------------------------------------- /config/identity/services/subscriber.yml: -------------------------------------------------------------------------------- 1 | services: 2 | identity.publish-stored-events-subscriber: 3 | class: Gaming\Identity\Port\Adapter\Messaging\PublishDomainEventsToMessageBrokerSubscriber 4 | arguments: ['@gaming.message-broker.gaming-exchange-publisher'] 5 | tags: [{ name: 'identity.stored-event-subscriber', key: 'publish-to-message-broker' }] 6 | -------------------------------------------------------------------------------- /config/identity/services/user.yml: -------------------------------------------------------------------------------- 1 | services: 2 | identity.user-repository: 3 | class: Gaming\Identity\Port\Adapter\Persistence\Repository\DoctrineUserRepository 4 | arguments: ['@identity.doctrine-orm'] 5 | 6 | Gaming\Identity\Application\User\UserService: 7 | arguments: ['@identity.user-repository'] 8 | tags: 9 | - { name: 'gaming_platform_bus.handler', bus: 'identity_query', match: '/\\Query\\/' } 10 | - { name: 'gaming_platform_bus.handler', bus: 'identity_command', match: '/\\Command\\/' } 11 | -------------------------------------------------------------------------------- /config/importmap.php: -------------------------------------------------------------------------------- 1 | files() 15 | ->in($projectDirectory . '/config/*') 16 | ->name('importmap.php'); 17 | 18 | $map = [ 19 | 'app' => ['path' => 'js/app.js', 'entrypoint' => true], 20 | 'notification-list' => ['path' => 'js/Common/NotificationList.js'], 21 | 'event-source-status' => ['path' => 'js/Common/EventSourceStatus.js'], 22 | 'confirmation-button' => ['path' => 'js/Common/ConfirmationButton.js'], 23 | 'uhtml/node.js' => ['version' => '4.7.0'], 24 | '@tabler/core/dist/css/tabler.min.css' => ['version' => '1.1.1', 'type' => 'css'] 25 | ]; 26 | 27 | foreach ($finder as $file) { 28 | $map = array_merge($map, require $file->getRealPath()); 29 | } 30 | 31 | return $map; 32 | } 33 | -------------------------------------------------------------------------------- /config/preload.php: -------------------------------------------------------------------------------- 1 | files() 16 | ->in($projectDirectory . '/src') 17 | ->name('*.php'); 18 | 19 | foreach ($finder as $file) { 20 | require_once $file->getRealPath(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /config/routing.yml: -------------------------------------------------------------------------------- 1 | Contexts: { resource: '*/routing.yml' } 2 | -------------------------------------------------------------------------------- /config/routing_dev.yml: -------------------------------------------------------------------------------- 1 | _wdt: 2 | resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' 3 | prefix: /_wdt 4 | 5 | _profiler: 6 | resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml' 7 | prefix: /_profiler 8 | 9 | _main: 10 | resource: routing.yml 11 | -------------------------------------------------------------------------------- /config/web-interface/services/persistence.yml: -------------------------------------------------------------------------------- 1 | services: 2 | web-interface.predis: 3 | class: Predis\Client 4 | arguments: ['%env(APP_WEB_INTERFACE_PREDIS_CLIENT_URL)%', { prefix: 'web-interface:' }] 5 | 6 | web-interface.session-handler: 7 | class: Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler 8 | arguments: ['@web-interface.predis', { prefix: 'session:', ttl: 86400 }] 9 | -------------------------------------------------------------------------------- /config/web-interface/services/security.yml: -------------------------------------------------------------------------------- 1 | services: 2 | web-interface.security: 3 | class: Gaming\WebInterface\Infrastructure\Security\Security 4 | arguments: ['@security.helper', '@identity.command-bus'] 5 | 6 | web-interface.security.user_provider: 7 | class: Gaming\WebInterface\Infrastructure\Security\UserProvider 8 | arguments: ['@identity.query-bus', '@clock'] 9 | -------------------------------------------------------------------------------- /deploy/load-test/scenario/go-to-page.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http'; 2 | 3 | const baseUrl = __ENV.BASE_URL; 4 | const pageUrl = __ENV.PAGE_URL; 5 | const headers = {'Origin': baseUrl}; 6 | const cookieJar = new http.CookieJar(); 7 | 8 | export default function () { 9 | for (let i = 0; i < 100; i++) { 10 | http.get(baseUrl + (pageUrl || '/'), {}, {cookieJar, headers}); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /deploy/load-test/stack/chat/mysql.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | chat-mysql: 5 | image: ghcr.io/gaming-platform/docker-mysql:8.2 6 | volumes: 7 | - chat-mysql:/var/lib/mysql 8 | deploy: 9 | placement: 10 | constraints: 11 | - "node.labels.chat-mysql==1" 12 | chat-mysql-exporter: 13 | image: prom/mysqld-exporter:v0.15.0 14 | command: --mysqld.username=root --mysqld.address chat-mysql:3306 15 | environment: 16 | MYSQLD_EXPORTER_PASSWORD: password 17 | deploy: 18 | placement: 19 | constraints: 20 | - "node.labels.chat-mysql==1" 21 | labels: 22 | - "prometheus-job=mysql" 23 | - "prometheus-port=9104" 24 | 25 | volumes: 26 | chat-mysql: 27 | -------------------------------------------------------------------------------- /deploy/load-test/stack/chat/redis.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | chat-redis: 5 | image: ghcr.io/gaming-platform/docker-redis:7.2 6 | volumes: 7 | - chat-redis:/data 8 | deploy: 9 | placement: 10 | constraints: 11 | - "node.labels.chat-redis==1" 12 | chat-redis-exporter: 13 | image: oliver006/redis_exporter:v1.51.0 14 | command: 15 | - '-redis.addr=chat-redis:6379' 16 | deploy: 17 | placement: 18 | constraints: 19 | - "node.labels.chat-redis==1" 20 | labels: 21 | - "prometheus-job=redis" 22 | - "prometheus-port=9121" 23 | 24 | volumes: 25 | chat-redis: 26 | -------------------------------------------------------------------------------- /deploy/load-test/stack/connect-four/mysql-1.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | connect-four-mysql-1: 5 | image: ghcr.io/gaming-platform/docker-mysql:8.2 6 | volumes: 7 | - connect-four-mysql-1:/var/lib/mysql 8 | deploy: 9 | placement: 10 | constraints: 11 | - "node.labels.connect-four-mysql-1==1" 12 | connect-four-mysql-1-exporter: 13 | image: prom/mysqld-exporter:v0.15.0 14 | command: --mysqld.username=root --mysqld.address connect-four-mysql-1:3306 15 | environment: 16 | MYSQLD_EXPORTER_PASSWORD: password 17 | deploy: 18 | placement: 19 | constraints: 20 | - "node.labels.connect-four-mysql-1==1" 21 | labels: 22 | - "prometheus-job=mysql" 23 | - "prometheus-port=9104" 24 | 25 | volumes: 26 | connect-four-mysql-1: 27 | -------------------------------------------------------------------------------- /deploy/load-test/stack/connect-four/mysql-2.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | connect-four-mysql-2: 5 | image: ghcr.io/gaming-platform/docker-mysql:8.2 6 | volumes: 7 | - connect-four-mysql-2:/var/lib/mysql 8 | deploy: 9 | placement: 10 | constraints: 11 | - "node.labels.connect-four-mysql-2==1" 12 | connect-four-mysql-2-exporter: 13 | image: prom/mysqld-exporter:v0.15.0 14 | command: --mysqld.username=root --mysqld.address connect-four-mysql-2:3306 15 | environment: 16 | MYSQLD_EXPORTER_PASSWORD: password 17 | deploy: 18 | placement: 19 | constraints: 20 | - "node.labels.connect-four-mysql-2==1" 21 | labels: 22 | - "prometheus-job=mysql" 23 | - "prometheus-port=9104" 24 | 25 | volumes: 26 | connect-four-mysql-2: 27 | -------------------------------------------------------------------------------- /deploy/load-test/stack/connect-four/mysql-3.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | connect-four-mysql-3: 5 | image: ghcr.io/gaming-platform/docker-mysql:8.2 6 | volumes: 7 | - connect-four-mysql-3:/var/lib/mysql 8 | deploy: 9 | placement: 10 | constraints: 11 | - "node.labels.connect-four-mysql-3==1" 12 | connect-four-mysql-3-exporter: 13 | image: prom/mysqld-exporter:v0.15.0 14 | command: --mysqld.username=root --mysqld.address connect-four-mysql-3:3306 15 | environment: 16 | MYSQLD_EXPORTER_PASSWORD: password 17 | deploy: 18 | placement: 19 | constraints: 20 | - "node.labels.connect-four-mysql-3==1" 21 | labels: 22 | - "prometheus-job=mysql" 23 | - "prometheus-port=9104" 24 | 25 | volumes: 26 | connect-four-mysql-3: 27 | -------------------------------------------------------------------------------- /deploy/load-test/stack/connect-four/mysql-4.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | connect-four-mysql-4: 5 | image: ghcr.io/gaming-platform/docker-mysql:8.2 6 | volumes: 7 | - connect-four-mysql-4:/var/lib/mysql 8 | deploy: 9 | placement: 10 | constraints: 11 | - "node.labels.connect-four-mysql-4==1" 12 | connect-four-mysql-4-exporter: 13 | image: prom/mysqld-exporter:v0.15.0 14 | command: --mysqld.username=root --mysqld.address connect-four-mysql-4:3306 15 | environment: 16 | MYSQLD_EXPORTER_PASSWORD: password 17 | deploy: 18 | placement: 19 | constraints: 20 | - "node.labels.connect-four-mysql-4==1" 21 | labels: 22 | - "prometheus-job=mysql" 23 | - "prometheus-port=9104" 24 | 25 | volumes: 26 | connect-four-mysql-4: 27 | -------------------------------------------------------------------------------- /deploy/load-test/stack/connect-four/mysql-5.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | connect-four-mysql-5: 5 | image: ghcr.io/gaming-platform/docker-mysql:8.2 6 | volumes: 7 | - connect-four-mysql-5:/var/lib/mysql 8 | deploy: 9 | placement: 10 | constraints: 11 | - "node.labels.connect-four-mysql-5==1" 12 | connect-four-mysql-5-exporter: 13 | image: prom/mysqld-exporter:v0.15.0 14 | command: --mysqld.username=root --mysqld.address connect-four-mysql-5:3306 15 | environment: 16 | MYSQLD_EXPORTER_PASSWORD: password 17 | deploy: 18 | placement: 19 | constraints: 20 | - "node.labels.connect-four-mysql-5==1" 21 | labels: 22 | - "prometheus-job=mysql" 23 | - "prometheus-port=9104" 24 | 25 | volumes: 26 | connect-four-mysql-5: 27 | -------------------------------------------------------------------------------- /deploy/load-test/stack/connect-four/redis.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | connect-four-redis: 5 | image: ghcr.io/gaming-platform/docker-redis:7.2 6 | volumes: 7 | - connect-four-redis:/data 8 | deploy: 9 | placement: 10 | constraints: 11 | - "node.labels.connect-four-redis==1" 12 | connect-four-redis-exporter: 13 | image: oliver006/redis_exporter:v1.51.0 14 | command: 15 | - '-redis.addr=connect-four-redis:6379' 16 | deploy: 17 | placement: 18 | constraints: 19 | - "node.labels.connect-four-redis==1" 20 | labels: 21 | - "prometheus-job=redis" 22 | - "prometheus-port=9121" 23 | 24 | volumes: 25 | connect-four-redis: 26 | -------------------------------------------------------------------------------- /deploy/load-test/stack/identity/app.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | identity-follow-event-store: 5 | image: marein/php-gaming-website:php-fpm 6 | command: bin/console identity:follow-event-store pointer all 7 | env_file: ../app.env 8 | volumes: 9 | - proxysql.sock:/var/run/proxysql 10 | deploy: 11 | placement: 12 | constraints: 13 | - "node.labels.long-running==1" 14 | 15 | volumes: 16 | proxysql.sock: 17 | -------------------------------------------------------------------------------- /deploy/load-test/stack/identity/mysql.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | identity-mysql: 5 | image: ghcr.io/gaming-platform/docker-mysql:8.2 6 | volumes: 7 | - identity-mysql:/var/lib/mysql 8 | deploy: 9 | placement: 10 | constraints: 11 | - "node.labels.identity-mysql==1" 12 | identity-mysql-exporter: 13 | image: prom/mysqld-exporter:v0.15.0 14 | command: --mysqld.username=root --mysqld.address identity-mysql:3306 15 | environment: 16 | MYSQLD_EXPORTER_PASSWORD: password 17 | deploy: 18 | placement: 19 | constraints: 20 | - "node.labels.identity-mysql==1" 21 | labels: 22 | - "prometheus-job=mysql" 23 | - "prometheus-port=9104" 24 | 25 | volumes: 26 | identity-mysql: 27 | -------------------------------------------------------------------------------- /deploy/load-test/stack/observability.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | grafana: 5 | image: ghcr.io/gaming-platform/docker-grafana:10.0 6 | ports: 7 | - "8083:3000" 8 | deploy: 9 | placement: 10 | constraints: 11 | - "node.labels.grafana==1" 12 | prometheus: 13 | image: ghcr.io/gaming-platform/docker-prometheus:2.45 14 | command: --config.file=/etc/prometheus/dockerswarm.yml 15 | volumes: 16 | - prometheus:/prometheus 17 | - /var/run/docker.sock:/var/run/docker.sock 18 | deploy: 19 | placement: 20 | constraints: 21 | - "node.labels.prometheus==1" 22 | node-exporter: 23 | image: prom/node-exporter:v1.6.0 24 | deploy: 25 | mode: global 26 | labels: 27 | - "prometheus-job=node" 28 | - "prometheus-port=9100" 29 | 30 | volumes: 31 | prometheus: 32 | -------------------------------------------------------------------------------- /deploy/load-test/stack/rabbitmq.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | rabbitmq: 5 | image: ghcr.io/gaming-platform/docker-rabbitmq:3.12 6 | hostname: rabbitmq 7 | volumes: 8 | - rabbitmq:/var/lib/rabbitmq/mnesia 9 | deploy: 10 | placement: 11 | constraints: 12 | - "node.labels.rabbitmq==1" 13 | labels: 14 | - "prometheus-job=rabbitmq" 15 | - "prometheus-port=15692" 16 | - "prometheus-path=/metrics/per-object" 17 | 18 | volumes: 19 | rabbitmq: 20 | -------------------------------------------------------------------------------- /deploy/load-test/stack/traefik.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | traefik: 5 | image: traefik:2.10 6 | command: 7 | - --metrics.prometheus=true 8 | - --providers.docker 9 | - --providers.docker.swarmmode=true 10 | - --providers.docker.exposedbydefault=false 11 | ports: 12 | - "80:80" 13 | volumes: 14 | - /var/run/docker.sock:/var/run/docker.sock 15 | deploy: 16 | placement: 17 | constraints: 18 | - "node.labels.traefik==1" 19 | labels: 20 | - "prometheus-job=traefik" 21 | - "prometheus-port=8080" 22 | -------------------------------------------------------------------------------- /deploy/load-test/stack/web-interface/app.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | web-interface-http: 5 | image: marein/php-gaming-website:php-fpm 6 | env_file: ../app.env 7 | volumes: 8 | - proxysql.sock:/var/run/proxysql 9 | deploy: 10 | mode: global 11 | placement: 12 | constraints: 13 | - "node.labels.web-interface-http==1" 14 | labels: 15 | - "traefik.enable=true" 16 | - "traefik.http.routers.web-interface-http.priority=10" 17 | - "traefik.http.routers.web-interface-http.rule=PathPrefix(`/`)" 18 | - "traefik.http.services.web-interface-http.loadbalancer.server.port=80" 19 | 20 | volumes: 21 | proxysql.sock: 22 | -------------------------------------------------------------------------------- /deploy/load-test/stack/web-interface/nchan.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | nchan: 5 | image: ghcr.io/gaming-platform/docker-nchan:1.3 6 | deploy: 7 | placement: 8 | constraints: 9 | - "node.labels.nchan==1" 10 | labels: 11 | - "traefik.enable=true" 12 | - "traefik.http.routers.nchan.priority=20" 13 | - "traefik.http.routers.nchan.rule=PathPrefix(`/sse`)" 14 | - "traefik.http.routers.nchan.middlewares=nchan-stripprefix" 15 | - "traefik.http.middlewares.nchan-stripprefix.stripprefix.prefixes=/sse" 16 | - "traefik.http.services.nchan.loadbalancer.server.port=80" 17 | - "prometheus-job=nchan" 18 | - "prometheus-port=81" 19 | -------------------------------------------------------------------------------- /deploy/load-test/stack/web-interface/redis.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | web-interface-redis: 5 | image: ghcr.io/gaming-platform/docker-redis:7.2 6 | volumes: 7 | - web-interface-redis:/data 8 | deploy: 9 | placement: 10 | constraints: 11 | - "node.labels.web-interface-redis==1" 12 | web-interface-redis-exporter: 13 | image: oliver006/redis_exporter:v1.51.0 14 | command: 15 | - '-redis.addr=web-interface-redis:6379' 16 | deploy: 17 | placement: 18 | constraints: 19 | - "node.labels.web-interface-redis==1" 20 | labels: 21 | - "prometheus-job=redis" 22 | - "prometheus-port=9121" 23 | 24 | volumes: 25 | web-interface-redis: 26 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG environment=development 2 | 3 | ############################## 4 | # Build dependencies # 5 | ############################## 6 | FROM ghcr.io/gaming-platform/docker-php-fpm:8.4-development as builder 7 | 8 | ARG environment=development 9 | 10 | WORKDIR /project 11 | 12 | COPY /docker/composer-install.sh /docker/composer-install-after-code-copy.sh / 13 | COPY /composer.json /composer.lock /project/ 14 | RUN /composer-install.sh 15 | 16 | COPY / /project 17 | RUN /composer-install-after-code-copy.sh 18 | 19 | ############################## 20 | # Build php-fpm # 21 | ############################## 22 | FROM ghcr.io/gaming-platform/docker-php-fpm:8.4-$environment 23 | 24 | ARG environment=development 25 | 26 | WORKDIR /project 27 | 28 | COPY /docker/entrypoint.sh /docker/warmup.sh / 29 | 30 | COPY --from=builder /project /project 31 | 32 | RUN /warmup.sh 33 | 34 | COPY /docker/${environment}.ini /etc/php/8.4/fpm/conf.d/ 35 | 36 | ENTRYPOINT ["/entrypoint.sh"] 37 | CMD ["php-http"] 38 | -------------------------------------------------------------------------------- /docker/composer-install-after-code-copy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ "$environment" = "production" ] 4 | then 5 | composer install --no-dev --optimize-autoloader --classmap-authoritative 6 | fi 7 | -------------------------------------------------------------------------------- /docker/composer-install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ "$environment" = "development" ] 4 | then 5 | composer install 6 | else 7 | # Call install without --optimize-autoloader --classmap-authoritative. 8 | # Those options will be enabled after the whole code is copied. 9 | # Look at composer-install-after-code-copy.sh. 10 | composer install --no-dev 11 | fi 12 | -------------------------------------------------------------------------------- /docker/development.ini: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marein/php-gaming-website/5e4fdb3962958142bc80e082bd3f8cd15c344b01/docker/development.ini -------------------------------------------------------------------------------- /docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | if [ "${APP_WAIT_FOR}" != "" ] 6 | then 7 | wait-for-tcp-server "${APP_WAIT_FOR}" 120 8 | fi 9 | 10 | find bin/*/onEntrypoint -print0 | xargs -0 -n 1 bash 11 | 12 | exec "$@" 13 | -------------------------------------------------------------------------------- /docker/production.ini: -------------------------------------------------------------------------------- 1 | opcache.preload = /project/config/preload.php 2 | opcache.preload_user = www-data 3 | -------------------------------------------------------------------------------- /docker/warmup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # Although environments variables are fake during docker build, 6 | # make them available for the warmup commands. 7 | set -a && source /project/.env && set +a 8 | 9 | if [ "$environment" = "development" ] 10 | then 11 | bin/console cache:warmup 12 | bin/console importmap:install 13 | else 14 | bin/console cache:warmup --env=prod 15 | bin/console importmap:install --env=prod 16 | bin/console asset-map:compile --env=prod 17 | fi 18 | 19 | chown -R www-data:www-data var 20 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | src/ 8 | tests/unit 9 | 10 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | paths: 3 | - src 4 | level: 8 5 | ignoreErrors: 6 | # Ignoring non-empty-string because there are currently many false positives. 7 | - message: '#(should return|expects) non-empty-string( but returns string|, string given)\.$#' 8 | - message: '#^Call to an undefined method Symfony[^:]+::children\(\)\.$#' 9 | - message: '#^While loop condition is always true\.$#' 10 | - message: "#^Property Gaming\\\\Identity\\\\Domain\\\\Model\\\\User\\\\User\\:\\:\\$version#" 11 | - message: "#^Property Gaming\\\\Memory\\\\Domain\\\\Model\\\\Game\\\\Game\\:\\:\\$cards is never read, only written\\.$#" 12 | - message: '#^Cannot access offset [0-9]+ .*Predis.*Pipeline\.$#' 13 | - message: '#expects array.*Predis.*Pipeline given\.$#' 14 | -------------------------------------------------------------------------------- /public/assets/.gitignore: -------------------------------------------------------------------------------- 1 | /* 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /public/icon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marein/php-gaming-website/5e4fdb3962958142bc80e082bd3f8cd15c344b01/public/icon/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/icon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marein/php-gaming-website/5e4fdb3962958142bc80e082bd3f8cd15c344b01/public/icon/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/icon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marein/php-gaming-website/5e4fdb3962958142bc80e082bd3f8cd15c344b01/public/icon/apple-touch-icon.png -------------------------------------------------------------------------------- /public/icon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marein/php-gaming-website/5e4fdb3962958142bc80e082bd3f8cd15c344b01/public/icon/favicon-16x16.png -------------------------------------------------------------------------------- /public/icon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marein/php-gaming-website/5e4fdb3962958142bc80e082bd3f8cd15c344b01/public/icon/favicon-32x32.png -------------------------------------------------------------------------------- /public/icon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marein/php-gaming-website/5e4fdb3962958142bc80e082bd3f8cd15c344b01/public/icon/favicon.ico -------------------------------------------------------------------------------- /public/icon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Gaming Platform", 3 | "short_name": "Gaming Platform", 4 | "icons": [ 5 | { 6 | "src": "icon/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "icon/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class InitiateChatCommand implements Request 13 | { 14 | /** 15 | * @param string[] $authors 16 | */ 17 | public function __construct( 18 | private readonly string $idempotencyKey, 19 | private readonly array $authors 20 | ) { 21 | } 22 | 23 | public function idempotencyKey(): string 24 | { 25 | return $this->idempotencyKey; 26 | } 27 | 28 | /** 29 | * @return string[] 30 | */ 31 | public function authors(): array 32 | { 33 | return $this->authors; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Chat/Application/Command/WriteMessageCommand.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class WriteMessageCommand implements Request 13 | { 14 | public function __construct( 15 | private readonly string $chatId, 16 | private readonly string $authorId, 17 | private readonly string $message 18 | ) { 19 | } 20 | 21 | public function chatId(): string 22 | { 23 | return $this->chatId; 24 | } 25 | 26 | public function authorId(): string 27 | { 28 | return $this->authorId; 29 | } 30 | 31 | public function message(): string 32 | { 33 | return $this->message; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Chat/Application/Event/ChatInitiated.php: -------------------------------------------------------------------------------- 1 | chatId = $chatId->toString(); 17 | } 18 | 19 | public function aggregateId(): string 20 | { 21 | return $this->chatId; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Chat/Application/Exception/AuthorNotAllowedException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class MessagesQuery implements Request 13 | { 14 | public function __construct( 15 | private readonly string $chatId, 16 | private readonly string $authorId, 17 | private readonly int $offset, 18 | private readonly int $limit 19 | ) { 20 | } 21 | 22 | public function chatId(): string 23 | { 24 | return $this->chatId; 25 | } 26 | 27 | public function authorId(): string 28 | { 29 | return $this->authorId; 30 | } 31 | 32 | public function offset(): int 33 | { 34 | return $this->offset; 35 | } 36 | 37 | public function limit(): int 38 | { 39 | return $this->limit; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Chat/Infrastructure/Jms/Common.Domain.DomainEvent.yml: -------------------------------------------------------------------------------- 1 | Gaming\Common\Domain\DomainEvent: 2 | discriminator: 3 | field_name: '#' 4 | map: 5 | ChatInitiated: Gaming\Chat\Application\Event\ChatInitiated 6 | MessageWritten: Gaming\Chat\Application\Event\MessageWritten 7 | -------------------------------------------------------------------------------- /src/Chat/Infrastructure/Migration/Version20170608203652.php: -------------------------------------------------------------------------------- 1 | createTable('chat'); 15 | 16 | $table->addColumn('id', 'uuid'); 17 | $table->addColumn('authors', 'json'); 18 | 19 | $table->setPrimaryKey(['id']); 20 | } 21 | 22 | public function down(Schema $schema): void 23 | { 24 | $schema->dropTable('chat'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Chat/Infrastructure/Migration/Version20170608203825.php: -------------------------------------------------------------------------------- 1 | createTable('message'); 15 | 16 | $table->addColumn('id', 'integer', ['autoincrement' => true]); 17 | $table->addColumn('chatId', 'uuid'); 18 | $table->addColumn('authorId', 'string', ['length' => 36, 'fixed' => true]); 19 | $table->addColumn('message', 'string', ['length' => 140]); 20 | $table->addColumn('writtenAt', 'datetime_immutable'); 21 | 22 | $table->setPrimaryKey(['id']); 23 | $table->addForeignKeyConstraint( 24 | 'chat', 25 | ['chatId'], 26 | ['id'] 27 | ); 28 | } 29 | 30 | public function down(Schema $schema): void 31 | { 32 | $schema->dropTable('message'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Chat/Infrastructure/Migration/Version20170609213426.php: -------------------------------------------------------------------------------- 1 | nchan->channel('/pub?id=' . implode(',', $channels))->publish( 21 | new PlainTextMessage( 22 | '', 23 | $name . ':' . $message 24 | ) 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Common/Bus/Bus.php: -------------------------------------------------------------------------------- 1 | $request 17 | * 18 | * @return TResponse 19 | * @throws ApplicationException 20 | * @throws BusException 21 | * @throws Exception Any application based exception 22 | */ 23 | public function handle(Request $request): mixed; 24 | } 25 | -------------------------------------------------------------------------------- /src/Common/Bus/Exception/BusException.php: -------------------------------------------------------------------------------- 1 | connection->transactional( 22 | fn() => $this->bus->handle($request) 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Common/Bus/PsrCallableRoutingBus.php: -------------------------------------------------------------------------------- 1 | container->has($request::class) 20 | ? $this->container->get($request::class) 21 | : throw BusException::missingHandler($request::class); 22 | 23 | return is_callable($handler) 24 | ? $handler($request) 25 | : throw BusException::missingHandler($request::class); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Common/Bus/PsrCompiledRoutingBus.php: -------------------------------------------------------------------------------- 1 | $routes 18 | */ 19 | public function __construct( 20 | private readonly ContainerInterface $container, 21 | private readonly array $routes 22 | ) { 23 | } 24 | 25 | public function handle(Request $request): mixed 26 | { 27 | $route = $this->routes[$request::class] ?? throw BusException::missingHandler($request::class); 28 | 29 | return $this->container->get($route['handlerId'])->{$route['method']}($request); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Common/Bus/Request.php: -------------------------------------------------------------------------------- 1 | propertyPath; 22 | } 23 | 24 | public function identifier(): string 25 | { 26 | return $this->identifier; 27 | } 28 | 29 | /** 30 | * @return ViolationParameter[] 31 | */ 32 | public function parameters(): array 33 | { 34 | return $this->parameters; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Common/Bus/ViolationParameter.php: -------------------------------------------------------------------------------- 1 | name; 18 | } 19 | 20 | public function value(): bool|int|float|string 21 | { 22 | return $this->value; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Common/DoctrineHeartbeatMiddleware/SchedulePeriodicHeartbeatMiddleware.php: -------------------------------------------------------------------------------- 1 | heartbeat <= 0) { 28 | return $driver; 29 | } 30 | 31 | return new ScheduleHeartbeatOnConnectDriver($driver, $this->scheduler, $this->heartbeat, $this->clock); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Common/Domain/AggregateRoot.php: -------------------------------------------------------------------------------- 1 | domainEvents, 0); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Common/EventStore/CleanableEventStore.php: -------------------------------------------------------------------------------- 1 | subscribers as $subscriber) { 20 | $subscriber->handle($domainEvent); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Common/EventStore/CompositeStoredEventSubscriber.php: -------------------------------------------------------------------------------- 1 | subscribers as $subscriber) { 20 | $subscriber->handle($domainEvent); 21 | } 22 | } 23 | 24 | public function commit(): void 25 | { 26 | foreach ($this->subscribers as $subscriber) { 27 | $subscriber->commit(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Common/EventStore/ContentSerializer.php: -------------------------------------------------------------------------------- 1 | $headers 11 | */ 12 | public function __construct( 13 | public readonly string $streamId, 14 | public readonly object $content, 15 | public readonly int $streamVersion = 0, 16 | public readonly array $headers = [] 17 | ) { 18 | } 19 | 20 | public function withHeader(string $name, string $value): self 21 | { 22 | return new self( 23 | $this->streamId, 24 | $this->content, 25 | $this->streamVersion, 26 | array_merge($this->headers, [$name => $value]) 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Common/EventStore/DomainEventSubscriber.php: -------------------------------------------------------------------------------- 1 | streamVersion; 22 | } 23 | 24 | public function append(object ...$contents): self 25 | { 26 | foreach ($contents as $content) { 27 | $this->domainEvents[] = new DomainEvent($this->streamId, $content, ++$this->streamVersion); 28 | } 29 | 30 | return $this; 31 | } 32 | 33 | /** 34 | * @return DomainEvent[] 35 | */ 36 | public function flush(): array 37 | { 38 | return array_splice($this->domainEvents, 0); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Common/EventStore/Event/EventsCommitted.php: -------------------------------------------------------------------------------- 1 | eventStorePointer->trackMostRecentPublishedStoredEventId($id); 19 | 20 | $this->cachedId = $id; 21 | } 22 | 23 | public function retrieveMostRecentPublishedStoredEventId(): int 24 | { 25 | return $this->cachedId ??= $this->eventStorePointer->retrieveMostRecentPublishedStoredEventId(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Common/EventStore/InMemoryEventStore.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | private array $domainEvents = []; 13 | 14 | public function byStreamId(string $streamId, int $fromStreamVersion = 0): array 15 | { 16 | return array_values( 17 | array_filter( 18 | $this->domainEvents[$streamId] ?? [], 19 | static fn(DomainEvent $domainEvent): bool => $domainEvent->streamVersion >= $fromStreamVersion 20 | ) 21 | ); 22 | } 23 | 24 | public function append(DomainEvent ...$domainEvents): void 25 | { 26 | foreach ($domainEvents as $domainEvent) { 27 | $this->domainEvents[$domainEvent->streamId][] = $domainEvent; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Common/EventStore/Integration/Doctrine/DoctrineEventStorePointerSchema.php: -------------------------------------------------------------------------------- 1 | createTable($tableName); 15 | 16 | $table->addColumn('name', Types::STRING, ['length' => 64]); 17 | $table->addColumn('value', Types::BIGINT, ['unsigned' => true]); 18 | 19 | $table->setPrimaryKey(['name']); 20 | } 21 | 22 | public static function down(Schema $schema, string $tableName): void 23 | { 24 | $schema->dropTable($tableName); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Common/EventStore/Integration/Doctrine/DoctrineMysqlEventStorePointerFactory.php: -------------------------------------------------------------------------------- 1 | connection, 23 | $this->tableName, 24 | $name 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Common/EventStore/Integration/Doctrine/IndexOption.php: -------------------------------------------------------------------------------- 1 | followEventStoreDispatcher->stop(...)); 22 | pcntl_signal(SIGTERM, $this->followEventStoreDispatcher->stop(...)); 23 | 24 | $this->followEventStoreDispatcher->start(); 25 | 26 | return 0; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Common/EventStore/Integration/ForkPool/Worker.php: -------------------------------------------------------------------------------- 1 | receive()) { 21 | match ($message) { 22 | 'COMMIT' => $this->handleCommit($channel), 23 | default => $this->storedEventSubscriber->handle($message) 24 | }; 25 | } 26 | 27 | return 0; 28 | } 29 | 30 | private function handleCommit(Channel $channel): void 31 | { 32 | $this->storedEventSubscriber->commit(); 33 | 34 | $channel->send('ACK'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Common/EventStore/Integration/Predis/PredisEventStorePointerFactory.php: -------------------------------------------------------------------------------- 1 | predis, 22 | $name 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Common/EventStore/Integration/Symfony/ResetServicesListener.php: -------------------------------------------------------------------------------- 1 | numberOfHandledCommits = 0; 19 | } 20 | 21 | public function eventsCommitted(EventsCommitted $event): void 22 | { 23 | if (++$this->numberOfHandledCommits % $this->everyNthCommit === 0) { 24 | $this->resettable->reset(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Common/EventStore/NoCommit.php: -------------------------------------------------------------------------------- 1 | id; 18 | } 19 | 20 | public function domainEvent(): DomainEvent 21 | { 22 | return $this->domainEvent; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Common/EventStore/StoredEventSubscriber.php: -------------------------------------------------------------------------------- 1 | parent; 18 | } 19 | 20 | public function fork(): Channel 21 | { 22 | return $this->fork; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Common/ForkPool/Channel/ChannelPairFactory.php: -------------------------------------------------------------------------------- 1 | createNullChannel(), 15 | $this->createNullChannel() 16 | ); 17 | } 18 | 19 | private function createNullChannel(): Channel 20 | { 21 | return new class implements Channel 22 | { 23 | public function send(mixed $message): void 24 | { 25 | throw new ForkPoolException('Cannot send to null channel.'); 26 | } 27 | 28 | public function receive(): mixed 29 | { 30 | throw new ForkPoolException('Cannot receive from null channel.'); 31 | } 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Common/ForkPool/Channel/StreamChannelPairFactory.php: -------------------------------------------------------------------------------- 1 | processId, $signal); 20 | } 21 | 22 | public function channel(): Channel 23 | { 24 | return $this->channel; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Common/ForkPool/Processes.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | private array $processes; 15 | 16 | public function __construct() 17 | { 18 | $this->processes = []; 19 | } 20 | 21 | public function add(int $processId, Channel $channel): Process 22 | { 23 | $process = new Process($processId, $channel); 24 | 25 | $this->processes[$processId] = $process; 26 | 27 | return $process; 28 | } 29 | 30 | public function remove(int $processId): void 31 | { 32 | if (array_key_exists($processId, $this->processes)) { 33 | unset($this->processes[$processId]); 34 | } 35 | } 36 | 37 | public function kill(int $signal): void 38 | { 39 | foreach ($this->processes as $process) { 40 | $process->kill($signal); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Common/ForkPool/SessionLeaderTask.php: -------------------------------------------------------------------------------- 1 | task->execute($channel); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Common/ForkPool/Task.php: -------------------------------------------------------------------------------- 1 | waitForNextProcess()) !== -1) { 18 | $this->processes->remove($processId); 19 | } 20 | 21 | return $this->forkPool; 22 | } 23 | 24 | public function any(): ForkPool 25 | { 26 | $this->processes->remove($this->waitForNextProcess()); 27 | 28 | return $this->forkPool; 29 | } 30 | 31 | public function killAllWhenAnyExits(int $signal): void 32 | { 33 | $this->any()->kill($signal)->wait()->all(); 34 | } 35 | 36 | private function waitForNextProcess(): int 37 | { 38 | return pcntl_wait($status); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Common/IdempotentStorage/IdempotentStorage.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | final class InMemoryIdempotentStorage implements IdempotentStorage 12 | { 13 | /** 14 | * @var T[] 15 | */ 16 | private array $storage = []; 17 | 18 | public function add(string $idempotencyKey, mixed $value): mixed 19 | { 20 | return $this->storage[$idempotencyKey] ??= $value; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Common/IdempotentStorage/PredisIdempotentStorage.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class PredisIdempotentStorage implements IdempotentStorage 14 | { 15 | public function __construct( 16 | private readonly ClientInterface $predis, 17 | private readonly int $expireTimeInSeconds 18 | ) { 19 | } 20 | 21 | public function add(string $idempotencyKey, mixed $value): mixed 22 | { 23 | $result = $this->predis->setnx($idempotencyKey, serialize($value)); 24 | $this->predis->expire($idempotencyKey, $this->expireTimeInSeconds); 25 | 26 | return $result === 1 ? $value : unserialize( 27 | (string)$this->predis->get($idempotencyKey) 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Common/MessageBroker/Consumer.php: -------------------------------------------------------------------------------- 1 | $metadata 14 | */ 15 | public function __construct( 16 | public readonly Message $message, 17 | public readonly Throwable $throwable, 18 | public readonly array $metadata 19 | ) { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Common/MessageBroker/Event/MessageHandled.php: -------------------------------------------------------------------------------- 1 | $metadata 13 | */ 14 | public function __construct( 15 | public readonly Message $message, 16 | public readonly array $metadata 17 | ) { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Common/MessageBroker/Event/MessageReceived.php: -------------------------------------------------------------------------------- 1 | $metadata 13 | */ 14 | public function __construct( 15 | public readonly Message $message, 16 | public readonly array $metadata 17 | ) { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Common/MessageBroker/Event/MessageReturned.php: -------------------------------------------------------------------------------- 1 | $metadata 13 | */ 14 | public function __construct( 15 | public readonly Message $message, 16 | public readonly string $cause, 17 | public readonly array $metadata 18 | ) { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Common/MessageBroker/Event/MessageSent.php: -------------------------------------------------------------------------------- 1 | $metadata 13 | */ 14 | public function __construct( 15 | public readonly Message $message, 16 | public readonly array $metadata 17 | ) { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Common/MessageBroker/Event/MessagesFlushed.php: -------------------------------------------------------------------------------- 1 | $metadata 11 | */ 12 | public function __construct( 13 | public readonly int $numberOfSentMessages, 14 | public readonly array $metadata 15 | ) { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Common/MessageBroker/Event/ReplySent.php: -------------------------------------------------------------------------------- 1 | $metadata 13 | */ 14 | public function __construct( 15 | public readonly Message $message, 16 | public readonly array $metadata 17 | ) { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Common/MessageBroker/Event/RequestSent.php: -------------------------------------------------------------------------------- 1 | $metadata 13 | */ 14 | public function __construct( 15 | public readonly Message $message, 16 | public readonly array $metadata 17 | ) { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Common/MessageBroker/EventListener/ThrowWhenMessageFailed.php: -------------------------------------------------------------------------------- 1 | logger->log( 22 | $this->logLevel, 23 | 'Message failed.', 24 | [ 25 | 'message' => $event->message->toArray(), 26 | 'throwable' => $event->throwable, 27 | 'metadata' => $event->metadata 28 | ] 29 | ); 30 | 31 | throw $event->throwable; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Common/MessageBroker/EventListener/ThrowWhenMessageReturned.php: -------------------------------------------------------------------------------- 1 | logger->log( 23 | $this->logLevel, 24 | 'Message returned.', 25 | [ 26 | 'message' => $event->message->toArray(), 27 | 'cause' => $event->cause, 28 | 'metadata' => $event->metadata 29 | ] 30 | ); 31 | 32 | throw new MessageBrokerException('Message returned.'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Common/MessageBroker/Exception/MessageBrokerException.php: -------------------------------------------------------------------------------- 1 | getMessage(), $throwable->getCode(), $throwable); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Common/MessageBroker/Integration/AmqpLib/ConnectionFactory/ConnectionFactory.php: -------------------------------------------------------------------------------- 1 | exchange, 20 | $message->name() 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Common/MessageBroker/Integration/AmqpLib/MessageTranslator/MessageTranslator.php: -------------------------------------------------------------------------------- 1 | definesQueues->queueNames() as $queueName) { 20 | $channel->basic_consume( 21 | queue: $queueName, 22 | callback: $callbackFactory->create($queueName) 23 | ); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Common/MessageBroker/Integration/AmqpLib/QueueConsumer/QueueConsumer.php: -------------------------------------------------------------------------------- 1 | $topologies 13 | */ 14 | public function __construct( 15 | private readonly iterable $topologies 16 | ) { 17 | } 18 | 19 | public function declare(AMQPChannel $channel): void 20 | { 21 | foreach ($this->topologies as $topology) { 22 | $topology->declare($channel); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Common/MessageBroker/Integration/AmqpLib/Topology/DefinesQueues.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | public function queueNames(): iterable; 13 | } 14 | -------------------------------------------------------------------------------- /src/Common/MessageBroker/Integration/AmqpLib/Topology/ExchangeTopology.php: -------------------------------------------------------------------------------- 1 | exchange_declare( 20 | $this->exchangeName, 21 | $this->exchangeType, 22 | false, 23 | true, 24 | false 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Common/MessageBroker/Integration/AmqpLib/Topology/Topology.php: -------------------------------------------------------------------------------- 1 | consumer->stop(...)); 26 | pcntl_signal(SIGTERM, $this->consumer->stop(...)); 27 | 28 | $this->consumer->start($this->parallelism); 29 | 30 | return 0; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Common/MessageBroker/Integration/Symfony/ResetServicesListener.php: -------------------------------------------------------------------------------- 1 | numberOfHandledMessages = 0; 19 | } 20 | 21 | public function messageHandled(MessageHandled $event): void 22 | { 23 | if (++$this->numberOfHandledMessages % $this->everyNthMessage === 0) { 24 | $this->resettable->reset(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Common/MessageBroker/MessageHandler.php: -------------------------------------------------------------------------------- 1 | $value 11 | * 12 | * @return array 13 | */ 14 | public function migrate(array $value): array; 15 | } 16 | -------------------------------------------------------------------------------- /src/Common/Normalizer/Normalizer.php: -------------------------------------------------------------------------------- 1 | key] = true; 22 | 23 | return $value; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Common/Normalizer/Test/TestNormalizer.php: -------------------------------------------------------------------------------- 1 | jobs = new Jobs(); 14 | } 15 | 16 | public function schedule(int $invokeAt, Handler $handler): void 17 | { 18 | $this->jobs->add($invokeAt, $handler); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Common/Scheduler/Handler.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class DoctrineMySqlSchemaShards implements Shards 16 | { 17 | /** 18 | * @param Shards $shards 19 | */ 20 | public function __construct( 21 | private readonly Shards $shards, 22 | private readonly Connection $connection 23 | ) { 24 | } 25 | 26 | public function lookup(string $value): mixed 27 | { 28 | try { 29 | $this->connection->executeStatement( 30 | 'USE ' . $this->connection->quoteIdentifier($this->shards->lookup($value)) 31 | ); 32 | } catch (Throwable $t) { 33 | throw new ShardingException($t->getMessage(), $t->getCode(), $t); 34 | } 35 | 36 | return $this->connection; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Common/Sharding/Shards.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class AbortCommand implements Request 13 | { 14 | public function __construct( 15 | private readonly string $gameId, 16 | private readonly string $playerId 17 | ) { 18 | } 19 | 20 | public function gameId(): string 21 | { 22 | return $this->gameId; 23 | } 24 | 25 | public function playerId(): string 26 | { 27 | return $this->playerId; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ConnectFour/Application/Game/Command/AbortHandler.php: -------------------------------------------------------------------------------- 1 | games = $games; 18 | } 19 | 20 | public function __invoke(AbortCommand $command): void 21 | { 22 | $this->games->update( 23 | GameId::fromString($command->gameId()), 24 | static fn(Game $game) => $game->abort($command->playerId()) 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ConnectFour/Application/Game/Command/AssignChatCommand.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class AssignChatCommand implements Request 13 | { 14 | public function __construct( 15 | private readonly string $gameId, 16 | private readonly string $chatId 17 | ) { 18 | } 19 | 20 | public function gameId(): string 21 | { 22 | return $this->gameId; 23 | } 24 | 25 | public function chatId(): string 26 | { 27 | return $this->chatId; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ConnectFour/Application/Game/Command/AssignChatHandler.php: -------------------------------------------------------------------------------- 1 | games = $games; 18 | } 19 | 20 | public function __invoke(AssignChatCommand $command): void 21 | { 22 | $this->games->update( 23 | GameId::fromString($command->gameId()), 24 | static fn(Game $game) => $game->assignChat($command->chatId()) 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ConnectFour/Application/Game/Command/JoinCommand.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class JoinCommand implements Request 13 | { 14 | public function __construct( 15 | private readonly string $gameId, 16 | private readonly string $playerId 17 | ) { 18 | } 19 | 20 | public function gameId(): string 21 | { 22 | return $this->gameId; 23 | } 24 | 25 | public function playerId(): string 26 | { 27 | return $this->playerId; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ConnectFour/Application/Game/Command/JoinHandler.php: -------------------------------------------------------------------------------- 1 | games = $games; 18 | } 19 | 20 | public function __invoke(JoinCommand $command): void 21 | { 22 | $this->games->update( 23 | GameId::fromString($command->gameId()), 24 | static fn(Game $game) => $game->join($command->playerId()) 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ConnectFour/Application/Game/Command/MoveCommand.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class MoveCommand implements Request 13 | { 14 | public function __construct( 15 | private readonly string $gameId, 16 | private readonly string $playerId, 17 | private readonly int $column 18 | ) { 19 | } 20 | 21 | public function gameId(): string 22 | { 23 | return $this->gameId; 24 | } 25 | 26 | public function playerId(): string 27 | { 28 | return $this->playerId; 29 | } 30 | 31 | public function column(): int 32 | { 33 | return $this->column; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/ConnectFour/Application/Game/Command/MoveHandler.php: -------------------------------------------------------------------------------- 1 | games = $games; 18 | } 19 | 20 | public function __invoke(MoveCommand $command): void 21 | { 22 | $this->games->update( 23 | GameId::fromString($command->gameId()), 24 | static fn(Game $game) => $game->move($command->playerId(), $command->column()) 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ConnectFour/Application/Game/Command/OpenCommand.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class OpenCommand implements Request 13 | { 14 | public function __construct( 15 | public readonly string $playerId, 16 | public readonly int $width, 17 | public readonly int $height, 18 | public readonly int $stone 19 | ) { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/ConnectFour/Application/Game/Command/ResignCommand.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class ResignCommand implements Request 13 | { 14 | public function __construct( 15 | private readonly string $gameId, 16 | private readonly string $playerId 17 | ) { 18 | } 19 | 20 | public function gameId(): string 21 | { 22 | return $this->gameId; 23 | } 24 | 25 | public function playerId(): string 26 | { 27 | return $this->playerId; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ConnectFour/Application/Game/Command/ResignHandler.php: -------------------------------------------------------------------------------- 1 | games = $games; 18 | } 19 | 20 | public function __invoke(ResignCommand $command): void 21 | { 22 | $this->games->update( 23 | GameId::fromString($command->gameId()), 24 | static fn(Game $game) => $game->resign($command->playerId()) 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ConnectFour/Application/Game/Query/GameHandler.php: -------------------------------------------------------------------------------- 1 | gameFinder = $gameFinder; 19 | } 20 | 21 | /** 22 | * @throws GameNotFoundException 23 | */ 24 | public function __invoke(GameQuery $query): Game 25 | { 26 | return $this->gameFinder->find(GameId::fromString($query->gameId())); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ConnectFour/Application/Game/Query/GameQuery.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class GameQuery implements Request 14 | { 15 | public function __construct( 16 | private readonly string $gameId 17 | ) { 18 | } 19 | 20 | public function gameId(): string 21 | { 22 | return $this->gameId; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/ConnectFour/Application/Game/Query/GamesByPlayerHandler.php: -------------------------------------------------------------------------------- 1 | gamesByPlayerFinder = $gamesByPlayerFinder; 17 | } 18 | 19 | public function __invoke(GamesByPlayerQuery $query): GamesByPlayer 20 | { 21 | return $this->gamesByPlayerFinder->search( 22 | $query->playerId, 23 | $query->state, 24 | $query->page, 25 | $query->limit 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ConnectFour/Application/Game/Query/GamesByPlayerQuery.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class GamesByPlayerQuery implements Request 15 | { 16 | public function __construct( 17 | public readonly string $playerId, 18 | public readonly State $state, 19 | public readonly int $page, 20 | public readonly int $limit 21 | ) { 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ConnectFour/Application/Game/Query/Model/Game/GameFinder.php: -------------------------------------------------------------------------------- 1 | games = $games; 20 | } 21 | 22 | /** 23 | * @return OpenGame[] 24 | */ 25 | public function games(): array 26 | { 27 | return $this->games; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ConnectFour/Application/Game/Query/Model/RunningGames/RunningGameStore.php: -------------------------------------------------------------------------------- 1 | count = $count; 14 | } 15 | 16 | public function count(): int 17 | { 18 | return $this->count; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ConnectFour/Application/Game/Query/OpenGamesHandler.php: -------------------------------------------------------------------------------- 1 | openGameStore = $openGameStore; 17 | } 18 | 19 | public function __invoke(OpenGamesQuery $query): OpenGames 20 | { 21 | return $this->openGameStore->all(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ConnectFour/Application/Game/Query/OpenGamesQuery.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class OpenGamesQuery implements Request 14 | { 15 | } 16 | -------------------------------------------------------------------------------- /src/ConnectFour/Application/Game/Query/PlayerSearchStatistics/PlayerSearchStatisticsQuery.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class PlayerSearchStatisticsQuery implements Request 13 | { 14 | public function __construct( 15 | public readonly ?string $playerId 16 | ) { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ConnectFour/Application/Game/Query/PlayerSearchStatistics/PlayerSearchStatisticsResponse.php: -------------------------------------------------------------------------------- 1 | $states 11 | */ 12 | public function __construct( 13 | public readonly array $states 14 | ) { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/ConnectFour/Application/Game/Query/RunningGamesHandler.php: -------------------------------------------------------------------------------- 1 | runningGamesFinder = $runningGamesFinder; 17 | } 18 | 19 | public function __invoke(RunningGamesQuery $query): RunningGames 20 | { 21 | return new RunningGames( 22 | $this->runningGamesFinder->count() 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ConnectFour/Application/Game/Query/RunningGamesQuery.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class RunningGamesQuery implements Request 14 | { 15 | } 16 | -------------------------------------------------------------------------------- /src/ConnectFour/Domain/Game/Board/Point.php: -------------------------------------------------------------------------------- 1 | x; 21 | } 22 | 23 | /** 24 | * @deprecated Use the property instead. 25 | */ 26 | public function y(): int 27 | { 28 | return $this->y; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/ConnectFour/Domain/Game/Board/Size.php: -------------------------------------------------------------------------------- 1 | height = $height; 29 | $this->width = $width; 30 | } 31 | 32 | public function width(): int 33 | { 34 | return $this->width; 35 | } 36 | 37 | public function height(): int 38 | { 39 | return $this->height; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/ConnectFour/Domain/Game/Board/Stone.php: -------------------------------------------------------------------------------- 1 | gameId = $gameId->toString(); 19 | $this->chatId = $chatId; 20 | } 21 | 22 | public function aggregateId(): string 23 | { 24 | return $this->gameId; 25 | } 26 | 27 | public function chatId(): string 28 | { 29 | return $this->chatId; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/ConnectFour/Domain/Game/Event/GameAborted.php: -------------------------------------------------------------------------------- 1 | gameId = $gameId->toString(); 20 | } 21 | 22 | public function aggregateId(): string 23 | { 24 | return $this->gameId; 25 | } 26 | 27 | public function abortedPlayerId(): string 28 | { 29 | return $this->abortedPlayerId; 30 | } 31 | 32 | public function opponentPlayerId(): string 33 | { 34 | return $this->opponentPlayerId; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ConnectFour/Domain/Game/Event/GameDrawn.php: -------------------------------------------------------------------------------- 1 | gameId = $gameId->toString(); 22 | } 23 | 24 | public function aggregateId(): string 25 | { 26 | return $this->gameId; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ConnectFour/Domain/Game/Event/PlayerJoined.php: -------------------------------------------------------------------------------- 1 | gameId = $gameId->toString(); 22 | } 23 | 24 | public function aggregateId(): string 25 | { 26 | return $this->gameId; 27 | } 28 | 29 | public function joinedPlayerId(): string 30 | { 31 | return $this->joinedPlayerId; 32 | } 33 | 34 | public function opponentPlayerId(): string 35 | { 36 | return $this->opponentPlayerId; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/ConnectFour/Domain/Game/Exception/ColumnAlreadyFilledException.php: -------------------------------------------------------------------------------- 1 | state = $state; 24 | $this->domainEvents = $domainEvents; 25 | } 26 | 27 | public function state(): State 28 | { 29 | return $this->state; 30 | } 31 | 32 | /** 33 | * @return DomainEvent[] 34 | */ 35 | public function domainEvents(): array 36 | { 37 | return $this->domainEvents; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/ConnectFour/Domain/Game/State/Won.php: -------------------------------------------------------------------------------- 1 | findFieldsInMainDiagonalByPoint($point), 17 | Field::empty(new Point(0, 0)), 18 | ...$board->findFieldsInCounterDiagonalByPoint($point) 19 | ]; 20 | } 21 | 22 | protected function name(): string 23 | { 24 | return 'diagonal'; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ConnectFour/Domain/Game/WinningRule/HorizontalWinningRule.php: -------------------------------------------------------------------------------- 1 | findFieldsByRow($point->y()); 15 | } 16 | 17 | protected function name(): string 18 | { 19 | return 'horizontal'; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/ConnectFour/Domain/Game/WinningRule/VerticalWinningRule.php: -------------------------------------------------------------------------------- 1 | findFieldsByColumn($point->x()); 15 | } 16 | 17 | protected function name(): string 18 | { 19 | return 'vertical'; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/ConnectFour/Domain/Game/WinningRule/WinningRule.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/ConnectFour/Port/Adapter/Http/View/open.html.twig: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/ConnectFour/Port/Adapter/Http/View/player-search-filter.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 5 |
6 |
State
7 |
8 | {% for state, total in playerSearchStatistics.states %} 9 | 11 | {{ state|capitalize }} 12 | {{ total }} 13 | 14 | {% endfor %} 15 |
16 |
17 | -------------------------------------------------------------------------------- /src/ConnectFour/Port/Adapter/Http/View/statistics.html.twig: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | {{ runningGames.count }} 5 | 6 | 7 | running games 8 |

9 | -------------------------------------------------------------------------------- /src/ConnectFour/Port/Adapter/Persistence/Jms/Common.Domain.DomainEvent.yml: -------------------------------------------------------------------------------- 1 | Gaming\Common\Domain\DomainEvent: 2 | discriminator: 3 | field_name: '#' 4 | map: 5 | ChatAssigned: Gaming\ConnectFour\Domain\Game\Event\ChatAssigned 6 | GameAborted: Gaming\ConnectFour\Domain\Game\Event\GameAborted 7 | GameDrawn: Gaming\ConnectFour\Domain\Game\Event\GameDrawn 8 | GameOpened: Gaming\ConnectFour\Domain\Game\Event\GameOpened 9 | GameResigned: Gaming\ConnectFour\Domain\Game\Event\GameResigned 10 | GameWon: Gaming\ConnectFour\Domain\Game\Event\GameWon 11 | PlayerJoined: Gaming\ConnectFour\Domain\Game\Event\PlayerJoined 12 | PlayerMoved: Gaming\ConnectFour\Domain\Game\Event\PlayerMoved 13 | -------------------------------------------------------------------------------- /src/ConnectFour/Port/Adapter/Persistence/Jms/ConnectFour.Domain.Game.Game.yml: -------------------------------------------------------------------------------- 1 | Gaming\ConnectFour\Domain\Game\Game: 2 | properties: 3 | domainEvents: 4 | exclude: true 5 | -------------------------------------------------------------------------------- /src/ConnectFour/Port/Adapter/Persistence/Jms/ConnectFour.Domain.Game.State.State.yml: -------------------------------------------------------------------------------- 1 | Gaming\ConnectFour\Domain\Game\State\State: 2 | discriminator: 3 | field_name: type 4 | map: 5 | aborted: Gaming\ConnectFour\Domain\Game\State\Aborted 6 | drawn: Gaming\ConnectFour\Domain\Game\State\Drawn 7 | open: Gaming\ConnectFour\Domain\Game\State\Open 8 | resigned: Gaming\ConnectFour\Domain\Game\State\Resigned 9 | running: Gaming\ConnectFour\Domain\Game\State\Running 10 | won: Gaming\ConnectFour\Domain\Game\State\Won 11 | -------------------------------------------------------------------------------- /src/ConnectFour/Port/Adapter/Persistence/Jms/ConnectFour.Domain.Game.WinningRule.WinningRule.yml: -------------------------------------------------------------------------------- 1 | Gaming\ConnectFour\Domain\Game\WinningRule\WinningRule: 2 | discriminator: 3 | field_name: type 4 | map: 5 | common: Gaming\ConnectFour\Domain\Game\WinningRule\CommonWinningRule 6 | horizontal: Gaming\ConnectFour\Domain\Game\WinningRule\HorizontalWinningRule 7 | vertical: Gaming\ConnectFour\Domain\Game\WinningRule\VerticalWinningRule 8 | diagonal: Gaming\ConnectFour\Domain\Game\WinningRule\DiagonalWinningRule 9 | multiple: Gaming\ConnectFour\Domain\Game\WinningRule\MultipleWinningRule 10 | -------------------------------------------------------------------------------- /src/ConnectFour/Port/Adapter/Persistence/Migration/Version20160903094031.php: -------------------------------------------------------------------------------- 1 | createTable('game'); 15 | 16 | $table->addColumn('id', 'uuid'); 17 | $table->addColumn('version', 'integer'); 18 | $table->addColumn('aggregate', 'json'); 19 | 20 | $table->setPrimaryKey(['id']); 21 | } 22 | 23 | public function down(Schema $schema): void 24 | { 25 | $schema->dropTable('game'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ConnectFour/Port/Adapter/Persistence/Migration/Version20160904024032.php: -------------------------------------------------------------------------------- 1 | predis->sadd($this->storageKey, [$gameId]); 21 | } 22 | 23 | public function remove(string $gameId): void 24 | { 25 | $this->predis->srem($this->storageKey, $gameId); 26 | } 27 | 28 | public function count(): int 29 | { 30 | return $this->predis->scard($this->storageKey); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Identity/Application/User/Command/ArriveCommand.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class ArriveCommand implements Request 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Identity/Application/User/Command/SignUpCommand.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class SignUpCommand implements Request 13 | { 14 | public function __construct( 15 | public readonly string $userId, 16 | public readonly string $email, 17 | public readonly string $username, 18 | public readonly bool $dryRun 19 | ) { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Identity/Application/User/Query/User.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class UserByEmailQuery implements Request 13 | { 14 | public function __construct( 15 | public readonly string $email 16 | ) { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Identity/Application/User/Query/UserQuery.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class UserQuery implements Request 13 | { 14 | public function __construct( 15 | public readonly string $userId 16 | ) { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Identity/Domain/Model/User/Event/UserArrived.php: -------------------------------------------------------------------------------- 1 | userId = $userId->toString(); 17 | } 18 | 19 | public function aggregateId(): string 20 | { 21 | return $this->userId; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Identity/Domain/Model/User/Event/UserSignedUp.php: -------------------------------------------------------------------------------- 1 | userId = $userId->toString(); 20 | } 21 | 22 | public function aggregateId(): string 23 | { 24 | return $this->userId; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Identity/Domain/Model/User/Exception/EmailAlreadyExistsException.php: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Identity/Port/Adapter/Persistence/Migration/Version20170526204325.php: -------------------------------------------------------------------------------- 1 | createTable('user'); 15 | 16 | $table->addColumn('id', 'uuid'); 17 | $table->addColumn('version', 'integer', ['default' => 1]); 18 | $table->addColumn('email', 'string', ['notNull' => false, 'length' => 255]); 19 | $table->addColumn('username', 'string', ['notNull' => false, 'length' => 20]); 20 | 21 | $table->setPrimaryKey(['id']); 22 | $table->addUniqueIndex(['email'], 'uniq_email'); 23 | $table->addUniqueIndex(['username'], 'uniq_username'); 24 | } 25 | 26 | public function down(Schema $schema): void 27 | { 28 | $schema->dropTable('user'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Identity/Port/Adapter/Persistence/Migration/Version20170923230032.php: -------------------------------------------------------------------------------- 1 | numberOfPairs = $numberOfPairs; 17 | } 18 | 19 | public function dealIn(): array 20 | { 21 | $cards = array_merge( 22 | range(1, $this->numberOfPairs), 23 | range(1, $this->numberOfPairs) 24 | ); 25 | 26 | sort($cards); 27 | 28 | return $cards; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Memory/Domain/Model/Game/Dealer/ShuffleDealer.php: -------------------------------------------------------------------------------- 1 | numberOfPairs = $numberOfPairs; 14 | } 15 | 16 | public function dealIn(): array 17 | { 18 | $cards = array_merge( 19 | range(1, $this->numberOfPairs), 20 | range(1, $this->numberOfPairs) 21 | ); 22 | 23 | shuffle($cards); 24 | 25 | return $cards; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Memory/Domain/Model/Game/Event/GameClosed.php: -------------------------------------------------------------------------------- 1 | gameId = $gameId->toString(); 17 | } 18 | 19 | public function aggregateId(): string 20 | { 21 | return $this->gameId; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Memory/Domain/Model/Game/Event/GameOpened.php: -------------------------------------------------------------------------------- 1 | gameId = $gameId->toString(); 22 | $this->numberOfCards = $numberOfCards; 23 | $this->playerId = $player->id(); 24 | } 25 | 26 | public function aggregateId(): string 27 | { 28 | return $this->gameId; 29 | } 30 | 31 | public function numberOfCards(): int 32 | { 33 | return $this->numberOfCards; 34 | } 35 | 36 | public function playerId(): string 37 | { 38 | return $this->playerId; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Memory/Domain/Model/Game/Event/PlayerJoined.php: -------------------------------------------------------------------------------- 1 | gameId = $gameId->toString(); 20 | $this->playerId = $player->id(); 21 | } 22 | 23 | public function aggregateId(): string 24 | { 25 | return $this->gameId; 26 | } 27 | 28 | public function playerId(): string 29 | { 30 | return $this->playerId; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Memory/Domain/Model/Game/Event/PlayerLeft.php: -------------------------------------------------------------------------------- 1 | gameId = $gameId->toString(); 20 | $this->playerId = $player->id(); 21 | } 22 | 23 | public function aggregateId(): string 24 | { 25 | return $this->gameId; 26 | } 27 | 28 | public function playerId(): string 29 | { 30 | return $this->playerId; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Memory/Domain/Model/Game/Exception/GameException.php: -------------------------------------------------------------------------------- 1 | id = $id; 14 | } 15 | 16 | public function id(): string 17 | { 18 | return $this->id; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/WebInterface/Infrastructure/Security/Security.php: -------------------------------------------------------------------------------- 1 | security->getUser() instanceof User) { 22 | return $this->security->getUser(); 23 | } 24 | 25 | $this->security->login( 26 | $user = new User($this->identityCommandBus->handle(new ArriveCommand())) 27 | ); 28 | 29 | return $user; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/WebInterface/Infrastructure/Symfony/ReplaceSymfonyToolbar.php: -------------------------------------------------------------------------------- 1 | getRequest()->isXmlHttpRequest()) { 14 | $event->getResponse()->headers->set('Symfony-Debug-Toolbar-Replace', '1'); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/WebInterface/Presentation/Http/View/lobby.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout/condensed.html.twig' %} 2 | 3 | {% set page_title = 'Lobby' %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |
10 | {{ render_ssi(controller('connect-four.fragment-controller::statisticsAction')) }} 11 |
12 |
13 |
14 |
15 | {{ render_ssi(controller('connect-four.fragment-controller::openGamesAction')) }} 16 |
17 |
18 |
19 |
20 | {{ render_ssi(controller('connect-four.fragment-controller::openAction', {}, {wide: 1})) }} 21 |
22 |
23 |
24 |
25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /src/WebInterface/Presentation/Http/View/login/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@web-interface/login/layout/base.html.twig' %} 2 | 3 | {% set page_title = 'Login to your account' %} 4 | 5 | {% block card %} 6 |

Login to your account

7 | {{ form(form) }} 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /src/WebInterface/Presentation/Http/View/login/layout/base.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout/center-tight.html.twig' %} 2 | 3 | {% block content %} 4 |
5 |
6 | {% block card %}{% endblock %} 7 |
8 |
9 |
10 | Don't have an account yet? Sign up 11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /src/WebInterface/Presentation/Http/View/signup/confirm.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@web-interface/signup/layout/base.html.twig' %} 2 | 3 | {% set page_title = 'Confirm and play!' %} 4 | {% set step = 3 %} 5 | 6 | {% block card %} 7 |

Confirm and play!

8 | {{ form(form) }} 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /src/WebInterface/Presentation/Http/View/signup/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@web-interface/signup/layout/base.html.twig' %} 2 | 3 | {% set page_title = 'Sign up to play!' %} 4 | {% set step = 1 %} 5 | 6 | {% block card %} 7 |

Sign up to play!

8 | {{ form(form) }} 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /src/WebInterface/Presentation/Http/View/signup/layout/base.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout/center-tight.html.twig' %} 2 | 3 | {% block content %} 4 | 9 |
10 |
11 | {% block card %}{% endblock %} 12 |
13 |
14 |
15 | Already have an account? Sign in 16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /templates/layout/partial/footer-links.html.twig: -------------------------------------------------------------------------------- 1 |
  • 2 | 6 | Contact Us 7 | 8 |
  • 9 |
  • 10 | 14 | Terms & Conditions 15 | 16 |
  • 17 |
  • 18 | 22 | Imprint 23 | 24 |
  • 25 | -------------------------------------------------------------------------------- /templates/macro/alert.html.twig: -------------------------------------------------------------------------------- 1 | {%- macro alert(message, type, important, classes) -%} 2 | {%- import 'macro/icons.html.twig' as icons -%} 3 | 4 | {%- set classes = ['alert-' ~ type|default('info')]|merge(classes|default([])) -%} 5 | {%- if important -%}{% set classes = classes|merge(['alert-important']) %}{%- endif -%} 6 | 7 |
    9 |
    10 | {%- if type == 'danger' -%}{{ icons.danger() }} 11 | {%- elseif type == 'warning' -%}{{ icons.warning() }} 12 | {%- elseif type == 'success' -%}{{ icons.success() }} 13 | {%- else -%}{{ icons.info() }}{%- endif -%} 14 |
    15 | {{- message -}} 16 |
    17 | {%- endmacro -%} 18 | -------------------------------------------------------------------------------- /tests/_data/.gitignore: -------------------------------------------------------------------------------- 1 | /* 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/_output/.gitignore: -------------------------------------------------------------------------------- 1 | /* 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/_support/AcceptanceTester.php: -------------------------------------------------------------------------------- 1 | retry(3); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/_support/Helper/Acceptance.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class FirstRequest implements Request 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /tests/unit/Common/Bus/Fixture/SecondRequest.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class SecondRequest implements Request 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /tests/unit/Common/Bus/Fixture/ThirdRequest.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class ThirdRequest implements Request 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /tests/unit/Common/Bus/Fixture/UniversalHandler.php: -------------------------------------------------------------------------------- 1 | assertSame($point->x(), 3); 20 | $this->assertSame($point->y(), 4); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/unit/ConnectFour/Domain/Game/PlayerTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($id, $player->id()); 25 | $this->assertEquals($stone, $player->stone()); 26 | } 27 | 28 | /** 29 | * @test 30 | */ 31 | public function itShouldNotBeCreatedWithNoneStone(): void 32 | { 33 | $this->expectException(PlayerHasInvalidStoneException::class); 34 | 35 | $id = uniqid(); 36 | $stone = Stone::None; 37 | 38 | new Player($id, $stone); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/unit/Memory/Domain/Model/Game/Dealer/LazyDealerTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($expectedCards, $dealer->dealIn()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/unit/Memory/Domain/Model/Game/Dealer/ShuffleDealerTest.php: -------------------------------------------------------------------------------- 1 | dealIn()); 20 | 21 | $this->assertCount(3, $cardsWithoutDuplicates); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/unit/Memory/Domain/Model/Game/PlayerTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($id, $player->id()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /var/.gitignore: -------------------------------------------------------------------------------- 1 | /* 2 | !.gitignore 3 | --------------------------------------------------------------------------------