├── .editorconfig ├── .github └── workflows │ └── check-code-quality.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── backend ├── .env ├── .env.test ├── .gitignore ├── bin │ └── console ├── composer.json ├── composer.lock ├── config │ ├── bundles.php │ ├── packages │ │ ├── cache.yaml │ │ ├── dama_doctrine_test_bundle.yaml │ │ ├── doctrine.yaml │ │ ├── doctrine_migrations.yaml │ │ ├── framework.yaml │ │ ├── lock.yaml │ │ ├── mailer.yaml │ │ ├── messenger.yaml │ │ ├── monolog.yaml │ │ ├── nyholm_psr7.yaml │ │ ├── routing.yaml │ │ ├── test │ │ │ └── dama_doctrine_test_bundle.yaml │ │ ├── twig.yaml │ │ ├── uid.yaml │ │ ├── valinor.yaml │ │ └── web_profiler.yaml │ ├── preload.php │ └── routes │ │ ├── app.yaml │ │ ├── framework.yaml │ │ └── web_profiler.yaml ├── migrations │ ├── Version20220603114543.php │ ├── Version20240719151750.php │ ├── Version20240719151819.php │ ├── Version20240719151844.php │ ├── Version20240719151911.php │ ├── Version20240719151932.php │ ├── Version20240719151955.php │ ├── Version20240719152016.php │ ├── Version20240727071640.php │ ├── Version20240730133406.php │ └── Version20250311190206.php ├── public │ └── index.php ├── src-dev │ ├── Infrastructure │ │ ├── EventListener │ │ │ └── TestExceptionEventListener.php │ │ └── di.php │ ├── Maker │ │ ├── CustomStr.php │ │ ├── EntityFieldsManipulator.php │ │ ├── README.md │ │ ├── Resources │ │ │ ├── help │ │ │ │ └── MakeModule.txt │ │ │ ├── openapi.yaml.twig │ │ │ └── skeleton │ │ │ │ ├── domain │ │ │ │ ├── Entity.tpl.php │ │ │ │ └── Repository.tpl.php │ │ │ │ ├── http │ │ │ │ ├── EntityArgumentValueResolver.tpl.php │ │ │ │ ├── InfoAction.tpl.php │ │ │ │ ├── ListAction.tpl.php │ │ │ │ ├── RemoveAction.tpl.php │ │ │ │ ├── create │ │ │ │ │ ├── CreateAction.tpl.php │ │ │ │ │ └── CreateRequest.tpl.php │ │ │ │ └── update │ │ │ │ │ ├── UpdateAction.tpl.php │ │ │ │ │ └── UpdateRequest.tpl.php │ │ │ │ └── tests │ │ │ │ ├── CreateTest.tpl.php │ │ │ │ ├── InfoTest.tpl.php │ │ │ │ ├── ListTest.tpl.php │ │ │ │ ├── RemoveTest.tpl.php │ │ │ │ ├── SDK.tpl.php │ │ │ │ └── UpdateTest.tpl.php │ │ ├── SimpleModule │ │ │ ├── CRUDGenerator.php │ │ │ ├── FunctionalTestsGenerator.php │ │ │ └── MakeModule.php │ │ ├── Vendor │ │ │ ├── CustomGenerator.php │ │ │ ├── EntityClassGeneratorForModule.php │ │ │ └── EntityGenerator.php │ │ └── di.php │ ├── OpenApi │ │ ├── .spectral.yaml │ │ ├── ConsoleCommand │ │ │ ├── GenerateOpenApiSpecCommand.php │ │ │ └── OpenApiRoutesDiffCommand.php │ │ ├── EventListener │ │ │ └── ValidateOpenApiSchema.php │ │ ├── README.md │ │ ├── openapi-admin.yaml │ │ ├── openapi-site.yaml │ │ ├── openapi.yaml │ │ └── resources │ │ │ ├── _template_module.yaml │ │ │ ├── admin │ │ │ ├── article.yaml │ │ │ ├── seo.yaml │ │ │ └── setting.yaml │ │ │ ├── base.yaml │ │ │ ├── common.yaml │ │ │ └── site │ │ │ ├── article.yaml │ │ │ ├── auth.yaml │ │ │ ├── ping.yaml │ │ │ ├── profile.yaml │ │ │ ├── seo.yaml │ │ │ ├── setting.yaml │ │ │ └── task.yaml │ ├── PHPCsFixer │ │ ├── Comment │ │ │ └── ClassDocCommentFixer.php │ │ ├── PhpUnit │ │ │ ├── DocCommentHelper.php │ │ │ ├── TestdoxFixer.php │ │ │ └── TestdoxForMethods.php │ │ └── php-cs-fixer-config.php │ ├── PHPStan │ │ ├── EmbeddablePropertiesExtension.php │ │ ├── object-manager.php │ │ ├── phpstan-baseline.neon │ │ └── phpstan-config.neon │ ├── Rector │ │ ├── Rules │ │ │ ├── AssertMustHaveMessageRector.php │ │ │ ├── OneFlushInClassRector.php │ │ │ ├── RequestMethodInsteadOfStringRector.php │ │ │ └── ResolversInActionRector.php │ │ └── rector.config.php │ ├── Tests │ │ ├── Functional │ │ │ ├── Article │ │ │ │ ├── Admin │ │ │ │ │ ├── ArticleInfoTest.php │ │ │ │ │ ├── ArticleListByIdsTest.php │ │ │ │ │ ├── ArticleListTest.php │ │ │ │ │ ├── CreateArticleTest.php │ │ │ │ │ ├── RemoveArticleTest.php │ │ │ │ │ └── UpdateArticleTest.php │ │ │ │ └── Site │ │ │ │ │ ├── ArticleInfoTest.php │ │ │ │ │ └── ArticleListTest.php │ │ │ ├── Infrastructure │ │ │ │ ├── Request │ │ │ │ │ ├── AcceptHeaderTest.php │ │ │ │ │ └── TraceIdHeaderTest.php │ │ │ │ └── Response │ │ │ │ │ └── ApiSystemExceptionTest.php │ │ │ ├── PingActionTest.php │ │ │ ├── SDK │ │ │ │ ├── ApiWebTestCase.php │ │ │ │ ├── Article.php │ │ │ │ ├── Profile.php │ │ │ │ ├── Seo.php │ │ │ │ ├── Setting.php │ │ │ │ ├── Task.php │ │ │ │ ├── TaskComment.php │ │ │ │ └── User.php │ │ │ ├── Seo │ │ │ │ ├── Admin │ │ │ │ │ └── SeoSaveTest.php │ │ │ │ └── Site │ │ │ │ │ └── SeoTest.php │ │ │ ├── Setting │ │ │ │ ├── Admin │ │ │ │ │ ├── SettingListTest.php │ │ │ │ │ └── SettingSaveTest.php │ │ │ │ └── Site │ │ │ │ │ └── SettingListTest.php │ │ │ ├── Task │ │ │ │ ├── Scheduler │ │ │ │ │ └── SendUncompletedTaskToUserSchedulerTest.php │ │ │ │ └── Site │ │ │ │ │ ├── ChangeTaskNameTest.php │ │ │ │ │ ├── CompleteTaskTest.php │ │ │ │ │ ├── CreateCommentOnTaskTest.php │ │ │ │ │ ├── CreateTaskTest.php │ │ │ │ │ ├── ExportTasksTest.php │ │ │ │ │ ├── RemoveTaskTest.php │ │ │ │ │ ├── TaskInfoTest.php │ │ │ │ │ └── TaskListTest.php │ │ │ └── User │ │ │ │ └── Site │ │ │ │ ├── ChangePasswordTest.php │ │ │ │ ├── ConfirmEmailTest.php │ │ │ │ ├── LogoutTest.php │ │ │ │ ├── ProfileInfoTest.php │ │ │ │ ├── ProfileSaveTest.php │ │ │ │ ├── RecoverPasswordTest.php │ │ │ │ ├── SignInTest.php │ │ │ │ └── SignUpTest.php │ │ ├── Rector │ │ │ ├── AssertMustHaveMessageRector │ │ │ │ ├── AssertMustHaveMessageTest.php │ │ │ │ ├── Fixture │ │ │ │ │ ├── skip_rule_test_fixture.php.inc │ │ │ │ │ └── test_fixture.php.inc │ │ │ │ └── config │ │ │ │ │ └── config.php │ │ │ ├── OneFlushInClassRector │ │ │ │ ├── Fixture │ │ │ │ │ ├── skip_rule_test_fixture.php.inc │ │ │ │ │ └── test_fixture.php.inc │ │ │ │ ├── OneFlushInClassTest.php │ │ │ │ └── config │ │ │ │ │ └── config.php │ │ │ ├── RequestMethodInsteadOfStringRector │ │ │ │ ├── Fixture │ │ │ │ │ ├── skip_rule_test_fixture.php.inc │ │ │ │ │ └── test_fixture.php.inc │ │ │ │ ├── RequestMethodInsteadOfStringTest.php │ │ │ │ └── config │ │ │ │ │ └── config.php │ │ │ └── ResolversInAction │ │ │ │ ├── Fixture │ │ │ │ ├── skip_rule_test_fixture.php.inc │ │ │ │ └── test_fixture.php.inc │ │ │ │ ├── ResolversInActionTest.php │ │ │ │ └── config │ │ │ │ └── config.php │ │ ├── Unit │ │ │ ├── Infrastructure │ │ │ │ ├── EmailTest.php │ │ │ │ ├── Pagination │ │ │ │ │ ├── PaginationRequestTest.php │ │ │ │ │ └── PaginationResponseTest.php │ │ │ │ └── PhoneTest.php │ │ │ ├── Task │ │ │ │ └── Domain │ │ │ │ │ ├── TaskCommentBodyTest.php │ │ │ │ │ ├── TaskCommentIdTest.php │ │ │ │ │ ├── TaskIdTest.php │ │ │ │ │ ├── TaskNameTest.php │ │ │ │ │ └── TaskTest.php │ │ │ └── User │ │ │ │ ├── Profile │ │ │ │ └── Domain │ │ │ │ │ └── ProfileIdTest.php │ │ │ │ ├── SignUp │ │ │ │ └── Domain │ │ │ │ │ └── ConfirmTokenTest.php │ │ │ │ └── User │ │ │ │ └── Domain │ │ │ │ ├── AuthTokenTest.php │ │ │ │ └── UserPasswordTest.php │ │ └── bootstrap.php │ ├── deptrac.yaml │ ├── phpunit.xml │ ├── psalm-baseline.xml │ └── psalm.xml ├── src │ ├── Article │ │ ├── Domain │ │ │ ├── Article.php │ │ │ └── ArticleRepository.php │ │ ├── Http │ │ │ ├── Admin │ │ │ │ ├── ArticleArgumentValueResolver.php │ │ │ │ ├── ArticleInfoAction.php │ │ │ │ ├── ArticleListAction.php │ │ │ │ ├── ArticleListByIdsAction.php │ │ │ │ ├── ArticleListByIdsRequest.php │ │ │ │ ├── ArticleListData.php │ │ │ │ ├── CreateArticleAction.php │ │ │ │ ├── CreateArticleRequest.php │ │ │ │ ├── RemoveArticleAction.php │ │ │ │ ├── UpdateArticleAction.php │ │ │ │ └── UpdateArticleRequest.php │ │ │ └── Site │ │ │ │ ├── ArticleInfoAction.php │ │ │ │ ├── ArticleInfoData.php │ │ │ │ ├── ArticleListAction.php │ │ │ │ └── ArticleListData.php │ │ └── README.md │ ├── Infrastructure │ │ ├── ApiException │ │ │ ├── ApiAccessForbiddenException.php │ │ │ ├── ApiBadRequestException.php │ │ │ ├── ApiBadResponseException.php │ │ │ ├── ApiErrorCode.php │ │ │ ├── ApiErrorResponse.php │ │ │ ├── ApiException.php │ │ │ ├── ApiHeaders.php │ │ │ ├── ApiNotFoundException.php │ │ │ ├── ApiRateLimiterException.php │ │ │ ├── ApiSystemException.php │ │ │ └── ApiUnauthorizedException.php │ │ ├── CheckRateLimiter.php │ │ ├── EventListener │ │ │ └── FillMailFields.php │ │ ├── Flush.php │ │ ├── KeepCacheArrayAdapter.php │ │ ├── Kernel.php │ │ ├── Message.php │ │ ├── README.md │ │ ├── Request │ │ │ ├── ApiRequestMapper.php │ │ │ ├── ApiRequestMappingException.php │ │ │ ├── ApiRequestValueResolver.php │ │ │ ├── BuildValidationError.php │ │ │ ├── Pagination │ │ │ │ ├── PaginationRequest.php │ │ │ │ └── PaginationRequestArgumentResolver.php │ │ │ └── ValidateAcceptHeader.php │ │ ├── Response │ │ │ ├── ApiListObjectResponse.php │ │ │ ├── ApiObjectResponse.php │ │ │ ├── ApiResponse.php │ │ │ ├── PaginationResponse.php │ │ │ ├── ResponseStatus.php │ │ │ ├── SerializeApiResponse.php │ │ │ ├── SerializeExceptionResponse.php │ │ │ └── SuccessResponse.php │ │ ├── ValueObject │ │ │ ├── Email.php │ │ │ └── Phone.php │ │ └── di.php │ ├── Logger │ │ └── Http │ │ │ ├── RequestIdListener.php │ │ │ ├── RequestIdLoggerProcessor.php │ │ │ └── RequestLogger.php │ ├── Mailer │ │ ├── Notification │ │ │ ├── EmailConfirmation │ │ │ │ ├── ConfirmEmailMessage.php │ │ │ │ └── SendEmailConfirmToken.php │ │ │ ├── PasswordRecovery │ │ │ │ ├── RecoveryPasswordMessage.php │ │ │ │ └── SendPasswordRecoveryToken.php │ │ │ └── UncompletedTasks │ │ │ │ ├── SendUncompletedTasksToUser.php │ │ │ │ ├── TaskData.php │ │ │ │ └── UncompletedTasksMessage.php │ │ ├── README.md │ │ ├── config.php │ │ └── templates │ │ │ ├── emails │ │ │ ├── confirm.html.twig │ │ │ ├── recoverPassword.html.twig │ │ │ └── uncompleted-tasks.html.twig │ │ │ └── layout.html.twig │ ├── Ping │ │ ├── Http │ │ │ └── Site │ │ │ │ ├── PingAction.php │ │ │ │ └── Pong.php │ │ └── README.md │ ├── Seo │ │ ├── Command │ │ │ ├── SaveSeo.php │ │ │ └── SaveSeoCommand.php │ │ ├── Domain │ │ │ ├── Seo.php │ │ │ ├── SeoRepository.php │ │ │ └── SeoResourceType.php │ │ ├── Http │ │ │ ├── Admin │ │ │ │ └── SeoSaveAction.php │ │ │ └── Site │ │ │ │ ├── SeoAction.php │ │ │ │ └── SeoData.php │ │ └── README.md │ ├── Setting │ │ ├── Command │ │ │ ├── SaveSetting.php │ │ │ └── SaveSettingCommand.php │ │ ├── Domain │ │ │ ├── Setting.php │ │ │ ├── SettingNotFoundException.php │ │ │ ├── SettingType.php │ │ │ └── SettingsRepository.php │ │ ├── Http │ │ │ ├── Admin │ │ │ │ ├── ListAction.php │ │ │ │ ├── SettingListData.php │ │ │ │ └── SettingSaveAction.php │ │ │ └── Site │ │ │ │ ├── SettingListAction.php │ │ │ │ └── SettingListData.php │ │ └── README.md │ ├── Task │ │ ├── Command │ │ │ ├── Comment │ │ │ │ └── Add │ │ │ │ │ ├── AddCommentOnTask.php │ │ │ │ │ └── AddCommentOnTaskCommand.php │ │ │ ├── CompleteTask.php │ │ │ ├── CreateTask │ │ │ │ ├── CreateTask.php │ │ │ │ └── CreateTaskCommand.php │ │ │ ├── RemoveTask.php │ │ │ └── UpdateTaskName │ │ │ │ ├── UpdateTaskName.php │ │ │ │ └── UpdateTaskNameCommand.php │ │ ├── Domain │ │ │ ├── AddCommentToCompletedTaskException.php │ │ │ ├── Task.php │ │ │ ├── TaskAlreadyIsDoneException.php │ │ │ ├── TaskComment.php │ │ │ ├── TaskCommentBody.php │ │ │ ├── TaskCommentId.php │ │ │ ├── TaskId.php │ │ │ ├── TaskName.php │ │ │ └── TaskRepository.php │ │ ├── Http │ │ │ └── Site │ │ │ │ ├── Comment │ │ │ │ ├── AddCommentOnTaskAction.php │ │ │ │ └── TaskCommentsListAction.php │ │ │ │ ├── CompleteTaskAction.php │ │ │ │ ├── CreateTask │ │ │ │ ├── CreateTaskAction.php │ │ │ │ └── TaskData.php │ │ │ │ ├── Export │ │ │ │ ├── Csv │ │ │ │ │ ├── CsvExporter.php │ │ │ │ │ └── CsvTaskData.php │ │ │ │ ├── ExportTaskAction.php │ │ │ │ ├── ExportTasks.php │ │ │ │ ├── Exporter.php │ │ │ │ ├── Format.php │ │ │ │ ├── NotFoundTasksForExportException.php │ │ │ │ └── Xml │ │ │ │ │ ├── XmlExporter.php │ │ │ │ │ └── XmlTaskData.php │ │ │ │ ├── RemoveTaskAction.php │ │ │ │ ├── TaskArgumentValueResolver.php │ │ │ │ ├── TaskInfoAction.php │ │ │ │ ├── TaskList │ │ │ │ ├── TaskListAction.php │ │ │ │ └── TaskListMetaData.php │ │ │ │ └── UpdateTaskNameAction.php │ │ ├── Query │ │ │ ├── Comment │ │ │ │ └── FindAll │ │ │ │ │ ├── CommentData.php │ │ │ │ │ ├── FindAllCommentQuery.php │ │ │ │ │ └── FindAllCommentsByTaskIdAndUserId.php │ │ │ └── Task │ │ │ │ ├── FindAllByUserId │ │ │ │ ├── CountAllTasksByUserId.php │ │ │ │ ├── Filter.php │ │ │ │ ├── FindAllTasksByUserId.php │ │ │ │ ├── FindAllTasksByUserIdQuery.php │ │ │ │ └── TaskData.php │ │ │ │ ├── FindById │ │ │ │ ├── FindTaskById.php │ │ │ │ ├── FindTaskByIdQuery.php │ │ │ │ ├── TaskData.php │ │ │ │ └── TaskNotFoundException.php │ │ │ │ └── FindUncompletedTasksByUserId │ │ │ │ ├── FindUncompletedTasksByUserId.php │ │ │ │ ├── FindUncompletedTasksByUserIdQuery.php │ │ │ │ └── TaskData.php │ │ ├── README.md │ │ └── Scheduler │ │ │ └── SendUncompletedTaskToUserScheduler.php │ └── User │ │ ├── Password │ │ ├── Command │ │ │ ├── ChangePassword.php │ │ │ ├── ChangePasswordCommand.php │ │ │ ├── GenerateRecoveryToken.php │ │ │ ├── GenerateRecoveryTokenCommand.php │ │ │ ├── RecoverPassword.php │ │ │ └── RecoverPasswordCommand.php │ │ ├── Domain │ │ │ ├── RecoveryToken.php │ │ │ └── RecoveryTokenRepository.php │ │ └── Http │ │ │ ├── ChangePasswordAction.php │ │ │ ├── ChangePasswordRequest.php │ │ │ ├── RecoverPasswordAction.php │ │ │ └── RequestPasswordRecoveryAction.php │ │ ├── Profile │ │ ├── Command │ │ │ └── SaveProfile │ │ │ │ ├── CreateProfile.php │ │ │ │ ├── SaveProfile.php │ │ │ │ ├── SaveProfileCommand.php │ │ │ │ └── UpdateProfile.php │ │ ├── Domain │ │ │ ├── Profile.php │ │ │ ├── ProfileId.php │ │ │ └── ProfileRepository.php │ │ ├── Http │ │ │ ├── ProfileInfoAction.php │ │ │ └── ProfileSaveAction.php │ │ └── Query │ │ │ └── FindByUserId │ │ │ ├── FindProfileByUserId.php │ │ │ ├── FindProfileByUserIdQuery.php │ │ │ └── ProfileData.php │ │ ├── README.md │ │ ├── Security │ │ ├── Http │ │ │ ├── IsGranted.php │ │ │ ├── IsGrantedSubscriber.php │ │ │ └── UserIdArgumentValueResolver.php │ │ └── Service │ │ │ ├── CheckRoleGranted.php │ │ │ ├── TokenException.php │ │ │ └── TokenManager.php │ │ ├── SignIn │ │ ├── Command │ │ │ ├── CreateToken.php │ │ │ ├── DeleteToken.php │ │ │ ├── SignIn.php │ │ │ └── SignInCommand.php │ │ └── Http │ │ │ ├── LogoutAction.php │ │ │ ├── SignInAction.php │ │ │ ├── SignInRequest.php │ │ │ └── UserTokenData.php │ │ ├── SignUp │ │ ├── Command │ │ │ ├── ConfirmEmail.php │ │ │ ├── SignUp.php │ │ │ └── SignUpCommand.php │ │ └── Http │ │ │ ├── ConfirmEmailAction.php │ │ │ └── SignUpAction.php │ │ ├── User │ │ ├── Domain │ │ │ ├── AuthToken.php │ │ │ ├── ConfirmToken.php │ │ │ ├── Exception │ │ │ │ ├── EmailAlreadyIsConfirmedException.php │ │ │ │ ├── EmailIsNotConfirmedException.php │ │ │ │ ├── UserAlreadyExistException.php │ │ │ │ └── UserNotFoundException.php │ │ │ ├── User.php │ │ │ ├── UserId.php │ │ │ ├── UserPassword.php │ │ │ ├── UserRepository.php │ │ │ ├── UserRole.php │ │ │ ├── UserToken.php │ │ │ ├── UserTokenId.php │ │ │ └── UserTokenRepository.php │ │ └── Query │ │ │ ├── FindAllUsers.php │ │ │ ├── FindUser.php │ │ │ ├── FindUserQuery.php │ │ │ ├── UserData.php │ │ │ └── UserListData.php │ │ └── config.php └── symfony.lock └── docker ├── .env.dist ├── backend ├── Dockerfile ├── messenger │ ├── messenger-worker.conf │ ├── scheduler.conf │ └── supervisord.conf ├── php.ini └── www.conf ├── bin ├── check_commit_message.bash ├── replace_env.bash └── setup_envs.bash ├── docker-compose.local.yml ├── nginx ├── Dockerfile └── default.conf └── pgsql ├── .env.dist ├── Dockerfile └── pgsql.conf /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 4 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 120 11 | tab_width = 4 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | 17 | [*.yaml] 18 | indent_size = 2 19 | ij_yaml_spaces_within_brackets = true 20 | -------------------------------------------------------------------------------- /.github/workflows/check-code-quality.yml: -------------------------------------------------------------------------------- 1 | name: Check Code Quality 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - master 7 | 8 | jobs: 9 | check: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Setup docker env 15 | run: ./docker/bin/setup_envs.bash setup-envs && cat .env 16 | 17 | - name: Validate commit message 18 | run: ./docker/bin/check_commit_message.bash 19 | 20 | - name: Cache Composer packages 21 | id: composer-cache 22 | uses: actions/cache@v3 23 | with: 24 | path: vendor 25 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 26 | restore-keys: | 27 | ${{ runner.os }}-php- 28 | 29 | - name: Install packages 30 | run: make composer-install 31 | 32 | - name: Validate all composer rules 33 | run: make composer-check-all 34 | 35 | - name: Install services 36 | run: make init 37 | 38 | - name: Validate database mapping 39 | run: make db-validate 40 | 41 | - name: Check code quality 42 | run: make lint 43 | 44 | - name: Check difference between OpenApi and application endpoints 45 | run: make check-openapi-diff 46 | 47 | - name: Check OpenApi schema 48 | run: docker run --rm -v ${PWD}/backend:/app stoplight/spectral lint /app/src-dev/OpenApi/openapi.yaml -F warn --ruleset=/app/src-dev/OpenApi/.spectral.yaml 49 | 50 | - name: Run functional tests with code coverage 51 | run: make test-coverage 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | 3 | ###> docker ### 4 | /docker/**/.env 5 | ###< docker ### 6 | 7 | /.env 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Studio 15 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. 22 | -------------------------------------------------------------------------------- /backend/.env.test: -------------------------------------------------------------------------------- 1 | # define your env variables for the test env here 2 | KERNEL_CLASS='App\Infrastructure\Kernel' 3 | APP_SECRET='$ecretf0rt3st' 4 | SYMFONY_DEPRECATIONS_HELPER=999999 5 | HASHER_COST=4 6 | -------------------------------------------------------------------------------- /backend/.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/ 8 | /vendor/ 9 | ###< symfony/framework-bundle ### 10 | 11 | /src-dev/cache/* 12 | !/src-dev/cache/.gitkeep 13 | -------------------------------------------------------------------------------- /backend/bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | ['all' => true], 5 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 6 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 7 | Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], 8 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], 9 | Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], 10 | Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], 11 | DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true], 12 | CuyZ\ValinorBundle\ValinorBundle::class => ['all' => true], 13 | Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], 14 | ]; 15 | -------------------------------------------------------------------------------- /backend/config/packages/cache.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | cache: 3 | app: cache.adapter.filesystem 4 | system: cache.adapter.system 5 | 6 | # Unique name of your app: used to compute stable namespaces for cache keys. 7 | #prefix_seed: your_vendor_name/app_name 8 | 9 | # The "app" cache stores to the filesystem by default. 10 | # The data in this cache should persist between deploys. 11 | # Other options include: 12 | 13 | # Redis 14 | #app: cache.adapter.redis 15 | #default_redis_provider: redis://localhost 16 | 17 | # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) 18 | #app: cache.adapter.apcu 19 | 20 | # Namespaced pools use the above "app" backend by default 21 | #pools: 22 | #my.dedicated.cache: null 23 | 24 | when@test: 25 | framework: 26 | cache: 27 | app: cache.adapter.array 28 | system: cache.adapter.array 29 | -------------------------------------------------------------------------------- /backend/config/packages/dama_doctrine_test_bundle.yaml: -------------------------------------------------------------------------------- 1 | when@test: 2 | dama_doctrine_test: 3 | enable_static_connection: true 4 | enable_static_meta_data_cache: true 5 | enable_static_query_cache: true 6 | -------------------------------------------------------------------------------- /backend/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 | 'DoctrineMigrations': '%kernel.project_dir%/migrations' 6 | enable_profiler: '%kernel.debug%' 7 | -------------------------------------------------------------------------------- /backend/config/packages/framework.yaml: -------------------------------------------------------------------------------- 1 | # see https://symfony.com/doc/current/reference/configuration/framework.html 2 | framework: 3 | secret: '%env(APP_SECRET)%' 4 | 5 | # Note that the session will be started ONLY if you read or write from it. 6 | session: false 7 | 8 | #esi: true 9 | #fragments: true 10 | 11 | when@test: 12 | framework: 13 | test: true 14 | session: 15 | storage_factory_id: session.storage.factory.mock_file 16 | -------------------------------------------------------------------------------- /backend/config/packages/lock.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | lock: '%env(LOCK_DSN)%' 3 | -------------------------------------------------------------------------------- /backend/config/packages/mailer.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | mailer: 3 | dsn: '%env(MAILER_DSN)%' 4 | -------------------------------------------------------------------------------- /backend/config/packages/nyholm_psr7.yaml: -------------------------------------------------------------------------------- 1 | when@dev: 2 | services: 3 | # Register nyholm/psr7 services for autowiring with PSR-17 (HTTP factories) 4 | Psr\Http\Message\RequestFactoryInterface: '@nyholm.psr7.psr17_factory' 5 | Psr\Http\Message\ResponseFactoryInterface: '@nyholm.psr7.psr17_factory' 6 | Psr\Http\Message\ServerRequestFactoryInterface: '@nyholm.psr7.psr17_factory' 7 | Psr\Http\Message\StreamFactoryInterface: '@nyholm.psr7.psr17_factory' 8 | Psr\Http\Message\UploadedFileFactoryInterface: '@nyholm.psr7.psr17_factory' 9 | Psr\Http\Message\UriFactoryInterface: '@nyholm.psr7.psr17_factory' 10 | 11 | # Register nyholm/psr7 services for autowiring with HTTPlug factories 12 | Http\Message\MessageFactory: '@nyholm.psr7.httplug_factory' 13 | Http\Message\RequestFactory: '@nyholm.psr7.httplug_factory' 14 | Http\Message\ResponseFactory: '@nyholm.psr7.httplug_factory' 15 | Http\Message\StreamFactory: '@nyholm.psr7.httplug_factory' 16 | Http\Message\UriFactory: '@nyholm.psr7.httplug_factory' 17 | 18 | nyholm.psr7.psr17_factory: 19 | class: Nyholm\Psr7\Factory\Psr17Factory 20 | 21 | nyholm.psr7.httplug_factory: 22 | class: Nyholm\Psr7\Factory\HttplugFactory 23 | -------------------------------------------------------------------------------- /backend/config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | # Configure how to generate URLs in non-HTTP contexts, such as CLI commands. 4 | # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands 5 | #default_uri: http://localhost 6 | 7 | when@prod: 8 | framework: 9 | router: 10 | strict_requirements: null 11 | -------------------------------------------------------------------------------- /backend/config/packages/test/dama_doctrine_test_bundle.yaml: -------------------------------------------------------------------------------- 1 | dama_doctrine_test: 2 | enable_static_connection: true 3 | enable_static_meta_data_cache: true 4 | enable_static_query_cache: true 5 | -------------------------------------------------------------------------------- /backend/config/packages/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | default_path: '%kernel.project_dir%/templates' 3 | paths: 4 | '%kernel.project_dir%/src/Mailer/templates': mails 5 | 6 | when@test: 7 | twig: 8 | strict_variables: true 9 | -------------------------------------------------------------------------------- /backend/config/packages/uid.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | uid: 3 | default_uuid_version: 7 4 | time_based_uuid_version: 7 5 | -------------------------------------------------------------------------------- /backend/config/packages/valinor.yaml: -------------------------------------------------------------------------------- 1 | valinor: 2 | mapper: 3 | # Date formats that will be supported by the mapper by default. 4 | # date_formats_supported: 5 | # - 'Y-m-d' 6 | # - 'Y-m-d H:i:s' 7 | 8 | # For security reasons, exceptions thrown in a constructor will not be 9 | # caught by the mapper unless they are specifically allowed by giving 10 | # their class names to the configuration below. 11 | allowed_exceptions: 12 | - \Webmozart\Assert\InvalidArgumentException 13 | 14 | console: 15 | # When a mapping error occurs during a console command, the output will 16 | # automatically be enhanced to show information about errors. The 17 | # maximum number of errors that will be displayed can be configured 18 | # below, or set to 0 to disable this feature entirely. 19 | mapping_errors_to_output: 15 20 | 21 | cache: 22 | # By default, mapper cache entries are stored in the filesystem. This 23 | # can be changed by setting the name of a PSR-16 cache service below. 24 | # service: app.custom_cache 25 | 26 | # Cache entries representing class definitions won't be cleared when 27 | # files are modified during development of the application. This can be 28 | # changed by setting in which environments cache entries will be 29 | # unvalidated. 30 | env_where_files_are_watched: [ 'dev' ] 31 | -------------------------------------------------------------------------------- /backend/config/packages/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | when@dev: 2 | web_profiler: 3 | toolbar: true 4 | 5 | framework: 6 | profiler: 7 | collect_serializer_data: true 8 | 9 | when@test: 10 | framework: 11 | profiler: { collect: false } 12 | -------------------------------------------------------------------------------- /backend/config/preload.php: -------------------------------------------------------------------------------- 1 | addSql("SELECT '1';"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /backend/migrations/Version20240719151750.php: -------------------------------------------------------------------------------- 1 | addSql('CREATE TABLE "user" (id UUID NOT NULL, user_role VARCHAR(255) NOT NULL, is_confirmed BOOLEAN NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, user_email_value VARCHAR(255) NOT NULL, confirm_token_value UUID NOT NULL, user_password_value VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); 22 | $this->addSql('CREATE UNIQUE INDEX UNIQ_8D93D649B6244599 ON "user" (confirm_token_value)'); 23 | $this->addSql('CREATE TABLE user_token (id UUID NOT NULL, user_id UUID NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); 24 | $this->addSql('CREATE INDEX ix_user_token_user_id ON user_token (user_id)'); 25 | } 26 | 27 | #[Override] 28 | public function down(Schema $schema): void 29 | { 30 | $this->addSql('DROP TABLE "user"'); 31 | $this->addSql('DROP TABLE user_token'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /backend/migrations/Version20240719151819.php: -------------------------------------------------------------------------------- 1 | addSql('CREATE TABLE recovery_token (id UUID NOT NULL, user_id UUID NOT NULL, token UUID NOT NULL, PRIMARY KEY(id))'); 22 | $this->addSql('CREATE INDEX ix_recovery_token_user_id ON recovery_token (user_id)'); 23 | } 24 | 25 | #[Override] 26 | public function down(Schema $schema): void 27 | { 28 | $this->addSql('DROP TABLE recovery_token'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/migrations/Version20240719151844.php: -------------------------------------------------------------------------------- 1 | addSql('CREATE TABLE profile (id UUID NOT NULL, user_id UUID NOT NULL, name VARCHAR(255) NOT NULL, phone_value VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); 22 | $this->addSql('CREATE INDEX ix_profile_user_id ON profile (user_id)'); 23 | } 24 | 25 | #[Override] 26 | public function down(Schema $schema): void 27 | { 28 | $this->addSql('DROP TABLE profile'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/migrations/Version20240719151932.php: -------------------------------------------------------------------------------- 1 | addSql('CREATE TABLE article (id UUID NOT NULL, title VARCHAR(255) NOT NULL, alias VARCHAR(255) NOT NULL, body TEXT NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); 22 | } 23 | 24 | #[Override] 25 | public function down(Schema $schema): void 26 | { 27 | $this->addSql('DROP TABLE article'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /backend/migrations/Version20240719152016.php: -------------------------------------------------------------------------------- 1 | addSql('CREATE TABLE seo (id UUID NOT NULL, type VARCHAR(255) NOT NULL, identity VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL, description TEXT DEFAULT NULL, keywords TEXT DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); 22 | $this->addSql('CREATE UNIQUE INDEX UNIQ_6C71EC308CDE5729 ON seo (type)'); 23 | } 24 | 25 | #[Override] 26 | public function down(Schema $schema): void 27 | { 28 | $this->addSql('DROP TABLE seo'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/migrations/Version20240727071640.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE "user" ALTER COLUMN confirm_token_value DROP NOT NULL;'); 22 | } 23 | 24 | #[Override] 25 | public function down(Schema $schema): void 26 | { 27 | $this->addSql('ALTER TABLE "user" ALTER COLUMN confirm_token_value SET NOT NULL'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /backend/migrations/Version20240730133406.php: -------------------------------------------------------------------------------- 1 | addSql('TRUNCATE TABLE user_token'); 22 | $this->addSql('ALTER TABLE user_token ADD hash VARCHAR(255) NOT NULL'); 23 | } 24 | 25 | #[Override] 26 | public function down(Schema $schema): void 27 | { 28 | $this->addSql('ALTER TABLE user_token DROP hash'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/migrations/Version20250311190206.php: -------------------------------------------------------------------------------- 1 | addSql('INSERT INTO "user" (id, user_role, is_confirmed, created_at, user_email_value, user_password_value) VALUES (gen_random_uuid(), \'ROLE_USER\', true, now(), \'user@example.test\', \'$2y$08$CxUXg6YoeRyhAKqEXS5tl.H95Y78AKgji/8jEkRrdtdHkwSID0YeC\')'); 22 | $this->addSql('INSERT INTO "user" (id, user_role, is_confirmed, created_at, user_email_value, user_password_value) VALUES (gen_random_uuid(), \'ROLE_ADMIN\', true, now(), \'admin@example.test\', \'$2y$08$CxUXg6YoeRyhAKqEXS5tl.H95Y78AKgji/8jEkRrdtdHkwSID0YeC\')'); 23 | } 24 | 25 | #[Override] 26 | public function down(Schema $schema): void 27 | { 28 | $this->addSql('DELETE FROM "user" WHERE user_email_value IN (\'user@example.test\', \'admin@example.test\')'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/public/index.php: -------------------------------------------------------------------------------- 1 | getThrowable(); 23 | if ($e instanceof ValidationFailed) { 24 | throw $e; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/src-dev/Infrastructure/di.php: -------------------------------------------------------------------------------- 1 | services()->defaults()->autowire()->autoconfigure(); 11 | 12 | $services 13 | ->load('Dev\\', '../*') 14 | ->exclude([ 15 | './{di.php}', 16 | '../**/{di.php}', 17 | '../**/{config.php}', 18 | '../Tests', 19 | ]); 20 | }; 21 | -------------------------------------------------------------------------------- /backend/src-dev/Maker/CustomStr.php: -------------------------------------------------------------------------------- 1 | %command.name% command generates a new module. 2 | 3 | php %command.full_name% Article 4 | 5 | The %command.name% command creates or updates an entity and repository class. 6 | 7 | php %command.full_name% BlogPost 8 | 9 | If the argument is missing, the command will ask for the entity class name interactively. 10 | 11 | You can also mark this class as an API Platform resource. A hypermedia CRUD API will 12 | automatically be available for this entity class: 13 | 14 | php %command.full_name% --api-resource 15 | 16 | Symfony can also broadcast all changes made to the entity to the client using Symfony 17 | UX Turbo. 18 | 19 | php %command.full_name% --broadcast 20 | 21 | You can also generate all the getter/setter/adder/remover methods 22 | for the properties of existing entities: 23 | 24 | php %command.full_name% --regenerate 25 | 26 | You can also *overwrite* any existing methods: 27 | 28 | php %command.full_name% --regenerate --overwrite 29 | -------------------------------------------------------------------------------- /backend/src-dev/Maker/Resources/skeleton/domain/Entity.tpl.php: -------------------------------------------------------------------------------- 1 | 10 | 11 | declare(strict_types=1); 12 | 13 | namespace ; 14 | 15 | 16 | 17 | /** 18 | * @final 19 | * 20 | * 21 | */ 22 | #[ORM\Entity] 23 | class 24 | { 25 | #[ORM\Id, ORM\Column(type: 'uuid', unique: true)] 26 | private readonly Uuid $id; 27 | 28 | #[ORM\Column] 29 | private readonly DateTimeImmutable $createdAt; 30 | 31 | #[ORM\Column(nullable: true)] 32 | private ?DateTimeImmutable $updatedAt; 33 | 34 | public function __construct( 35 | Uuid $id, 36 | // todo entity fields 37 | ) { 38 | $this->id = $id; 39 | 40 | // todo set entity fields 41 | 42 | $this->createdAt = new DateTimeImmutable(); 43 | $this->updatedAt = null; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /backend/src-dev/Maker/Resources/skeleton/http/InfoAction.tpl.php: -------------------------------------------------------------------------------- 1 | 14 | 15 | declare(strict_types=1); 16 | 17 | namespace ; 18 | 19 | 20 | 21 | /** 22 | * Ручка получения информации 23 | */ 24 | #[AsController] 25 | #[Route('', )] 26 | #[IsGranted()] 27 | final readonly class 28 | { 29 | public function __invoke( 30 | #[ValueResolver(ArgumentValueResolver::class)] 31 | $entity, 32 | ): ApiObjectResponse { 33 | return new ApiObjectResponse($entity); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/src-dev/Maker/Resources/skeleton/http/ListAction.tpl.php: -------------------------------------------------------------------------------- 1 | 15 | 16 | declare(strict_types=1); 17 | 18 | namespace ; 19 | 20 | 21 | 22 | /** 23 | * Ручка получения списка 24 | */ 25 | #[AsController] 26 | #[Route('', )] 27 | #[IsGranted()] 28 | final readonly class 29 | { 30 | public function __construct( 31 | private $repository, 32 | ) {} 33 | 34 | public function __invoke(): ApiListObjectResponse 35 | { 36 | $list = $this->repository->getAll(); 37 | 38 | $pagination = new PaginationResponse( 39 | total: \count($list), 40 | ); 41 | 42 | return new ApiListObjectResponse( 43 | data: $list, 44 | pagination: $pagination, 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /backend/src-dev/Maker/Resources/skeleton/http/RemoveAction.tpl.php: -------------------------------------------------------------------------------- 1 | 15 | 16 | declare(strict_types=1); 17 | 18 | namespace ; 19 | 20 | 21 | 22 | /** 23 | * Ручка удаления 24 | */ 25 | #[AsController] 26 | #[Route('', )] 27 | #[IsGranted()] 28 | final readonly class 29 | { 30 | public function __construct( 31 | private $repository, 32 | private Flush $flush, 33 | ) {} 34 | 35 | public function __invoke( 36 | #[ValueResolver(ArgumentValueResolver::class)] 37 | $entity, 38 | ): ApiObjectResponse { 39 | $this->repository->remove($entity); 40 | ($this->flush)(); 41 | 42 | return new ApiObjectResponse( 43 | new SuccessResponse(), 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /backend/src-dev/Maker/Resources/skeleton/http/create/CreateRequest.tpl.php: -------------------------------------------------------------------------------- 1 | > $entity_fields 10 | */ 11 | echo " 12 | 13 | declare(strict_types=1); 14 | 15 | namespace ; 16 | 17 | /** 18 | * Запрос для создания 19 | */ 20 | final readonly class CreateRequest 21 | { 22 | /** 23 | 24 | nullable)) { 25 | continue; 26 | }?> 27 | * @param non-empty-string $propertyName.PHP_EOL; ?> 28 | 29 | */ 30 | public function __construct() {} 31 | } 32 | -------------------------------------------------------------------------------- /backend/src-dev/Maker/Resources/skeleton/http/update/UpdateAction.tpl.php: -------------------------------------------------------------------------------- 1 | 14 | 15 | declare(strict_types=1); 16 | 17 | namespace ; 18 | 19 | 20 | 21 | /** 22 | * Ручка обновления 23 | */ 24 | #[AsController] 25 | #[Route('', )] 26 | #[IsGranted()] 27 | final readonly class 28 | { 29 | public function __construct( 30 | private Flush $flush, 31 | private GetAction $infoAction, 32 | ) {} 33 | 34 | public function __invoke( 35 | #[ValueResolver(ArgumentValueResolver::class)] 36 | $entity, 37 | #[ValueResolver(ApiRequestValueResolver::class)] 38 | UpdateRequest $updateRequest, 39 | ): ApiObjectResponse { 40 | // TODO: обновить сущность 41 | ($this->flush)(); 42 | 43 | return ($this->infoAction)($entity); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /backend/src-dev/Maker/Resources/skeleton/http/update/UpdateRequest.tpl.php: -------------------------------------------------------------------------------- 1 | > $entity_fields 10 | */ 11 | echo " 12 | 13 | declare(strict_types=1); 14 | 15 | namespace ; 16 | 17 | /** 18 | * Запрос для обновления 19 | */ 20 | final readonly class UpdateRequest 21 | { 22 | /** 23 | 24 | nullable)) { 25 | continue; 26 | }?> 27 | * @param non-empty-string $propertyName.PHP_EOL; ?> 28 | 29 | */ 30 | public function __construct() {} 31 | } 32 | -------------------------------------------------------------------------------- /backend/src-dev/OpenApi/.spectral.yaml: -------------------------------------------------------------------------------- 1 | extends: 2 | - "spectral:oas" 3 | - "spectral:asyncapi" 4 | rules: 5 | path-casing: { 6 | "given": [ 7 | "$.paths" 8 | ], 9 | "severity": "error", 10 | "then": { 11 | "function": "pattern", 12 | "functionOptions": { 13 | "match": "^(\/|[a-z0-9-.]+|{[a-zA-Z0-9]+})+$" 14 | }, 15 | "field": "@key" 16 | }, 17 | "description": "Paths must be `kebab-case`, with hyphens separating words.\n\n**Invalid Example**\n\n`userInfo` must be separated with a hyphen.\n\n```json\n{\n \"/userInfo\": {\n \"post: }\n ....\n}\n``` \n\n**Valid Example**\n\n```json\n{\n \"/user-info\": {\n \"post: }\n ....\n}\n```", 18 | "message": "Paths must be kebab-case" 19 | } 20 | -------------------------------------------------------------------------------- /backend/src-dev/OpenApi/README.md: -------------------------------------------------------------------------------- 1 | # Инструменты для работы с OpenAPI 2 | 3 | ## Сборка openapi.yaml 4 | 5 | Для удобства работы спецификация OpenAPI разбита на отдельные файлы и собирается с помощью команды. 6 | 7 | Структура файлов: 8 | * `backend/src-dev/OpenApi/resources/base.yaml` - базовые файлы с мета-информацией. 9 | Директивы верхнего уровня `info`, `servers` и `security` нужно прописывать в них. 10 | * `backend/src-dev/OpenApi/resources/%module_name%.yaml` - файлы с конфигурацией модулей. 11 | Обязательно должны содержать директивы верхнего уровня `openapi`, `info`, `tags`, `paths` и `components`. 12 | * `backend/src-dev/OpenApi/resources/common.yaml` - файл с общими для всех модулей компонентами. 13 | * `backend/src-dev/OpenApi/resources/_template_module.yaml` - файл-шаблон для новых модулей, 14 | содержит минимально необходимые директивы. 15 | 16 | Для сборки следует запустить команду: 17 | ```shell 18 | make generate-openapi 19 | ``` 20 | -------------------------------------------------------------------------------- /backend/src-dev/OpenApi/resources/_template_module.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: symfony-starter-kit 4 | version: 1.0.0 5 | tags: [] 6 | paths: {} 7 | components: 8 | securitySchemes: {} 9 | headers: {} 10 | parameters: {} 11 | requestBodies: {} 12 | responses: {} 13 | schemas: {} 14 | -------------------------------------------------------------------------------- /backend/src-dev/OpenApi/resources/base.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: symfony-starter-kit 4 | version: 1.0.0 5 | description: Заготовка для проектов на Symfony, примеры модулей 6 | contact: 7 | name: Студия 15 8 | url: https://www.15web.ru 9 | email: info@15web.ru 10 | servers: 11 | - url: /api 12 | security: 13 | - ApiTokenAuth: [ ] 14 | tags: [ ] 15 | paths: { } 16 | components: { } 17 | -------------------------------------------------------------------------------- /backend/src-dev/OpenApi/resources/site/ping.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: symfony-starter-kit 4 | version: 1.0.0 5 | tags: 6 | - name: general 7 | description: Общее 8 | paths: 9 | /ping: 10 | get: 11 | operationId: ping 12 | summary: Пинг приложения 13 | description: Пинг приложения 14 | tags: 15 | - general 16 | security: [ ] 17 | responses: 18 | '200': 19 | $ref: '#/components/responses/ping' 20 | components: 21 | responses: 22 | ping: 23 | description: Возвращает ответ Pong 24 | content: 25 | application/json: 26 | schema: 27 | type: object 28 | properties: 29 | data: 30 | type: object 31 | required: 32 | - result 33 | additionalProperties: false 34 | properties: 35 | result: 36 | type: string 37 | description: Ответ 38 | minLength: 1 39 | example: Pong 40 | status: 41 | $ref: '../common.yaml#/components/schemas/succeedStatus' 42 | -------------------------------------------------------------------------------- /backend/src-dev/OpenApi/resources/site/setting.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: symfony-starter-kit 4 | version: 1.0.0 5 | tags: 6 | - name: setting 7 | description: Настройки 8 | paths: 9 | /settings: 10 | get: 11 | operationId: settingList 12 | summary: Получить список настроек 13 | description: Получить список настроек 14 | tags: 15 | - setting 16 | security: [ ] 17 | responses: 18 | '200': 19 | $ref: '#/components/responses/settingList' 20 | components: 21 | responses: 22 | settingList: 23 | description: Список настроек 24 | content: 25 | application/json: 26 | schema: 27 | type: object 28 | properties: 29 | data: 30 | type: array 31 | items: 32 | type: object 33 | additionalProperties: false 34 | required: 35 | - type 36 | - value 37 | properties: 38 | type: 39 | $ref: '#/components/schemas/settingType' 40 | value: 41 | $ref: '#/components/schemas/settingValue' 42 | headers: 43 | X-Request-TraceId: 44 | $ref: '../common.yaml#/components/headers/requestTraceId' 45 | schemas: 46 | settingType: 47 | type: string 48 | description: Тип настройки 49 | example: site_name 50 | settingValue: 51 | type: string 52 | description: Значение настройки 53 | example: symfony-starter-kit 54 | 55 | -------------------------------------------------------------------------------- /backend/src-dev/PHPStan/EmbeddablePropertiesExtension.php: -------------------------------------------------------------------------------- 1 | getDeclaringClass()->getNativeReflection(); 25 | $classAttributes = $reflectionClass->getAttributes(); 26 | foreach ($classAttributes as $attribute) { 27 | if ($attribute->getName() === Embeddable::class) { 28 | return true; 29 | } 30 | } 31 | 32 | return false; 33 | } 34 | 35 | #[Override] 36 | public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool 37 | { 38 | return false; 39 | } 40 | 41 | #[Override] 42 | public function isInitialized(PropertyReflection $property, string $propertyName): bool 43 | { 44 | return false; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /backend/src-dev/PHPStan/object-manager.php: -------------------------------------------------------------------------------- 1 | bootEnv(__DIR__.'/../../.env'); 12 | 13 | /** 14 | * @var string $appEnv 15 | */ 16 | $appEnv = $_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? 'dev'; 17 | 18 | $kernel = new Kernel($appEnv, (bool) ($_SERVER['APP_DEBUG'] ?? true)); 19 | $kernel->boot(); 20 | 21 | /** @var ManagerRegistry $doctrine */ 22 | $doctrine = $kernel->getContainer()->get('doctrine'); 23 | 24 | return $doctrine->getManager(); 25 | -------------------------------------------------------------------------------- /backend/src-dev/PHPStan/phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | -------------------------------------------------------------------------------- /backend/src-dev/PHPStan/phpstan-config.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 9 3 | fileExtensions: 4 | - php 5 | paths: 6 | - ../../src 7 | - ../Tests 8 | - ../ 9 | tmpDir: ../../var/cache/phpstan 10 | excludePaths: 11 | - ../../src/*/config.php 12 | - ../Rector/rector.config.php 13 | - ../Maker 14 | - ../PHPCsFixer 15 | parallel: 16 | maximumNumberOfProcesses: 8 17 | checkUninitializedProperties: true 18 | doctrine: 19 | objectManagerLoader: object-manager.php 20 | 21 | includes: 22 | - ../../vendor/phpstan/phpstan/conf/bleedingEdge.neon 23 | - ../../vendor/phpstan/phpstan-doctrine/extension.neon 24 | - ../../vendor/phpstan/phpstan-doctrine/rules.neon 25 | - ../../vendor/phpstan/phpstan-strict-rules/rules.neon 26 | - phpstan-baseline.neon 27 | 28 | rules: 29 | - PHPStan\Rules\Doctrine\ORM\EntityNotFinalRule 30 | 31 | services: 32 | - 33 | class: Dev\PHPStan\EmbeddablePropertiesExtension 34 | tags: 35 | - phpstan.properties.readWriteExtension 36 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Functional/Infrastructure/Response/ApiSystemExceptionTest.php: -------------------------------------------------------------------------------- 1 | getContent()); 31 | 32 | self::assertSuccessResponse($response); 33 | self::assertSame('pong', $data['data']['result']); 34 | self::assertSame(ResponseStatus::Success->value, $data['status']); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Functional/SDK/Profile.php: -------------------------------------------------------------------------------- 1 | $setting['isPublic'], 23 | ); 24 | 25 | $publicSettings = Setting::publicList(); 26 | 27 | self::assertNotEmpty($publicSettings); 28 | 29 | self::assertCount(\count($filteredSettings), $publicSettings); 30 | 31 | foreach ($publicSettings as $publicSetting) { 32 | self::assertNotEmpty($publicSetting['type']); 33 | self::assertNotEmpty($publicSetting['value']); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Rector/AssertMustHaveMessageRector/AssertMustHaveMessageTest.php: -------------------------------------------------------------------------------- 1 | doTestFile($filePath); 24 | } 25 | 26 | public static function provideData(): Iterator 27 | { 28 | return self::yieldFilesFromDirectory(__DIR__.'/Fixture'); 29 | } 30 | 31 | #[Override] 32 | public function provideConfigFilePath(): string 33 | { 34 | return __DIR__.'/config/config.php'; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Rector/AssertMustHaveMessageRector/Fixture/skip_rule_test_fixture.php.inc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Rector/AssertMustHaveMessageRector/Fixture/test_fixture.php.inc: -------------------------------------------------------------------------------- 1 | 22 | ----- 23 | 44 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Rector/AssertMustHaveMessageRector/config/config.php: -------------------------------------------------------------------------------- 1 | withRules([ 10 | AssertMustHaveMessageRector::class, 11 | ]); 12 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Rector/OneFlushInClassRector/Fixture/skip_rule_test_fixture.php.inc: -------------------------------------------------------------------------------- 1 | em->flush(); 17 | } 18 | } 19 | 20 | final readonly class AnotherClass 21 | { 22 | public function __construct( 23 | private Flush $flush, 24 | ) {} 25 | 26 | public function handle(): void 27 | { 28 | ($this->flush)(); 29 | } 30 | } 31 | ?> -------------------------------------------------------------------------------- /backend/src-dev/Tests/Rector/OneFlushInClassRector/Fixture/test_fixture.php.inc: -------------------------------------------------------------------------------- 1 | em->flush(); 18 | ($this->flush)(); 19 | } 20 | 21 | public function anotherOne(): void 22 | { 23 | $this->em->flush(); 24 | } 25 | } 26 | ?> 27 | ----- 28 | em->flush(); 45 | } 46 | 47 | public function anotherOne(): void 48 | { 49 | } 50 | } 51 | ?> 52 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Rector/OneFlushInClassRector/OneFlushInClassTest.php: -------------------------------------------------------------------------------- 1 | doTestFile($filePath); 24 | } 25 | 26 | public static function provideData(): Iterator 27 | { 28 | return self::yieldFilesFromDirectory(__DIR__.'/Fixture'); 29 | } 30 | 31 | #[Override] 32 | public function provideConfigFilePath(): string 33 | { 34 | return __DIR__.'/config/config.php'; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Rector/OneFlushInClassRector/config/config.php: -------------------------------------------------------------------------------- 1 | withRules([ 10 | OneFlushInClassRector::class, 11 | ]); 12 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Rector/RequestMethodInsteadOfStringRector/Fixture/skip_rule_test_fixture.php.inc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Rector/RequestMethodInsteadOfStringRector/Fixture/test_fixture.php.inc: -------------------------------------------------------------------------------- 1 | 25 | ----- 26 | 50 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Rector/RequestMethodInsteadOfStringRector/RequestMethodInsteadOfStringTest.php: -------------------------------------------------------------------------------- 1 | doTestFile($filePath); 24 | } 25 | 26 | public static function provideData(): Iterator 27 | { 28 | return self::yieldFilesFromDirectory(__DIR__.'/Fixture'); 29 | } 30 | 31 | #[Override] 32 | public function provideConfigFilePath(): string 33 | { 34 | return __DIR__.'/config/config.php'; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Rector/RequestMethodInsteadOfStringRector/config/config.php: -------------------------------------------------------------------------------- 1 | withRules([ 10 | RequestMethodInsteadOfStringRector::class, 11 | ]); 12 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Rector/ResolversInAction/Fixture/skip_rule_test_fixture.php.inc: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Rector/ResolversInAction/Fixture/test_fixture.php.inc: -------------------------------------------------------------------------------- 1 | 20 | ----- 21 | 42 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Rector/ResolversInAction/ResolversInActionTest.php: -------------------------------------------------------------------------------- 1 | doTestFile($filePath); 24 | } 25 | 26 | public static function provideData(): Iterator 27 | { 28 | return self::yieldFilesFromDirectory(__DIR__.'/Fixture'); 29 | } 30 | 31 | #[Override] 32 | public function provideConfigFilePath(): string 33 | { 34 | return __DIR__.'/config/config.php'; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Rector/ResolversInAction/config/config.php: -------------------------------------------------------------------------------- 1 | withRules([ 10 | ResolversInActionRector::class, 11 | ]); 12 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Unit/Infrastructure/EmailTest.php: -------------------------------------------------------------------------------- 1 | value); 24 | } 25 | 26 | #[TestDox('Невалидный формат email')] 27 | public function testInvalidEmail(): void 28 | { 29 | $this->expectException(InvalidArgumentException::class); 30 | 31 | new Email('test'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Unit/Infrastructure/Pagination/PaginationRequestTest.php: -------------------------------------------------------------------------------- 1 | offset); 23 | self::assertSame(10, $paginationRequest->limit); 24 | } 25 | 26 | #[TestDox('Проверка аргументов')] 27 | public function testSuccess(): void 28 | { 29 | $paginationRequest = new PaginationRequest( 30 | offset: 3, 31 | limit: 15, 32 | ); 33 | 34 | self::assertSame(3, $paginationRequest->offset); 35 | self::assertSame(15, $paginationRequest->limit); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Unit/Infrastructure/Pagination/PaginationResponseTest.php: -------------------------------------------------------------------------------- 1 | total); 24 | } 25 | 26 | #[TestDox('Общее кол-во не может быть отрицательным')] 27 | public function testIncorrect(): void 28 | { 29 | $this->expectException(InvalidArgumentException::class); 30 | new PaginationResponse(-1); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Unit/Infrastructure/PhoneTest.php: -------------------------------------------------------------------------------- 1 | equalTo($profilePhone2)); 27 | } 28 | 29 | #[DataProvider('incorrectPhones')] 30 | #[TestDox('Невалидный номер телефона')] 31 | public function testIncorrectNumber(string $phone): void 32 | { 33 | $this->expectException(InvalidArgumentException::class); 34 | 35 | /** 36 | * @psalm-suppress ArgumentTypeCoercion 37 | * 38 | * @phpstan-ignore-next-line 39 | */ 40 | new Phone($phone); 41 | } 42 | 43 | public static function incorrectPhones(): Iterator 44 | { 45 | yield 'Неверный формат' => ['неправильный телефон']; 46 | 47 | yield 'Пустой номер' => ['']; 48 | 49 | yield '10 цифр' => ['8922222222']; 50 | 51 | yield '12 цифр' => ['892222222222']; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Unit/Task/Domain/TaskCommentBodyTest.php: -------------------------------------------------------------------------------- 1 | equalTo($body2)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Unit/Task/Domain/TaskCommentIdTest.php: -------------------------------------------------------------------------------- 1 | equalTo($taskCommentId2)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Unit/Task/Domain/TaskIdTest.php: -------------------------------------------------------------------------------- 1 | equalTo($taskId2)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Unit/Task/Domain/TaskNameTest.php: -------------------------------------------------------------------------------- 1 | equalTo($taskName2)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Unit/Task/Domain/TaskTest.php: -------------------------------------------------------------------------------- 1 | markAsDone(); 30 | 31 | $this->expectException(TaskAlreadyIsDoneException::class); 32 | $task->markAsDone(); 33 | } 34 | 35 | #[TestDox('Нельзя комментировать выполненную задачу')] 36 | public function testAddCommentToCompletedTask(): void 37 | { 38 | $task = new Task(new TaskId(), new TaskName('new task'), new UserId()); 39 | $task->markAsDone(); 40 | 41 | $comment = new TaskComment($task, new TaskCommentId(), new TaskCommentBody('Комментарий')); 42 | 43 | $this->expectException(AddCommentToCompletedTaskException::class); 44 | $task->addComment($comment); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Unit/User/Profile/Domain/ProfileIdTest.php: -------------------------------------------------------------------------------- 1 | equalTo($profileId2)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Unit/User/SignUp/Domain/ConfirmTokenTest.php: -------------------------------------------------------------------------------- 1 | equalTo($confirmToken2)); 26 | } 27 | 28 | #[TestDox('Разные токены')] 29 | public function testNotEquals(): void 30 | { 31 | $confirmToken1 = new ConfirmToken(new UuidV7()); 32 | $confirmToken2 = new ConfirmToken(new UuidV7()); 33 | 34 | self::assertFalse($confirmToken1->equalTo($confirmToken2)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/Unit/User/User/Domain/UserPasswordTest.php: -------------------------------------------------------------------------------- 1 | hash(); 27 | 28 | self::assertStringStartsWith('$2y$04', $hash); 29 | } 30 | 31 | #[TestDox('Проверка хэша')] 32 | public function testVerifyMethod(): void 33 | { 34 | $userPassword = new UserPassword( 35 | cleanPassword: 'password', 36 | hashCost: 4, 37 | ); 38 | 39 | $hash = password_hash( 40 | password: 'password', 41 | algo: PASSWORD_BCRYPT, 42 | options: ['cost' => 4], 43 | ); 44 | 45 | self::assertTrue($userPassword->verify($hash)); 46 | } 47 | 48 | #[TestDox('Недостаточная длина пароля')] 49 | public function testInvalidLength(): void 50 | { 51 | $this->expectException(InvalidArgumentException::class); 52 | 53 | new UserPassword( 54 | cleanPassword: 'tiny', 55 | hashCost: 4, 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /backend/src-dev/Tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | bootEnv(dirname(__DIR__).'/../.env'); 14 | } 15 | 16 | set_exception_handler([new ErrorHandler(), 'handleException']); 17 | -------------------------------------------------------------------------------- /backend/src-dev/phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Tests 21 | 22 | 23 | 24 | 25 | 26 | ../src 27 | 28 | 29 | /app/src/User/config.php 30 | /app/src/Mailer/config.php 31 | /app/src/Infrastructure/Kernel.php 32 | /app/src/Infrastructure/di.php 33 | /app/src/Infrastructure/Request/ValinorConfigurator.php 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /backend/src-dev/psalm-baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /backend/src/Article/Http/Admin/ArticleInfoAction.php: -------------------------------------------------------------------------------- 1 | $ids 16 | */ 17 | public function __construct( 18 | public array $ids, 19 | ) {} 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/Article/Http/Admin/ArticleListData.php: -------------------------------------------------------------------------------- 1 | articleRepository->remove($article); 34 | ($this->flush)(); 35 | 36 | return new ApiObjectResponse( 37 | data: new SuccessResponse(), 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /backend/src/Article/Http/Admin/UpdateArticleRequest.php: -------------------------------------------------------------------------------- 1 | $errors 21 | */ 22 | public function __construct( 23 | private readonly array $errors, 24 | ?Throwable $previous = null, 25 | ) { 26 | parent::__construct(previous: $previous); 27 | } 28 | 29 | #[Override] 30 | public function getHttpCode(): int 31 | { 32 | return Response::HTTP_FORBIDDEN; 33 | } 34 | 35 | #[Override] 36 | public function getApiCode(): int 37 | { 38 | return Response::HTTP_FORBIDDEN; 39 | } 40 | 41 | #[Override] 42 | public function getErrorMessage(): string 43 | { 44 | return self::MESSAGE; 45 | } 46 | 47 | #[Override] 48 | public function getErrors(): array 49 | { 50 | return $this->errors; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /backend/src/Infrastructure/ApiException/ApiBadRequestException.php: -------------------------------------------------------------------------------- 1 | $errors 21 | */ 22 | public function __construct( 23 | private readonly array $errors, 24 | ?Throwable $previous = null, 25 | ) { 26 | parent::__construct(previous: $previous); 27 | } 28 | 29 | #[Override] 30 | public function getErrorMessage(): string 31 | { 32 | return self::MESSAGE; 33 | } 34 | 35 | #[Override] 36 | public function getHttpCode(): int 37 | { 38 | return Response::HTTP_BAD_REQUEST; 39 | } 40 | 41 | #[Override] 42 | public function getApiCode(): int 43 | { 44 | return Response::HTTP_BAD_REQUEST; 45 | } 46 | 47 | /** 48 | * @return non-empty-list 49 | */ 50 | #[Override] 51 | public function getErrors(): array 52 | { 53 | return $this->errors; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /backend/src/Infrastructure/ApiException/ApiBadResponseException.php: -------------------------------------------------------------------------------- 1 | $errors 20 | */ 21 | public function __construct(private readonly array $errors, private readonly ApiErrorCode $apiCode) 22 | { 23 | parent::__construct(); 24 | } 25 | 26 | #[Override] 27 | public function getErrorMessage(): string 28 | { 29 | return self::MESSAGE; 30 | } 31 | 32 | #[Override] 33 | public function getHttpCode(): int 34 | { 35 | return Response::HTTP_OK; 36 | } 37 | 38 | #[Override] 39 | public function getApiCode(): int 40 | { 41 | return $this->apiCode->value; 42 | } 43 | 44 | /** 45 | * @return non-empty-list 46 | */ 47 | #[Override] 48 | public function getErrors(): array 49 | { 50 | return $this->errors; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /backend/src/Infrastructure/ApiException/ApiErrorCode.php: -------------------------------------------------------------------------------- 1 | $errors 14 | */ 15 | public function __construct( 16 | private string $message, 17 | private array $errors, 18 | private int $code, 19 | ) {} 20 | 21 | public function getCode(): int 22 | { 23 | return $this->code; 24 | } 25 | 26 | public function getMessage(): string 27 | { 28 | return $this->message; 29 | } 30 | 31 | /** 32 | * @return non-empty-list 33 | */ 34 | public function getErrors(): array 35 | { 36 | return $this->errors; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend/src/Infrastructure/ApiException/ApiException.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | public function getErrors(): array; 22 | } 23 | -------------------------------------------------------------------------------- /backend/src/Infrastructure/ApiException/ApiHeaders.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function getHeaders(): array; 16 | } 17 | -------------------------------------------------------------------------------- /backend/src/Infrastructure/ApiException/ApiNotFoundException.php: -------------------------------------------------------------------------------- 1 | $errors 20 | */ 21 | public function __construct(private readonly array $errors) 22 | { 23 | parent::__construct(); 24 | } 25 | 26 | #[Override] 27 | public function getErrorMessage(): string 28 | { 29 | return self::MESSAGE; 30 | } 31 | 32 | #[Override] 33 | public function getHttpCode(): int 34 | { 35 | return Response::HTTP_NOT_FOUND; 36 | } 37 | 38 | #[Override] 39 | public function getApiCode(): int 40 | { 41 | return Response::HTTP_NOT_FOUND; 42 | } 43 | 44 | /** 45 | * @return non-empty-list 46 | */ 47 | #[Override] 48 | public function getErrors(): array 49 | { 50 | return $this->errors; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /backend/src/Infrastructure/ApiException/ApiSystemException.php: -------------------------------------------------------------------------------- 1 | $errors 20 | */ 21 | public function __construct( 22 | private readonly array $errors, 23 | private readonly int $status, 24 | ?Throwable $previous = null, 25 | ) { 26 | parent::__construct(previous: $previous); 27 | } 28 | 29 | #[Override] 30 | public function getErrorMessage(): string 31 | { 32 | return self::MESSAGE; 33 | } 34 | 35 | #[Override] 36 | public function getHttpCode(): int 37 | { 38 | return $this->status; 39 | } 40 | 41 | #[Override] 42 | public function getApiCode(): int 43 | { 44 | return $this->status; 45 | } 46 | 47 | #[Override] 48 | public function getErrors(): array 49 | { 50 | return $this->errors; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /backend/src/Infrastructure/ApiException/ApiUnauthorizedException.php: -------------------------------------------------------------------------------- 1 | $errors 20 | */ 21 | public function __construct(private readonly array $errors) 22 | { 23 | parent::__construct(); 24 | } 25 | 26 | #[Override] 27 | public function getErrorMessage(): string 28 | { 29 | return self::MESSAGE; 30 | } 31 | 32 | #[Override] 33 | public function getHttpCode(): int 34 | { 35 | return Response::HTTP_UNAUTHORIZED; 36 | } 37 | 38 | #[Override] 39 | public function getApiCode(): int 40 | { 41 | return Response::HTTP_UNAUTHORIZED; 42 | } 43 | 44 | /** 45 | * @return non-empty-list 46 | */ 47 | #[Override] 48 | public function getErrors(): array 49 | { 50 | return $this->errors; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /backend/src/Infrastructure/CheckRateLimiter.php: -------------------------------------------------------------------------------- 1 | create($key); 31 | $limit = $limiter->consume(); 32 | 33 | if ($limit->isAccepted() === false) { 34 | $this->logger->info( 35 | message: 'Превышено допустимое количество запросов', 36 | context: ['key' => $key], 37 | ); 38 | 39 | throw new ApiRateLimiterException($limit); 40 | } 41 | 42 | return $limiter; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /backend/src/Infrastructure/EventListener/FillMailFields.php: -------------------------------------------------------------------------------- 1 | getMessage(); 32 | if (!$email instanceof Email) { 33 | return; 34 | } 35 | 36 | $email->from(new Address($this->fromEmail, $this->fromName)); 37 | $email->subject(\sprintf('%s. %s', $this->fromName, $email->getSubject() ?? '')); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /backend/src/Infrastructure/Flush.php: -------------------------------------------------------------------------------- 1 | entityManager->flush(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/Infrastructure/KeepCacheArrayAdapter.php: -------------------------------------------------------------------------------- 1 | DTO, последовательно используется в методах 12 | * - ApiRequestMapper::registerUuidConstructor() 13 | * - ApiRequestMapper::filterAllowedExceptions() 14 | */ 15 | final class ApiRequestMappingException extends Exception 16 | { 17 | public function __construct(Throwable $previous) 18 | { 19 | parent::__construct(message: $previous->getMessage(), previous: $previous); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/src/Infrastructure/Request/BuildValidationError.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | public function __invoke(MappingError $error): array 19 | { 20 | $messages = Messages::flattenFromNode( 21 | node: $error->node(), 22 | ); 23 | 24 | $allMessages = []; 25 | foreach ($messages->errors() as $message) { 26 | $allMessages[] = $message 27 | ->withBody('{node_path}: {original_message}') 28 | ->toString(); 29 | } 30 | 31 | /** 32 | * @var non-empty-list $allMessages 33 | */ 34 | return $allMessages; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/src/Infrastructure/Request/Pagination/PaginationRequest.php: -------------------------------------------------------------------------------- 1 | $offset 14 | * @param positive-int $limit 15 | */ 16 | public function __construct(public int $offset = 0, public int $limit = 10) {} 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/Infrastructure/Request/ValidateAcceptHeader.php: -------------------------------------------------------------------------------- 1 | getRequest(); 20 | $acceptableContentTypes = $request->getAcceptableContentTypes(); 21 | 22 | if (\in_array('application/json', $acceptableContentTypes, true)) { 23 | return; 24 | } 25 | 26 | if ( 27 | $acceptableContentTypes === [] 28 | || \in_array('*/*', $acceptableContentTypes, true) 29 | || \in_array('application/*', $acceptableContentTypes, true) 30 | ) { 31 | $request->headers->set('Accept', 'application/json'); 32 | 33 | return; 34 | } 35 | 36 | throw new ApiBadRequestException(['Укажите заголовок Accept: application/json']); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend/src/Infrastructure/Response/ApiListObjectResponse.php: -------------------------------------------------------------------------------- 1 | $data 14 | * @param object|null $meta Дополнительная мета-информация в ответе (фильтры, ссылки и т.п.) 15 | */ 16 | public function __construct( 17 | public iterable $data, 18 | public PaginationResponse $pagination, 19 | public ?object $meta = null, 20 | public ResponseStatus $status = ResponseStatus::Success, 21 | ) {} 22 | } 23 | -------------------------------------------------------------------------------- /backend/src/Infrastructure/Response/ApiObjectResponse.php: -------------------------------------------------------------------------------- 1 | status = ResponseStatus::Success; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/Infrastructure/ValueObject/Email.php: -------------------------------------------------------------------------------- 1 | value = $value; 31 | } 32 | 33 | /** 34 | * @param object $other 35 | */ 36 | public function equalTo(mixed $other): bool 37 | { 38 | return $other::class === self::class && $this->value === $other->value; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /backend/src/Infrastructure/di.php: -------------------------------------------------------------------------------- 1 | services()->defaults()->autowire()->autoconfigure(); 11 | 12 | $services 13 | ->load('App\\', '../*') 14 | ->exclude([ 15 | './{di.php}', 16 | '../**/{di.php}', 17 | '../**/{config.php}', 18 | ]); 19 | }; 20 | -------------------------------------------------------------------------------- /backend/src/Logger/Http/RequestIdLoggerProcessor.php: -------------------------------------------------------------------------------- 1 | requestStack->getCurrentRequest(); 27 | 28 | if ($request !== null && $request->headers->has(RequestIdListener::TRACE_ID_HEADER)) { 29 | $record->extra['traceId'] = $request->headers->get(RequestIdListener::TRACE_ID_HEADER); 30 | } 31 | 32 | return $record; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /backend/src/Mailer/Notification/EmailConfirmation/ConfirmEmailMessage.php: -------------------------------------------------------------------------------- 1 | to($message->email->value) 23 | ->subject('Подтверждение email') 24 | ->htmlTemplate('@mails/emails/confirm.html.twig') 25 | ->context([ 26 | 'confirmToken' => $message->confirmToken, 27 | ]); 28 | 29 | $email->getHeaders()->addTextHeader('confirmToken', (string) $message->confirmToken); 30 | 31 | $this->mailer->send($email); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /backend/src/Mailer/Notification/PasswordRecovery/RecoveryPasswordMessage.php: -------------------------------------------------------------------------------- 1 | to($message->email->value) 25 | ->subject($subject) 26 | ->htmlTemplate('@mails/emails/recoverPassword.html.twig') 27 | ->context([ 28 | 'recoverToken' => $message->token, 29 | ]); 30 | 31 | $email->getHeaders()->addTextHeader('recoverToken', (string) $message->token); 32 | 33 | $this->mailer->send($email); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/src/Mailer/Notification/UncompletedTasks/SendUncompletedTasksToUser.php: -------------------------------------------------------------------------------- 1 | to($message->email->value) 23 | ->subject('Невыполненные задачи') 24 | ->htmlTemplate('@mails/emails/uncompleted-tasks.html.twig') 25 | ->context([ 26 | 'tasks' => $message->tasks, 27 | ]); 28 | 29 | $this->mailer->send($email); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /backend/src/Mailer/Notification/UncompletedTasks/TaskData.php: -------------------------------------------------------------------------------- 1 | messenger()->routing(Message::class); 15 | 16 | if ($containerConfigurator->env() === 'dev') { 17 | $messengerRouting->senders(['async']); 18 | } 19 | 20 | if ($containerConfigurator->env() === 'test') { 21 | $messengerRouting->senders(['sync']); 22 | } 23 | 24 | if ($containerConfigurator->env() === 'prod') { 25 | $messengerRouting->senders(['async']); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /backend/src/Mailer/templates/emails/confirm.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@mails/layout.html.twig' %} 2 | {% block body %} 3 |

