├── docker
├── backend
│ ├── php.ini
│ ├── www.conf
│ ├── messenger
│ │ ├── scheduler.conf
│ │ ├── messenger-worker.conf
│ │ └── supervisord.conf
│ └── Dockerfile
├── pgsql
│ ├── .env.dist
│ ├── Dockerfile
│ └── pgsql.conf
├── .env.dist
├── nginx
│ ├── Dockerfile
│ └── default.conf
└── bin
│ ├── replace_env.bash
│ ├── check_commit_message.bash
│ └── setup_envs.bash
├── backend
├── config
│ ├── packages
│ │ ├── lock.yaml
│ │ ├── mailer.yaml
│ │ ├── property_info.yaml
│ │ ├── uid.yaml
│ │ ├── test
│ │ │ └── dama_doctrine_test_bundle.yaml
│ │ ├── dama_doctrine_test_bundle.yaml
│ │ ├── twig.yaml
│ │ ├── web_profiler.yaml
│ │ ├── doctrine_migrations.yaml
│ │ ├── routing.yaml
│ │ ├── framework.yaml
│ │ ├── cache.yaml
│ │ ├── nyholm_psr7.yaml
│ │ └── valinor.yaml
│ ├── routes
│ │ ├── app.yaml
│ │ ├── framework.yaml
│ │ └── web_profiler.yaml
│ ├── preload.php
│ └── bundles.php
├── src-dev
│ ├── PHPStan
│ │ ├── phpstan-baseline.neon
│ │ ├── object-manager.php
│ │ ├── phpstan-config.neon
│ │ └── EmbeddablePropertiesExtension.php
│ ├── Tests
│ │ ├── Rector
│ │ │ ├── OneFlushInClassRector
│ │ │ │ ├── config
│ │ │ │ │ └── config.php
│ │ │ │ ├── Fixture
│ │ │ │ │ ├── skip_rule_test_fixture.php.inc
│ │ │ │ │ └── test_fixture.php.inc
│ │ │ │ └── OneFlushInClassTest.php
│ │ │ ├── ResolversInAction
│ │ │ │ ├── config
│ │ │ │ │ └── config.php
│ │ │ │ ├── Fixture
│ │ │ │ │ ├── skip_rule_test_fixture.php.inc
│ │ │ │ │ └── test_fixture.php.inc
│ │ │ │ └── ResolversInActionTest.php
│ │ │ ├── AssertMustHaveMessageRector
│ │ │ │ ├── config
│ │ │ │ │ └── config.php
│ │ │ │ ├── Fixture
│ │ │ │ │ ├── skip_rule_test_fixture.php.inc
│ │ │ │ │ └── test_fixture.php.inc
│ │ │ │ └── AssertMustHaveMessageTest.php
│ │ │ └── RequestMethodInsteadOfStringRector
│ │ │ │ ├── config
│ │ │ │ └── config.php
│ │ │ │ ├── Fixture
│ │ │ │ ├── skip_rule_test_fixture.php.inc
│ │ │ │ └── test_fixture.php.inc
│ │ │ │ └── RequestMethodInsteadOfStringTest.php
│ │ ├── bootstrap.php
│ │ ├── Unit
│ │ │ ├── Task
│ │ │ │ └── Domain
│ │ │ │ │ ├── TaskIdTest.php
│ │ │ │ │ ├── TaskNameTest.php
│ │ │ │ │ ├── TaskCommentBodyTest.php
│ │ │ │ │ ├── TaskCommentIdTest.php
│ │ │ │ │ └── TaskTest.php
│ │ │ ├── User
│ │ │ │ ├── Profile
│ │ │ │ │ └── Domain
│ │ │ │ │ │ └── ProfileIdTest.php
│ │ │ │ ├── SignUp
│ │ │ │ │ └── Domain
│ │ │ │ │ │ └── ConfirmTokenTest.php
│ │ │ │ └── User
│ │ │ │ │ └── Domain
│ │ │ │ │ └── UserPasswordTest.php
│ │ │ └── Infrastructure
│ │ │ │ ├── EmailTest.php
│ │ │ │ ├── Pagination
│ │ │ │ ├── PaginationResponseTest.php
│ │ │ │ └── PaginationRequestTest.php
│ │ │ │ └── PhoneTest.php
│ │ └── Functional
│ │ │ ├── Infrastructure
│ │ │ └── Response
│ │ │ │ └── ApiSystemExceptionTest.php
│ │ │ ├── SDK
│ │ │ ├── TaskComment.php
│ │ │ ├── Profile.php
│ │ │ └── Seo.php
│ │ │ ├── PingActionTest.php
│ │ │ └── Setting
│ │ │ └── Site
│ │ │ └── SettingListTest.php
│ ├── OpenApi
│ │ ├── resources
│ │ │ ├── _template_module.yaml
│ │ │ ├── base.yaml
│ │ │ └── site
│ │ │ │ ├── ping.yaml
│ │ │ │ └── setting.yaml
│ │ ├── .spectral.yaml
│ │ └── README.md
│ ├── psalm-baseline.xml
│ ├── Infrastructure
│ │ ├── di.php
│ │ └── EventListener
│ │ │ └── TestExceptionEventListener.php
│ ├── Maker
│ │ ├── CustomStr.php
│ │ └── Resources
│ │ │ ├── skeleton
│ │ │ ├── http
│ │ │ │ ├── create
│ │ │ │ │ └── CreateRequest.tpl.php
│ │ │ │ ├── update
│ │ │ │ │ ├── UpdateRequest.tpl.php
│ │ │ │ │ └── UpdateAction.tpl.php
│ │ │ │ ├── InfoAction.tpl.php
│ │ │ │ ├── ListAction.tpl.php
│ │ │ │ └── RemoveAction.tpl.php
│ │ │ └── domain
│ │ │ │ └── Entity.tpl.php
│ │ │ └── help
│ │ │ └── MakeModule.txt
│ └── phpunit.xml
├── src
│ ├── Ping
│ │ ├── README.md
│ │ └── Http
│ │ │ └── Site
│ │ │ ├── Pong.php
│ │ │ └── PingAction.php
│ ├── Seo
│ │ ├── README.md
│ │ ├── Domain
│ │ │ ├── SeoResourceType.php
│ │ │ └── SeoRepository.php
│ │ ├── Http
│ │ │ └── Site
│ │ │ │ └── SeoData.php
│ │ └── Command
│ │ │ ├── SaveSeoCommand.php
│ │ │ └── SaveSeo.php
│ ├── Mailer
│ │ ├── templates
│ │ │ ├── emails
│ │ │ │ ├── confirm.html.twig
│ │ │ │ ├── recoverPassword.html.twig
│ │ │ │ └── uncompleted-tasks.html.twig
│ │ │ └── layout.html.twig
│ │ ├── README.md
│ │ ├── Notification
│ │ │ ├── UncompletedTasks
│ │ │ │ ├── TaskData.php
│ │ │ │ ├── UncompletedTasksMessage.php
│ │ │ │ └── SendUncompletedTasksToUser.php
│ │ │ ├── EmailConfirmation
│ │ │ │ ├── ConfirmEmailMessage.php
│ │ │ │ └── SendEmailConfirmToken.php
│ │ │ └── PasswordRecovery
│ │ │ │ ├── RecoveryPasswordMessage.php
│ │ │ │ └── SendPasswordRecoveryToken.php
│ │ └── config.php
│ ├── Infrastructure
│ │ ├── Message.php
│ │ ├── Response
│ │ │ ├── ApiResponse.php
│ │ │ ├── ResponseStatus.php
│ │ │ ├── SuccessResponse.php
│ │ │ ├── ApiObjectResponse.php
│ │ │ ├── PaginationResponse.php
│ │ │ └── ApiListObjectResponse.php
│ │ ├── ApiException
│ │ │ ├── ApiHeaders.php
│ │ │ ├── ApiErrorCode.php
│ │ │ ├── ApiException.php
│ │ │ ├── ApiErrorResponse.php
│ │ │ ├── ApiSystemException.php
│ │ │ ├── ApiNotFoundException.php
│ │ │ ├── ApiAccessForbiddenException.php
│ │ │ ├── ApiUnauthorizedException.php
│ │ │ ├── ApiBadResponseException.php
│ │ │ └── ApiBadRequestException.php
│ │ ├── Flush.php
│ │ ├── Request
│ │ │ ├── Pagination
│ │ │ │ └── PaginationRequest.php
│ │ │ ├── ApiRequestMappingException.php
│ │ │ ├── BuildValidationError.php
│ │ │ └── ValidateAcceptHeader.php
│ │ ├── KeepCacheArrayAdapter.php
│ │ ├── README.md
│ │ ├── di.php
│ │ ├── ValueObject
│ │ │ ├── Email.php
│ │ │ └── Phone.php
│ │ ├── CheckRateLimiter.php
│ │ └── EventListener
│ │ │ └── FillMailFields.php
│ ├── Setting
│ │ ├── README.md
│ │ ├── Http
│ │ │ ├── Site
│ │ │ │ ├── SettingListData.php
│ │ │ │ └── SettingListAction.php
│ │ │ └── Admin
│ │ │ │ └── SettingListData.php
│ │ ├── Domain
│ │ │ ├── SettingNotFoundException.php
│ │ │ ├── SettingType.php
│ │ │ └── SettingsRepository.php
│ │ └── Command
│ │ │ ├── SaveSettingCommand.php
│ │ │ └── SaveSetting.php
│ ├── Task
│ │ ├── Http
│ │ │ └── Site
│ │ │ │ ├── Export
│ │ │ │ ├── Format.php
│ │ │ │ ├── NotFoundTasksForExportException.php
│ │ │ │ ├── Csv
│ │ │ │ │ └── CsvTaskData.php
│ │ │ │ ├── Xml
│ │ │ │ │ └── XmlTaskData.php
│ │ │ │ └── Exporter.php
│ │ │ │ ├── CreateTask
│ │ │ │ └── TaskData.php
│ │ │ │ └── TaskList
│ │ │ │ └── TaskListMetaData.php
│ │ ├── Query
│ │ │ ├── Task
│ │ │ │ ├── FindById
│ │ │ │ │ ├── TaskNotFoundException.php
│ │ │ │ │ ├── FindTaskByIdQuery.php
│ │ │ │ │ ├── TaskData.php
│ │ │ │ │ └── FindTaskById.php
│ │ │ │ ├── FindUncompletedTasksByUserId
│ │ │ │ │ ├── TaskData.php
│ │ │ │ │ ├── FindUncompletedTasksByUserIdQuery.php
│ │ │ │ │ └── FindUncompletedTasksByUserId.php
│ │ │ │ └── FindAllByUserId
│ │ │ │ │ ├── TaskData.php
│ │ │ │ │ ├── Filter.php
│ │ │ │ │ ├── FindAllTasksByUserIdQuery.php
│ │ │ │ │ └── CountAllTasksByUserId.php
│ │ │ └── Comment
│ │ │ │ └── FindAll
│ │ │ │ ├── FindAllCommentQuery.php
│ │ │ │ ├── CommentData.php
│ │ │ │ └── FindAllCommentsByTaskIdAndUserId.php
│ │ ├── Domain
│ │ │ ├── TaskAlreadyIsDoneException.php
│ │ │ ├── AddCommentToCompletedTaskException.php
│ │ │ ├── TaskId.php
│ │ │ ├── TaskCommentBody.php
│ │ │ ├── TaskCommentId.php
│ │ │ ├── TaskName.php
│ │ │ ├── TaskRepository.php
│ │ │ └── TaskComment.php
│ │ ├── Command
│ │ │ ├── CreateTask
│ │ │ │ ├── CreateTaskCommand.php
│ │ │ │ └── CreateTask.php
│ │ │ ├── UpdateTaskName
│ │ │ │ ├── UpdateTaskNameCommand.php
│ │ │ │ └── UpdateTaskName.php
│ │ │ ├── Comment
│ │ │ │ └── Add
│ │ │ │ │ ├── AddCommentOnTaskCommand.php
│ │ │ │ │ └── AddCommentOnTask.php
│ │ │ ├── CompleteTask.php
│ │ │ └── RemoveTask.php
│ │ └── README.md
│ ├── User
│ │ ├── Security
│ │ │ ├── Service
│ │ │ │ └── TokenException.php
│ │ │ └── Http
│ │ │ │ ├── IsGranted.php
│ │ │ │ ├── UserIdArgumentValueResolver.php
│ │ │ │ └── IsGrantedSubscriber.php
│ │ ├── User
│ │ │ ├── Domain
│ │ │ │ ├── UserRole.php
│ │ │ │ ├── Exception
│ │ │ │ │ ├── UserNotFoundException.php
│ │ │ │ │ ├── EmailIsNotConfirmedException.php
│ │ │ │ │ ├── EmailAlreadyIsConfirmedException.php
│ │ │ │ │ └── UserAlreadyExistException.php
│ │ │ │ ├── UserTokenId.php
│ │ │ │ ├── UserId.php
│ │ │ │ ├── UserRepository.php
│ │ │ │ ├── ConfirmToken.php
│ │ │ │ ├── UserTokenRepository.php
│ │ │ │ ├── UserPassword.php
│ │ │ │ └── UserToken.php
│ │ │ └── Query
│ │ │ │ ├── UserListData.php
│ │ │ │ ├── UserData.php
│ │ │ │ ├── FindAllUsers.php
│ │ │ │ └── FindUserQuery.php
│ │ ├── README.md
│ │ ├── Password
│ │ │ ├── Command
│ │ │ │ ├── GenerateRecoveryTokenCommand.php
│ │ │ │ ├── RecoverPasswordCommand.php
│ │ │ │ ├── ChangePasswordCommand.php
│ │ │ │ ├── ChangePassword.php
│ │ │ │ └── RecoverPassword.php
│ │ │ ├── Domain
│ │ │ │ ├── RecoveryTokenRepository.php
│ │ │ │ └── RecoveryToken.php
│ │ │ └── Http
│ │ │ │ └── ChangePasswordRequest.php
│ │ ├── Profile
│ │ │ ├── Query
│ │ │ │ └── FindByUserId
│ │ │ │ │ ├── FindProfileByUserIdQuery.php
│ │ │ │ │ ├── ProfileData.php
│ │ │ │ │ └── FindProfileByUserId.php
│ │ │ ├── Command
│ │ │ │ └── SaveProfile
│ │ │ │ │ ├── SaveProfileCommand.php
│ │ │ │ │ ├── UpdateProfile.php
│ │ │ │ │ ├── CreateProfile.php
│ │ │ │ │ └── SaveProfile.php
│ │ │ └── Domain
│ │ │ │ ├── ProfileId.php
│ │ │ │ ├── ProfileRepository.php
│ │ │ │ └── Profile.php
│ │ ├── SignIn
│ │ │ ├── Http
│ │ │ │ ├── UserTokenData.php
│ │ │ │ └── SignInRequest.php
│ │ │ └── Command
│ │ │ │ ├── SignInCommand.php
│ │ │ │ ├── DeleteToken.php
│ │ │ │ └── CreateToken.php
│ │ ├── SignUp
│ │ │ └── Command
│ │ │ │ ├── SignUpCommand.php
│ │ │ │ └── ConfirmEmail.php
│ │ └── config.php
│ ├── Article
│ │ ├── README.md
│ │ └── Http
│ │ │ ├── Site
│ │ │ ├── ArticleInfoData.php
│ │ │ └── ArticleListData.php
│ │ │ └── Admin
│ │ │ ├── ArticleListByIdsRequest.php
│ │ │ ├── ArticleListData.php
│ │ │ ├── CreateArticleRequest.php
│ │ │ ├── UpdateArticleRequest.php
│ │ │ ├── ArticleInfoAction.php
│ │ │ └── RemoveArticleAction.php
│ └── Logger
│ │ └── Http
│ │ └── RequestIdLoggerProcessor.php
├── .env.test
├── public
│ └── index.php
├── .gitignore
├── migrations
│ ├── Version20220603114543.php
│ ├── Version20240727071640.php
│ ├── Version20240730133406.php
│ ├── Version20240719151819.php
│ ├── Version20240719151844.php
│ ├── Version20240719151932.php
│ ├── Version20240719152016.php
│ ├── Version20250311190206.php
│ └── Version20240719151750.php
└── bin
│ └── console
├── .gitignore
├── .editorconfig
├── LICENSE
└── .github
└── workflows
└── check-code-quality.yml
/docker/backend/php.ini:
--------------------------------------------------------------------------------
1 | memory_limit=512M
2 |
--------------------------------------------------------------------------------
/backend/config/packages/lock.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | lock: '%env(LOCK_DSN)%'
3 |
--------------------------------------------------------------------------------
/backend/src-dev/PHPStan/phpstan-baseline.neon:
--------------------------------------------------------------------------------
1 | parameters:
2 | ignoreErrors:
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea
2 |
3 | ###> docker ###
4 | /docker/**/.env
5 | ###< docker ###
6 |
7 | /.env
8 |
--------------------------------------------------------------------------------
/backend/config/packages/mailer.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | mailer:
3 | dsn: '%env(MAILER_DSN)%'
4 |
--------------------------------------------------------------------------------
/backend/config/routes/app.yaml:
--------------------------------------------------------------------------------
1 | app:
2 | resource: ../../src/
3 | type: attribute
4 | prefix: /api
5 |
--------------------------------------------------------------------------------
/backend/src/Ping/README.md:
--------------------------------------------------------------------------------
1 | # Пинг приложения
2 |
3 | Пример пинга приложения, может использоваться для мониторинга.
4 |
--------------------------------------------------------------------------------
/backend/config/packages/property_info.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | property_info:
3 | with_constructor_extractor: true
4 |
--------------------------------------------------------------------------------
/backend/config/packages/uid.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | uid:
3 | default_uuid_version: 7
4 | time_based_uuid_version: 7
5 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/backend/config/routes/framework.yaml:
--------------------------------------------------------------------------------
1 | when@dev:
2 | _errors:
3 | resource: '@FrameworkBundle/Resources/config/routing/errors.php'
4 | prefix: /_error
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 |
--------------------------------------------------------------------------------
/backend/src/Seo/README.md:
--------------------------------------------------------------------------------
1 | # SEO модуль
2 |
3 | Пример простого модуля для seo-оптимизации. Данные по seo всех модулей хранятся в единой таблице и разделяются типом.
4 |
5 |
6 |
--------------------------------------------------------------------------------
/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/nginx/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx:1.29.3
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 |
--------------------------------------------------------------------------------
/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/.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/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/src/Mailer/README.md:
--------------------------------------------------------------------------------
1 | # Отправка электронной почты
2 |
3 | Демонстрация асинхронной работы по отправке писем с помощью symfony messenger.
4 |
5 | Шаблоны писем вынесены в папку templates в самом модуле.
6 |
--------------------------------------------------------------------------------
/backend/src/Infrastructure/Message.php:
--------------------------------------------------------------------------------
1 |
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/Setting/README.md:
--------------------------------------------------------------------------------
1 | # Настройки приложения
2 |
3 | Модуль настроек приложения. Каждая настройка хранится отдельной строкой в таблице в формате "ключ - значение".
4 |
5 | В публичной части доступны только опубликованные настройки, например "заголовок сайта".
6 |
--------------------------------------------------------------------------------
/backend/public/index.php:
--------------------------------------------------------------------------------
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/src/Task/Http/Site/Export/NotFoundTasksForExportException.php:
--------------------------------------------------------------------------------
1 | withRules([
10 | OneFlushInClassRector::class,
11 | ]);
12 |
--------------------------------------------------------------------------------
/backend/src-dev/Tests/Rector/ResolversInAction/config/config.php:
--------------------------------------------------------------------------------
1 | withRules([
10 | ResolversInActionRector::class,
11 | ]);
12 |
--------------------------------------------------------------------------------
/backend/src/Ping/Http/Site/Pong.php:
--------------------------------------------------------------------------------
1 | bootEnv(dirname(__DIR__).'/../.env');
10 |
11 | if ((bool) $_SERVER['APP_DEBUG']) {
12 | umask(0o000);
13 | }
14 |
--------------------------------------------------------------------------------
/backend/src/User/User/Domain/Exception/EmailIsNotConfirmedException.php:
--------------------------------------------------------------------------------
1 | withRules([
10 | AssertMustHaveMessageRector::class,
11 | ]);
12 |
--------------------------------------------------------------------------------
/backend/src/User/User/Domain/Exception/EmailAlreadyIsConfirmedException.php:
--------------------------------------------------------------------------------
1 | withRules([
10 | RequestMethodInsteadOfStringRector::class,
11 | ]);
12 |
--------------------------------------------------------------------------------
/backend/src/Article/Http/Site/ArticleInfoData.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | registerUuidConstructor(...)]]>
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/backend/src/Setting/Http/Site/SettingListData.php:
--------------------------------------------------------------------------------
1 | У вас есть невыполненные задачи:
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/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/src/Infrastructure/ApiException/ApiHeaders.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | public function getHeaders(): array;
16 | }
17 |
--------------------------------------------------------------------------------
/backend/src/User/User/Domain/UserTokenId.php:
--------------------------------------------------------------------------------
1 | status = ResponseStatus::Success;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/backend/src/Setting/Domain/SettingNotFoundException.php:
--------------------------------------------------------------------------------
1 | entityManager->flush();
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/backend/src/Task/Http/Site/Export/Csv/CsvTaskData.php:
--------------------------------------------------------------------------------
1 | $ids
16 | */
17 | public function __construct(
18 | public array $ids,
19 | ) {}
20 | }
21 |
--------------------------------------------------------------------------------
/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/Task/Query/Task/FindUncompletedTasksByUserId/FindUncompletedTasksByUserIdQuery.php:
--------------------------------------------------------------------------------
1 | markAsDone();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/backend/src/Infrastructure/Response/PaginationResponse.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/src/User/Password/Command/RecoverPasswordCommand.php:
--------------------------------------------------------------------------------
1 | taskRepository->remove($task);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/backend/src/Article/Http/Admin/ArticleListData.php:
--------------------------------------------------------------------------------
1 |
20 | */
21 | public function getErrors(): array;
22 | }
23 |
--------------------------------------------------------------------------------
/backend/src/Task/Command/UpdateTaskName/UpdateTaskName.php:
--------------------------------------------------------------------------------
1 | changeTaskName(new TaskName($command->taskName));
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/backend/src/Task/Http/Site/Export/Xml/XmlTaskData.php:
--------------------------------------------------------------------------------
1 | andWhere('t.user_id = :user_id')
18 | ->setParameter('user_id', $query->userId);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/backend/src/User/SignUp/Command/SignUpCommand.php:
--------------------------------------------------------------------------------
1 | addSql("SELECT '1';");
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/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/Task/Domain/TaskId.php:
--------------------------------------------------------------------------------
1 | value->equals($other->value);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/backend/src/User/Profile/Command/SaveProfile/SaveProfileCommand.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/src/User/Password/Command/ChangePasswordCommand.php:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/backend/src/User/Profile/Command/SaveProfile/UpdateProfile.php:
--------------------------------------------------------------------------------
1 | changeName($command->name);
20 | $profile->changePhone(new Phone($command->phone));
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/backend/src/User/User/Domain/UserId.php:
--------------------------------------------------------------------------------
1 | value->equals($other->value);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/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/User/Profile/Domain/ProfileId.php:
--------------------------------------------------------------------------------
1 | value->equals($other->value);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/backend/src/Setting/Domain/SettingType.php:
--------------------------------------------------------------------------------
1 | >${ENV_FILE_PATH}
22 | fi
23 |
--------------------------------------------------------------------------------
/backend/src/Task/Http/Site/Export/Exporter.php:
--------------------------------------------------------------------------------
1 | equalTo($taskId2));
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/backend/src/Mailer/Notification/PasswordRecovery/RecoveryPasswordMessage.php:
--------------------------------------------------------------------------------
1 | email = new Email($email);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/backend/src/Infrastructure/Request/ApiRequestMappingException.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/ValueObject/Email.php:
--------------------------------------------------------------------------------
1 | equalTo($taskName2));
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/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/Tests/Unit/User/Profile/Domain/ProfileIdTest.php:
--------------------------------------------------------------------------------
1 | equalTo($profileId2));
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/backend/src-dev/Tests/Unit/Task/Domain/TaskCommentBodyTest.php:
--------------------------------------------------------------------------------
1 | equalTo($body2));
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/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/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-dev/Tests/Unit/Task/Domain/TaskCommentIdTest.php:
--------------------------------------------------------------------------------
1 | equalTo($taskCommentId2));
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/backend/src/Seo/Command/SaveSeoCommand.php:
--------------------------------------------------------------------------------
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/Task/Query/Task/FindAllByUserId/FindAllTasksByUserIdQuery.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/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/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-dev/Tests/Functional/Infrastructure/Response/ApiSystemExceptionTest.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/User/User/Query/UserData.php:
--------------------------------------------------------------------------------
1 | taskName), $userId);
26 |
27 | $this->taskRepository->add($task);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/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/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/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/src-dev/Tests/Functional/SDK/TaskComment.php:
--------------------------------------------------------------------------------
1 | tokenId,
25 | userId: $userId,
26 | hash: $token->hash(),
27 | );
28 |
29 | $this->userTokenRepository->add($userToken);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/backend/src/User/User/Domain/ConfirmToken.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-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/config/bundles.php:
--------------------------------------------------------------------------------
1 | ['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/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/src/Infrastructure/ApiException/ApiErrorResponse.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/Setting/Command/SaveSetting.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/Infrastructure/Request/BuildValidationError.php:
--------------------------------------------------------------------------------
1 |
16 | */
17 | public function __invoke(MappingError $error): array
18 | {
19 | $messages = $error->messages();
20 |
21 | $allMessages = [];
22 | foreach ($messages->errors() as $message) {
23 | $allMessages[] = $message
24 | ->withBody('{node_path}: {original_message}')
25 | ->toString();
26 | }
27 |
28 | /**
29 | * @var non-empty-list $allMessages
30 | */
31 | return $allMessages;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/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/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/src-dev/Infrastructure/EventListener/TestExceptionEventListener.php:
--------------------------------------------------------------------------------
1 | getThrowable();
23 | if ($e instanceof ValidationFailed) {
24 | throw $e;
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/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/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/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/Ping/Http/Site/PingAction.php:
--------------------------------------------------------------------------------
1 | connection->fetchOne("select 'pong'");
28 |
29 | return new ApiObjectResponse(new Pong($result));
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/backend/src/Mailer/config.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/User/User/Query/FindUserQuery.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/backend/src/Task/Command/Comment/Add/AddCommentOnTask.php:
--------------------------------------------------------------------------------
1 | commentBody),
30 | );
31 |
32 | $task->addComment($comment);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/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-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/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/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/Seo/Domain/SeoRepository.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/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/src/Article/Http/Admin/ArticleInfoAction.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-dev/Tests/Rector/OneFlushInClassRector/OneFlushInClassTest.php:
--------------------------------------------------------------------------------
1 | doTestFile($filePath);
24 | }
25 |
26 | public static function provideCases(): 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/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/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-dev/Tests/Functional/SDK/Profile.php:
--------------------------------------------------------------------------------
1 | doTestFile($filePath);
24 | }
25 |
26 | public static function provideCases(): 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/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/src-dev/Tests/Rector/ResolversInAction/Fixture/test_fixture.php.inc:
--------------------------------------------------------------------------------
1 |
20 | -----
21 |
42 |
--------------------------------------------------------------------------------
/backend/src-dev/Tests/Rector/AssertMustHaveMessageRector/AssertMustHaveMessageTest.php:
--------------------------------------------------------------------------------
1 | doTestFile($filePath);
24 | }
25 |
26 | public static function provideCases(): 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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/backend/src-dev/Maker/Resources/help/MakeModule.txt:
--------------------------------------------------------------------------------
1 | The %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/Tests/Functional/SDK/Seo.php:
--------------------------------------------------------------------------------
1 | doTestFile($filePath);
24 | }
25 |
26 | public static function provideCases(): 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/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/Functional/PingActionTest.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/Setting/Site/SettingListTest.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/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/Mailer/Notification/EmailConfirmation/SendEmailConfirmToken.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 |
--------------------------------------------------------------------------------
/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/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/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/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/Mailer/Notification/PasswordRecovery/SendPasswordRecoveryToken.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/Task/Domain/TaskComment.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-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/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/User/Password/Command/ChangePassword.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/Seo/Command/SaveSeo.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/User/Password/Domain/RecoveryToken.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-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 | - ../../vendor/cuyz/valinor/qa/PHPStan/valinor-phpstan-configuration.php
27 | - ../../vendor/cuyz/valinor/qa/PHPStan/valinor-phpstan-suppress-pure-errors.php
28 | - phpstan-baseline.neon
29 |
30 | rules:
31 | - PHPStan\Rules\Doctrine\ORM\EntityNotFinalRule
32 |
33 | services:
34 | -
35 | class: Dev\PHPStan\EmbeddablePropertiesExtension
36 | tags:
37 | - phpstan.properties.readWriteExtension
38 |
--------------------------------------------------------------------------------
/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/Task/Query/Task/FindById/FindTaskById.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/User/User/Domain/UserTokenRepository.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/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/Task/Query/Task/FindUncompletedTasksByUserId/FindUncompletedTasksByUserId.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/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/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/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/src/Infrastructure/ApiException/ApiAccessForbiddenException.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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/backend/src/Task/Query/Comment/FindAll/FindAllCommentsByTaskIdAndUserId.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 |
--------------------------------------------------------------------------------
/docker/backend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM php:8.4.14-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.12 /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 |
--------------------------------------------------------------------------------
/backend/src/Article/Http/Admin/RemoveArticleAction.php:
--------------------------------------------------------------------------------
1 | articleRepository->remove($article);
34 | ($this->flush)();
35 |
36 | return new ApiObjectResponse(
37 | data: new SuccessResponse(),
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/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/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/User/Password/Command/RecoverPassword.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-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/User/SignUp/Command/ConfirmEmail.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/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/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/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/src-dev/Tests/Rector/RequestMethodInsteadOfStringRector/Fixture/test_fixture.php.inc:
--------------------------------------------------------------------------------
1 |
25 | -----
26 |
50 |
--------------------------------------------------------------------------------
/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/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/User/Security/Http/IsGrantedSubscriber.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-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/Tests/Rector/AssertMustHaveMessageRector/Fixture/test_fixture.php.inc:
--------------------------------------------------------------------------------
1 |
22 | -----
23 |
44 |
--------------------------------------------------------------------------------
/backend/src-dev/Tests/Unit/Infrastructure/PhoneTest.php:
--------------------------------------------------------------------------------
1 | equalTo($profilePhone2));
27 | }
28 |
29 | #[DataProvider('provideIncorrectNumberCases')]
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 provideIncorrectNumberCases(): Iterator
44 | {
45 | yield 'Неверный формат' => ['неправильный телефон'];
46 |
47 | yield 'Пустой номер' => [''];
48 |
49 | yield '10 цифр' => ['8922222222'];
50 |
51 | yield '12 цифр' => ['892222222222'];
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/backend/src/Setting/Http/Site/SettingListAction.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/backend/src/User/User/Domain/UserToken.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-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/Setting/Domain/SettingsRepository.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-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 |
--------------------------------------------------------------------------------