├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── code ├── .env ├── .env.test ├── .gitignore ├── .php_cs.dist ├── bin │ ├── console │ └── phpunit ├── composer.json ├── composer.lock ├── config │ ├── bundles.php │ ├── packages │ │ ├── cache.yaml │ │ ├── dev │ │ │ └── monolog.yaml │ │ ├── doctrine.yaml │ │ ├── doctrine_migrations.yaml │ │ ├── framework.yaml │ │ ├── messenger.yaml │ │ ├── prod │ │ │ ├── deprecations.yaml │ │ │ ├── doctrine.yaml │ │ │ ├── monolog.yaml │ │ │ └── routing.yaml │ │ ├── routing.yaml │ │ ├── security.yaml │ │ ├── test │ │ │ ├── doctrine.yaml │ │ │ ├── framework.yaml │ │ │ ├── monolog.yaml │ │ │ └── twig.yaml │ │ └── twig.yaml │ ├── preload.php │ ├── routes.yaml │ ├── routes │ │ ├── annotations.yaml │ │ └── dev │ │ │ └── framework.yaml │ └── services.yaml ├── coverage.clover ├── data │ ├── english.txt │ └── russian.txt ├── depfile.yaml ├── ecs.php ├── lint.php ├── phpunit.xml ├── psalm.xml ├── public │ ├── index.php │ └── swagger │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── index.html │ │ ├── oauth2-redirect.html │ │ ├── swagger-ui-bundle.js │ │ ├── swagger-ui-bundle.js.map │ │ ├── swagger-ui-standalone-preset.js │ │ ├── swagger-ui-standalone-preset.js.map │ │ ├── swagger-ui.css │ │ ├── swagger-ui.css.map │ │ ├── swagger-ui.js │ │ └── swagger-ui.js.map ├── rector.php ├── src │ ├── Crossword │ │ ├── Features │ │ │ ├── Constructor │ │ │ │ ├── ConstructorFactory.php │ │ │ │ ├── ConstructorInterface.php │ │ │ │ ├── CrosswordDto.php │ │ │ │ ├── Dictionary │ │ │ │ │ ├── ApiClientException.php │ │ │ │ │ ├── DictionarySearchInterface.php │ │ │ │ │ ├── DictionaryWordDto.php │ │ │ │ │ ├── Word.php │ │ │ │ │ └── WordSearchCriteria.php │ │ │ │ ├── Figured │ │ │ │ │ └── FiguredConstructor.php │ │ │ │ ├── LineDto.php │ │ │ │ ├── Message │ │ │ │ │ ├── GenerateCrosswordMessage.php │ │ │ │ │ └── GenerateCrosswordMessageHandler.php │ │ │ │ ├── Normal │ │ │ │ │ ├── AttemptWordFinder.php │ │ │ │ │ ├── NextLineFoundException.php │ │ │ │ │ └── NormalConstructor.php │ │ │ │ ├── PersistCrosswordRepositoryInterface.php │ │ │ │ ├── Scanner │ │ │ │ │ ├── Exception │ │ │ │ │ │ ├── CellNotFoundException.php │ │ │ │ │ │ ├── SearchMaskIsShortException.php │ │ │ │ │ │ └── WordNotFitException.php │ │ │ │ │ ├── Grid │ │ │ │ │ │ ├── Cell.php │ │ │ │ │ │ ├── Coordinate.php │ │ │ │ │ │ ├── Grid.php │ │ │ │ │ │ ├── Line.php │ │ │ │ │ │ ├── Row.php │ │ │ │ │ │ └── RowMask.php │ │ │ │ │ ├── GridScanner.php │ │ │ │ │ ├── Move.php │ │ │ │ │ ├── RowXScanner.php │ │ │ │ │ └── RowYScanner.php │ │ │ │ ├── Type │ │ │ │ │ ├── Type.php │ │ │ │ │ └── TypeAssert.php │ │ │ │ ├── WordFinder.php │ │ │ │ └── WordFoundException.php │ │ │ ├── Generator │ │ │ │ ├── Console │ │ │ │ │ ├── AbstractCommand.php │ │ │ │ │ └── GenerateCommand.php │ │ │ │ ├── CrosswordGenerator.php │ │ │ │ └── GenerateCriteria.php │ │ │ ├── Languages │ │ │ │ ├── Dictionary │ │ │ │ │ ├── ApiClientException.php │ │ │ │ │ ├── DictionaryLanguagesDto.php │ │ │ │ │ └── DictionaryLanguagesInterface.php │ │ │ │ ├── LanguagesAction.php │ │ │ │ ├── NotFoundSupportedLanguagesException.php │ │ │ │ ├── Request │ │ │ │ │ ├── RequestAssert.php │ │ │ │ │ └── RequestException.php │ │ │ │ ├── Response │ │ │ │ │ ├── Error │ │ │ │ │ │ ├── ErrorCriteria.php │ │ │ │ │ │ └── ErrorFactory.php │ │ │ │ │ ├── FailedApiResponse.php │ │ │ │ │ ├── HttpStatusCode.php │ │ │ │ │ ├── ResponseInterface.php │ │ │ │ │ └── SuccessApiResponse.php │ │ │ │ └── SupportedLanguages.php │ │ │ ├── Receiver │ │ │ │ ├── ConstructAction.php │ │ │ │ ├── CrosswordNotFoundException.php │ │ │ │ ├── CrosswordReceiver.php │ │ │ │ ├── ReadCrosswordRepositoryInterface.php │ │ │ │ ├── ReceiveCrosswordException.php │ │ │ │ ├── Request │ │ │ │ │ ├── ConstructRequest.php │ │ │ │ │ ├── RequestAssert.php │ │ │ │ │ └── RequestException.php │ │ │ │ ├── Response │ │ │ │ │ ├── Error │ │ │ │ │ │ ├── ErrorCriteria.php │ │ │ │ │ │ └── ErrorFactory.php │ │ │ │ │ ├── FailedApiResponse.php │ │ │ │ │ ├── HttpStatusCode.php │ │ │ │ │ ├── ResponseInterface.php │ │ │ │ │ └── SuccessApiResponse.php │ │ │ │ └── Type │ │ │ │ │ ├── Type.php │ │ │ │ │ └── TypeAssert.php │ │ │ └── Types │ │ │ │ ├── Response │ │ │ │ ├── HttpStatusCode.php │ │ │ │ ├── ResponseInterface.php │ │ │ │ └── SuccessApiResponse.php │ │ │ │ ├── SupportedTypes.php │ │ │ │ ├── Type.php │ │ │ │ └── TypesAction.php │ │ ├── Infrastructure │ │ │ ├── Adapter │ │ │ │ └── Dictionary │ │ │ │ │ ├── ApiDictionaryAdapter.php │ │ │ │ │ ├── DirectDictionaryAdapter.php │ │ │ │ │ └── InMemoryDictionaryAdapter.php │ │ │ ├── Cache │ │ │ │ ├── CacheItem.php │ │ │ │ ├── InMemoryClient.php │ │ │ │ └── RedisClient.php │ │ │ ├── HttpClient │ │ │ │ ├── ResponseDataExtractor.php │ │ │ │ └── ResponseDataExtractorInterface.php │ │ │ └── Repository │ │ │ │ └── Redis │ │ │ │ ├── PersistCrosswordRepository.php │ │ │ │ └── ReadCrosswordRepository.php │ │ ├── README.md │ │ ├── config │ │ │ ├── depfile.yaml │ │ │ ├── ecs.php │ │ │ ├── routes.yaml │ │ │ └── services.yaml │ │ └── tests │ │ │ ├── CrosswordTestCase.php │ │ │ ├── Features │ │ │ ├── Constructor │ │ │ │ ├── ConstructorFactoryTest.php │ │ │ │ ├── Messages │ │ │ │ │ └── GenerateCrosswordMessageHandlerTest.php │ │ │ │ ├── Normal │ │ │ │ │ └── AttemptWordFinderTest.php │ │ │ │ ├── Scaner │ │ │ │ │ └── Grid │ │ │ │ │ │ ├── CellTest.php │ │ │ │ │ │ ├── CoordinateTest.php │ │ │ │ │ │ ├── GridTest.php │ │ │ │ │ │ ├── LineTest.php │ │ │ │ │ │ ├── RowMaskTest.php │ │ │ │ │ │ └── RowTest.php │ │ │ │ └── WordFinderTest.php │ │ │ ├── Generator │ │ │ │ └── CrosswordGeneratorTest.php │ │ │ ├── Languages │ │ │ │ ├── Response │ │ │ │ │ └── SuccessResponseTest.php │ │ │ │ └── SupportedLanguagesTest.php │ │ │ ├── Receiver │ │ │ │ ├── CrosswordReceiverTest.php │ │ │ │ ├── Request │ │ │ │ │ └── RequestAssertTest.php │ │ │ │ ├── Response │ │ │ │ │ └── SuccessResponseTest.php │ │ │ │ └── Type │ │ │ │ │ └── TypeAssertTest.php │ │ │ └── Types │ │ │ │ └── Response │ │ │ │ └── SuccessResponseTest.php │ │ │ ├── Infrastructure │ │ │ └── HttpClient │ │ │ │ └── ResponseDataExtractorTest.php │ │ │ └── bootstrap.php │ ├── Dictionary │ │ ├── Features │ │ │ ├── Languages │ │ │ │ ├── LanguagesAction.php │ │ │ │ ├── Response │ │ │ │ │ ├── Error │ │ │ │ │ │ ├── ErrorCriteria.php │ │ │ │ │ │ └── ErrorFactory.php │ │ │ │ │ ├── FailedApiResponse.php │ │ │ │ │ ├── HttpStatusCode.php │ │ │ │ │ ├── ResponseInterface.php │ │ │ │ │ └── SuccessApiResponse.php │ │ │ │ ├── Storage │ │ │ │ │ ├── LanguageStorageInterface.php │ │ │ │ │ └── NotFoundSupportedLanguagesException.php │ │ │ │ └── SupportedLanguages.php │ │ │ ├── PopulateStorage │ │ │ │ ├── FileReader │ │ │ │ │ ├── FileReaderInterface.php │ │ │ │ │ └── OpenFileException.php │ │ │ │ ├── Populate │ │ │ │ │ ├── Console │ │ │ │ │ │ ├── AbstractCommand.php │ │ │ │ │ │ └── PopulateCommand.php │ │ │ │ │ ├── FileAssert.php │ │ │ │ │ ├── Message │ │ │ │ │ │ ├── SearchWordDefinitionMessage.php │ │ │ │ │ │ └── SearchWordDefinitionMessageHandler.php │ │ │ │ │ ├── Port │ │ │ │ │ │ ├── DefinitionNotFoundInApiGateway.php │ │ │ │ │ │ └── WordDefinitionApiGatewayInterface.php │ │ │ │ │ ├── WordsStoragePopulate.php │ │ │ │ │ └── WordsStoragePopulateCriteria.php │ │ │ │ ├── SaveStorage │ │ │ │ │ ├── Message │ │ │ │ │ │ ├── SaveToStorageMessage.php │ │ │ │ │ │ └── SaveToStorageMessageHandler.php │ │ │ │ │ ├── Storage │ │ │ │ │ │ ├── FailedSaveToStorageException.php │ │ │ │ │ │ └── WriteWordsStorageInterface.php │ │ │ │ │ └── Word.php │ │ │ │ └── Upload │ │ │ │ │ ├── Console │ │ │ │ │ ├── AbstractCommand.php │ │ │ │ │ └── UploadCommand.php │ │ │ │ │ ├── FileAssert.php │ │ │ │ │ ├── WordsStorageUpload.php │ │ │ │ │ └── WordsStorageUploadCriteria.php │ │ │ └── WordsFinder │ │ │ │ ├── Mask │ │ │ │ ├── Mask.php │ │ │ │ └── SearchMaskIsShortException.php │ │ │ │ ├── NotFoundWordException.php │ │ │ │ ├── Request │ │ │ │ ├── RequestAssert.php │ │ │ │ ├── RequestException.php │ │ │ │ └── WordRequest.php │ │ │ │ ├── Response │ │ │ │ ├── Error │ │ │ │ │ ├── ErrorCriteria.php │ │ │ │ │ └── ErrorFactory.php │ │ │ │ ├── FailedApiResponse.php │ │ │ │ ├── HttpStatusCode.php │ │ │ │ ├── ResponseInterface.php │ │ │ │ └── SuccessApiResponse.php │ │ │ │ ├── Storage │ │ │ │ ├── ReadWordsStorageInterface.php │ │ │ │ └── WordNotFoundInStorageException.php │ │ │ │ ├── Word │ │ │ │ ├── Word.php │ │ │ │ ├── WordDto.php │ │ │ │ └── WordDtoCollection.php │ │ │ │ ├── WordAction.php │ │ │ │ └── WordsFinder.php │ │ ├── Infrastructure │ │ │ ├── FileReader │ │ │ │ ├── CsvFileReader.php │ │ │ │ └── TextFileReader.php │ │ │ ├── Gateway │ │ │ │ ├── AbstractWordDefinition.php │ │ │ │ ├── Google │ │ │ │ │ ├── GoogleWordDefinitionDto.php │ │ │ │ │ └── WordDefinitionGoogleApiGateway.php │ │ │ │ ├── InMemory │ │ │ │ │ └── WordDefinitionApiGatewayInMemory.php │ │ │ │ └── Wikipedia │ │ │ │ │ ├── WikipediaWordDefinitionDto.php │ │ │ │ │ └── WordDefinitionWikipediaApiGateway.php │ │ │ ├── HttpClient │ │ │ │ ├── ResponseDataExtractor.php │ │ │ │ └── ResponseDataExtractorInterface.php │ │ │ └── Repository │ │ │ │ └── Elastic │ │ │ │ ├── ClientFactory.php │ │ │ │ ├── ReadWordsStorage.php │ │ │ │ ├── StorageWordDto.php │ │ │ │ ├── StorageWordDtoCollection.php │ │ │ │ └── WriteWordsStorage.php │ │ ├── README.md │ │ ├── config │ │ │ ├── depfile.yaml │ │ │ ├── ecs.php │ │ │ ├── routes.yaml │ │ │ └── services.yaml │ │ └── tests │ │ │ ├── DictionaryTestCase.php │ │ │ ├── Features │ │ │ ├── Languages │ │ │ │ └── Response │ │ │ │ │ └── SuccessResponseTest.php │ │ │ ├── PopulateStorage │ │ │ │ ├── Populate │ │ │ │ │ ├── FileAssertTest.php │ │ │ │ │ ├── Message │ │ │ │ │ │ └── SearchWordDefinitionMessageHandlerTest.php │ │ │ │ │ └── WordsStoragePopulateTest.php │ │ │ │ └── Upload │ │ │ │ │ ├── FileAssertTest.php │ │ │ │ │ └── WordsStorageUploadTest.php │ │ │ └── WordsFinder │ │ │ │ ├── Mask │ │ │ │ └── MaskTest.php │ │ │ │ ├── Request │ │ │ │ └── RequestAssertTest.php │ │ │ │ └── Response │ │ │ │ └── SuccessResponseTest.php │ │ │ ├── Infrastructure │ │ │ └── HttpClient │ │ │ │ └── ResponseDataExtractorTest.php │ │ │ └── bootstrap.php │ ├── Game │ │ ├── Features │ │ │ ├── Answers │ │ │ │ ├── Answers.php │ │ │ │ ├── AnswersValidator.php │ │ │ │ ├── Authentication │ │ │ │ │ ├── PlayerFromTokenExtractor.php │ │ │ │ │ └── PlayerNotFoundInTokenStorageException.php │ │ │ │ ├── CheckAnswerAction.php │ │ │ │ ├── CorrectAnswersAssert.php │ │ │ │ ├── CrosswordPuzzleSolvedEvent.php │ │ │ │ ├── LetterEncoderInterface.php │ │ │ │ ├── Player │ │ │ │ │ ├── PlayerDto.php │ │ │ │ │ └── PlayerId.php │ │ │ │ ├── Request │ │ │ │ │ ├── AnswersRequest.php │ │ │ │ │ ├── RequestAssert.php │ │ │ │ │ └── RequestException.php │ │ │ │ ├── Response │ │ │ │ │ ├── Error │ │ │ │ │ │ ├── ErrorCriteria.php │ │ │ │ │ │ └── ErrorFactory.php │ │ │ │ │ ├── FailedApiResponse.php │ │ │ │ │ ├── HttpStatusCode.php │ │ │ │ │ ├── ResponseInterface.php │ │ │ │ │ └── SuccessApiResponse.php │ │ │ │ ├── Twig │ │ │ │ │ └── EncodeExtension.php │ │ │ │ └── WrongAnswerException.php │ │ │ ├── Authorization │ │ │ │ ├── PlayerAuthAction.php │ │ │ │ ├── PlayerDto.php │ │ │ │ ├── PlayerLogin.php │ │ │ │ ├── PlayerLoginException.php │ │ │ │ ├── PlayerTokenHack.php │ │ │ │ ├── RefreshPlayerInTokenEventHandler.php │ │ │ │ ├── Repository │ │ │ │ │ ├── PlayerNotFoundException.php │ │ │ │ │ └── ReadPlayerRepositoryInterface.php │ │ │ │ ├── Request │ │ │ │ │ ├── LoginRequest.php │ │ │ │ │ ├── RequestAssert.php │ │ │ │ │ └── RequestException.php │ │ │ │ └── ViewLoginPageAction.php │ │ │ ├── GamePlay │ │ │ │ ├── Authentication │ │ │ │ │ ├── PlayerFromTokenExtractor.php │ │ │ │ │ └── PlayerNotFoundInTokenStorageException.php │ │ │ │ ├── Crossword │ │ │ │ │ ├── ApiClientException.php │ │ │ │ │ ├── CrosswordConstructor.php │ │ │ │ │ ├── CrosswordCriteria.php │ │ │ │ │ ├── CrosswordDto.php │ │ │ │ │ ├── CrosswordInterface.php │ │ │ │ │ ├── CrosswordNotConstructedException.php │ │ │ │ │ ├── GameDto.php │ │ │ │ │ ├── Grid.php │ │ │ │ │ └── Type.php │ │ │ │ ├── GamePlay.php │ │ │ │ ├── NewGameAction.php │ │ │ │ └── Player │ │ │ │ │ ├── PlayerDto.php │ │ │ │ │ ├── PlayerId.php │ │ │ │ │ └── Role.php │ │ │ ├── History │ │ │ │ ├── History.php │ │ │ │ ├── HistoryDaoInterface.php │ │ │ │ ├── HistoryId.php │ │ │ │ ├── HistoryRatingDto.php │ │ │ │ ├── PersistHistoryRepositoryInterface.php │ │ │ │ ├── PlayerHistory.php │ │ │ │ ├── PlayerId.php │ │ │ │ ├── PlayerRatingAction.php │ │ │ │ ├── Response │ │ │ │ │ ├── Error │ │ │ │ │ │ ├── ErrorCriteria.php │ │ │ │ │ │ └── ErrorFactory.php │ │ │ │ │ ├── FailedApiResponse.php │ │ │ │ │ ├── HttpStatusCode.php │ │ │ │ │ ├── ResponseInterface.php │ │ │ │ │ └── SuccessApiResponse.php │ │ │ │ └── SaveHistoryEventHandler.php │ │ │ ├── Player │ │ │ │ ├── AggregateRoot.php │ │ │ │ ├── CrosswordPuzzleSolvedEventHandler.php │ │ │ │ ├── DomainEventInterface.php │ │ │ │ ├── DomainEventsSubscriber.php │ │ │ │ ├── Level │ │ │ │ │ ├── Level.php │ │ │ │ │ ├── LevelUpEvent.php │ │ │ │ │ └── PlayerLevelRepositoryInterface.php │ │ │ │ ├── Player │ │ │ │ │ ├── Player.php │ │ │ │ │ ├── PlayerDto.php │ │ │ │ │ ├── PlayerId.php │ │ │ │ │ ├── PlayerNotFoundException.php │ │ │ │ │ └── Role.php │ │ │ │ └── RaiseEventsInterface.php │ │ │ └── Registration │ │ │ │ ├── Console │ │ │ │ ├── AbstractCommand.php │ │ │ │ └── CreatePlayerCommand.php │ │ │ │ ├── PasswordAssert.php │ │ │ │ ├── Player │ │ │ │ ├── PlayerDto.php │ │ │ │ └── PlayerId.php │ │ │ │ ├── PlayerRegister.php │ │ │ │ ├── PlayerRegisterCriteria.php │ │ │ │ ├── PlayerRepositoryInterface.php │ │ │ │ └── Role │ │ │ │ ├── Role.php │ │ │ │ └── RoleAssert.php │ │ ├── Infrastructure │ │ │ ├── Adapter │ │ │ │ └── Crossword │ │ │ │ │ ├── ApiCrosswordAdapter.php │ │ │ │ │ ├── DirectCrosswordAdapter.php │ │ │ │ │ └── InMemoryCrosswordAdapter.php │ │ │ ├── Dao │ │ │ │ ├── Doctrine │ │ │ │ │ └── HistoryDao.php │ │ │ │ └── InMemory │ │ │ │ │ └── InMemoryHistoryDao.php │ │ │ ├── Encoder │ │ │ │ ├── Base64Encoder.php │ │ │ │ ├── MD5Encoder.php │ │ │ │ └── PasswordEncoderInterface.php │ │ │ ├── HttpClient │ │ │ │ ├── ResponseDataExtractor.php │ │ │ │ └── ResponseDataExtractorInterface.php │ │ │ └── Repository │ │ │ │ ├── Doctrine │ │ │ │ ├── PersistHistoryRepository.php │ │ │ │ ├── PersistPlayerRepository.php │ │ │ │ ├── PlayerAssembler.php │ │ │ │ ├── ReadHistoryRepository.php │ │ │ │ └── ReadPlayerRepository.php │ │ │ │ ├── DoctrineMigrations │ │ │ │ └── Version20210411114720.php │ │ │ │ ├── DoctrineTypes │ │ │ │ ├── HistoryIdType.php │ │ │ │ └── PlayerIdType.php │ │ │ │ └── InMemory │ │ │ │ └── InMemoryPlayerRepository.php │ │ ├── README.md │ │ ├── config │ │ │ ├── depfile.yaml │ │ │ ├── doctrine │ │ │ │ ├── history │ │ │ │ │ └── History.orm.xml │ │ │ │ └── player │ │ │ │ │ └── Player.orm.xml │ │ │ ├── ecs.php │ │ │ ├── routes.yaml │ │ │ └── services.yaml │ │ ├── templates │ │ │ ├── login.html.twig │ │ │ └── play.html.twig │ │ └── tests │ │ │ ├── Features │ │ │ ├── Answers │ │ │ │ ├── AnswersTest.php │ │ │ │ ├── AnswersValidatorTest.php │ │ │ │ ├── Authentication │ │ │ │ │ └── PlayerFromTokenExtractorTest.php │ │ │ │ ├── CorrectAnswersAssertTest.php │ │ │ │ ├── Request │ │ │ │ │ └── RequestAssertTest.php │ │ │ │ └── Response │ │ │ │ │ └── SuccessResponseTest.php │ │ │ ├── Authorization │ │ │ │ ├── PlayerLoginTest.php │ │ │ │ └── Request │ │ │ │ │ └── RequestAssertTest.php │ │ │ ├── GamePlay │ │ │ │ ├── Authentication │ │ │ │ │ └── PlayerFromTokenExtractorTest.php │ │ │ │ ├── Crossword │ │ │ │ │ └── GridTest.php │ │ │ │ └── GamePlayTest.php │ │ │ ├── History │ │ │ │ ├── PlayerHistoryTest.php │ │ │ │ └── Response │ │ │ │ │ └── SuccessResponseTest.php │ │ │ ├── Player │ │ │ │ └── PlayerTest.php │ │ │ └── Registration │ │ │ │ ├── Console │ │ │ │ └── CreatePlayerCommandTest.php │ │ │ │ ├── PasswordAssertTest.php │ │ │ │ ├── PlayerRegisterTest.php │ │ │ │ └── Role │ │ │ │ └── RoleAssertTest.php │ │ │ ├── GameTestCase.php │ │ │ ├── Infrastructure │ │ │ ├── HttpClient │ │ │ │ └── ResponseDataExtractorTest.php │ │ │ └── Repository │ │ │ │ └── Doctrine │ │ │ │ └── PlayerAssemblerTest.php │ │ │ └── bootstrap.php │ ├── Kernel.php │ ├── SharedKernel │ │ ├── Infrastructure │ │ │ ├── HttpClient │ │ │ │ ├── GuzzleClient.php │ │ │ │ └── Middleware │ │ │ │ │ ├── LoggerMiddleware.php │ │ │ │ │ └── RetryMiddleware.php │ │ │ ├── Messenger │ │ │ │ └── Middleware │ │ │ │ │ ├── AuditMiddleware.php │ │ │ │ │ └── FailureLoggerMiddleware.php │ │ │ └── Responder │ │ │ │ ├── JsonResponder.php │ │ │ │ ├── WebResponder.php │ │ │ │ └── XmlResponder.php │ │ └── config │ │ │ ├── ecs.php │ │ │ └── services.yaml │ └── Swagger │ │ ├── SwaggerIndexAction.php │ │ ├── SwaggerUpdateAction.php │ │ └── config │ │ ├── routes.yaml │ │ └── services.yaml ├── symfony.lock └── var │ └── database │ ├── db.sqlite │ └── db.sqlite.dist ├── docker ├── .env ├── data │ ├── elasticsearch │ │ └── .gitignore │ └── redis │ │ └── .gitignore ├── docker-compose.mac.yml ├── docker-compose.test.yml ├── docker-compose.yml ├── newman │ └── Dockerfile ├── nginx │ ├── Dockerfile │ └── site.conf ├── php-ci │ └── Dockerfile ├── php │ └── Dockerfile ├── rabbit │ └── enabled_plugins └── scripts │ ├── composer │ ├── console │ └── php ├── docs ├── example.gif ├── model.png └── view.png └── postman ├── postman_collection.json └── postman_environment.json /.gitignore: -------------------------------------------------------------------------------- 1 | /logs 2 | /vendor 3 | /.idea 4 | /docker/data/elasticsearch/nodes/* 5 | /docker/data/redis/appendonly.aof 6 | /docker/data/redis/dump.rdb 7 | .DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - '8.0' 5 | 6 | services: 7 | - docker 8 | 9 | cache: bundler 10 | 11 | jobs: 12 | fast_finish: true 13 | 14 | env: 15 | global: 16 | - DOCKER_COMPOSE=1.25.5 17 | 18 | sudo: required 19 | 20 | before_install: 21 | - sudo rm /usr/local/bin/docker-compose 22 | - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE}/docker-compose-`uname -s`-`uname -m` > docker-compose 23 | - chmod +x docker-compose 24 | - sudo mv docker-compose /usr/local/bin 25 | script: 26 | - cd docker && docker-compose -f docker-compose.test.yml run tests 27 | after_script: 28 | - wget https://scrutinizer-ci.com/ocular.phar 29 | - php ocular.phar code-coverage:upload --format=php-clover code/coverage.clover -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Dykyi Roman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /code/.env.test: -------------------------------------------------------------------------------- 1 | # define your env variables for the test env here 2 | KERNEL_CLASS='App\Kernel' 3 | APP_SECRET='$ecretf0rt3st' 4 | SYMFONY_DEPRECATIONS_HELPER=999999 5 | PANTHER_APP_ENV=panther 6 | PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots 7 | -------------------------------------------------------------------------------- /code/.gitignore: -------------------------------------------------------------------------------- 1 | ###> symfony/framework-bundle ### 2 | /.env.local 3 | /.env.local.php 4 | /.env.*.local 5 | /config/secrets/prod/prod.decrypt.private.php 6 | /public/bundles/ 7 | /var/cache/* 8 | /var/log/* 9 | /vendor/ 10 | ###< symfony/framework-bundle ### 11 | 12 | ###> symfony/phpunit-bridge ### 13 | .phpunit 14 | .phpunit.result.cache 15 | /phpunit.xml 16 | ###< symfony/phpunit-bridge ### 17 | 18 | ###> squizlabs/php_codesniffer ### 19 | /.phpcs-cache 20 | /phpcs.xml 21 | ###< squizlabs/php_codesniffer ### 22 | ###> qossmic/deptrac-shim ### 23 | /.deptrac.cache 24 | ###< qossmic/deptrac-shim ### 25 | coverage.clover -------------------------------------------------------------------------------- /code/.php_cs.dist: -------------------------------------------------------------------------------- 1 | in(__DIR__) 5 | ->exclude('var') 6 | ; 7 | 8 | return PhpCsFixer\Config::create() 9 | ->setRules([ 10 | '@Symfony' => true, 11 | ]) 12 | ->setFinder($finder) 13 | ; 14 | -------------------------------------------------------------------------------- /code/bin/phpunit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | ['all' => true], 5 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], 6 | Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], 7 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 8 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 9 | Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], 10 | ]; 11 | -------------------------------------------------------------------------------- /code/config/packages/cache.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | cache: 3 | # Unique name of your app: used to compute stable namespaces for cache keys. 4 | #prefix_seed: your_vendor_name/app_name 5 | 6 | # The "app" cache stores to the filesystem by default. 7 | # The data in this cache should persist between deploys. 8 | # Other options include: 9 | 10 | # Redis 11 | #app: cache.adapter.redis 12 | #default_redis_provider: redis://localhost 13 | 14 | # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) 15 | #app: cache.adapter.apcu 16 | 17 | # Namespaced pools use the above "app" backend by default 18 | #pools: 19 | #my.dedicated.cache: null 20 | -------------------------------------------------------------------------------- /code/config/packages/dev/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: stream 5 | path: "%kernel.logs_dir%/%kernel.environment%.log" 6 | level: debug 7 | channels: ["!event"] 8 | # uncomment to get logging in your browser 9 | # you may have to allow bigger header sizes in your Web server configuration 10 | #firephp: 11 | # type: firephp 12 | # level: info 13 | #chromephp: 14 | # type: chromephp 15 | # level: info 16 | console: 17 | type: console 18 | process_psr_3_messages: false 19 | channels: ["!event", "!doctrine", "!console"] 20 | -------------------------------------------------------------------------------- /code/config/packages/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | dbal: 3 | override_url: true 4 | url: '%env(resolve:DATABASE_URL)%' 5 | 6 | types: 7 | playerId: App\Game\Infrastructure\Repository\DoctrineTypes\PlayerIdType 8 | historyId: App\Game\Infrastructure\Repository\DoctrineTypes\HistoryIdType 9 | 10 | # IMPORTANT: You MUST configure your server version, 11 | # either here or in the DATABASE_URL env var (see .env file) 12 | #server_version: '13' 13 | orm: 14 | auto_generate_proxy_classes: true 15 | naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware 16 | auto_mapping: true 17 | mappings: 18 | App\Game\Features\Player: 19 | is_bundle: false 20 | type: xml 21 | dir: '%kernel.project_dir%/src/Game/config/doctrine/player' 22 | prefix: 'App\Game\Features\Player\Player' 23 | App\Game\Features\History: 24 | is_bundle: false 25 | type: xml 26 | dir: '%kernel.project_dir%/src/Game/config/doctrine/history' 27 | prefix: 'App\Game\Features\History' -------------------------------------------------------------------------------- /code/config/packages/doctrine_migrations.yaml: -------------------------------------------------------------------------------- 1 | doctrine_migrations: 2 | migrations_paths: 3 | # namespace is arbitrary but should be different from App\Migrations 4 | # as migrations classes should NOT be autoloaded 5 | 'App\Game\Infrastructure\Repository\DoctrineMigrations': '%kernel.project_dir%/src/Game/Infrastructure/Repository/DoctrineMigrations' 6 | enable_profiler: '%kernel.debug%' 7 | -------------------------------------------------------------------------------- /code/config/packages/messenger.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | messenger: 3 | # Uncomment this (and the failed transport below) to send failed messages to this transport for later handling. 4 | # failure_transport: failed 5 | 6 | transports: 7 | # messages: '%env(MESSENGER_TRANSPORT_DSN)%/messages' 8 | # events: '%env(MESSENGER_TRANSPORT_DSN)%/events' 9 | messages: 'sync://' 10 | events: 'sync://' 11 | # failed: 'doctrine://default?queue_name=failed' 12 | 13 | routing: 14 | 'App\Dictionary\Features\PopulateStorage\Populate\Message\SearchWordDefinitionMessage': messages 15 | 'App\Dictionary\Features\PopulateStorage\SaveStorage\Message\SaveToStorageMessage': messages 16 | 'App\Crossword\Features\Constructor\Message\GenerateCrosswordMessage': messages 17 | 'App\Game\Features\Player\Level\LevelUpEvent': events 18 | -------------------------------------------------------------------------------- /code/config/packages/prod/deprecations.yaml: -------------------------------------------------------------------------------- 1 | # As of Symfony 5.1, deprecations are logged in the dedicated "deprecation" channel when it exists 2 | #monolog: 3 | # channels: [deprecation] 4 | # handlers: 5 | # deprecation: 6 | # type: stream 7 | # channels: [deprecation] 8 | # path: "%kernel.logs_dir%/%kernel.environment%.deprecations.log" 9 | -------------------------------------------------------------------------------- /code/config/packages/prod/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | orm: 3 | auto_generate_proxy_classes: false 4 | metadata_cache_driver: 5 | type: pool 6 | pool: doctrine.system_cache_pool 7 | query_cache_driver: 8 | type: pool 9 | pool: doctrine.system_cache_pool 10 | result_cache_driver: 11 | type: pool 12 | pool: doctrine.result_cache_pool 13 | 14 | framework: 15 | cache: 16 | pools: 17 | doctrine.result_cache_pool: 18 | adapter: cache.app 19 | doctrine.system_cache_pool: 20 | adapter: cache.system 21 | -------------------------------------------------------------------------------- /code/config/packages/prod/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: fingers_crossed 5 | action_level: error 6 | handler: nested 7 | excluded_http_codes: [404, 405] 8 | buffer_size: 50 # How many messages should be saved? Prevent memory leaks 9 | nested: 10 | type: stream 11 | path: "%kernel.logs_dir%/%kernel.environment%.log" 12 | level: debug 13 | console: 14 | type: console 15 | process_psr_3_messages: false 16 | channels: ["!event", "!doctrine"] 17 | -------------------------------------------------------------------------------- /code/config/packages/prod/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: null 4 | -------------------------------------------------------------------------------- /code/config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | utf8: true 4 | 5 | # Configure how to generate URLs in non-HTTP contexts, such as CLI commands. 6 | # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands 7 | #default_uri: http://localhost 8 | -------------------------------------------------------------------------------- /code/config/packages/security.yaml: -------------------------------------------------------------------------------- 1 | security: 2 | # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers 3 | providers: 4 | users: 5 | entity: 6 | class: App\Game\Features\Player\Player\Player 7 | property: nickname 8 | firewalls: 9 | dev: 10 | pattern: ^/(_(profiler|wdt)|css|images|js)/ 11 | security: false 12 | main: 13 | anonymous: true 14 | lazy: true 15 | 16 | # activate different ways to authenticate 17 | # https://symfony.com/doc/current/security.html#firewalls-authentication 18 | 19 | # https://symfony.com/doc/current/security/impersonating_user.html 20 | # switch_user: true 21 | 22 | # Easy way to control access for large sections of your site 23 | # Note: Only the *first* access control that matches will be used 24 | access_control: 25 | # - { path: ^/admin, roles: ROLE_ADMIN } 26 | # - { path: ^/profile, roles: ROLE_USER } 27 | -------------------------------------------------------------------------------- /code/config/packages/test/doctrine.yaml: -------------------------------------------------------------------------------- 1 | #doctrine: 2 | # dbal: 3 | # # Overrides the database name in the test environment only 4 | # # "host", "port", "username", & "password" can also be set to override their respective url parts 5 | # # 6 | # # If you're using ParaTest, "TEST_TOKEN" is set by ParaTest otherwise nothing is appended to the database name. 7 | # dbname: main_test%env(default::TEST_TOKEN)% 8 | -------------------------------------------------------------------------------- /code/config/packages/test/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | test: true 3 | session: 4 | storage_id: session.storage.mock_file 5 | -------------------------------------------------------------------------------- /code/config/packages/test/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: fingers_crossed 5 | action_level: error 6 | handler: nested 7 | excluded_http_codes: [404, 405] 8 | channels: ["!event"] 9 | nested: 10 | type: stream 11 | path: "%kernel.logs_dir%/%kernel.environment%.log" 12 | level: debug 13 | -------------------------------------------------------------------------------- /code/config/packages/test/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | strict_variables: true 3 | -------------------------------------------------------------------------------- /code/config/packages/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | paths: 3 | '%kernel.project_dir%/src/Game/templates': game -------------------------------------------------------------------------------- /code/config/preload.php: -------------------------------------------------------------------------------- 1 | add(new LintCommand()) 10 | ->getApplication() 11 | ->setDefaultCommand('lint:yaml', true) 12 | ->run(); -------------------------------------------------------------------------------- /code/phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 | src 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | src/Crossword/tests 25 | src/Dictionary/tests 26 | src/Game/tests 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /code/psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /code/public/index.php: -------------------------------------------------------------------------------- 1 | bootEnv(dirname(__DIR__).'/.env'); 11 | 12 | if ($_SERVER['APP_DEBUG']) { 13 | umask(0000); 14 | 15 | Debug::enable(); 16 | } 17 | 18 | $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); 19 | $request = Request::createFromGlobals(); 20 | $response = $kernel->handle($request); 21 | $response->send(); 22 | $kernel->terminate($request, $response); 23 | -------------------------------------------------------------------------------- /code/public/swagger/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dykyi-roman/crossword/00cd7c526b66cdba591eaf421d670602f7dfc9d2/code/public/swagger/favicon-16x16.png -------------------------------------------------------------------------------- /code/public/swagger/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dykyi-roman/crossword/00cd7c526b66cdba591eaf421d670602f7dfc9d2/code/public/swagger/favicon-32x32.png -------------------------------------------------------------------------------- /code/rector.php: -------------------------------------------------------------------------------- 1 | parameters(); 13 | 14 | // Define what rule sets will be applied 15 | $parameters->set(Option::SETS, [ 16 | SetList::DEAD_CODE, 17 | SetList::DEAD_DOC_BLOCK, 18 | ]); 19 | 20 | // get services (needed for register a single rule) 21 | $services = $containerConfigurator->services(); 22 | 23 | // register a single rule 24 | $services->set(TypedPropertyRector::class); 25 | }; 26 | -------------------------------------------------------------------------------- /code/src/Crossword/Features/Constructor/ConstructorFactory.php: -------------------------------------------------------------------------------- 1 | wordFinder = $wordFinder; 22 | } 23 | 24 | public function create(Type $type): ConstructorInterface 25 | { 26 | return match ((string) $type->getValue()) { 27 | Type::NORMAL => new NormalConstructor( 28 | new AttemptWordFinder($this->wordFinder), 29 | new GridScanner(new RowXScanner(), new RowYScanner()) 30 | ), 31 | Type::FIGURED => new FiguredConstructor(), 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /code/src/Crossword/Features/Constructor/ConstructorInterface.php: -------------------------------------------------------------------------------- 1 | lines = $lines; 22 | } 23 | 24 | public function withLine(LineDto $line): self 25 | { 26 | return new self($line, ...$this->lines); 27 | } 28 | 29 | /** 30 | * @psalm-suppress ImpureFunctionCall 31 | */ 32 | public function jsonSerialize(): array 33 | { 34 | return array_map(static fn (LineDto $line): array => $line->jsonSerialize(), $this->lines); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /code/src/Crossword/Features/Constructor/Dictionary/ApiClientException.php: -------------------------------------------------------------------------------- 1 | payload = $payload; 19 | } 20 | 21 | public function count(): int 22 | { 23 | return $this->payload['success'] ? 1 : 0; 24 | } 25 | 26 | public function word(): string 27 | { 28 | return $this->isSuccess() ? $this->payload['data'][0]['word'] : ''; 29 | } 30 | 31 | public function definition(): string 32 | { 33 | return $this->isSuccess() ? $this->payload['data'][0]['definition'] : ''; 34 | } 35 | 36 | private function isSuccess(): bool 37 | { 38 | return (bool) $this->payload['success']; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /code/src/Crossword/Features/Constructor/Dictionary/Word.php: -------------------------------------------------------------------------------- 1 | value = $value; 20 | $this->definition = $definition; 21 | } 22 | 23 | public function value(): string 24 | { 25 | return $this->value; 26 | } 27 | 28 | public function definition(): string 29 | { 30 | return $this->definition; 31 | } 32 | 33 | public function length(): int 34 | { 35 | return strlen($this->value); 36 | } 37 | 38 | public function jsonSerialize(): array 39 | { 40 | return [ 41 | 'word' => $this->value, 42 | 'definition' => $this->definition, 43 | 'length' => $this->length(), 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /code/src/Crossword/Features/Constructor/Dictionary/WordSearchCriteria.php: -------------------------------------------------------------------------------- 1 | language = $language; 15 | $this->mask = $mask; 16 | } 17 | 18 | public function mask(): string 19 | { 20 | return $this->mask; 21 | } 22 | 23 | public function language(): string 24 | { 25 | return $this->language; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /code/src/Crossword/Features/Constructor/Figured/FiguredConstructor.php: -------------------------------------------------------------------------------- 1 | line = $line; 22 | $this->word = $word; 23 | } 24 | 25 | public function line(): Line 26 | { 27 | return $this->line; 28 | } 29 | 30 | /** 31 | * @psalm-suppress ImpureMethodCall 32 | */ 33 | public function jsonSerialize(): array 34 | { 35 | return [ 36 | 'line' => $this->line->jsonSerialize(), 37 | 'word' => $this->word->jsonSerialize(), 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /code/src/Crossword/Features/Constructor/Message/GenerateCrosswordMessage.php: -------------------------------------------------------------------------------- 1 | type = $type; 19 | $this->language = $language; 20 | $this->wordCount = $wordCount; 21 | } 22 | 23 | public function type(): string 24 | { 25 | return $this->type; 26 | } 27 | 28 | public function wordCount(): int 29 | { 30 | return $this->wordCount; 31 | } 32 | 33 | public function language(): string 34 | { 35 | return $this->language; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /code/src/Crossword/Features/Constructor/Normal/NextLineFoundException.php: -------------------------------------------------------------------------------- 1 | rowXScanner = $rowXScanner; 20 | $this->rowYScanner = $rowYScanner; 21 | } 22 | 23 | /** 24 | * @return Row[] 25 | */ 26 | public function scan(Grid $grid): array 27 | { 28 | $rows = array_merge( 29 | $this->rowXScanner->scan($grid, self::SCAN_ROW_LENGTH), 30 | $this->rowYScanner->scan($grid, self::SCAN_ROW_LENGTH) 31 | ); 32 | shuffle($rows); 33 | 34 | return $rows; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /code/src/Crossword/Features/Constructor/Scanner/Move.php: -------------------------------------------------------------------------------- 1 | messageBus = $messageBus; 17 | } 18 | 19 | public function generate(GenerateCriteria $criteria): void 20 | { 21 | $counter = 1; 22 | do { 23 | $this->messageBus->dispatch( 24 | new GenerateCrosswordMessage( 25 | $criteria->language(), 26 | $criteria->type(), 27 | $criteria->wordCount() 28 | ) 29 | ); 30 | $counter++; 31 | } while ($counter <= $criteria->limit()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /code/src/Crossword/Features/Generator/GenerateCriteria.php: -------------------------------------------------------------------------------- 1 | type = $type; 22 | $this->wordCount = $wordCount; 23 | $this->limit = $limit; 24 | $this->language = $language; 25 | } 26 | 27 | public function type(): string 28 | { 29 | return $this->type; 30 | } 31 | 32 | public function language(): string 33 | { 34 | return $this->language; 35 | } 36 | 37 | public function wordCount(): int 38 | { 39 | return $this->wordCount; 40 | } 41 | 42 | public function limit(): int 43 | { 44 | return $this->limit; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /code/src/Crossword/Features/Languages/Dictionary/ApiClientException.php: -------------------------------------------------------------------------------- 1 | payload = $payload; 19 | } 20 | 21 | public function count(): int 22 | { 23 | return count($this->languages()); 24 | } 25 | 26 | public function languages(): array 27 | { 28 | return $this->isSuccess() ? $this->payload['data'] : []; 29 | } 30 | 31 | private function isSuccess(): bool 32 | { 33 | return (bool) $this->payload['success']; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /code/src/Crossword/Features/Languages/Dictionary/DictionaryLanguagesInterface.php: -------------------------------------------------------------------------------- 1 | code = $code; 21 | $this->message = $message; 22 | $this->data = $data; 23 | } 24 | 25 | public function jsonSerialize(): array 26 | { 27 | return [ 28 | 'code' => $this->code, 29 | 'message' => $this->message, 30 | 'data' => $this->data, 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /code/src/Crossword/Features/Languages/Response/Error/ErrorFactory.php: -------------------------------------------------------------------------------- 1 | status = $status; 20 | $this->error = $error; 21 | } 22 | 23 | /** 24 | * @psalm-suppress ImpureMethodCall 25 | */ 26 | public function body(): array 27 | { 28 | return [ 29 | 'success' => false, 30 | 'error' => $this->error->jsonSerialize(), 31 | ]; 32 | } 33 | 34 | public function status(): int 35 | { 36 | return $this->status; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /code/src/Crossword/Features/Languages/Response/HttpStatusCode.php: -------------------------------------------------------------------------------- 1 | data = $data; 18 | $this->status = $status; 19 | } 20 | 21 | public function body(): array 22 | { 23 | return [ 24 | 'success' => true, 25 | 'data' => $this->data, 26 | ]; 27 | } 28 | 29 | public function status(): int 30 | { 31 | return $this->status; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /code/src/Crossword/Features/Receiver/CrosswordNotFoundException.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 18 | $this->readCrosswordRepository = $readCrosswordRepository; 19 | } 20 | 21 | /** 22 | * @throws ReceiveCrosswordException 23 | */ 24 | public function receive(string $key): array 25 | { 26 | try { 27 | return $this->readCrosswordRepository->get($key); 28 | } catch (Throwable $exception) { 29 | $this->logger->error($exception->getMessage()); 30 | } 31 | 32 | throw new ReceiveCrosswordException(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /code/src/Crossword/Features/Receiver/ReadCrosswordRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | code = $code; 21 | $this->message = $message; 22 | $this->data = $data; 23 | } 24 | 25 | public function jsonSerialize(): array 26 | { 27 | return [ 28 | 'code' => $this->code, 29 | 'message' => $this->message, 30 | 'data' => $this->data, 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /code/src/Crossword/Features/Receiver/Response/Error/ErrorFactory.php: -------------------------------------------------------------------------------- 1 | status = $status; 20 | $this->error = $error; 21 | } 22 | 23 | /** 24 | * @psalm-suppress ImpureMethodCall 25 | */ 26 | public function body(): array 27 | { 28 | return [ 29 | 'success' => false, 30 | 'error' => $this->error->jsonSerialize(), 31 | ]; 32 | } 33 | 34 | public function status(): int 35 | { 36 | return $this->status; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /code/src/Crossword/Features/Receiver/Response/HttpStatusCode.php: -------------------------------------------------------------------------------- 1 | data = $data; 18 | $this->status = $status; 19 | } 20 | 21 | public function body(): array 22 | { 23 | return [ 24 | 'success' => true, 25 | 'data' => $this->data, 26 | ]; 27 | } 28 | 29 | public function status(): int 30 | { 31 | return $this->status; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /code/src/Crossword/Features/Receiver/Type/Type.php: -------------------------------------------------------------------------------- 1 | data = $data; 18 | $this->status = $status; 19 | } 20 | 21 | public function body(): array 22 | { 23 | return [ 24 | 'success' => true, 25 | 'data' => $this->data, 26 | ]; 27 | } 28 | 29 | public function status(): int 30 | { 31 | return $this->status; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /code/src/Crossword/Features/Types/SupportedTypes.php: -------------------------------------------------------------------------------- 1 | receive()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /code/src/Crossword/Infrastructure/HttpClient/ResponseDataExtractor.php: -------------------------------------------------------------------------------- 1 | getBody(); 14 | 15 | return (array) json_decode($body->getContents(), true); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /code/src/Crossword/Infrastructure/HttpClient/ResponseDataExtractorInterface.php: -------------------------------------------------------------------------------- 1 | cacheItemPool = $cacheItemPool; 19 | } 20 | 21 | public function save(string $key, CrosswordDto $crosswordDto): void 22 | { 23 | $this->cacheItemPool->save(new CacheItem($key, json_encode($crosswordDto, JSON_THROW_ON_ERROR))); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /code/src/Crossword/Infrastructure/Repository/Redis/ReadCrosswordRepository.php: -------------------------------------------------------------------------------- 1 | cacheItemPool = $cacheItemPool; 18 | } 19 | 20 | public function get(string $key): array 21 | { 22 | $item = $this->cacheItemPool->getItem($key); 23 | if ($item->isHit()) { 24 | $list = array_values($item->get()); 25 | shuffle($list); 26 | 27 | return json_decode(array_shift($list), true, 512, JSON_THROW_ON_ERROR); 28 | } 29 | 30 | throw new CrosswordNotFoundException(sprintf('Crossword not found in the storage. Search key: "%s"', $key)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /code/src/Crossword/README.md: -------------------------------------------------------------------------------- 1 | # Crossword 2 | 3 | Used to build crosswords and store them in the cache. Redis - used as cache storage. 4 | 5 | ### Rest Api 6 | 7 | | Path | Method | Scheme | Grant | 8 | | ------------------------------------------------------- | -------| ------ | ----- | 9 | | /api/crossword/construct/{LANGUAGE}/{TYPE}/{WORD-COUNT} | GET | ANY | ALL | 10 | | /api/crossword/languages | GET | ANY | ALL | 11 | | /api/crossword/types | GET | ANY | ALL | 12 | 13 | #### Response formats 14 | 15 | * `json` 16 | * `xml` 17 | 18 | ### Commands 19 | 20 | Used to generate a new crossword: 21 | 22 | ``` 23 | php bin/console crossword:generate {type} {language} {WORD-COUNT} --{LIMIT} 24 | ``` 25 | 26 | ## Author 27 | [Dykyi Roman](https://www.linkedin.com/in/roman-dykyi-43428543/), e-mail: [mr.dukuy@gmail.com](mailto:mr.dukuy@gmail.com) 28 | -------------------------------------------------------------------------------- /code/src/Crossword/config/depfile.yaml: -------------------------------------------------------------------------------- 1 | paths: 2 | - ../Features/ 3 | exclude_files: ~ 4 | layers: 5 | - name: Constructor 6 | collectors: 7 | - type: bool 8 | must: 9 | - type: className 10 | regex: App\\Crossword\\Features\\Constructor\\.* 11 | must_not: 12 | - type: className 13 | regex: App\\Crossword\\Features\\Constructor\\Message\\GenerateCrosswordMessage 14 | 15 | - name: Generator 16 | collectors: 17 | - type: className 18 | regex: App\\Crossword\\Features\\Generator\\.* 19 | 20 | - name: Languages 21 | collectors: 22 | - type: className 23 | regex: App\\Crossword\\Features\\Languages\\.* 24 | 25 | - name: Receiver 26 | collectors: 27 | - type: className 28 | regex: App\\Crossword\\Features\\Receiver\\.* 29 | 30 | - name: Types 31 | collectors: 32 | - type: className 33 | regex: App\\Crossword\\Features\\Types\\.* 34 | 35 | ruleset: 36 | Constructor: ~ 37 | Generator: ~ 38 | Languages: ~ 39 | Receiver: ~ 40 | Types: ~ -------------------------------------------------------------------------------- /code/src/Crossword/config/ecs.php: -------------------------------------------------------------------------------- 1 | messageBusMockWithConsecutive( 24 | self::once(), 25 | new GenerateCrosswordMessage('en', 'normal', 3) 26 | ) 27 | ); 28 | 29 | $crosswordGenerator->generate(new GenerateCriteria('normal', 'en', 3, 1)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /code/src/Crossword/tests/Features/Languages/Response/SuccessResponseTest.php: -------------------------------------------------------------------------------- 1 | status(), HttpStatusCode::HTTP_OK); 25 | } 26 | 27 | /** 28 | * @covers ::body 29 | */ 30 | public function testSuccessResponseBody(): void 31 | { 32 | $data = ['test' => Factory::create()->word]; 33 | 34 | $response = new SuccessApiResponse($data); 35 | 36 | self::assertSame($response->body()['data'], $data); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /code/src/Crossword/tests/Features/Receiver/Request/RequestAssertTest.php: -------------------------------------------------------------------------------- 1 | expectException(RequestException::class); 23 | 24 | RequestAssert::missingRequest(null); 25 | } 26 | 27 | /** 28 | * @covers ::missingRequest 29 | */ 30 | public function testSuccessfullyRequest(): void 31 | { 32 | RequestAssert::missingRequest(new Request()); 33 | 34 | self::assertTrue(true); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /code/src/Crossword/tests/Features/Receiver/Response/SuccessResponseTest.php: -------------------------------------------------------------------------------- 1 | status(), HttpStatusCode::HTTP_OK); 25 | } 26 | 27 | /** 28 | * @covers ::body 29 | */ 30 | public function testSuccessResponseBody(): void 31 | { 32 | $data = ['test' => Factory::create()->word]; 33 | 34 | $response = new SuccessApiResponse($data); 35 | 36 | self::assertSame($response->body()['data'], $data); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /code/src/Crossword/tests/Features/Receiver/Type/TypeAssertTest.php: -------------------------------------------------------------------------------- 1 | expectException(RuntimeException::class); 34 | 35 | TypeAssert::assertSupportedType('test'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /code/src/Crossword/tests/Features/Types/Response/SuccessResponseTest.php: -------------------------------------------------------------------------------- 1 | status(), HttpStatusCode::HTTP_OK); 25 | } 26 | 27 | /** 28 | * @covers ::body 29 | */ 30 | public function testSuccessResponseBody(): void 31 | { 32 | $data = ['test' => Factory::create()->word]; 33 | 34 | $response = new SuccessApiResponse($data); 35 | 36 | self::assertSame($response->body()['data'], $data); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /code/src/Crossword/tests/Infrastructure/HttpClient/ResponseDataExtractorTest.php: -------------------------------------------------------------------------------- 1 | 'test']; 22 | 23 | $response = new Response(200, ['Content-Type' => 'application/json'], json_encode($body)); 24 | $responseDataExtractor = new ResponseDataExtractor(); 25 | 26 | self::assertSame($responseDataExtractor->extract($response), $body); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /code/src/Crossword/tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | bootEnv(dirname(__DIR__) . '/../../.env'); 11 | } 12 | -------------------------------------------------------------------------------- /code/src/Dictionary/Features/Languages/Response/Error/ErrorCriteria.php: -------------------------------------------------------------------------------- 1 | code = $code; 21 | $this->message = $message; 22 | $this->data = $data; 23 | } 24 | 25 | public function jsonSerialize(): array 26 | { 27 | return [ 28 | 'code' => $this->code, 29 | 'message' => $this->message, 30 | 'data' => $this->data, 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /code/src/Dictionary/Features/Languages/Response/Error/ErrorFactory.php: -------------------------------------------------------------------------------- 1 | status = $status; 20 | $this->error = $error; 21 | } 22 | 23 | /** 24 | * @psalm-suppress ImpureMethodCall 25 | */ 26 | public function body(): array 27 | { 28 | return [ 29 | 'success' => false, 30 | 'error' => $this->error->jsonSerialize(), 31 | ]; 32 | } 33 | 34 | public function status(): int 35 | { 36 | return $this->status; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /code/src/Dictionary/Features/Languages/Response/HttpStatusCode.php: -------------------------------------------------------------------------------- 1 | data = $data; 18 | $this->status = $status; 19 | } 20 | 21 | public function body(): array 22 | { 23 | return [ 24 | 'success' => true, 25 | 'data' => $this->data, 26 | ]; 27 | } 28 | 29 | public function status(): int 30 | { 31 | return $this->status; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /code/src/Dictionary/Features/Languages/Storage/LanguageStorageInterface.php: -------------------------------------------------------------------------------- 1 | readWordsStorage = $readWordsStorage; 16 | } 17 | 18 | public function languages(): array 19 | { 20 | return $this->readWordsStorage->language(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /code/src/Dictionary/Features/PopulateStorage/FileReader/FileReaderInterface.php: -------------------------------------------------------------------------------- 1 | word = $word; 20 | $this->language = $language; 21 | } 22 | 23 | public function word(): string 24 | { 25 | return $this->word; 26 | } 27 | 28 | public function language(): string 29 | { 30 | return $this->language; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /code/src/Dictionary/Features/PopulateStorage/Populate/Port/DefinitionNotFoundInApiGateway.php: -------------------------------------------------------------------------------- 1 | language = $language; 20 | $this->filePath = $filePath; 21 | } 22 | 23 | public function language(): string 24 | { 25 | return $this->language; 26 | } 27 | 28 | public function filePath(): string 29 | { 30 | return $this->filePath; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /code/src/Dictionary/Features/PopulateStorage/SaveStorage/Message/SaveToStorageMessage.php: -------------------------------------------------------------------------------- 1 | word = $word; 23 | $this->language = $language; 24 | $this->definition = $definition; 25 | } 26 | 27 | public function language(): string 28 | { 29 | return $this->language; 30 | } 31 | 32 | public function word(): Word 33 | { 34 | return new Word($this->word, $this->definition); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /code/src/Dictionary/Features/PopulateStorage/SaveStorage/Message/SaveToStorageMessageHandler.php: -------------------------------------------------------------------------------- 1 | writeWordsStorage = $writeWordsStorage; 17 | } 18 | 19 | public function __invoke(SaveToStorageMessage $message): void 20 | { 21 | $this->writeWordsStorage->save($message->language(), $message->word()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /code/src/Dictionary/Features/PopulateStorage/SaveStorage/Storage/FailedSaveToStorageException.php: -------------------------------------------------------------------------------- 1 | value = $value; 20 | $this->definition = $definition; 21 | } 22 | 23 | public function value(): string 24 | { 25 | return $this->value; 26 | } 27 | 28 | public function definition(): string 29 | { 30 | return $this->definition; 31 | } 32 | 33 | public function length(): int 34 | { 35 | return strlen($this->value); 36 | } 37 | 38 | public function jsonSerialize(): array 39 | { 40 | return [ 41 | 'word' => $this->value, 42 | 'definition' => $this->definition, 43 | 'length' => $this->length(), 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /code/src/Dictionary/Features/PopulateStorage/Upload/FileAssert.php: -------------------------------------------------------------------------------- 1 | filePath = $filePath; 19 | } 20 | 21 | public function filePath(): string 22 | { 23 | return $this->filePath; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /code/src/Dictionary/Features/WordsFinder/Mask/SearchMaskIsShortException.php: -------------------------------------------------------------------------------- 1 | requestStack = $requestStack; 18 | } 19 | 20 | public function mask(): string 21 | { 22 | RequestAssert::missingRequest($request = $this->requestStack->getCurrentRequest()); 23 | 24 | $query = $request->query; 25 | 26 | return (string) $query->get('mask', ''); 27 | } 28 | 29 | public function language(): string 30 | { 31 | RequestAssert::missingRequest($request = $this->requestStack->getCurrentRequest()); 32 | 33 | return (string) $request->get('language', 'en'); 34 | } 35 | 36 | public function limit(): int 37 | { 38 | RequestAssert::missingRequest($request = $this->requestStack->getCurrentRequest()); 39 | 40 | $headers = $request->headers; 41 | 42 | return (int) $headers->get('X-LIMIT', (string) self::LIMIT); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /code/src/Dictionary/Features/WordsFinder/Response/Error/ErrorCriteria.php: -------------------------------------------------------------------------------- 1 | code = $code; 21 | $this->message = $message; 22 | $this->data = $data; 23 | } 24 | 25 | public function jsonSerialize(): array 26 | { 27 | return [ 28 | 'code' => $this->code, 29 | 'message' => $this->message, 30 | 'data' => $this->data, 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /code/src/Dictionary/Features/WordsFinder/Response/Error/ErrorFactory.php: -------------------------------------------------------------------------------- 1 | status = $status; 20 | $this->error = $error; 21 | } 22 | 23 | /** 24 | * @psalm-suppress ImpureMethodCall 25 | */ 26 | public function body(): array 27 | { 28 | return [ 29 | 'success' => false, 30 | 'error' => $this->error->jsonSerialize(), 31 | ]; 32 | } 33 | 34 | public function status(): int 35 | { 36 | return $this->status; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /code/src/Dictionary/Features/WordsFinder/Response/HttpStatusCode.php: -------------------------------------------------------------------------------- 1 | data = $data; 18 | $this->status = $status; 19 | } 20 | 21 | public function body(): array 22 | { 23 | return [ 24 | 'success' => true, 25 | 'data' => $this->data, 26 | ]; 27 | } 28 | 29 | public function status(): int 30 | { 31 | return $this->status; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /code/src/Dictionary/Features/WordsFinder/Storage/ReadWordsStorageInterface.php: -------------------------------------------------------------------------------- 1 | value = $value; 20 | $this->definition = $definition; 21 | } 22 | 23 | public function value(): string 24 | { 25 | return $this->value; 26 | } 27 | 28 | public function definition(): string 29 | { 30 | return $this->definition; 31 | } 32 | 33 | public function length(): int 34 | { 35 | return strlen($this->value); 36 | } 37 | 38 | public function jsonSerialize(): array 39 | { 40 | return [ 41 | 'word' => $this->value, 42 | 'definition' => $this->definition, 43 | 'length' => $this->length(), 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /code/src/Dictionary/Features/WordsFinder/Word/WordDto.php: -------------------------------------------------------------------------------- 1 | word = $word; 20 | $this->language = $language; 21 | } 22 | 23 | public function language(): string 24 | { 25 | return $this->language; 26 | } 27 | 28 | public function word(): Word 29 | { 30 | return $this->word; 31 | } 32 | 33 | /** 34 | * @psalm-suppress ImpureFunctionCall 35 | */ 36 | public function jsonSerialize(): array 37 | { 38 | return array_merge( 39 | [ 40 | 'language' => $this->language, 41 | ], 42 | $this->word->jsonSerialize() 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /code/src/Dictionary/Features/WordsFinder/Word/WordDtoCollection.php: -------------------------------------------------------------------------------- 1 | words = $words; 23 | } 24 | 25 | public function count(): int 26 | { 27 | return count($this->words); 28 | } 29 | 30 | /** 31 | * @psalm-suppress ImpureFunctionCall 32 | */ 33 | public function jsonSerialize(): array 34 | { 35 | return array_map(static fn (WordDto $word): array => $word->jsonSerialize(), $this->words); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /code/src/Dictionary/Features/WordsFinder/WordsFinder.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 21 | $this->wordsStorage = $readWordsStorage; 22 | } 23 | 24 | public function find(string $language, string $mask, int $limit): WordDtoCollection 25 | { 26 | try { 27 | return $this->wordsStorage->search($language, new Mask($mask), $limit); 28 | } catch (WordNotFoundInStorageException $exception) { 29 | $this->logger->error($exception->getMessage()); 30 | 31 | throw new NotFoundWordException(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /code/src/Dictionary/Infrastructure/FileReader/CsvFileReader.php: -------------------------------------------------------------------------------- 1 | next = $next; 17 | 18 | return $next; 19 | } 20 | 21 | public function search(string $word, string $language): string 22 | { 23 | if ($this->next instanceof WordDefinitionApiGatewayInterface) { 24 | return $this->next->search($word, $language); 25 | } 26 | 27 | throw new DefinitionNotFoundInApiGateway($word, $language); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /code/src/Dictionary/Infrastructure/Gateway/Google/GoogleWordDefinitionDto.php: -------------------------------------------------------------------------------- 1 | payload = $payload; 17 | } 18 | 19 | public function word(): ?string 20 | { 21 | if (array_key_exists('resolution', $this->payload) && array_key_exists('message', $this->payload)) { 22 | return null; 23 | } 24 | 25 | return $this->payload[0]['meanings'][0]['definitions'][0]['definition']; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /code/src/Dictionary/Infrastructure/Gateway/InMemory/WordDefinitionApiGatewayInMemory.php: -------------------------------------------------------------------------------- 1 | definition = $definition; 17 | } 18 | 19 | public function search(string $word, string $language): string 20 | { 21 | if ('' === $this->definition) { 22 | throw new DefinitionNotFoundInApiGateway($word, $language); 23 | } 24 | 25 | return $this->definition; 26 | } 27 | 28 | public function setNext(WordDefinitionApiGatewayInterface $apiGateway): WordDefinitionApiGatewayInterface 29 | { 30 | throw new DefinitionNotFoundInApiGateway('test-word', 'test-language'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /code/src/Dictionary/Infrastructure/Gateway/Wikipedia/WikipediaWordDefinitionDto.php: -------------------------------------------------------------------------------- 1 | payload = $payload; 17 | } 18 | 19 | public function word(): ?string 20 | { 21 | $pages = array_values($this->payload['query']['pages']); 22 | $text = $pages[0]['extract']; 23 | if ($text && ((false === stripos($text, 'refer to')) || (false === stripos($text, 'refers to')))) { 24 | return explode('.', $text)[0]; 25 | } 26 | 27 | return null; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /code/src/Dictionary/Infrastructure/HttpClient/ResponseDataExtractor.php: -------------------------------------------------------------------------------- 1 | getBody(); 14 | 15 | return (array) json_decode($body->getContents(), true); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /code/src/Dictionary/Infrastructure/HttpClient/ResponseDataExtractorInterface.php: -------------------------------------------------------------------------------- 1 | elasticHosts = $elasticHosts; 17 | } 18 | 19 | public function create(): Client 20 | { 21 | return ClientBuilder::create()->setHosts($this->elasticHosts)->build(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /code/src/Dictionary/Infrastructure/Repository/Elastic/StorageWordDto.php: -------------------------------------------------------------------------------- 1 | attributes = $attributes; 17 | } 18 | 19 | public function language(): string 20 | { 21 | return $this->attributes['_index']; 22 | } 23 | 24 | public function word(): string 25 | { 26 | return $this->attributes['_source']['word']; 27 | } 28 | 29 | public function definition(): string 30 | { 31 | return $this->attributes['_source']['definition']; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /code/src/Dictionary/Infrastructure/Repository/Elastic/StorageWordDtoCollection.php: -------------------------------------------------------------------------------- 1 | words = array_map( 23 | static fn (array $attributes): StorageWordDto => new StorageWordDto($attributes), 24 | $words 25 | ); 26 | } 27 | 28 | public function getIterator(): ArrayIterator 29 | { 30 | return new ArrayIterator($this->words); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /code/src/Dictionary/README.md: -------------------------------------------------------------------------------- 1 | # Dictionary 2 | 3 | It is used as the main storage for words and their definitions. 4 | Elasticsearch - used as storage, it allows store and search word by differences masks. 5 | 6 | ### Rest Api 7 | 8 | | Path | Method | Scheme | Grant | 9 | | ------------------------------------------------ | -------| ------ | ----- | 10 | | /api/dictionary/languages | GET | ANY | ALL | 11 | | /api/dictionary/words/{LANGUAGE}?mask={MASK} | GET | ANY | ALL | 12 | 13 | #### Response formats 14 | 15 | * `json` 16 | * `xml` 17 | 18 | ### Commands 19 | 20 | Used to fill a dictionary with the help of a third party API providers: 21 | 22 | ``` 23 | php bin/console dictionary:populate {LANGUAGE-CODE} --{FILE-PATH} 24 | ``` 25 | 26 | Used to fill a dictionary from file: 27 | ``` 28 | php bin/console dictionary:upload {FILE-PATH} 29 | ``` 30 | 31 | Collections with words for populate can be found: ``cd /data`` 32 | 33 | ## Author 34 | [Dykyi Roman](https://www.linkedin.com/in/roman-dykyi-43428543/), e-mail: [mr.dukuy@gmail.com](mailto:mr.dukuy@gmail.com) 35 | -------------------------------------------------------------------------------- /code/src/Dictionary/config/depfile.yaml: -------------------------------------------------------------------------------- 1 | paths: 2 | - ../Features/ 3 | exclude_files: ~ 4 | layers: 5 | - name: PopulateStorage 6 | collectors: 7 | - type: className 8 | regex: App\\Dictionary\\Features\\PopulateStorage\\.* 9 | - name: Languages 10 | collectors: 11 | - type: className 12 | regex: App\\Dictionary\\Features\\Languages\\.* 13 | 14 | - name: WordsFinder 15 | collectors: 16 | - type: className 17 | regex: App\\Dictionary\\Features\\WordsFinder\\.* 18 | 19 | ruleset: 20 | PopulateStorage: ~ 21 | Languages: ~ 22 | WordsFinder: ~ -------------------------------------------------------------------------------- /code/src/Dictionary/config/ecs.php: -------------------------------------------------------------------------------- 1 | status(), HttpStatusCode::HTTP_OK); 25 | } 26 | 27 | /** 28 | * @covers ::body 29 | */ 30 | public function testSuccessResponseBody(): void 31 | { 32 | $data = ['test' => Factory::create()->word]; 33 | 34 | $response = new SuccessApiResponse($data); 35 | 36 | self::assertSame($response->body()['data'], $data); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /code/src/Dictionary/tests/Features/WordsFinder/Request/RequestAssertTest.php: -------------------------------------------------------------------------------- 1 | expectException(RequestException::class); 23 | 24 | RequestAssert::missingRequest(null); 25 | } 26 | 27 | /** 28 | * @covers ::missingRequest 29 | */ 30 | public function testSuccessfullyRequest(): void 31 | { 32 | RequestAssert::missingRequest(new Request()); 33 | 34 | self::assertTrue(true); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /code/src/Dictionary/tests/Features/WordsFinder/Response/SuccessResponseTest.php: -------------------------------------------------------------------------------- 1 | status(), HttpStatusCode::HTTP_OK); 25 | } 26 | 27 | /** 28 | * @covers ::body 29 | */ 30 | public function testSuccessResponseBody(): void 31 | { 32 | $data = ['test' => Factory::create()->word]; 33 | 34 | $response = new SuccessApiResponse($data); 35 | 36 | self::assertSame($response->body()['data'], $data); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /code/src/Dictionary/tests/Infrastructure/HttpClient/ResponseDataExtractorTest.php: -------------------------------------------------------------------------------- 1 | 'test']; 22 | 23 | $response = new Response(200, ['Content-Type' => 'application/json'], json_encode($body)); 24 | $responseDataExtractor = new ResponseDataExtractor(); 25 | 26 | self::assertSame($responseDataExtractor->extract($response), $body); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /code/src/Dictionary/tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | bootEnv(dirname(__DIR__) . '/../../.env'); 11 | } 12 | -------------------------------------------------------------------------------- /code/src/Game/Features/Answers/Answers.php: -------------------------------------------------------------------------------- 1 | answersValidator = $answersValidator; 22 | $this->playerFromTokenExtractor = $playerFromTokenExtractor; 23 | $this->messageBus = $messageBus; 24 | } 25 | 26 | public function __invoke(array $payload) 27 | { 28 | $player = $this->playerFromTokenExtractor->player(); 29 | $this->answersValidator->validate($payload); 30 | 31 | $this->messageBus->dispatch(new CrosswordPuzzleSolvedEvent($player->id(), $player->level())); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /code/src/Game/Features/Answers/AnswersValidator.php: -------------------------------------------------------------------------------- 1 | encoder = $encoder; 14 | } 15 | 16 | /** 17 | * @throws WrongAnswerException 18 | */ 19 | public function validate(array $answers): void 20 | { 21 | $result = []; 22 | foreach ($answers as $item) { 23 | $numbers = explode('/', (string) $item['index']); 24 | foreach ($numbers as $number) { 25 | $result[$number]['l'][] = $this->encoder->decode($item['letter']); 26 | $result[$number]['v'][] = '' === $item['value'] ? '?' : $item['value']; 27 | } 28 | } 29 | 30 | CorrectAnswersAssert::assertCorrectAnswers($result); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /code/src/Game/Features/Answers/Authentication/PlayerNotFoundInTokenStorageException.php: -------------------------------------------------------------------------------- 1 | answers()); 19 | 20 | return new SuccessApiResponse(); 21 | } catch (WrongAnswerException $exception) { 22 | return new FailedApiResponse(ErrorFactory::wrongAnswers($exception->rightAnswers())); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /code/src/Game/Features/Answers/CorrectAnswersAssert.php: -------------------------------------------------------------------------------- 1 | strtolower(implode('', $item['l'])), 20 | $answers 21 | ); 22 | 23 | throw new WrongAnswerException($correct); 24 | } 25 | } 26 | } 27 | 28 | private static function compareAnswers(array $right, array $answer): bool 29 | { 30 | return self::splitToLine($right) !== self::splitToLine($answer); 31 | } 32 | 33 | private static function splitToLine(array $data): string 34 | { 35 | return strtolower(implode('', $data)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /code/src/Game/Features/Answers/CrosswordPuzzleSolvedEvent.php: -------------------------------------------------------------------------------- 1 | level = $level; 18 | $this->playerId = $playerId->id(); 19 | } 20 | 21 | public function playerId(): UuidV4 22 | { 23 | return $this->playerId; 24 | } 25 | 26 | public function level(): int 27 | { 28 | return $this->level; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /code/src/Game/Features/Answers/LetterEncoderInterface.php: -------------------------------------------------------------------------------- 1 | playerId = $playerId; 15 | $this->level = $level; 16 | } 17 | 18 | public function level(): int 19 | { 20 | return $this->level; 21 | } 22 | 23 | public function id(): PlayerId 24 | { 25 | return $this->playerId; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /code/src/Game/Features/Answers/Player/PlayerId.php: -------------------------------------------------------------------------------- 1 | id = $id ?? UuidV4::v4(); 20 | } 21 | 22 | public function id(): UuidV4 23 | { 24 | return $this->id; 25 | } 26 | 27 | public function equals(self $anId): bool 28 | { 29 | return $this->id === $anId->id(); 30 | } 31 | 32 | /** 33 | * @psalm-suppress ImpureMethodCall 34 | */ 35 | public function __toString(): string 36 | { 37 | return $this->id->toRfc4122(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /code/src/Game/Features/Answers/Request/AnswersRequest.php: -------------------------------------------------------------------------------- 1 | requestStack = $requestStack; 16 | } 17 | 18 | public function answers(): array 19 | { 20 | RequestAssert::missingRequest($request = $this->requestStack->getCurrentRequest()); 21 | 22 | return (array) json_decode($request->getContent(), true); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /code/src/Game/Features/Answers/Request/RequestAssert.php: -------------------------------------------------------------------------------- 1 | code = $code; 21 | $this->message = $message; 22 | $this->data = $data; 23 | } 24 | 25 | public function jsonSerialize(): array 26 | { 27 | return [ 28 | 'code' => $this->code, 29 | 'message' => $this->message, 30 | 'data' => $this->data, 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /code/src/Game/Features/Answers/Response/Error/ErrorFactory.php: -------------------------------------------------------------------------------- 1 | status = $status; 20 | $this->error = $error; 21 | } 22 | 23 | /** 24 | * @psalm-suppress ImpureMethodCall 25 | */ 26 | public function body(): array 27 | { 28 | return [ 29 | 'success' => false, 30 | 'error' => $this->error->jsonSerialize(), 31 | ]; 32 | } 33 | 34 | public function status(): int 35 | { 36 | return $this->status; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /code/src/Game/Features/Answers/Response/HttpStatusCode.php: -------------------------------------------------------------------------------- 1 | data = $data; 18 | $this->status = $status; 19 | } 20 | 21 | public function body(): array 22 | { 23 | return [ 24 | 'success' => true, 25 | 'data' => $this->data, 26 | ]; 27 | } 28 | 29 | public function status(): int 30 | { 31 | return $this->status; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /code/src/Game/Features/Answers/Twig/EncodeExtension.php: -------------------------------------------------------------------------------- 1 | encoder = $encoder; 18 | } 19 | 20 | public function getFilters(): array 21 | { 22 | return [ 23 | new TwigFilter('base64', [$this, 'encode']), 24 | ]; 25 | } 26 | 27 | public function encode(string $value): string 28 | { 29 | return $this->encoder->encode($value); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /code/src/Game/Features/Answers/WrongAnswerException.php: -------------------------------------------------------------------------------- 1 | rightAnswers = $rightAnswers; 17 | 18 | parent::__construct($message, $code, $previous); 19 | } 20 | 21 | public function rightAnswers(): array 22 | { 23 | return $this->rightAnswers; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /code/src/Game/Features/Authorization/PlayerAuthAction.php: -------------------------------------------------------------------------------- 1 | nickname(), $request->password()); 15 | 16 | return 'Player successfully logged!'; 17 | } catch (PlayerLoginException) { 18 | return 'Failed!'; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /code/src/Game/Features/Authorization/PlayerDto.php: -------------------------------------------------------------------------------- 1 | playerId = $playerId; 23 | $this->role = $role; 24 | $this->level = $level; 25 | $this->nickname = $nickname; 26 | } 27 | 28 | /** 29 | * @psalm-suppress ImpureMethodCall 30 | */ 31 | public function jsonSerialize(): array 32 | { 33 | return [ 34 | 'id' => $this->playerId->toRfc4122(), 35 | 'nickname' => $this->nickname, 36 | 'level' => $this->level, 37 | 'role' => $this->role, 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /code/src/Game/Features/Authorization/PlayerLoginException.php: -------------------------------------------------------------------------------- 1 | tokenStorage = $tokenStorage; 20 | $this->session = $session; 21 | } 22 | 23 | /** 24 | * @throws JsonException 25 | */ 26 | public function refresh(PlayerDto $playerDto): void 27 | { 28 | $token = new UsernamePasswordToken(json_encode($playerDto, JSON_THROW_ON_ERROR), null, 'main'); 29 | $this->tokenStorage->setToken($token); 30 | $this->session->set('_security_main', serialize($token)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /code/src/Game/Features/Authorization/RefreshPlayerInTokenEventHandler.php: -------------------------------------------------------------------------------- 1 | playerToken = $playerToken; 21 | $this->readPlayerRepository = $readPlayerRepository; 22 | } 23 | 24 | /** 25 | * @throws JsonException 26 | */ 27 | public function __invoke(LevelUpEvent $event): void 28 | { 29 | $playerDto = $this->readPlayerRepository->findPlayerById(UuidV4::fromString($event->playerId())); 30 | $this->playerToken->refresh($playerDto); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /code/src/Game/Features/Authorization/Repository/PlayerNotFoundException.php: -------------------------------------------------------------------------------- 1 | requestStack = $requestStack; 16 | } 17 | 18 | public function nickname(): string 19 | { 20 | RequestAssert::missingRequest($request = $this->requestStack->getCurrentRequest()); 21 | 22 | $post = $request->request; 23 | 24 | return (string) $post->get('nickname'); 25 | } 26 | 27 | public function password(): string 28 | { 29 | RequestAssert::missingRequest($request = $this->requestStack->getCurrentRequest()); 30 | 31 | $post = $request->request; 32 | 33 | return (string) $post->get('password'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /code/src/Game/Features/Authorization/Request/RequestAssert.php: -------------------------------------------------------------------------------- 1 | render('@game/login.html.twig'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /code/src/Game/Features/GamePlay/Authentication/PlayerNotFoundInTokenStorageException.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 17 | $this->crossword = $crossword; 18 | } 19 | 20 | /** 21 | * @throws CrosswordNotConstructedException 22 | */ 23 | public function construct(CrosswordCriteria $criteria): array 24 | { 25 | try { 26 | $crosswordDto = $this->crossword->construct($criteria); 27 | 28 | return $crosswordDto->count() ? $crosswordDto->crossword() : []; 29 | } catch (ApiClientException $exception) { 30 | $this->logger->error($exception->getMessage()); 31 | } 32 | 33 | throw new CrosswordNotConstructedException(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /code/src/Game/Features/GamePlay/Crossword/CrosswordCriteria.php: -------------------------------------------------------------------------------- 1 | type = $type; 16 | $this->language = $language; 17 | $this->wordCount = $wordCount; 18 | } 19 | 20 | public function language(): string 21 | { 22 | return $this->language; 23 | } 24 | 25 | public function type(): string 26 | { 27 | return $this->type; 28 | } 29 | 30 | public function wordCount(): int 31 | { 32 | return $this->wordCount; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /code/src/Game/Features/GamePlay/Crossword/CrosswordDto.php: -------------------------------------------------------------------------------- 1 | payload = $payload; 19 | } 20 | 21 | public function count(): int 22 | { 23 | return count($this->crossword()); 24 | } 25 | 26 | public function crossword(): array 27 | { 28 | return $this->isSuccess() ? $this->payload['data'] : []; 29 | } 30 | 31 | private function isSuccess(): bool 32 | { 33 | return (bool) $this->payload['success']; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /code/src/Game/Features/GamePlay/Crossword/CrosswordInterface.php: -------------------------------------------------------------------------------- 1 | grid = $grid; 19 | $this->size = $size; 20 | $this->definitions = $definitions; 21 | } 22 | 23 | public function grid(): array 24 | { 25 | return $this->grid; 26 | } 27 | 28 | public function size(): int 29 | { 30 | return $this->size; 31 | } 32 | 33 | public function definitions(): array 34 | { 35 | return $this->definitions; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /code/src/Game/Features/GamePlay/Crossword/Type.php: -------------------------------------------------------------------------------- 1 | getValue()) { 17 | Role::SIMPLE_PLAYER => self::NORMAL, 18 | Role::PREMIUM_PLAYER => self::FIGURED, 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /code/src/Game/Features/GamePlay/NewGameAction.php: -------------------------------------------------------------------------------- 1 | extractor = $extractor; 19 | } 20 | 21 | public function __invoke(GamePlay $game, Environment $twig): string 22 | { 23 | try { 24 | $playerDto = $this->extractor->player(); 25 | } catch (PlayerNotFoundInTokenStorageException) { 26 | return 'Login session is over.'; 27 | } 28 | 29 | $gameDto = $game->new('en', Type::byRole($playerDto->role()), $playerDto->level() * 3); 30 | 31 | return $twig->render('@game/play.html.twig', [ 32 | 'player' => $playerDto, 33 | 'game' => $gameDto, 34 | ]); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /code/src/Game/Features/GamePlay/Player/PlayerId.php: -------------------------------------------------------------------------------- 1 | id = $id ?? UuidV4::v4(); 20 | } 21 | 22 | public function id(): UuidV4 23 | { 24 | return $this->id; 25 | } 26 | 27 | public function equals(self $anId): bool 28 | { 29 | return $this->id === $anId->id(); 30 | } 31 | 32 | /** 33 | * @psalm-suppress ImpureMethodCall 34 | */ 35 | public function __toString(): string 36 | { 37 | return $this->id->toRfc4122(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /code/src/Game/Features/GamePlay/Player/Role.php: -------------------------------------------------------------------------------- 1 | id = $id ?? UuidV4::v4(); 20 | } 21 | 22 | public function id(): UuidV4 23 | { 24 | return $this->id; 25 | } 26 | 27 | public function equals(self $anId): bool 28 | { 29 | return $this->id === $anId->id(); 30 | } 31 | 32 | /** 33 | * @psalm-suppress ImpureMethodCall 34 | */ 35 | public function __toString(): string 36 | { 37 | return $this->id->toRfc4122(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /code/src/Game/Features/History/HistoryRatingDto.php: -------------------------------------------------------------------------------- 1 | level = $level; 20 | $this->nickname = $nickname; 21 | } 22 | 23 | public function level(): int 24 | { 25 | return $this->level; 26 | } 27 | 28 | public function nickname(): string 29 | { 30 | return $this->nickname; 31 | } 32 | 33 | public function jsonSerialize(): array 34 | { 35 | return [ 36 | 'nickname' => $this->nickname, 37 | 'level' => $this->level, 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /code/src/Game/Features/History/PersistHistoryRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | historyDao = $historyDao; 14 | } 15 | 16 | public function __invoke(): array 17 | { 18 | return array_map( 19 | static fn (HistoryRatingDto $dto): array => $dto->jsonSerialize(), 20 | $this->historyDao->ratingHistory() 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /code/src/Game/Features/History/PlayerId.php: -------------------------------------------------------------------------------- 1 | id = $id ?? UuidV4::v4(); 20 | } 21 | 22 | public function id(): UuidV4 23 | { 24 | return $this->id; 25 | } 26 | 27 | public function equals(self $anId): bool 28 | { 29 | return $this->id === $anId->id(); 30 | } 31 | 32 | /** 33 | * @psalm-suppress ImpureMethodCall 34 | */ 35 | public function __toString(): string 36 | { 37 | return $this->id->toRfc4122(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /code/src/Game/Features/History/PlayerRatingAction.php: -------------------------------------------------------------------------------- 1 | code = $code; 21 | $this->message = $message; 22 | $this->data = $data; 23 | } 24 | 25 | public function jsonSerialize(): array 26 | { 27 | return [ 28 | 'code' => $this->code, 29 | 'message' => $this->message, 30 | 'data' => $this->data, 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /code/src/Game/Features/History/Response/Error/ErrorFactory.php: -------------------------------------------------------------------------------- 1 | status = $status; 20 | $this->error = $error; 21 | } 22 | 23 | /** 24 | * @psalm-suppress ImpureMethodCall 25 | */ 26 | public function body(): array 27 | { 28 | return [ 29 | 'success' => false, 30 | 'error' => $this->error->jsonSerialize(), 31 | ]; 32 | } 33 | 34 | public function status(): int 35 | { 36 | return $this->status; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /code/src/Game/Features/History/Response/HttpStatusCode.php: -------------------------------------------------------------------------------- 1 | data = $data; 18 | $this->status = $status; 19 | } 20 | 21 | public function body(): array 22 | { 23 | return [ 24 | 'success' => true, 25 | 'data' => $this->data, 26 | ]; 27 | } 28 | 29 | public function status(): int 30 | { 31 | return $this->status; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /code/src/Game/Features/History/SaveHistoryEventHandler.php: -------------------------------------------------------------------------------- 1 | persistHistoryRepository = $persistHistoryRepository; 18 | } 19 | 20 | public function __invoke(LevelUpEvent $event): void 21 | { 22 | $this->persistHistoryRepository->createHistory( 23 | new HistoryId(), 24 | new PlayerId(UuidV4::fromString($event->playerId())), 25 | $event->level() 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /code/src/Game/Features/Player/AggregateRoot.php: -------------------------------------------------------------------------------- 1 | events; 17 | $this->events = []; 18 | 19 | return $events; 20 | } 21 | 22 | public function clearEvents(): void 23 | { 24 | $this->events = []; 25 | } 26 | 27 | public function raise(DomainEventInterface $event): void 28 | { 29 | $this->events[] = $event; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /code/src/Game/Features/Player/CrosswordPuzzleSolvedEventHandler.php: -------------------------------------------------------------------------------- 1 | playerLevelRepository = $playerLevelRepository; 20 | } 21 | 22 | public function __invoke(CrosswordPuzzleSolvedEvent $event) 23 | { 24 | if ($event->level() < Level::LAST_LEVEL) { 25 | $this->playerLevelRepository->changeLevel(new PlayerId($event->playerId())); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /code/src/Game/Features/Player/DomainEventInterface.php: -------------------------------------------------------------------------------- 1 | playerId = (string) $playerId; 18 | $this->level = $level; 19 | } 20 | 21 | public function level(): int 22 | { 23 | return $this->level; 24 | } 25 | 26 | public function playerId(): string 27 | { 28 | return $this->playerId; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /code/src/Game/Features/Player/Level/PlayerLevelRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | id = $id ?? UuidV4::v4(); 20 | } 21 | 22 | public function id(): UuidV4 23 | { 24 | return $this->id; 25 | } 26 | 27 | public function equals(self $anId): bool 28 | { 29 | return $this->id === $anId->id(); 30 | } 31 | 32 | /** 33 | * @psalm-suppress ImpureMethodCall 34 | */ 35 | public function __toString(): string 36 | { 37 | return $this->id->toRfc4122(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /code/src/Game/Features/Player/Player/PlayerNotFoundException.php: -------------------------------------------------------------------------------- 1 | id = $id ?? UuidV4::v4(); 20 | } 21 | 22 | public function id(): UuidV4 23 | { 24 | return $this->id; 25 | } 26 | 27 | public function equals(self $anId): bool 28 | { 29 | return $this->id === $anId->id(); 30 | } 31 | 32 | /** 33 | * @psalm-suppress ImpureMethodCall 34 | */ 35 | public function __toString(): string 36 | { 37 | return $this->id->toRfc4122(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /code/src/Game/Features/Registration/PlayerRegister.php: -------------------------------------------------------------------------------- 1 | playerRepository = $playerRepository; 17 | } 18 | 19 | public function execute(PlayerRegisterCriteria $criteria): void 20 | { 21 | $playerDto = new PlayerDto(new PlayerId(), $criteria->password(), $criteria->nickname(), $criteria->role()); 22 | $this->playerRepository->createPlayer($playerDto); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /code/src/Game/Features/Registration/PlayerRegisterCriteria.php: -------------------------------------------------------------------------------- 1 | nickname = $nickname; 23 | $this->password = $password; 24 | $this->role = new Role($role ?? Role::SIMPLE_PLAYER); 25 | } 26 | 27 | public function role(): Role 28 | { 29 | return $this->role; 30 | } 31 | 32 | public function nickname(): string 33 | { 34 | return $this->nickname; 35 | } 36 | 37 | public function password(): string 38 | { 39 | return $this->password; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /code/src/Game/Features/Registration/PlayerRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | crosswordReceiver = $crosswordReceiver; 19 | } 20 | 21 | public function construct(CrosswordCriteria $criteria): CrosswordDto 22 | { 23 | $key = sprintf('%s-%s-%d', $criteria->language(), $criteria->type(), $criteria->wordCount()); 24 | 25 | return new CrosswordDto([ 26 | 'success' => true, 27 | 'data' => $this->crosswordReceiver->receive($key), 28 | ]); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /code/src/Game/Infrastructure/Adapter/Crossword/InMemoryCrosswordAdapter.php: -------------------------------------------------------------------------------- 1 | crosswordDto = $crosswordDto; 19 | } 20 | 21 | public function construct(CrosswordCriteria $criteria): CrosswordDto 22 | { 23 | if (null === $this->crosswordDto) { 24 | throw ApiClientException::badRequest('test error message'); 25 | } 26 | 27 | return $this->crosswordDto; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /code/src/Game/Infrastructure/Encoder/Base64Encoder.php: -------------------------------------------------------------------------------- 1 | encodePassword($raw, $salt); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /code/src/Game/Infrastructure/Encoder/PasswordEncoderInterface.php: -------------------------------------------------------------------------------- 1 | getBody(); 14 | 15 | return (array) json_decode($body->getContents(), true); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /code/src/Game/Infrastructure/HttpClient/ResponseDataExtractorInterface.php: -------------------------------------------------------------------------------- 1 | playerId()->id(), 16 | $player->nickname(), 17 | $player->level(), 18 | $player->role()->getValue() 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /code/src/Game/Infrastructure/Repository/Doctrine/ReadHistoryRepository.php: -------------------------------------------------------------------------------- 1 | getGuidTypeDeclarationSQL($column); 16 | } 17 | 18 | public function convertToPHPValue($value, AbstractPlatform $platform): HistoryId 19 | { 20 | return new HistoryId($value); 21 | } 22 | 23 | public function convertToDatabaseValue($value, AbstractPlatform $platform): string 24 | { 25 | $id = $value->id(); 26 | 27 | return $id->toRfc4122(); 28 | } 29 | 30 | public function getName(): string 31 | { 32 | return 'historyId'; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /code/src/Game/Infrastructure/Repository/DoctrineTypes/PlayerIdType.php: -------------------------------------------------------------------------------- 1 | getGuidTypeDeclarationSQL($column); 17 | } 18 | 19 | public function convertToPHPValue($value, AbstractPlatform $platform): PlayerId 20 | { 21 | return new PlayerId(UuidV4::fromString($value)); 22 | } 23 | 24 | public function convertToDatabaseValue($value, AbstractPlatform $platform): string 25 | { 26 | $id = $value->id(); 27 | 28 | return $id->toRfc4122(); 29 | } 30 | 31 | public function getName(): string 32 | { 33 | return 'playerId'; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /code/src/Game/README.md: -------------------------------------------------------------------------------- 1 | # Game 2 | 3 | It used for building a gameplay functionality. 4 | Responsible for creating players and managing their levels and roles. 5 | SQLite used as storage for players and his game history. 6 | 7 | | Game | Info | 8 | | ----------------- | -------------------- | 9 | | Levels | 5 - ...n | 10 | | Types | NORMAL / FIGURED | 11 | | Roles | SIMPLE / PREMIUM | 12 | | Languages | en, ru, ...n | 13 | 14 | ### Commands 15 | 16 | Used to create a new player with SIMPLE role: 17 | 18 | ``` 19 | php bin/console game:create-player test 1q2w3e4r 20 | ``` 21 | Used to create a new player with PREMIUM role: 22 | 23 | ``` 24 | php bin/console game:create-player test 1q2w3e4r --role=ROLE_PREMIUM 25 | ``` 26 | 27 | ### Web Url 28 | 29 | | Path | Method | Scheme | Grant | 30 | | ------------ | -------| ------ | ----- | 31 | | /game/play | GET | ANY | ALL | 32 | | /game/login | GET | ANY | ALL | 33 | 34 | ## Author 35 | [Dykyi Roman](https://www.linkedin.com/in/roman-dykyi-43428543/), e-mail: [mr.dukuy@gmail.com](mailto:mr.dukuy@gmail.com) 36 | -------------------------------------------------------------------------------- /code/src/Game/config/doctrine/history/History.orm.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /code/src/Game/config/doctrine/player/Player.orm.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /code/src/Game/config/ecs.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Login page 5 | 6 | 7 |
8 |

Login

9 | 10 | 11 | 12 | 13 | 14 | 17 |
18 | 19 | -------------------------------------------------------------------------------- /code/src/Game/tests/Features/Answers/Request/RequestAssertTest.php: -------------------------------------------------------------------------------- 1 | expectException(RequestException::class); 23 | 24 | RequestAssert::missingRequest(null); 25 | } 26 | 27 | /** 28 | * @covers ::missingRequest 29 | */ 30 | public function testSuccessfullyRequest(): void 31 | { 32 | RequestAssert::missingRequest(new Request()); 33 | 34 | self::assertTrue(true); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /code/src/Game/tests/Features/Answers/Response/SuccessResponseTest.php: -------------------------------------------------------------------------------- 1 | status(), HttpStatusCode::HTTP_OK); 25 | } 26 | 27 | /** 28 | * @covers ::body 29 | */ 30 | public function testSuccessResponseBody(): void 31 | { 32 | $data = ['test' => Factory::create()->word]; 33 | 34 | $response = new SuccessApiResponse($data); 35 | 36 | self::assertSame($response->body()['data'], $data); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /code/src/Game/tests/Features/Authorization/Request/RequestAssertTest.php: -------------------------------------------------------------------------------- 1 | expectException(RequestException::class); 23 | 24 | RequestAssert::missingRequest(null); 25 | } 26 | 27 | /** 28 | * @covers ::missingRequest 29 | */ 30 | public function testSuccessfullyRequest(): void 31 | { 32 | RequestAssert::missingRequest(new Request()); 33 | 34 | self::assertTrue(true); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /code/src/Game/tests/Features/History/PlayerHistoryTest.php: -------------------------------------------------------------------------------- 1 | changeLevel(2); 25 | 26 | $history2 = new History(new HistoryId()); 27 | $history2->changeLevel(1); 28 | 29 | $repository = new InMemoryHistoryDao($history, $history2); 30 | $playerHistory = new PlayerHistory($repository); 31 | 32 | $result = $playerHistory->__invoke(); 33 | 34 | self::assertCount(2, $result); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /code/src/Game/tests/Features/History/Response/SuccessResponseTest.php: -------------------------------------------------------------------------------- 1 | status(), HttpStatusCode::HTTP_OK); 25 | } 26 | 27 | /** 28 | * @covers ::body 29 | */ 30 | public function testSuccessResponseBody(): void 31 | { 32 | $data = ['test' => Factory::create()->word]; 33 | 34 | $response = new SuccessApiResponse($data); 35 | 36 | self::assertSame($response->body()['data'], $data); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /code/src/Game/tests/Features/Player/PlayerTest.php: -------------------------------------------------------------------------------- 1 | createPlayer(new PlayerId()); 21 | $player->changeLevel(4); 22 | 23 | self::assertCount(2, $player->popEvents()); 24 | } 25 | 26 | /** 27 | * @covers ::changeLevel 28 | */ 29 | public function testRaiseEventWhenPlayerCreated(): void 30 | { 31 | $player = $this->createPlayer(new PlayerId()); 32 | 33 | self::assertCount(1, $player->popEvents()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /code/src/Game/tests/Features/Registration/PlayerRegisterTest.php: -------------------------------------------------------------------------------- 1 | execute(new PlayerRegisterCriteria('test', '1q2w3e4r', null)); 25 | 26 | self::assertCount(1, $repository->players()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /code/src/Game/tests/Features/Registration/Role/RoleAssertTest.php: -------------------------------------------------------------------------------- 1 | expectException(InvalidArgumentException::class); 23 | 24 | RoleAssert::assertSupportedRole('test'); 25 | } 26 | /** 27 | * @covers ::assertSupportedRole 28 | */ 29 | public function testWithCorrectAnswer(): void 30 | { 31 | RoleAssert::assertSupportedRole(Role::PREMIUM_PLAYER); 32 | RoleAssert::assertSupportedRole(Role::SIMPLE_PLAYER); 33 | 34 | self::assertTrue(true); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /code/src/Game/tests/Infrastructure/HttpClient/ResponseDataExtractorTest.php: -------------------------------------------------------------------------------- 1 | 'test']; 22 | 23 | $response = new Response(200, ['Content-Type' => 'application/json'], json_encode($body)); 24 | $responseDataExtractor = new ResponseDataExtractor(); 25 | 26 | self::assertSame($responseDataExtractor->extract($response), $body); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /code/src/Game/tests/Infrastructure/Repository/Doctrine/PlayerAssemblerTest.php: -------------------------------------------------------------------------------- 1 | toPlayerDto($this->createPlayer(new PlayerId())); 24 | 25 | self::assertInstanceOf(PlayerDto::class, $playerDto); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /code/src/Game/tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | bootEnv(dirname(__DIR__) . '/../../.env'); 11 | } 12 | -------------------------------------------------------------------------------- /code/src/SharedKernel/Infrastructure/Messenger/Middleware/FailureLoggerMiddleware.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 20 | } 21 | 22 | public function handle(Envelope $envelope, StackInterface $stack): Envelope 23 | { 24 | $next = $stack->next(); 25 | try { 26 | return $next->handle($envelope, $stack); 27 | } catch (HandlerFailedException $exception) { 28 | $this->logger->error($exception->getMessage()); 29 | 30 | throw $exception; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /code/src/SharedKernel/Infrastructure/Responder/JsonResponder.php: -------------------------------------------------------------------------------- 1 | getRequest(); 19 | if (self::SUPPORTED_CONTENT_TYPE !== $request->getContentType()) { 20 | return; 21 | } 22 | 23 | $response = $viewEvent->getControllerResult(); 24 | $viewEvent->setResponse(new JsonResponse($response->body(), $response->status())); 25 | } 26 | 27 | public static function getSubscribedEvents(): array 28 | { 29 | return [ 30 | KernelEvents::VIEW => ['__invoke'], 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /code/src/SharedKernel/Infrastructure/Responder/WebResponder.php: -------------------------------------------------------------------------------- 1 | getRequest(); 19 | if (!in_array($request->getContentType(), self::SUPPORTED_CONTENT_TYPE, true)) { 20 | return; 21 | } 22 | 23 | $viewEvent->setResponse((new Response())->setContent($viewEvent->getControllerResult())); 24 | } 25 | 26 | public static function getSubscribedEvents(): array 27 | { 28 | return [ 29 | KernelEvents::VIEW => ['__invoke'], 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /code/src/SharedKernel/config/ecs.php: -------------------------------------------------------------------------------- 1 | redirect('/swagger/index.html'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /code/src/Swagger/SwaggerUpdateAction.php: -------------------------------------------------------------------------------- 1 | get('SCAN_DIR'))->toYaml(); 18 | 19 | return new Response(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /code/src/Swagger/config/routes.yaml: -------------------------------------------------------------------------------- 1 | swagger_index: 2 | path: /swagger 3 | methods: GET 4 | controller: App\Swagger\SwaggerIndexAction 5 | 6 | swagger_update: 7 | path: /swagger/update 8 | methods: GET 9 | controller: App\Swagger\SwaggerUpdateAction -------------------------------------------------------------------------------- /code/src/Swagger/config/services.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | SCAN_DIR: '%kernel.project_dir%/src/' 3 | 4 | services: 5 | App\Swagger\: 6 | resource: '.' 7 | tags: [ 'controller.service_arguments' ] -------------------------------------------------------------------------------- /code/var/database/db.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dykyi-roman/crossword/00cd7c526b66cdba591eaf421d670602f7dfc9d2/code/var/database/db.sqlite -------------------------------------------------------------------------------- /code/var/database/db.sqlite.dist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dykyi-roman/crossword/00cd7c526b66cdba591eaf421d670602f7dfc9d2/code/var/database/db.sqlite.dist -------------------------------------------------------------------------------- /docker/.env: -------------------------------------------------------------------------------- 1 | RABBITMQ_ERLANG_COOKIE=SWQOKODSQALRPCLNMEQG 2 | RABBITMQ_DEFAULT_USER=rabbitmq 3 | RABBITMQ_DEFAULT_PASS=rabbitmq 4 | RABBITMQ_DEFAULT_VHOST=/ -------------------------------------------------------------------------------- /docker/data/elasticsearch/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dykyi-roman/crossword/00cd7c526b66cdba591eaf421d670602f7dfc9d2/docker/data/elasticsearch/.gitignore -------------------------------------------------------------------------------- /docker/data/redis/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dykyi-roman/crossword/00cd7c526b66cdba591eaf421d670602f7dfc9d2/docker/data/redis/.gitignore -------------------------------------------------------------------------------- /docker/docker-compose.mac.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | app: 5 | volumes: 6 | - "app-native-osx-sync:/var/www/html/code:nocopy" 7 | - "../.git:/var/www/html/code/.git:cached" 8 | - "../public:/var/www/html/code/public:delegated" 9 | - "../vendor:/var/www/html/code/vendor:delegated" 10 | volumes: 11 | app-native-osx-sync: 12 | external: true -------------------------------------------------------------------------------- /docker/docker-compose.test.yml: -------------------------------------------------------------------------------- 1 | version: '2.2' 2 | services: 3 | tests: 4 | build: php-ci 5 | container_name: test_ci 6 | command: bash -c "composer install --dev && composer test" 7 | volumes: 8 | - ../code:/var/www/html/code 9 | environment: 10 | XDEBUG_MODE: coverage 11 | analyzer: 12 | build: php-ci 13 | container_name: php-analyzer_ci 14 | command: bash -c "composer install --dev && composer analyzer" 15 | volumes: 16 | - ../code:/var/www/html/code -------------------------------------------------------------------------------- /docker/newman/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:13.13.0-alpine 2 | 3 | RUN npm install -g newman newman-reporter-html 4 | 5 | WORKDIR /etc/newman 6 | 7 | ENTRYPOINT ["newman"] 8 | -------------------------------------------------------------------------------- /docker/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:stable-alpine 2 | 3 | RUN apk upgrade --update-cache --available && \ 4 | apk add openssl && \ 5 | rm -rf /var/cache/apk/* 6 | 7 | RUN mkdir -p /etc/nginx/certs/self-signed/ 8 | RUN openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/certs/self-signed/app.test.key -out /etc/nginx/certs/self-signed/app.test.crt -subj "/C=US/ST=Florida/L=Orlando/O=Development/OU=Dev/CN=app.test" 9 | RUN openssl dhparam -out /etc/nginx/certs/dhparam.pem 2048 -------------------------------------------------------------------------------- /docker/php-ci/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.0-fpm 2 | 3 | WORKDIR /var/www/html/code 4 | 5 | RUN pecl install xdebug 6 | 7 | RUN apt-get update && apt-get install -y \ 8 | libzip-dev \ 9 | libmcrypt-dev \ 10 | libcurl4-openssl-dev \ 11 | libonig-dev \ 12 | libicu-dev \ 13 | libevent-dev \ 14 | && rm -rf /var/lib/apt/lists/* 15 | 16 | RUN docker-php-ext-install -j$(nproc) iconv \ 17 | && docker-php-ext-enable xdebug \ 18 | && docker-php-ext-install pcntl \ 19 | && docker-php-ext-install intl \ 20 | && docker-php-ext-install zip \ 21 | && docker-php-ext-enable zip 22 | 23 | RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer 24 | -------------------------------------------------------------------------------- /docker/rabbit/enabled_plugins: -------------------------------------------------------------------------------- 1 | [rabbitmq_management, rabbitmq_management_visualiser]. 2 | -------------------------------------------------------------------------------- /docker/scripts/composer: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker exec -it game_php bash -c "composer $*" 4 | -------------------------------------------------------------------------------- /docker/scripts/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker exec -it game_php bash -c "php bin/console $*" 4 | -------------------------------------------------------------------------------- /docker/scripts/php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker exec -it game_php bash -c "php $*" 4 | -------------------------------------------------------------------------------- /docs/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dykyi-roman/crossword/00cd7c526b66cdba591eaf421d670602f7dfc9d2/docs/example.gif -------------------------------------------------------------------------------- /docs/model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dykyi-roman/crossword/00cd7c526b66cdba591eaf421d670602f7dfc9d2/docs/model.png -------------------------------------------------------------------------------- /docs/view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dykyi-roman/crossword/00cd7c526b66cdba591eaf421d670602f7dfc9d2/docs/view.png -------------------------------------------------------------------------------- /postman/postman_environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "74dea915-dc1f-4bd7-a492-66794784d382", 3 | "name": "crossword_game", 4 | "values": [ 5 | { 6 | "key": "host", 7 | "value": "https://game_web:443", 8 | "enabled": true 9 | } 10 | ], 11 | "_postman_variable_scope": "environment", 12 | "_postman_exported_at": "2021-03-16T21:16:10.822Z", 13 | "_postman_exported_using": "Postman/7.36.5" 14 | } --------------------------------------------------------------------------------