Токен подтверждения email {{ confirmToken }}

4 | {% endblock %} 5 | -------------------------------------------------------------------------------- /backend/src/Mailer/templates/emails/recoverPassword.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@mails/layout.html.twig' %} 2 | {% block body %} 3 |

Токен восстановления пароля {{ recoverToken }}

4 | {% endblock %} 5 | -------------------------------------------------------------------------------- /backend/src/Mailer/templates/emails/uncompleted-tasks.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@mails/layout.html.twig' %} 2 | {% block body %} 3 |

У вас есть невыполненные задачи:

4 |
    5 | {% for task in tasks %} 6 |
  • 7 | Задача {{ task.taskName }} от {{ task.createdAt|date('Y-m-d H:i:s') }} 8 |
  • 9 | {% endfor %} 10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /backend/src/Mailer/templates/layout.html.twig: -------------------------------------------------------------------------------- 1 | {% block header %} 2 |

3 | symfony-starter-kit 4 |

5 | {% endblock %} 6 | {% block body %} 7 | {% endblock %} 8 | {% block footer %} 9 | https://www.15web.ru 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /backend/src/Ping/Http/Site/PingAction.php: -------------------------------------------------------------------------------- 1 | connection->fetchOne("select 'pong'"); 28 | 29 | return new ApiObjectResponse(new Pong($result)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /backend/src/Ping/Http/Site/Pong.php: -------------------------------------------------------------------------------- 1 | seoRepository->findByTypeIdentity( 21 | type: $command->type, 22 | identity: $command->identity, 23 | ); 24 | 25 | if ($seo === null) { 26 | $seo = new Seo( 27 | type: $command->type, 28 | identity: $command->identity, 29 | title: $command->title, 30 | ); 31 | 32 | $this->seoRepository->add($seo); 33 | } 34 | 35 | $seo->change( 36 | title: $command->title, 37 | description: $command->description, 38 | keywords: $command->keywords, 39 | ); 40 | 41 | ($this->flush)(); 42 | 43 | return $seo; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /backend/src/Seo/Command/SaveSeoCommand.php: -------------------------------------------------------------------------------- 1 | entityManager->persist($entity); 19 | } 20 | 21 | public function findByTypeIdentity(SeoResourceType $type, string $identity): ?Seo 22 | { 23 | return $this->entityManager->getRepository(Seo::class)->findOneBy([ 24 | 'type' => $type->value, 25 | 'identity' => $identity, 26 | ]); 27 | } 28 | 29 | public function findOneByTypeAndIdentity(string $type, string $identity): ?Seo 30 | { 31 | return $this->entityManager->getRepository(Seo::class)->findOneBy(['type' => $type, 'identity' => $identity]); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /backend/src/Seo/Domain/SeoResourceType.php: -------------------------------------------------------------------------------- 1 | seoRepository->findOneByTypeAndIdentity( 32 | type: $type, 33 | identity: $identity, 34 | ); 35 | 36 | return new ApiObjectResponse( 37 | data: $this->buildResponseData($seo), 38 | ); 39 | } 40 | 41 | private function buildResponseData(?Seo $seo): SeoData 42 | { 43 | return new SeoData( 44 | title: $seo?->getTitle(), 45 | description: $seo?->getDescription(), 46 | keywords: $seo?->getKeywords(), 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /backend/src/Seo/Http/Site/SeoData.php: -------------------------------------------------------------------------------- 1 | settingsRepository->findByType($command->type); 24 | 25 | if ($setting === null) { 26 | throw new SettingNotFoundException(); 27 | } 28 | 29 | $setting->change($command->value); 30 | 31 | return $setting; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /backend/src/Setting/Command/SaveSettingCommand.php: -------------------------------------------------------------------------------- 1 | entityManager->persist($entity); 19 | } 20 | 21 | public function findByType(SettingType $type): ?Setting 22 | { 23 | return $this->entityManager 24 | ->getRepository(Setting::class) 25 | ->findOneBy(['type' => $type->value]); 26 | } 27 | 28 | /** 29 | * @return list 30 | */ 31 | public function getAll(): array 32 | { 33 | /** @var list $settings */ 34 | $settings = $this->entityManager 35 | ->getRepository(Setting::class) 36 | ->createQueryBuilder('s') 37 | ->orderBy('s.createdAt', 'DESC') 38 | ->getQuery() 39 | ->getResult(); 40 | 41 | return $settings; 42 | } 43 | 44 | /** 45 | * @return list 46 | */ 47 | public function getAllPublic(): array 48 | { 49 | /** @var list $settings */ 50 | $settings = $this->entityManager 51 | ->getRepository(Setting::class) 52 | ->createQueryBuilder('s') 53 | ->where('s.isPublic = true') 54 | ->getQuery()->getResult(); 55 | 56 | return $settings; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /backend/src/Setting/Http/Admin/SettingListData.php: -------------------------------------------------------------------------------- 1 | settingsRepository->getAllPublic(); 27 | 28 | return new ApiListObjectResponse( 29 | data: $this->buildResponseData($settings), 30 | pagination: new PaginationResponse(total: \count($settings)), 31 | ); 32 | } 33 | 34 | /** 35 | * @param list $settings 36 | * 37 | * @return iterable 38 | */ 39 | private function buildResponseData(array $settings): iterable 40 | { 41 | foreach ($settings as $setting) { 42 | yield new SettingListData( 43 | type: $setting->getType()->value, 44 | value: $setting->getValue(), 45 | ); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /backend/src/Setting/Http/Site/SettingListData.php: -------------------------------------------------------------------------------- 1 | commentBody), 30 | ); 31 | 32 | $task->addComment($comment); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /backend/src/Task/Command/Comment/Add/AddCommentOnTaskCommand.php: -------------------------------------------------------------------------------- 1 | markAsDone(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /backend/src/Task/Command/CreateTask/CreateTask.php: -------------------------------------------------------------------------------- 1 | taskName), $userId); 26 | 27 | $this->taskRepository->add($task); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /backend/src/Task/Command/CreateTask/CreateTaskCommand.php: -------------------------------------------------------------------------------- 1 | taskRepository->remove($task); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/src/Task/Command/UpdateTaskName/UpdateTaskName.php: -------------------------------------------------------------------------------- 1 | changeTaskName(new TaskName($command->taskName)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/src/Task/Command/UpdateTaskName/UpdateTaskNameCommand.php: -------------------------------------------------------------------------------- 1 | id = $commentId->getValue(); 37 | $this->body = $taskCommentBody; 38 | $this->task = $task; 39 | $this->createdAt = new DateTimeImmutable(); 40 | $this->updatedAt = null; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /backend/src/Task/Domain/TaskCommentBody.php: -------------------------------------------------------------------------------- 1 | value = $value; 24 | } 25 | 26 | /** 27 | * @param object $other 28 | */ 29 | public function equalTo(mixed $other): bool 30 | { 31 | return $other::class === self::class && $this->value === $other->value; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /backend/src/Task/Domain/TaskCommentId.php: -------------------------------------------------------------------------------- 1 | value = new UuidV7(); 20 | } 21 | 22 | /** 23 | * @param object $other 24 | */ 25 | public function equalTo(mixed $other): bool 26 | { 27 | return $other::class === self::class && $this->value->equals($other->value); 28 | } 29 | 30 | public function getValue(): Uuid 31 | { 32 | return $this->value; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /backend/src/Task/Domain/TaskId.php: -------------------------------------------------------------------------------- 1 | value->equals($other->value); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/src/Task/Domain/TaskName.php: -------------------------------------------------------------------------------- 1 | value = $value; 26 | } 27 | 28 | /** 29 | * @param object $other 30 | */ 31 | public function equalTo(mixed $other): bool 32 | { 33 | return $other::class === self::class && $this->value === $other->value; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/src/Task/Domain/TaskRepository.php: -------------------------------------------------------------------------------- 1 | entityManager->getRepository(Task::class)->findOneBy([ 19 | 'id' => $taskId->value, 20 | ]); 21 | } 22 | 23 | public function add(Task $task): void 24 | { 25 | $this->entityManager->persist($task); 26 | } 27 | 28 | public function remove(Task $task): void 29 | { 30 | $this->entityManager->remove($task); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend/src/Task/Http/Site/CreateTask/TaskData.php: -------------------------------------------------------------------------------- 1 | entityManager->createQuery($dql); 31 | $dqlQuery->setParameter('taskId', $findAllQuery->taskId); 32 | $dqlQuery->setParameter('userId', $findAllQuery->userId); 33 | 34 | /** @var CommentData[] $commentData */ 35 | $commentData = $dqlQuery->getResult(); 36 | 37 | return $commentData; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /backend/src/Task/Query/Task/FindAllByUserId/CountAllTasksByUserId.php: -------------------------------------------------------------------------------- 1 | connection->createQueryBuilder() 22 | ->select('COUNT(t.id)') 23 | ->from('task', 't'); 24 | 25 | $this->filter->applyFilter($queryBuilder, $query); 26 | 27 | /** @var int $result */ 28 | $result = $queryBuilder->executeQuery()->fetchOne(); 29 | 30 | return $result; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend/src/Task/Query/Task/FindAllByUserId/Filter.php: -------------------------------------------------------------------------------- 1 | andWhere('t.user_id = :user_id') 18 | ->setParameter('user_id', $query->userId); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/Task/Query/Task/FindAllByUserId/FindAllTasksByUserIdQuery.php: -------------------------------------------------------------------------------- 1 | entityManager->createQuery($dql); 26 | $dqlQuery->setParameter('id', $query->taskId); 27 | $dqlQuery->setParameter('userId', $query->userId); 28 | 29 | /** @var ?TaskData $result */ 30 | $result = $dqlQuery->getOneOrNullResult(); 31 | 32 | if ($result === null) { 33 | throw new TaskNotFoundException(); 34 | } 35 | 36 | return $result; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend/src/Task/Query/Task/FindById/FindTaskByIdQuery.php: -------------------------------------------------------------------------------- 1 | entityManager->createQuery($dql); 30 | $dqlQuery->setParameter('userId', $query->userId); 31 | 32 | /** @var TaskData[] $taskData */ 33 | $taskData = $dqlQuery->getResult(); 34 | 35 | return $taskData; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /backend/src/Task/Query/Task/FindUncompletedTasksByUserId/FindUncompletedTasksByUserIdQuery.php: -------------------------------------------------------------------------------- 1 | userRepository->findById($command->userId); 29 | 30 | if ($user === null) { 31 | throw new UserNotFoundException(); 32 | } 33 | 34 | $user->applyPassword( 35 | new UserPassword( 36 | cleanPassword: $command->newPassword, 37 | hashCost: $this->hashCost, 38 | ), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /backend/src/User/Password/Command/ChangePasswordCommand.php: -------------------------------------------------------------------------------- 1 | $hashCost 20 | */ 21 | public function __construct( 22 | #[Autowire('%app.hash_cost%')] 23 | private int $hashCost, 24 | private UserRepository $userRepository, 25 | ) {} 26 | 27 | /** 28 | * @throws UserNotFoundException 29 | */ 30 | public function __invoke( 31 | RecoveryToken $recoveryToken, 32 | RecoverPasswordCommand $recoverPasswordCommand, 33 | ): void { 34 | $user = $this->userRepository->findById($recoveryToken->getUserId()); 35 | 36 | if ($user === null) { 37 | throw new UserNotFoundException(); 38 | } 39 | 40 | $user->applyPassword( 41 | new UserPassword( 42 | cleanPassword: $recoverPasswordCommand->password, 43 | hashCost: $this->hashCost, 44 | ), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /backend/src/User/Password/Command/RecoverPasswordCommand.php: -------------------------------------------------------------------------------- 1 | id = $id; 37 | $this->userId = $userId->value; 38 | $this->token = $token; 39 | } 40 | 41 | public function getUserId(): UserId 42 | { 43 | return new UserId($this->userId); 44 | } 45 | 46 | public function getToken(): Uuid 47 | { 48 | return $this->token; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /backend/src/User/Password/Domain/RecoveryTokenRepository.php: -------------------------------------------------------------------------------- 1 | entityManager->getRepository(RecoveryToken::class) 20 | ->findOneBy(['token' => $token]); 21 | } 22 | 23 | public function add(RecoveryToken $recoveryToken): void 24 | { 25 | $this->entityManager->persist($recoveryToken); 26 | } 27 | 28 | public function remove(RecoveryToken $recoveryToken): void 29 | { 30 | $this->entityManager->remove($recoveryToken); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend/src/User/Password/Http/ChangePasswordRequest.php: -------------------------------------------------------------------------------- 1 | newPassword, UserPassword::MIN_LENGTH, 'newPassword: длина не может быть ментьше %2$s симоволов, указано %s'); 30 | Assert::eq($this->newPassword, $this->newPasswordConfirmation, 'newPasswordConfirmation: пароль и его повтор не совпадают'); 31 | Assert::notEq($this->newPassword, $this->currentPassword, 'newPassword: новый пароль не может совпадать с текущим'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /backend/src/User/Profile/Command/SaveProfile/CreateProfile.php: -------------------------------------------------------------------------------- 1 | phone), 28 | name: $command->name, 29 | ); 30 | 31 | $this->profileRepository->add($profile); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /backend/src/User/Profile/Command/SaveProfile/SaveProfile.php: -------------------------------------------------------------------------------- 1 | profileRepository->findByUserId($userId); 26 | 27 | if ($profile !== null) { 28 | ($this->updateProfile)( 29 | command: $command, 30 | profile: $profile, 31 | ); 32 | 33 | return; 34 | } 35 | 36 | ($this->createProfile)( 37 | command: $command, 38 | userId: $userId, 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /backend/src/User/Profile/Command/SaveProfile/SaveProfileCommand.php: -------------------------------------------------------------------------------- 1 | changeName($command->name); 20 | $profile->changePhone(new Phone($command->phone)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /backend/src/User/Profile/Domain/Profile.php: -------------------------------------------------------------------------------- 1 | id = $profileId->value; 40 | $this->userId = $userId->value; 41 | $this->phone = $phone; 42 | $this->name = $name; 43 | } 44 | 45 | public function changePhone(Phone $phone): void 46 | { 47 | $this->phone = $phone; 48 | } 49 | 50 | public function changeName(string $name): void 51 | { 52 | $this->name = $name; 53 | } 54 | 55 | public function getProfileId(): ProfileId 56 | { 57 | return new ProfileId($this->id); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /backend/src/User/Profile/Domain/ProfileId.php: -------------------------------------------------------------------------------- 1 | value->equals($other->value); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/src/User/Profile/Domain/ProfileRepository.php: -------------------------------------------------------------------------------- 1 | entityManager->getRepository(Profile::class) 20 | ->findOneBy(['userId' => $userId->value]); 21 | } 22 | 23 | public function add(Profile $task): void 24 | { 25 | $this->entityManager->persist($task); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/src/User/Profile/Query/FindByUserId/FindProfileByUserId.php: -------------------------------------------------------------------------------- 1 | entityManager->createQuery($dql); 26 | $dqlQuery->setParameter( 27 | key: 'userId', 28 | value: $query->userId, 29 | ); 30 | 31 | /** @var ?ProfileData $result */ 32 | $result = $dqlQuery->getOneOrNullResult(); 33 | 34 | return $result ?? new ProfileData(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/src/User/Profile/Query/FindByUserId/FindProfileByUserIdQuery.php: -------------------------------------------------------------------------------- 1 | |string> 24 | */ 25 | #[Override] 26 | public static function getSubscribedEvents(): array 27 | { 28 | return [ 29 | KernelEvents::CONTROLLER_ARGUMENTS => ['onKernelControllerArguments', 1000], 30 | ]; 31 | } 32 | 33 | public function onKernelControllerArguments(ControllerArgumentsEvent $event): void 34 | { 35 | /** @var IsGranted|null $attribute */ 36 | $attribute = $event->getAttributes()[IsGranted::class][0] ?? null; 37 | 38 | if ($attribute === null) { 39 | return; 40 | } 41 | 42 | $request = $event->getRequest(); 43 | 44 | ($this->checkRoleGranted)($request, $attribute->userRole); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /backend/src/User/Security/Http/UserIdArgumentValueResolver.php: -------------------------------------------------------------------------------- 1 | 27 | * 28 | * @throws ApiUnauthorizedException 29 | */ 30 | #[Override] 31 | public function resolve(Request $request, ArgumentMetadata $argument): iterable 32 | { 33 | if ($argument->getType() !== UserId::class) { 34 | return []; 35 | } 36 | 37 | try { 38 | $userToken = $this->tokenManager->getToken($request); 39 | } catch (TokenException) { 40 | throw new ApiUnauthorizedException(['Необходимо пройти аутентификацию']); 41 | } 42 | 43 | return [$userToken->getUserId()]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /backend/src/User/Security/Service/TokenException.php: -------------------------------------------------------------------------------- 1 | tokenId, 25 | userId: $userId, 26 | hash: $token->hash(), 27 | ); 28 | 29 | $this->userTokenRepository->add($userToken); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /backend/src/User/SignIn/Command/DeleteToken.php: -------------------------------------------------------------------------------- 1 | userTokenRepository->findById($userTokenId); 21 | 22 | if ($userToken === null) { 23 | throw new TokenException('Токен не найден'); 24 | } 25 | 26 | $this->userTokenRepository->remove($userToken); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/User/SignIn/Command/SignInCommand.php: -------------------------------------------------------------------------------- 1 | findUser)( 34 | new FindUserQuery(confirmToken: $confirmToken) 35 | ); 36 | 37 | if ($userData === null) { 38 | throw new UserNotFoundException(); 39 | } 40 | 41 | $user = $this->userRepository->findById(new UserId($userData->userId)); 42 | 43 | if ($user === null) { 44 | throw new UserNotFoundException(); 45 | } 46 | 47 | $user->confirm(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /backend/src/User/SignUp/Command/SignUpCommand.php: -------------------------------------------------------------------------------- 1 | value = $value; 25 | } 26 | 27 | /** 28 | * @param object $other 29 | */ 30 | public function equalTo(mixed $other): bool 31 | { 32 | return $other::class === self::class && $this->value->equals($other->value); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /backend/src/User/User/Domain/Exception/EmailAlreadyIsConfirmedException.php: -------------------------------------------------------------------------------- 1 | value->equals($other->value); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/src/User/User/Domain/UserPassword.php: -------------------------------------------------------------------------------- 1 | value = $this->hash(); 35 | } 36 | 37 | /** 38 | * @return non-empty-string 39 | */ 40 | public function hash(): string 41 | { 42 | /** @var non-empty-string $hash */ 43 | $hash = password_hash( 44 | password: $this->cleanPassword, 45 | algo: self::HASH_ALGO, 46 | options: [ 47 | 'cost' => $this->hashCost, 48 | ], 49 | ); 50 | 51 | return $hash; 52 | } 53 | 54 | public function verify(string $hash): bool 55 | { 56 | return password_verify( 57 | password: $this->cleanPassword, 58 | hash: $hash, 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /backend/src/User/User/Domain/UserRepository.php: -------------------------------------------------------------------------------- 1 | entityManager 19 | ->getRepository(User::class) 20 | ->find($userId->value); 21 | } 22 | 23 | public function add(User $user): void 24 | { 25 | $this->entityManager->persist($user); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/src/User/User/Domain/UserRole.php: -------------------------------------------------------------------------------- 1 | id = $id->value; 38 | $this->userId = $userId->value; 39 | $this->hash = $hash; 40 | 41 | $this->createdAt = new DateTimeImmutable(); 42 | } 43 | 44 | public function getId(): UserTokenId 45 | { 46 | return new UserTokenId($this->id); 47 | } 48 | 49 | public function getUserId(): UserId 50 | { 51 | return new UserId($this->userId); 52 | } 53 | 54 | /** 55 | * @return non-empty-string 56 | */ 57 | public function getHash(): string 58 | { 59 | /** 60 | * @var non-empty-string $hash 61 | */ 62 | $hash = $this->hash; 63 | 64 | return $hash; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /backend/src/User/User/Domain/UserTokenId.php: -------------------------------------------------------------------------------- 1 | entityManager 19 | ->getRepository(UserToken::class) 20 | ->find($userTokenId->value); 21 | } 22 | 23 | public function remove(UserToken $userToken): void 24 | { 25 | $this->entityManager->remove($userToken); 26 | } 27 | 28 | public function add(UserToken $userToken): void 29 | { 30 | $this->entityManager->persist($userToken); 31 | } 32 | 33 | public function removeAllByUserId(UserId $userId): void 34 | { 35 | $this->entityManager->createQueryBuilder() 36 | ->delete(UserToken::class, 't') 37 | ->where('t.userId = :userId') 38 | ->setParameter('userId', $userId->value) 39 | ->getQuery() 40 | ->execute(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /backend/src/User/User/Query/FindAllUsers.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public function __invoke(): array 20 | { 21 | $dql = <<<'DQL' 22 | SELECT 23 | NEW App\User\User\Query\UserListData(u.id, u.userEmail.value) 24 | FROM App\User\User\Domain\User as u 25 | DQL; 26 | 27 | /** 28 | * @var array $allUsers 29 | */ 30 | $allUsers = $this->entityManager->createQuery($dql)->getResult(); 31 | 32 | return $allUsers; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /backend/src/User/User/Query/FindUserQuery.php: -------------------------------------------------------------------------------- 1 | email = new Email($email); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/User/config.php: -------------------------------------------------------------------------------- 1 | env()) { 12 | 'test' => 4, 13 | 'dev' => 8, 14 | default => 12, 15 | }; 16 | 17 | $container->parameters() 18 | ->set('app.hash_cost', $hashCost); 19 | 20 | $framework 21 | ->rateLimiter() 22 | ->limiter('sign_in') 23 | ->policy('fixed_window') 24 | ->limit(3) 25 | ->interval('1 minute'); 26 | 27 | $framework 28 | ->rateLimiter() 29 | ->limiter('change_password') 30 | ->policy('fixed_window') 31 | ->limit(3) 32 | ->interval('1 minute'); 33 | }; 34 | -------------------------------------------------------------------------------- /docker/.env.dist: -------------------------------------------------------------------------------- 1 | COMPOSE_PROJECT_NAME='symfony-starter-kit-local' 2 | COMPOSE_FILE='docker-compose.local.yml' 3 | 4 | USER_ID=1000 5 | 6 | NGINX_PORT=8088 7 | PGSQL_PORT=8432 8 | -------------------------------------------------------------------------------- /docker/backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.3.20-fpm AS base 2 | 3 | RUN apt-get update; \ 4 | apt-get install -y --no-install-recommends unzip git; 5 | 6 | COPY php.ini /usr/local/etc/php/php.ini 7 | 8 | COPY --from=composer:2.8.8 /usr/bin/composer /usr/bin/composer 9 | 10 | COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/ 11 | RUN install-php-extensions pdo_pgsql intl sysvsem pcov bcmath 12 | 13 | RUN ln -sf /usr/share/zoneinfo/UTC /etc/localtime 14 | 15 | ARG USER_ID 16 | RUN groupadd --gid "$USER_ID" dev \ 17 | && useradd --uid "$USER_ID" --gid dev --shell /bin/bash --create-home dev 18 | 19 | COPY www.conf /usr/local/etc/php-fpm.d/www.conf 20 | 21 | RUN su dev -c 'mkdir -p /home/dev/.composer/ /home/dev/app/' 22 | 23 | USER dev 24 | 25 | WORKDIR /app/ 26 | 27 | FROM base AS messenger 28 | 29 | USER root 30 | 31 | RUN apt-get update; \ 32 | apt-get install -y --no-install-recommends supervisor; 33 | 34 | # https://symfony.com/doc/current/messenger.html#graceful-shutdown 35 | RUN install-php-extensions pcntl 36 | 37 | COPY messenger/messenger-worker.conf /etc/supervisor/conf.d/messenger-worker.conf 38 | COPY messenger/scheduler.conf /etc/supervisor/conf.d/scheduler.conf 39 | COPY messenger/supervisord.conf /etc/supervisor/supervisord.conf 40 | 41 | CMD ["supervisord", "-c", "/etc/supervisor/supervisord.conf"] 42 | -------------------------------------------------------------------------------- /docker/backend/messenger/messenger-worker.conf: -------------------------------------------------------------------------------- 1 | [program:messenger-consume] 2 | command=php /app/bin/console messenger:consume async -vv --time-limit=3600 --limit=100 3 | user=dev 4 | numprocs=2 5 | autostart=true 6 | autorestart=true 7 | startretries=35 8 | process_name=%(program_name)s_%(process_num)02d 9 | stdout_logfile_maxbytes = 0 10 | stderr_logfile_maxbytes = 0 11 | stderr_logfile = /dev/stdout 12 | stdout_logfile = /dev/stdout 13 | -------------------------------------------------------------------------------- /docker/backend/messenger/scheduler.conf: -------------------------------------------------------------------------------- 1 | [program:scheduler] 2 | command=php /app/bin/console messenger:consume scheduler_default -vv 3 | user=dev 4 | numprocs=1 5 | autostart=true 6 | autorestart=true 7 | startretries=35 8 | process_name=%(program_name)s_%(process_num)02d 9 | stdout_logfile_maxbytes = 0 10 | stderr_logfile_maxbytes = 0 11 | stderr_logfile = /dev/stdout 12 | stdout_logfile = /dev/stdout 13 | -------------------------------------------------------------------------------- /docker/backend/messenger/supervisord.conf: -------------------------------------------------------------------------------- 1 | ; supervisor config file 2 | 3 | [unix_http_server] 4 | file=/var/run/supervisor.sock ; (the path to the socket file) 5 | chmod=0700 ; sockef file mode (default 0700) 6 | 7 | [supervisord] 8 | logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log) 9 | pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid) 10 | childlogdir=/var/log/supervisor ; ('AUTO' child log dir, default $TEMP) 11 | nodaemon=true 12 | user=root 13 | 14 | ; the below section must remain in the config file for RPC 15 | ; (supervisorctl/web interface) to work, additional interfaces may be 16 | ; added by defining them in separate rpcinterface: sections 17 | [rpcinterface:supervisor] 18 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 19 | 20 | [supervisorctl] 21 | serverurl=unix:///var/run/supervisor.sock ; use a unix:// URL for a unix socket 22 | 23 | ; The [include] section can just contain the "files" setting. This 24 | ; setting can list multiple files (separated by whitespace or 25 | ; newlines). It can also contain wildcards. The filenames are 26 | ; interpreted as relative to this file. Included files *cannot* 27 | ; include files themselves. 28 | 29 | [include] 30 | files = /etc/supervisor/conf.d/*.conf 31 | -------------------------------------------------------------------------------- /docker/backend/php.ini: -------------------------------------------------------------------------------- 1 | memory_limit=512M 2 | -------------------------------------------------------------------------------- /docker/backend/www.conf: -------------------------------------------------------------------------------- 1 | [www] 2 | 3 | user = dev 4 | group = dev 5 | 6 | listen = 127.0.0.1:9000 7 | 8 | pm = static 9 | 10 | pm.max_children = 3 11 | -------------------------------------------------------------------------------- /docker/bin/check_commit_message.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | COMMIT_MSG=$(git log -1 --pretty=%B) 4 | COMMIT_STRUCTURE=$(echo "$COMMIT_MSG" | cat -A) 5 | LINE_COUNT=$(echo "$COMMIT_MSG" | wc -l) 6 | LAST_LINE=$(echo "$COMMIT_MSG" | tail -n 1) 7 | REGEX="^((Merge[ a-z-]* branch.*)|(Revert*)|((build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\(.*\))?!?: .*))" 8 | MERGE_REGEX="^Merge pull request .*" 9 | 10 | if [[ $(echo "$COMMIT_MSG" | head -n 1) =~ $MERGE_REGEX ]]; then 11 | echo "Обнаружен merge-коммит" 12 | exit 0 13 | fi 14 | 15 | if ! [[ $COMMIT_MSG =~ $REGEX ]]; then 16 | echo "ОШИБКА: Commit не соответствует стандарту Conventional Commits" 17 | echo "Допустимые типы: build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test" 18 | exit 1 19 | fi 20 | 21 | echo "Сообщение коммита корректно" 22 | exit 0 23 | -------------------------------------------------------------------------------- /docker/bin/replace_env.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -Eeuo pipefail 4 | #set -x # uncomment for debug 5 | 6 | os-depend-sed-in-place() { 7 | if [ "$(uname -s)" == 'Darwin' ]; then 8 | sed -i '' "$@" 9 | else 10 | sed --in-place "$@" 11 | fi 12 | } 13 | 14 | ENV_FILE_PATH="${1}" 15 | ENV_NAME="${2}" 16 | NEW_VALUE="${3}" 17 | 18 | if [ "$(grep ${ENV_NAME} ${ENV_FILE_PATH})" != '' ]; then 19 | os-depend-sed-in-place -E "s|^.*${ENV_NAME}=.*$|${ENV_NAME}='${NEW_VALUE}'|" ${ENV_FILE_PATH} 20 | else 21 | echo "${ENV_NAME}='${NEW_VALUE}'" >>${ENV_FILE_PATH} 22 | fi 23 | -------------------------------------------------------------------------------- /docker/bin/setup_envs.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -Eeuo pipefail 4 | 5 | replace_env() { 6 | ./docker/bin/replace_env.bash "$@" 7 | } 8 | 9 | COMPOSE_ENV_PATH="$(realpath ./.env)" 10 | COMPOSE_DIST_ENV_PATH="$(realpath ./docker/.env.dist)" 11 | 12 | # У docker compose странный баг: когда ./.env загружен, compose пытается загрузить ./docker/.env, 13 | # вероятно из-за того, что там находится docker-compose.local.yml, 14 | # это приводит к непредсказуемому поведению и ошибкам 15 | rm -f ./docker/.env 16 | 17 | [ ! -f "${COMPOSE_ENV_PATH}" ] && cp "${COMPOSE_DIST_ENV_PATH}" "${COMPOSE_ENV_PATH}" 18 | 19 | replace_env "${COMPOSE_ENV_PATH}" 'COMPOSE_PROJECT_NAME' 'symfony-starter-kit-local' 20 | replace_env "${COMPOSE_ENV_PATH}" 'COMPOSE_FILE' './docker/docker-compose.local.yml' 21 | replace_env "${COMPOSE_ENV_PATH}" 'USER_ID' "$(id -u)" 22 | 23 | [ ! -f 'docker/pgsql/.env' ] && cp 'docker/pgsql/.env.dist' 'docker/pgsql/.env' 24 | 25 | echo 'Envs set up!'; 26 | -------------------------------------------------------------------------------- /docker/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.27.4 2 | 3 | RUN ln -sf /usr/share/zoneinfo/UTC /etc/localtime 4 | 5 | COPY default.conf /etc/nginx/conf.d/default.conf 6 | 7 | WORKDIR /app/ 8 | -------------------------------------------------------------------------------- /docker/nginx/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | location /docs { 3 | proxy_pass http://docs:8080; 4 | } 5 | 6 | location = /mailhog { 7 | absolute_redirect off; 8 | rewrite /mailhog /mailhog/ permanent; 9 | } 10 | 11 | location ~ ^/mailhog { 12 | chunked_transfer_encoding on; 13 | proxy_set_header X-NginX-Proxy true; 14 | proxy_set_header Upgrade $http_upgrade; 15 | proxy_set_header Connection "upgrade"; 16 | proxy_http_version 1.1; 17 | proxy_redirect off; 18 | proxy_buffering off; 19 | rewrite ^/mailhog(/.*)$ $1 break; 20 | proxy_set_header Host $host; 21 | proxy_pass http://mailhog:8025; 22 | } 23 | 24 | location / { 25 | fastcgi_pass backend:9000; 26 | include fastcgi_params; 27 | 28 | fastcgi_buffer_size 128k; 29 | fastcgi_buffers 4 256k; 30 | fastcgi_busy_buffers_size 256k; 31 | 32 | fastcgi_param SCRIPT_FILENAME /app/public/index.php; 33 | fastcgi_param DOCUMENT_ROOT /app/public/; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /docker/pgsql/.env.dist: -------------------------------------------------------------------------------- 1 | PGDATA=/var/lib/postgresql/data/pgdata 2 | PGUSER=postgres 3 | POSTGRES_PASSWORD=db_password 4 | POSTGRES_DB=db_name 5 | -------------------------------------------------------------------------------- /docker/pgsql/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:17.4 2 | 3 | RUN ln -sf /usr/share/zoneinfo/UTC /etc/localtime 4 | 5 | COPY pgsql.conf /var/lib/postgresql/data/postgresql.conf 6 | -------------------------------------------------------------------------------- /docker/pgsql/pgsql.conf: -------------------------------------------------------------------------------- 1 | # ----------------------------- 2 | # PostgreSQL configuration file 3 | # ----------------------------- 4 | # 5 | # This file consists of lines of the form: 6 | # 7 | # name = value 8 | # 9 | # (The "=" is optional.) Whitespace may be used. Comments are introduced with 10 | # "#" anywhere on a line. The complete list of parameter names and allowed 11 | # values can be found in the PostgreSQL documentation. 12 | # 13 | # The commented-out settings shown in this file represent the default values. 14 | # Re-commenting a setting is NOT sufficient to revert it to the default value; 15 | # you need to reload the server. 16 | # 17 | # This file is read on server startup and when the server receives a SIGHUP 18 | # signal. If you edit the file on a running system, you have to SIGHUP the 19 | # server for the changes to take effect, or use "pg_ctl reload". Some 20 | # parameters, which are marked below, require a server shutdown and restart to 21 | # take effect. 22 | # 23 | # Any parameter can also be given as a command-line option to the server, e.g., 24 | # "postgres -c log_connections=on". Some parameters can be changed at run time 25 | # with the "SET" SQL command. 26 | # 27 | # Memory units: kB = kilobytes Time units: ms = milliseconds 28 | # MB = megabytes s = seconds 29 | # GB = gigabytes min = minutes 30 | # h = hours 31 | # d = days 32 | 33 | timezone = 'UTC' 34 | --------------------------------------------------------------------------------