├── .dockerignore ├── .editorconfig ├── .env ├── .env.test ├── .github └── workflows │ └── development.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── bin └── console ├── codeception.dist.yml ├── composer.json ├── composer.lock ├── config ├── bundles.php ├── docker │ ├── development.conf │ ├── entrypoint.conf │ ├── messenger.conf │ ├── nginx.conf │ ├── php.conf │ ├── supervisor.conf │ └── www.conf ├── markup │ ├── Reports │ │ ├── PhpcsGithubReport.php │ │ └── PhpcsTeamcityReport.php │ ├── Standards │ │ ├── Sniffs │ │ │ ├── Annotations │ │ │ │ └── AnnotationTagSniff.php │ │ │ ├── Classes │ │ │ │ ├── ClassStructureSniff.php │ │ │ │ └── ClassUsageSniff.php │ │ │ ├── Lists │ │ │ │ └── TrailingCommaMultilineSniff.php │ │ │ ├── Methods │ │ │ │ ├── MethodParameterTypeSniff.php │ │ │ │ └── MethodReturnTypeSniff.php │ │ │ ├── Operators │ │ │ │ ├── OperatorEqualitySniff.php │ │ │ │ └── OperatorIncDecSniff.php │ │ │ ├── Statements │ │ │ │ ├── StatementElseSniff.php │ │ │ │ └── StatementUseSniff.php │ │ │ ├── Strings │ │ │ │ └── SingleQuoteStringSniff.php │ │ │ └── Whitespaces │ │ │ │ ├── CodeWhitespaceSniff.php │ │ │ │ └── LineWhitespaceSniff.php │ │ └── ruleset.xml │ ├── phpcs.xml │ └── phpstan.neon ├── packages │ ├── cache.php │ ├── debug.php │ ├── doctrine.php │ ├── doctrine_migrations.php │ ├── framework.php │ ├── mailer.php │ ├── messenger.php │ ├── monolog.php │ ├── nelmio_api_doc.php │ ├── notifier.php │ ├── routing.php │ ├── security.php │ ├── translation.php │ ├── twig.php │ ├── validator.php │ ├── web_profiler.php │ └── workflow.php ├── preload.php ├── routes │ ├── attribute.php │ ├── profiler.php │ ├── security.php │ └── swagger.php └── services.php ├── docker-compose.yml ├── public └── index.php ├── src ├── Application │ ├── EventListener │ │ ├── AccountNotificationEventListener.php │ │ ├── AccountWorkflowEventListener.php │ │ ├── KernelExceptionEventListener.php │ │ └── RequestIdEventListener.php │ ├── MessageHandler │ │ ├── BlockAccountById │ │ │ ├── BlockAccountByIdHandler.php │ │ │ ├── BlockAccountByIdRequest.php │ │ │ └── BlockAccountByIdResult.php │ │ ├── CreateNewAccount │ │ │ ├── CreateNewAccountHandler.php │ │ │ ├── CreateNewAccountRequest.php │ │ │ └── CreateNewAccountResult.php │ │ ├── DeleteAccountById │ │ │ ├── DeleteAccountByIdHandler.php │ │ │ ├── DeleteAccountByIdRequest.php │ │ │ └── DeleteAccountByIdResult.php │ │ ├── GetAccountById │ │ │ ├── GetAccountByIdHandler.php │ │ │ ├── GetAccountByIdRequest.php │ │ │ └── GetAccountByIdResult.php │ │ ├── GetAccountsByCriteria │ │ │ ├── GetAccountsByCriteriaHandler.php │ │ │ ├── GetAccountsByCriteriaRequest.php │ │ │ └── GetAccountsByCriteriaResult.php │ │ ├── GetHealthStatus │ │ │ ├── GetHealthStatusHandler.php │ │ │ ├── GetHealthStatusRequest.php │ │ │ └── GetHealthStatusResult.php │ │ ├── GetSigninAccount │ │ │ ├── GetSigninAccountHandler.php │ │ │ ├── GetSigninAccountRequest.php │ │ │ └── GetSigninAccountResult.php │ │ ├── SigninIntoAccount │ │ │ ├── SigninIntoAccountHandler.php │ │ │ ├── SigninIntoAccountRequest.php │ │ │ └── SigninIntoAccountResult.php │ │ ├── SignupNewAccount │ │ │ ├── SignupNewAccountHandler.php │ │ │ ├── SignupNewAccountRequest.php │ │ │ └── SignupNewAccountResult.php │ │ ├── UnblockAccountById │ │ │ ├── UnblockAccountByIdHandler.php │ │ │ ├── UnblockAccountByIdRequest.php │ │ │ └── UnblockAccountByIdResult.php │ │ └── UpdateAccountById │ │ │ ├── UpdateAccountByIdHandler.php │ │ │ ├── UpdateAccountByIdRequest.php │ │ │ └── UpdateAccountByIdResult.php │ ├── Notification │ │ └── AccountRegisteredNotification.php │ └── Service │ │ ├── AccountWorkflowManager.php │ │ ├── AuthenticationPasswordHasher.php │ │ ├── AuthorizationTokenManager.php │ │ ├── InMemoryRequestIdStorage.php │ │ ├── RequestDataPrivacyProtector.php │ │ └── UuidV4RequestIdGenerator.php ├── Domain │ ├── Contract │ │ ├── Account │ │ │ ├── AccountEntityRepositoryInterface.php │ │ │ └── AccountWorkflowManagerInterface.php │ │ ├── Authentication │ │ │ └── AuthenticationPasswordHasherInterface.php │ │ ├── Authorization │ │ │ └── AuthorizationTokenManagerInterface.php │ │ ├── Identification │ │ │ ├── RequestIdGeneratorInterface.php │ │ │ └── RequestIdStorageInterface.php │ │ ├── Integration │ │ │ ├── HttpbinResponderInterface.php │ │ │ ├── JwtTokenManagerInterface.php │ │ │ └── TemplateRendererInterface.php │ │ └── Protection │ │ │ └── PrivacyProtectorInterface.php │ ├── Event │ │ └── AccountRegisteredEvent.php │ ├── Exception │ │ ├── Account │ │ │ ├── AccountActionInvalidException.php │ │ │ ├── AccountAlreadyExistsException.php │ │ │ └── AccountNotFoundException.php │ │ ├── Authorization │ │ │ ├── AuthorizationForbiddenException.php │ │ │ └── AuthorizationRequiredException.php │ │ ├── Integration │ │ │ ├── HttpbinResponderException.php │ │ │ ├── JwtTokenManagerException.php │ │ │ └── TemplateRendererException.php │ │ └── Validation │ │ │ ├── RequestExtraParamsException.php │ │ │ ├── RequestParamTypeException.php │ │ │ └── ValidationFailedException.php │ └── Model │ │ ├── Account.php │ │ ├── AccountAction.php │ │ ├── AccountIdentifier.php │ │ ├── AccountRoles.php │ │ ├── AccountSearchCriteria.php │ │ ├── AccountSearchResult.php │ │ ├── AccountStatus.php │ │ ├── AuthorizationToken.php │ │ ├── Common │ │ ├── AbstractUuidIdentifier.php │ │ ├── DateTimeUtc.php │ │ ├── EmailAddress.php │ │ └── HashedPassword.php │ │ ├── Health.php │ │ ├── HealthStatus.php │ │ ├── HttpSpecification.php │ │ ├── LocaleCode.php │ │ ├── Role.php │ │ └── SearchPagination.php ├── Infrastructure │ ├── Adapter │ │ ├── Kennethreitz │ │ │ └── KennethreitzHttpbinAdapter.php │ │ ├── Lcobucci │ │ │ └── LcobucciJwtAdapter.php │ │ └── Sensiolabs │ │ │ └── SensiolabsTwigAdapter.php │ ├── Doctrine │ │ ├── Fixture │ │ │ └── AccountFixture.php │ │ ├── Mapping │ │ │ └── AccountEntity.php │ │ ├── Migration │ │ │ └── Version20200101000000.php │ │ └── Repository │ │ │ └── Account │ │ │ ├── AccountEntityMapper.php │ │ │ └── AccountEntityRepository.php │ ├── HttpClient │ │ └── RequestIdHttpClient.php │ ├── HttpKernel │ │ └── RequestPayloadValueResolver.php │ ├── Messenger │ │ ├── Middleware │ │ │ ├── RequestIdMiddleware.php │ │ │ └── ValidationMiddleware.php │ │ └── Stamp │ │ │ └── RequestIdStamp.php │ ├── Monolog │ │ ├── AuthorizationProcessor.php │ │ └── RequestIdProcessor.php │ ├── Security │ │ ├── DatabaseUserProvider.php │ │ ├── JsonLoginAuthenticator.php │ │ ├── JwtAccessTokenHandler.php │ │ ├── PasswordAuthenticatedUser.php │ │ ├── PasswordAuthenticatedUserChecker.php │ │ └── TokenAuthenticatedUser.php │ ├── Serializer │ │ ├── ExceptionNormalizer.php │ │ └── RequestNormalizer.php │ └── Workflow │ │ └── AccountMarkingStore.php ├── Kernel.php └── Presentation │ ├── Command │ └── SymfonyRunCommand.php │ ├── Controller │ ├── AbstractController.php │ ├── Account │ │ ├── BlockAccountByIdController.php │ │ ├── CreateNewAccountController.php │ │ ├── DeleteAccountByIdController.php │ │ ├── GetAccountByIdController.php │ │ ├── GetAccountsByCriteriaController.php │ │ ├── UnblockAccountByIdController.php │ │ └── UpdateAccountByIdController.php │ ├── Auth │ │ ├── GetSigninAccountController.php │ │ ├── SigninIntoAccountController.php │ │ └── SignupNewAccountController.php │ └── Health │ │ └── GetHealthStatusController.php │ ├── Resource │ ├── Template │ │ ├── bundles │ │ │ ├── DoctrineMigrationsBundle │ │ │ │ └── migration.php.twig │ │ │ └── NelmioApiDocBundle │ │ │ │ └── SwaggerUi │ │ │ │ └── index.html.twig │ │ ├── emails │ │ │ ├── account.registered.html.twig │ │ │ └── default │ │ │ │ └── body.html.twig │ │ └── views │ │ │ └── .gitignore │ └── Translation │ │ ├── exceptions+intl-icu.uk-UA.yaml │ │ ├── messages+intl-icu.uk-UA.yaml │ │ └── validators+intl-icu.uk-UA.yaml │ └── Scheduler │ └── SymfonyCronTask.php ├── symfony.lock └── tests ├── Acceptance.suite.yml ├── Acceptance └── .gitignore ├── Functional.suite.yml ├── Functional ├── AppSymfonyRunCest.php ├── BlockAccountByIdCest.php ├── CreateNewAccountCest.php ├── DeleteAccountByIdCest.php ├── GetAccountByIdCest.php ├── GetAccountsByCriteriaCest.php ├── GetHealthStatusCest.php ├── GetSigninAccountCest.php ├── SigninIntoAccountCest.php ├── SignupNewAccountCest.php ├── UnblockAccountByIdCest.php └── UpdateAccountByIdCest.php ├── Support ├── AcceptanceTester.php ├── Data │ ├── Fixture │ │ ├── AccountActivatedAdminFixture.php │ │ ├── AccountActivatedEmmaFixture.php │ │ ├── AccountActivatedJamesFixture.php │ │ ├── AccountBlockedHenryFixture.php │ │ └── AccountRegisteredOliviaFixture.php │ ├── Response │ │ └── HttpbinResponderGetJsonResponse.json │ └── Schema │ │ ├── ApplicationExceptionSchema.json │ │ ├── CreateNewAccountSchema.json │ │ ├── GetAccountByIdSchema.json │ │ ├── GetAccountsByCriteriaSchema.json │ │ ├── GetHealthStatusSchema.json │ │ ├── GetSigninAccountSchema.json │ │ └── SigninIntoAccountSchema.json ├── FunctionalTester.php ├── Helper │ └── .gitignore ├── UnitTester.php └── _generated │ └── .gitignore ├── Unit.suite.yml ├── Unit ├── AbstractControllerTest.php ├── AccountMarkingStoreTest.php ├── AccountWorkflowEventListenerTest.php ├── AuthorizationTokenManagerTest.php ├── DatabaseUserProviderTest.php ├── ExceptionNormalizerTest.php ├── GetSigninAccountHandlerTest.php ├── InMemoryRequestIdStorageTest.php ├── KennethreitzHttpbinAdapterTest.php ├── KernelExceptionEventListenerTest.php ├── LcobucciJwtAdapterTest.php ├── PasswordAuthenticatedUserCheckerTest.php ├── PasswordAuthenticatedUserTest.php ├── RequestDataPrivacyProtectorTest.php ├── RequestIdEventListenerTest.php ├── RequestIdHttpClientTest.php ├── RequestIdMiddlewareTest.php ├── RequestNormalizerTest.php ├── SensiolabsTwigAdapterTest.php ├── SigninIntoAccountHandlerTest.php ├── SymfonyCronTaskTest.php ├── SymfonyRunCommandTest.php └── TokenAuthenticatedUserTest.php └── _output └── .gitignore /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | .editorconfig 3 | .git/ 4 | .github/ 5 | .gitignore 6 | .idea/ 7 | Dockerfile 8 | LICENSE 9 | docker-compose.yml 10 | var/ 11 | vendor/ 12 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | ###> symfony/framework-bundle ### 2 | APP_DEBUG=true 3 | APP_ENV=dev 4 | APP_NAME=symfony 5 | APP_PORT=80 6 | APP_SECRET= 7 | APP_URL=http://localhost 8 | APP_VERSION=1.0.0 9 | DOCTRINE_DEPRECATIONS=0 10 | REDIS_DSN=redis://redis:6379?timeout=1&read_timeout=1 11 | TRUSTED_PROXIES=127.0.0.1,REMOTE_ADDR 12 | ###< symfony/framework-bundle ### 13 | 14 | ###> doctrine/doctrine-bundle ### 15 | DATABASE_URL=postgresql://admin:password@postgres:5432/symfony?serverVersion=17&charset=utf8 16 | ###< doctrine/doctrine-bundle ### 17 | 18 | ###> lcobucci/jwt ### 19 | JWT_ISSUER=http://localhost 20 | JWT_LIFETIME=86400 21 | JWT_PASSPHRASE=9f58129324cc3fc4ab32e6e60a79f7ca 22 | ###< lcobucci/jwt ### 23 | 24 | ###> symfony/mailer ### 25 | MAILER_DSN=smtp://mailcatcher:1025 26 | MAILER_SENDER=noreply@example.com 27 | ###< symfony/mailer ### 28 | 29 | ###> symfony/messenger ### 30 | MESSENGER_TRANSPORT_DSN=amqp://rabbitmq:5672/%2f/messages 31 | ###< symfony/messenger ### 32 | 33 | ###> symfony/http-client ### 34 | HTTPBIN_URL=https://httpbin.org 35 | ###< symfony/http-client ### 36 | 37 | ###> codeception/codeception ### 38 | MOCK_SERVER_URL=http://mockserver:1080 39 | ###< codeception/codeception ### 40 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | ###> codeception/codeception ### 2 | APP_DEBUG=false 3 | APP_SECRET=$ecretf0rt3st 4 | HTTPBIN_URL=http://mockserver:1080/httpbin.org/ 5 | KERNEL_CLASS=App\Kernel 6 | MAILER_DSN=null://null 7 | MESSENGER_TRANSPORT_DSN=sync:// 8 | MOCK_SERVER_URL=http://mockserver:1080 9 | PANTHER_APP_ENV=panther 10 | PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots 11 | SYMFONY_DEPRECATIONS_HELPER=999999 12 | ###< codeception/codeception ### 13 | -------------------------------------------------------------------------------- /.github/workflows/development.yml: -------------------------------------------------------------------------------- 1 | name: development 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | run: 9 | runs-on: ubuntu-latest 10 | timeout-minutes: 3 11 | 12 | services: 13 | postgres: 14 | image: postgres:17.3 15 | env: 16 | POSTGRES_DB: root 17 | POSTGRES_PASSWORD: root 18 | POSTGRES_USER: root 19 | options: >- 20 | --health-cmd pg_isready 21 | --health-interval 2s 22 | --health-timeout 2s 23 | --health-retries 5 24 | ports: 25 | - 5432:5432 26 | mockserver: 27 | image: mockserver/mockserver:latest 28 | ports: 29 | - 1080:1080 30 | 31 | env: 32 | DATABASE_URL: postgresql://root:root@localhost:5432/root?serverVersion=17&charset=utf8 33 | HTTPBIN_URL: http://localhost:1080/httpbin/ 34 | MAILER_DSN: null://null 35 | MESSENGER_TRANSPORT_DSN: sync:// 36 | MOCK_SERVER_URL: http://localhost:1080 37 | PHP_VERSION: 8.4 38 | 39 | steps: 40 | - name: Setup virtual environment 41 | uses: shivammathur/setup-php@v2 42 | with: 43 | php-version: ${{ env.PHP_VERSION }} 44 | 45 | - name: Checkout project sources 46 | uses: actions/checkout@v4 47 | 48 | - name: Install composer dependencies 49 | run: /usr/bin/composer install --no-scripts 50 | 51 | - name: Run automated code quality assurance 52 | run: /usr/bin/composer auto-analyze 53 | 54 | - name: Run automated project quality assurance 55 | run: /usr/bin/composer auto-quality 56 | 57 | - name: Upload project code coverage reports 58 | uses: codecov/codecov-action@v5 59 | with: 60 | token: ${{ secrets.CODECOV_TOKEN }} 61 | slug: opifex/symfony 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ###> symfony/framework-bundle ### 2 | /.env.*.local 3 | /.env.local 4 | /.idea/ 5 | /config/secrets/prod/prod.decrypt.private.php 6 | /public/bundles/ 7 | /var/ 8 | /vendor/ 9 | ###< symfony/framework-bundle ### 10 | 11 | ###> codeception/codeception ### 12 | /codeception.yml 13 | ###< codeception/codeception ### 14 | 15 | ###> phpunit/phpunit ### 16 | /.phpunit.cache/ 17 | /phpunit.xml 18 | ###< phpunit/phpunit ### 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-present Mykyta Zinchenko 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 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | ['all' => true], 7 | Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], 8 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 9 | Nelmio\ApiDocBundle\NelmioApiDocBundle::class => ['all' => true], 10 | Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true], 11 | Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true], 12 | Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], 13 | Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], 14 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], 15 | Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], 16 | Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], 17 | ]; 18 | -------------------------------------------------------------------------------- /config/docker/development.conf: -------------------------------------------------------------------------------- 1 | [php] 2 | error_reporting = E_ALL & ~E_DEPRECATED 3 | expose_php = Off 4 | max_execution_time = 60 5 | max_input_time = 60 6 | memory_limit = 512M 7 | output_buffering = 4096 8 | realpath_cache_size = 4096K 9 | realpath_cache_ttl = 120 10 | short_open_tag = Off 11 | 12 | [opcache] 13 | opcache.preload = 14 | opcache.validate_timestamps = On 15 | 16 | [xdebug] 17 | zend_extension = xdebug.so 18 | xdebug.client_host = host.docker.internal 19 | xdebug.idekey = IDE 20 | xdebug.log = /dev/null 21 | xdebug.mode = coverage,debug,develop 22 | xdebug.start_with_request = On 23 | -------------------------------------------------------------------------------- /config/docker/entrypoint.conf: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | COMMAND="$1" 4 | 5 | if [ "$APP_ENV" != "prod" ]; then 6 | /bin/echo "Running development environment." 7 | /bin/cp $PWD/config/docker/development.conf /usr/local/etc/php/php.ini 8 | ENTRYPOINT="/usr/bin/supervisord -c /etc/supervisor/supervisord.conf" 9 | COMPOSER="install" 10 | else 11 | /bin/echo "Running production environment." 12 | ENTRYPOINT="/usr/bin/supervisord -c /etc/supervisor/supervisord.conf" 13 | COMPOSER="auto-scripts" 14 | fi 15 | 16 | if [ "$COMMAND" = "messenger" ]; then 17 | /bin/echo "Handling messages from async queue." 18 | ENTRYPOINT="/usr/bin/supervisord -c /etc/supervisor/messenger.conf" 19 | COMPOSER="" 20 | elif [ "$COMMAND" = "migration" ]; then 21 | /bin/echo "Upgrading database by migrations." 22 | COMPOSER="auto-migrate" 23 | ENTRYPOINT="" 24 | elif [ "$COMMAND" = "quality" ]; then 25 | /bin/echo "Starting code quality assurance tests." 26 | /bin/cp $PWD/config/docker/development.conf /usr/local/etc/php/php.ini 27 | COMPOSER="install --no-scripts;auto-analyze;auto-quality" 28 | ENTRYPOINT="" 29 | fi 30 | 31 | export IFS=";" 32 | for script in $COMPOSER; do 33 | eval "/sbin/runuser -u www-data -- /usr/bin/composer $script" 34 | done 35 | export IFS=" " 36 | 37 | if [ "$ENTRYPOINT" != "" ]; then 38 | eval "$ENTRYPOINT" 39 | fi 40 | -------------------------------------------------------------------------------- /config/docker/messenger.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | logfile = /dev/null 3 | logfile_maxbytes = 0 4 | nodaemon = true 5 | pidfile = /usr/local/var/run/supervisord.pid 6 | user = root 7 | loglevel = error 8 | 9 | [program:messenger-scheduler-default] 10 | autorestart = true 11 | autostart = true 12 | command = php bin/console messenger:consume scheduler_default --time-limit=250 13 | redirect_stderr = true 14 | stdout_logfile = /dev/stdout 15 | stdout_logfile_maxbytes = 0 16 | startsecs = 0 17 | user = www-data 18 | 19 | [program:messenger-notifications-email] 20 | autorestart = true 21 | autostart = true 22 | command = php bin/console messenger:consume notifications_email --time-limit=250 23 | redirect_stderr = true 24 | stdout_logfile = /dev/stdout 25 | stdout_logfile_maxbytes = 0 26 | startsecs = 0 27 | user = www-data 28 | -------------------------------------------------------------------------------- /config/docker/php.conf: -------------------------------------------------------------------------------- 1 | [php] 2 | error_reporting = E_ALL & ~E_DEPRECATED 3 | expose_php = Off 4 | max_execution_time = 30 5 | max_input_time = 60 6 | memory_limit = 512M 7 | output_buffering = 4096 8 | realpath_cache_size = 4096K 9 | realpath_cache_ttl = 600 10 | short_open_tag = Off 11 | 12 | [opcache] 13 | opcache.max_accelerated_files = 100000 14 | opcache.memory_consumption = 256 15 | opcache.preload = /opt/project/config/preload.php 16 | opcache.preload_user = www-data 17 | opcache.validate_timestamps = Off 18 | -------------------------------------------------------------------------------- /config/docker/supervisor.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | logfile = /dev/null 3 | logfile_maxbytes = 0 4 | nodaemon = true 5 | pidfile = /usr/local/var/run/supervisord.pid 6 | user = root 7 | loglevel = error 8 | 9 | [program:nginx] 10 | autorestart = true 11 | autostart = true 12 | command = nginx 13 | priority = 3 14 | redirect_stderr = true 15 | stderr_logfile_maxbytes = 0 16 | stdout_logfile = /dev/stdout 17 | stdout_logfile_maxbytes = 0 18 | 19 | [program:php-fpm] 20 | autorestart = true 21 | autostart = true 22 | command = php-fpm 23 | priority = 2 24 | redirect_stderr = true 25 | stderr_logfile_maxbytes = 0 26 | stdout_logfile = /dev/stdout 27 | stdout_logfile_maxbytes = 0 28 | -------------------------------------------------------------------------------- /config/docker/www.conf: -------------------------------------------------------------------------------- 1 | [global] 2 | pid = /usr/local/var/run/php-fpm.pid 3 | 4 | daemonize = no 5 | log_limit = 8192 6 | 7 | error_log = /proc/self/fd/2 8 | 9 | [www] 10 | user = www-data 11 | group = www-data 12 | 13 | listen = /usr/local/var/run/php-fpm.sock 14 | listen.owner = www-data 15 | listen.group = www-data 16 | listen.mode = 0660 17 | 18 | access.log = /dev/null 19 | 20 | catch_workers_output = yes 21 | clear_env = no 22 | decorate_workers_output = no 23 | 24 | pm = dynamic 25 | pm.max_children = 5 26 | pm.max_requests = 200 27 | pm.max_spare_servers = 4 28 | pm.min_spare_servers = 2 29 | pm.start_servers = 3 30 | -------------------------------------------------------------------------------- /config/markup/Standards/Sniffs/Annotations/AnnotationTagSniff.php: -------------------------------------------------------------------------------- 1 | getTokens()[$stackPtr]['content']; 19 | 20 | if (!in_array($tagName, $this->allowedTags)) { 21 | $phpcsFile->addError( 22 | error: 'Annotation tag %s is not allowed. Allowed tags are: %s', 23 | stackPtr: $stackPtr, 24 | code: 'AnnotationTag', 25 | data: [$tagName, implode(separator: ', ', array: $this->allowedTags)], 26 | ); 27 | } 28 | } 29 | 30 | #[Override] 31 | public function register(): array 32 | { 33 | return [T_DOC_COMMENT_TAG]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /config/markup/Standards/Sniffs/Classes/ClassUsageSniff.php: -------------------------------------------------------------------------------- 1 | getTokens()[$stackPtr - 1]; 17 | 18 | if ($previousToken['code'] !== T_STRING) { 19 | $usageEndPtr = $phpcsFile->findNext([T_STRING, T_NS_SEPARATOR], start: $stackPtr + 1, exclude: true); 20 | $usageName = $phpcsFile->getTokensAsString($stackPtr, length: $usageEndPtr - $stackPtr); 21 | 22 | $phpcsFile->addError( 23 | error: 'Missing import for "%s" via use statement', 24 | stackPtr: $stackPtr, 25 | code: 'ClassUsage', 26 | data: [$usageName], 27 | ); 28 | } 29 | } 30 | 31 | #[Override] 32 | public function register(): array 33 | { 34 | return [T_NS_SEPARATOR]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /config/markup/Standards/Sniffs/Lists/TrailingCommaMultilineSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 17 | $currentToken = $tokens[$stackPtr]; 18 | 19 | if ($currentToken['code'] === T_CLOSE_SHORT_ARRAY) { 20 | $scopeOpener = $currentToken['bracket_opener']; 21 | $scopeCloser = $currentToken['bracket_closer']; 22 | } elseif ($currentToken['code'] === T_MATCH) { 23 | $scopeOpener = $currentToken['scope_opener']; 24 | $scopeCloser = $currentToken['scope_closer']; 25 | } elseif ($currentToken['code'] === T_CLOSE_PARENTHESIS) { 26 | $scopeOpener = $currentToken['parenthesis_opener']; 27 | $scopeCloser = $currentToken['parenthesis_closer']; 28 | } 29 | 30 | if (isset($scopeOpener) && isset($scopeCloser)) { 31 | for ($index = $scopeCloser; $index >= $scopeOpener; $index--) { 32 | if ($tokens[$index]['code'] === T_WHITESPACE && $tokens[$index]['content'] === PHP_EOL) { 33 | if ($tokens[$index - 1]['code'] !== T_COMMA) { 34 | $phpcsFile->addFixableError( 35 | error: 'Multi-line arrays, arguments and match expressions must have a trailing comma', 36 | stackPtr: $index - 1, 37 | code: 'TrailingCommaMultiline', 38 | ); 39 | $phpcsFile->fixer->addContent(stackPtr: $index - 1, content: ','); 40 | } 41 | 42 | break; 43 | } 44 | } 45 | } 46 | } 47 | 48 | #[Override] 49 | public function register(): array 50 | { 51 | return [T_CLOSE_SHORT_ARRAY, T_MATCH, T_CLOSE_PARENTHESIS]; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /config/markup/Standards/Sniffs/Methods/MethodParameterTypeSniff.php: -------------------------------------------------------------------------------- 1 | getDeclarationName($stackPtr) ?? 'closure'; 17 | $methodParameters = $phpcsFile->getMethodParameters($stackPtr); 18 | 19 | foreach ($methodParameters as $parameter) { 20 | if ($parameter['type_hint'] === '') { 21 | $phpcsFile->addError( 22 | error: 'The method "%s" has parameter %s without type hinting', 23 | stackPtr: $stackPtr, 24 | code: 'MethodParameterType', 25 | data: [$methodName, $parameter['name']], 26 | ); 27 | } 28 | } 29 | } 30 | 31 | #[Override] 32 | public function register(): array 33 | { 34 | return [T_CLOSURE, T_FUNCTION]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /config/markup/Standards/Sniffs/Methods/MethodReturnTypeSniff.php: -------------------------------------------------------------------------------- 1 | getTokens()[$stackPtr]; 17 | $methodName = $phpcsFile->getDeclarationName($stackPtr) ?? 'closure'; 18 | $methodProperties = $phpcsFile->getMethodProperties($stackPtr); 19 | $methodReturnType = strval($methodProperties['return_type']); 20 | $methodScopeOpener = $currentToken['scope_opener'] ?? 0; 21 | $methodScopeCloser = $currentToken['scope_closer'] ?? 0; 22 | $methodHasReturn = boolval($phpcsFile->findNext([T_RETURN], $methodScopeOpener, $methodScopeCloser)); 23 | $methodIsMagic = str_starts_with($methodName, '__'); 24 | 25 | if ($methodReturnType === '' && (!$methodIsMagic || $methodHasReturn)) { 26 | $phpcsFile->addError( 27 | error: 'Return type for the method "%s" is not specified', 28 | stackPtr: $stackPtr, 29 | code: 'MethodReturnType', 30 | data: [$methodName], 31 | ); 32 | } 33 | } 34 | 35 | #[Override] 36 | public function register(): array 37 | { 38 | return [T_CLOSURE, T_FUNCTION]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /config/markup/Standards/Sniffs/Operators/OperatorEqualitySniff.php: -------------------------------------------------------------------------------- 1 | getTokens()[$stackPtr]; 17 | $phpcsFile->addFixableError( 18 | error: 'Using not strict equality comparison is forbidden', 19 | stackPtr: $stackPtr, 20 | code: 'OperatorEquality', 21 | ); 22 | 23 | if ($currentToken['code'] === T_IS_EQUAL) { 24 | $phpcsFile->fixer->replaceToken($stackPtr, content: '==='); 25 | } elseif ($currentToken['code'] === T_IS_NOT_EQUAL) { 26 | $phpcsFile->fixer->replaceToken($stackPtr, content: '!=='); 27 | } 28 | } 29 | 30 | #[Override] 31 | public function register(): array 32 | { 33 | return [T_IS_EQUAL, T_IS_NOT_EQUAL]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /config/markup/Standards/Sniffs/Operators/OperatorIncDecSniff.php: -------------------------------------------------------------------------------- 1 | getTokens()[($stackPtr - 1)]; 17 | $nextToken = $phpcsFile->getTokens()[($stackPtr + 1)]; 18 | 19 | $operatorIsPostfix = $previousToken['code'] === T_VARIABLE; 20 | $operatorIsPrefix = $nextToken['code'] === T_VARIABLE; 21 | 22 | if (!$operatorIsPostfix && ($nextToken['code'] === T_WHITESPACE || $operatorIsPrefix)) { 23 | $phpcsFile->addError( 24 | error: 'Increment and decrement operators must have postfix format.', 25 | stackPtr: $stackPtr, 26 | code: 'OperatorIncDec', 27 | ); 28 | } 29 | } 30 | 31 | #[Override] 32 | public function register(): array 33 | { 34 | return [T_DEC, T_INC]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /config/markup/Standards/Sniffs/Statements/StatementElseSniff.php: -------------------------------------------------------------------------------- 1 | findNext([T_WHITESPACE], start: $stackPtr + 1, exclude: true); 17 | $nextToken = $phpcsFile->getTokens()[$nextTokenPtr]; 18 | 19 | if ($nextToken['code'] !== T_IF) { 20 | $phpcsFile->addError( 21 | error: 'Usage of ELSE statement are basically not necessary', 22 | stackPtr: $stackPtr, 23 | code: 'StatementElse', 24 | ); 25 | } 26 | } 27 | 28 | #[Override] 29 | public function register(): array 30 | { 31 | return [T_ELSE]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /config/markup/Standards/Sniffs/Strings/SingleQuoteStringSniff.php: -------------------------------------------------------------------------------- 1 | getTokens()[$stackPtr]['content']; 17 | 18 | if (str_starts_with($constantContent, '"') && str_ends_with($constantContent, '"')) { 19 | if (!preg_match(pattern: '/[$\\\]/', subject: $constantContent)) { 20 | $phpcsFile->addFixableError( 21 | error: 'String must be enclosed in single quotes', 22 | stackPtr: $stackPtr, 23 | code: 'ConstantString', 24 | ); 25 | $replacedContent = strtr(trim($constantContent, characters: '"'), ['\'' => '\\\'', '\"' => '"']); 26 | $phpcsFile->fixer->replaceToken($stackPtr, content: '\'' . $replacedContent . '\''); 27 | } 28 | } 29 | } 30 | 31 | #[Override] 32 | public function register(): array 33 | { 34 | return [T_CONSTANT_ENCAPSED_STRING]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /config/markup/Standards/Sniffs/Whitespaces/CodeWhitespaceSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 17 | $currentToken = $tokens[$stackPtr] ?? null; 18 | $previousToken = $tokens[$stackPtr - 1] ?? null; 19 | $nextToken = $tokens[$stackPtr + 1] ?? null; 20 | $openTags = [T_ATTRIBUTE, T_BOOLEAN_NOT, T_OPEN_CURLY_BRACKET, T_OPEN_PARENTHESIS, T_OPEN_SQUARE_BRACKET]; 21 | $closeTags = [T_ATTRIBUTE_END, T_CLOSE_CURLY_BRACKET, T_CLOSE_PARENTHESIS, T_CLOSE_SQUARE_BRACKET, T_SEMICOLON]; 22 | 23 | $spaceIsLong = strlen($currentToken['content']) > 1; 24 | $spaceIsComment = ($previousToken['code'] ?? null) === T_COMMENT; 25 | $spaceInBegin = in_array(needle: $previousToken['code'] ?? null, haystack: $openTags); 26 | $spaceInEnd = in_array(needle: $nextToken['code'] ?? null, haystack: $closeTags); 27 | 28 | if ($currentToken['content'] !== PHP_EOL && ($spaceIsLong || $spaceInBegin || $spaceInEnd)) { 29 | if ($previousToken['content'] !== PHP_EOL && !$spaceIsComment && $nextToken['content'] !== PHP_EOL) { 30 | $phpcsFile->addFixableError( 31 | error: 'Extra whitespaces must be removed', 32 | stackPtr: $stackPtr, 33 | code: 'CodeWhitespace', 34 | ); 35 | 36 | if ($spaceInBegin || $spaceInEnd) { 37 | $whitespace = ''; 38 | } 39 | 40 | $phpcsFile->fixer->replaceToken(stackPtr: $stackPtr, content: $whitespace ?? ' '); 41 | } 42 | } 43 | } 44 | 45 | #[Override] 46 | public function register(): array 47 | { 48 | return [T_WHITESPACE]; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /config/markup/Standards/Sniffs/Whitespaces/LineWhitespaceSniff.php: -------------------------------------------------------------------------------- 1 | getTokens() as $key => $token) { 20 | if ($token['code'] !== T_WHITESPACE) { 21 | $lineNumbersList[$token['line']] = false; 22 | $emptyLineCount = $token['code'] === T_OPEN_CURLY_BRACKET ? 1 : 0; 23 | 24 | continue; 25 | } 26 | 27 | if (!isset($lineNumbersList[$token['line']])) { 28 | $lineNumbersList[$token['line']] = true; 29 | $emptyLineCount++; 30 | } 31 | 32 | if ($emptyLineCount > 2) { 33 | $phpcsFile->addFixableError( 34 | error: 'Extra empty line must be removed', 35 | stackPtr: $key - 1, 36 | code: 'LineWhitespace', 37 | ); 38 | $phpcsFile->fixer->replaceToken(stackPtr: $key - 1, content: ''); 39 | } 40 | } 41 | } 42 | 43 | #[Override] 44 | public function register(): array 45 | { 46 | return [T_OPEN_TAG]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /config/markup/Standards/ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Code style standards 4 | 5 | -------------------------------------------------------------------------------- /config/markup/phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - ../../vendor/phpstan/phpstan-deprecation-rules/rules.neon 3 | - ../../vendor/phpstan/phpstan-doctrine/extension.neon 4 | - ../../vendor/phpstan/phpstan-phpunit/extension.neon 5 | - ../../vendor/phpstan/phpstan-phpunit/rules.neon 6 | - ../../vendor/phpstan/phpstan-symfony/extension.neon 7 | parameters: 8 | level: max 9 | paths: 10 | - ../../src 11 | -------------------------------------------------------------------------------- /config/packages/cache.php: -------------------------------------------------------------------------------- 1 | extension(namespace: 'framework', config: [ 9 | 'cache' => [ 10 | 'prefix_seed' => '%env(APP_NAME)%', 11 | 'default_redis_provider' => '%env(REDIS_DSN)%', 12 | 'pools' => [ 13 | 'cache.doctrine' => [ 14 | 'default_lifetime' => 60, 15 | 'adapters' => [ 16 | 'cache.adapter.redis', 17 | 'cache.adapter.array', 18 | ], 19 | ], 20 | 'cache.storage' => [ 21 | 'default_lifetime' => 60, 22 | 'adapters' => [ 23 | 'cache.adapter.redis', 24 | 'cache.adapter.array', 25 | ], 26 | ], 27 | ], 28 | ], 29 | ]); 30 | }; 31 | -------------------------------------------------------------------------------- /config/packages/debug.php: -------------------------------------------------------------------------------- 1 | env() === 'dev') { 9 | $configurator->extension(namespace: 'debug', config: [ 10 | 'dump_destination' => 'tcp://%env(VAR_DUMPER_SERVER)%', 11 | ]); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /config/packages/doctrine_migrations.php: -------------------------------------------------------------------------------- 1 | extension(namespace: 'doctrine_migrations', config: [ 9 | 'all_or_nothing' => true, 10 | 'custom_template' => '%kernel.project_dir%/src/Presentation/Resource/Template/bundles/DoctrineMigrationsBundle/migration.php.twig', 11 | 'enable_profiler' => '%kernel.debug%', 12 | 'migrations_paths' => [ 13 | 'App\Infrastructure\Doctrine\Migration' => '%kernel.project_dir%/src/Infrastructure/Doctrine/Migration', 14 | ], 15 | 'storage' => [ 16 | 'table_storage' => [ 17 | 'table_name' => 'migration', 18 | ], 19 | ], 20 | 'transactional' => true, 21 | ]); 22 | }; 23 | -------------------------------------------------------------------------------- /config/packages/framework.php: -------------------------------------------------------------------------------- 1 | extension(namespace: 'framework', config: [ 9 | 'secret' => '%env(APP_SECRET)%', 10 | 'trusted_proxies' => '%env(TRUSTED_PROXIES)%', 11 | 'handle_all_throwables' => true, 12 | 'http_method_override' => false, 13 | 'set_locale_from_accept_language' => true, 14 | 'set_content_language_from_locale' => true, 15 | 'serializer' => [ 16 | 'name_converter' => 'serializer.name_converter.camel_case_to_snake_case', 17 | ], 18 | 'php_errors' => [ 19 | 'log' => true, 20 | ], 21 | ]); 22 | 23 | if ($configurator->env() === 'test') { 24 | $configurator->extension(namespace: 'framework', config: [ 25 | 'test' => true, 26 | 'session' => [ 27 | 'storage_factory_id' => 'session.storage.factory.mock_file', 28 | ], 29 | ]); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /config/packages/mailer.php: -------------------------------------------------------------------------------- 1 | extension(namespace: 'framework', config: [ 9 | 'mailer' => [ 10 | 'dsn' => '%env(MAILER_DSN)%', 11 | 'envelope' => [ 12 | 'sender' => '%env(MAILER_SENDER)%', 13 | ], 14 | ], 15 | ]); 16 | }; 17 | -------------------------------------------------------------------------------- /config/packages/monolog.php: -------------------------------------------------------------------------------- 1 | env() === 'dev') { 9 | $configurator->extension(namespace: 'monolog', config: [ 10 | 'handlers' => [ 11 | 'nested' => [ 12 | 'type' => 'stream', 13 | 'level' => 'debug', 14 | 'path' => 'php://stdout', 15 | 'channels' => ['!deprecation', '!doctrine', '!event', '!http_client', '!php'], 16 | 'formatter' => 'monolog.formatter.json', 17 | ], 18 | ], 19 | ]); 20 | } 21 | 22 | if ($configurator->env() === 'test') { 23 | $configurator->extension(namespace: 'monolog', config: [ 24 | 'handlers' => [ 25 | 'main' => [ 26 | 'type' => 'fingers_crossed', 27 | 'action_level' => 'error', 28 | 'handler' => 'nested', 29 | 'excluded_http_codes' => [404, 405], 30 | 'channels' => ['!event'], 31 | ], 32 | 'nested' => [ 33 | 'type' => 'stream', 34 | 'path' => '%kernel.logs_dir%/%kernel.environment%.log', 35 | 'level' => 'debug', 36 | ], 37 | ], 38 | ]); 39 | } 40 | 41 | if ($configurator->env() === 'prod') { 42 | $configurator->extension(namespace: 'monolog', config: [ 43 | 'handlers' => [ 44 | 'nested' => [ 45 | 'type' => 'stream', 46 | 'level' => 'info', 47 | 'path' => 'php://stdout', 48 | 'channels' => ['!deprecation', '!doctrine', '!event', '!php', '!security'], 49 | 'formatter' => 'monolog.formatter.json', 50 | ], 51 | ], 52 | ]); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /config/packages/nelmio_api_doc.php: -------------------------------------------------------------------------------- 1 | extension(namespace: 'nelmio_api_doc', config: [ 9 | 'documentation' => [ 10 | 'info' => [ 11 | 'title' => '%env(APP_NAME)%', 12 | 'version' => '%env(APP_VERSION)%', 13 | ], 14 | 'servers' => [ 15 | [ 16 | 'url' => '%env(APP_URL)%', 17 | 'description' => 'API Gateway', 18 | ], 19 | ], 20 | 'components' => [ 21 | 'securitySchemes' => [ 22 | 'bearer' => [ 23 | 'type' => 'http', 24 | 'scheme' => 'bearer', 25 | ], 26 | ], 27 | ], 28 | ], 29 | 'areas' => [ 30 | 'path_patterns' => [ 31 | '^/api', 32 | ], 33 | ], 34 | ]); 35 | }; 36 | -------------------------------------------------------------------------------- /config/packages/notifier.php: -------------------------------------------------------------------------------- 1 | extension(namespace: 'framework', config: [ 9 | 'notifier' => [ 10 | // 'chatter_transports' => [ 11 | // 'slack' => '%env(SLACK_DSN)%', 12 | // 'telegram' => '%env(TELEGRAM_DSN)%', 13 | // ], 14 | // 'texter_transports' => [ 15 | // 'twilio' => '%env(TWILIO_DSN)%', 16 | // 'nexmo' => '%env(NEXMO_DSN)%', 17 | // ], 18 | 'channel_policy' => [ 19 | // chat/slack, chat/telegram, sms/twilio, sms/nexmo 20 | 'urgent' => ['email'], 21 | 'high' => ['email'], 22 | 'medium' => ['email'], 23 | 'low' => ['email'], 24 | ], 25 | 'admin_recipients' => [ 26 | ['email' => 'admin@example.com'], 27 | ], 28 | ], 29 | ]); 30 | }; 31 | -------------------------------------------------------------------------------- /config/packages/routing.php: -------------------------------------------------------------------------------- 1 | extension(namespace: 'framework', config: [ 9 | 'router' => [ 10 | 'utf8' => true, 11 | 'default_uri' => '%env(resolve:APP_URL)%', 12 | ], 13 | ]); 14 | 15 | if ($configurator->env() === 'prod') { 16 | $configurator->extension(namespace: 'framework', config: [ 17 | 'router' => [ 18 | 'strict_requirements' => null, 19 | ], 20 | ]); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /config/packages/security.php: -------------------------------------------------------------------------------- 1 | extension(namespace: 'security', config: [ 15 | 'password_hashers' => [ 16 | PasswordAuthenticatedUserInterface::class => 'auto', 17 | ], 18 | 'providers' => [ 19 | 'database' => [ 20 | 'id' => DatabaseUserProvider::class, 21 | ], 22 | ], 23 | 'firewalls' => [ 24 | 'development' => [ 25 | 'pattern' => '^/(_(profiler|wdt))', 26 | 'security' => false, 27 | ], 28 | 'authentication' => [ 29 | 'stateless' => true, 30 | 'pattern' => '^/api/auth/signin', 31 | 'provider' => 'database', 32 | 'user_checker' => PasswordAuthenticatedUserChecker::class, 33 | 'custom_authenticators' => [ 34 | JsonLoginAuthenticator::class, 35 | ], 36 | ], 37 | 'authorization' => [ 38 | 'stateless' => true, 39 | 'pattern' => '^/api', 40 | 'access_token' => [ 41 | 'token_handler' => JwtAccessTokenHandler::class, 42 | ], 43 | ], 44 | ], 45 | 'role_hierarchy' => [ 46 | Role::Admin->value => [ 47 | Role::User->value, 48 | ], 49 | ], 50 | ]); 51 | 52 | if ($configurator->env() === 'test') { 53 | $configurator->extension(namespace: 'security', config: [ 54 | 'password_hashers' => [ 55 | PasswordAuthenticatedUserInterface::class => 'plaintext', 56 | ], 57 | ]); 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /config/packages/translation.php: -------------------------------------------------------------------------------- 1 | extension(namespace: 'framework', config: [ 10 | 'default_locale' => LocaleCode::EnUs->value, 11 | 'translator' => [ 12 | 'default_path' => '%kernel.project_dir%/src/Presentation/Resource/Translation', 13 | 'fallbacks' => [LocaleCode::EnUs->value], 14 | ], 15 | ]); 16 | }; 17 | -------------------------------------------------------------------------------- /config/packages/twig.php: -------------------------------------------------------------------------------- 1 | extension(namespace: 'twig', config: [ 9 | 'default_path' => '%kernel.project_dir%/src/Presentation/Resource/Template', 10 | 'paths' => [ 11 | '%kernel.project_dir%/src/Presentation/Resource/Template/emails' => 'emails', 12 | '%kernel.project_dir%/src/Presentation/Resource/Template/views' => 'views', 13 | ], 14 | ]); 15 | 16 | if ($configurator->env() === 'test') { 17 | $configurator->extension(namespace: 'twig', config: [ 18 | 'strict_variables' => true, 19 | ]); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /config/packages/validator.php: -------------------------------------------------------------------------------- 1 | extension(namespace: 'framework', config: [ 9 | 'validation' => [ 10 | 'email_validation_mode' => 'html5', 11 | ], 12 | ]); 13 | 14 | if ($configurator->env() === 'test') { 15 | $configurator->extension(namespace: 'framework', config: [ 16 | 'validation' => [ 17 | 'not_compromised_password' => false, 18 | ], 19 | ]); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /config/packages/web_profiler.php: -------------------------------------------------------------------------------- 1 | env() === 'dev') { 9 | $configurator->extension(namespace: 'web_profiler', config: [ 10 | 'toolbar' => true, 11 | 'intercept_redirects' => false, 12 | ]); 13 | 14 | $configurator->extension(namespace: 'framework', config: [ 15 | 'profiler' => [ 16 | 'only_exceptions' => false, 17 | 'collect_serializer_data' => true, 18 | ], 19 | ]); 20 | } 21 | 22 | if ($configurator->env() === 'test') { 23 | $configurator->extension(namespace: 'web_profiler', config: [ 24 | 'toolbar' => false, 25 | 'intercept_redirects' => false, 26 | ]); 27 | 28 | $configurator->extension(namespace: 'framework', config: [ 29 | 'profiler' => [ 30 | 'collect' => false, 31 | ], 32 | ]); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /config/packages/workflow.php: -------------------------------------------------------------------------------- 1 | extension(namespace: 'framework', config: [ 13 | 'workflows' => [ 14 | 'account' => [ 15 | 'type' => 'state_machine', 16 | 'audit_trail' => [ 17 | 'enabled' => '%kernel.debug%', 18 | ], 19 | 'marking_store' => [ 20 | 'service' => AccountMarkingStore::class, 21 | ], 22 | 'supports' => [ 23 | Account::class, 24 | ], 25 | 'initial_marking' => AccountStatus::Created->value, 26 | 'places' => [ 27 | AccountStatus::Activated->value, 28 | AccountStatus::Blocked->value, 29 | AccountStatus::Created->value, 30 | AccountStatus::Registered->value, 31 | ], 32 | 'transitions' => [ 33 | AccountAction::Activate->value => [ 34 | 'from' => AccountStatus::Registered->value, 35 | 'to' => AccountStatus::Activated->value, 36 | ], 37 | AccountAction::Block->value => [ 38 | 'from' => AccountStatus::Activated->value, 39 | 'to' => AccountStatus::Blocked->value, 40 | ], 41 | AccountAction::Register->value => [ 42 | 'from' => AccountStatus::Created->value, 43 | 'to' => AccountStatus::Registered->value, 44 | ], 45 | AccountAction::Unblock->value => [ 46 | 'from' => AccountStatus::Blocked->value, 47 | 'to' => AccountStatus::Activated->value, 48 | ], 49 | ], 50 | ], 51 | ], 52 | ]); 53 | }; 54 | -------------------------------------------------------------------------------- /config/preload.php: -------------------------------------------------------------------------------- 1 | import(resource: '../../src/Presentation/Controller', type: 'attribute') 9 | ->prefix(prefix: '/api', trailingSlashOnRoot: false); 10 | }; 11 | -------------------------------------------------------------------------------- /config/routes/profiler.php: -------------------------------------------------------------------------------- 1 | env() === 'dev') { 9 | $configurator->import(resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml') 10 | ->prefix(prefix: '/_wdt'); 11 | 12 | $configurator->import(resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml') 13 | ->prefix(prefix: '/_profiler'); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /config/routes/security.php: -------------------------------------------------------------------------------- 1 | import(resource: 'security.route_loader.logout', type: 'service'); 9 | }; 10 | -------------------------------------------------------------------------------- /config/routes/swagger.php: -------------------------------------------------------------------------------- 1 | add(name: 'app_swagger_json', path: '/docs/json') 9 | ->methods(['GET']) 10 | ->defaults(['_controller' => 'nelmio_api_doc.controller.swagger_json']); 11 | 12 | $configurator->add(name: 'app_swagger_ui', path: '/docs') 13 | ->methods(['GET']) 14 | ->defaults(['_controller' => 'nelmio_api_doc.controller.swagger_ui']); 15 | }; 16 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | services()->defaults()->autowire()->autoconfigure(); 9 | $services->load(namespace: 'App\\', resource: dirname(path: __DIR__) . '/src/'); 10 | }; 11 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | getAccount()->getEmail()->toString()); 26 | $notification = new AccountRegisteredNotification($event->getAccount(), $this->translator); 27 | $this->notifier->send($notification, $recipient); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Application/EventListener/AccountWorkflowEventListener.php: -------------------------------------------------------------------------------- 1 | value)] 23 | public function onWorkflowAccountCompletedRegister(CompletedEvent $event): void 24 | { 25 | $account = $event->getSubject(); 26 | 27 | if (!$account instanceof Account) { 28 | throw new InvalidArgumentException(message: 'Subject expected to be a valid account.'); 29 | } 30 | 31 | $this->eventDispatcher->dispatch(new AccountRegisteredEvent($account)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Application/EventListener/RequestIdEventListener.php: -------------------------------------------------------------------------------- 1 | getCommand() instanceof Command) { 28 | return; 29 | } 30 | 31 | $requestId = $this->requestIdGenerator->generate(); 32 | $this->requestIdStorage->setRequestId($requestId); 33 | } 34 | 35 | #[AsEventListener(event: RequestEvent::class, priority: 100)] 36 | public function onRequest(RequestEvent $event): void 37 | { 38 | if (!$event->isMainRequest()) { 39 | return; 40 | } 41 | 42 | $requestId = $this->requestIdGenerator->generate(); 43 | $this->requestIdStorage->setRequestId($requestId); 44 | } 45 | 46 | #[AsEventListener(event: ResponseEvent::class, priority: -100)] 47 | public function onResponse(ResponseEvent $event): void 48 | { 49 | if (!$event->isMainRequest()) { 50 | return; 51 | } 52 | 53 | $requestId = $this->requestIdStorage->getRequestId(); 54 | 55 | if ($requestId !== null) { 56 | $event->getResponse()->headers->set( 57 | key: HttpSpecification::HEADER_X_REQUEST_ID, 58 | values: $requestId, 59 | ); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Application/MessageHandler/BlockAccountById/BlockAccountByIdHandler.php: -------------------------------------------------------------------------------- 1 | authorizationTokenManager->checkPermission(access: Role::Admin->toString())) { 28 | throw AuthorizationForbiddenException::create(); 29 | } 30 | 31 | $account = $this->accountEntityRepository->findOneById($request->id) 32 | ?? throw AccountNotFoundException::create(); 33 | 34 | $this->accountWorkflowManager->block($account); 35 | $this->accountEntityRepository->save($account); 36 | 37 | return BlockAccountByIdResult::success(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Application/MessageHandler/BlockAccountById/BlockAccountByIdRequest.php: -------------------------------------------------------------------------------- 1 | authorizationTokenManager->checkPermission(access: Role::Admin->toString())) { 31 | throw AuthorizationForbiddenException::create(); 32 | } 33 | 34 | if ($this->accountEntityRepository->findOneByEmail($request->email)) { 35 | throw AccountAlreadyExistsException::create(); 36 | } 37 | 38 | $hashedPassword = $this->authenticationPasswordHasher->hash($request->password); 39 | $account = Account::create($request->email, $hashedPassword, $request->locale); 40 | 41 | $this->accountWorkflowManager->register($account); 42 | $this->accountWorkflowManager->activate($account); 43 | $this->accountEntityRepository->save($account); 44 | 45 | return CreateNewAccountResult::success($account); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Application/MessageHandler/CreateNewAccount/CreateNewAccountRequest.php: -------------------------------------------------------------------------------- 1 | value, 28 | ) { 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Application/MessageHandler/CreateNewAccount/CreateNewAccountResult.php: -------------------------------------------------------------------------------- 1 | $account->getId()->toString(), 20 | ], 21 | status: Response::HTTP_CREATED, 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Application/MessageHandler/DeleteAccountById/DeleteAccountByIdHandler.php: -------------------------------------------------------------------------------- 1 | authorizationTokenManager->checkPermission(access: Role::Admin->toString())) { 26 | throw AuthorizationForbiddenException::create(); 27 | } 28 | 29 | $account = $this->accountEntityRepository->findOneById($request->id) 30 | ?? throw AccountNotFoundException::create(); 31 | 32 | $this->accountEntityRepository->delete($account); 33 | 34 | return DeleteAccountByIdResult::success(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Application/MessageHandler/DeleteAccountById/DeleteAccountByIdRequest.php: -------------------------------------------------------------------------------- 1 | authorizationTokenManager->checkPermission(access: Role::Admin->toString())) { 26 | throw AuthorizationForbiddenException::create(); 27 | } 28 | 29 | $account = $this->accountEntityRepository->findOneById($request->id) 30 | ?? throw AccountNotFoundException::create(); 31 | 32 | return GetAccountByIdResult::success($account); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Application/MessageHandler/GetAccountById/GetAccountByIdRequest.php: -------------------------------------------------------------------------------- 1 | $account->getId()->toString(), 20 | 'email' => $account->getEmail()->toString(), 21 | 'locale' => $account->getLocale()->toString(), 22 | 'status' => $account->getStatus()->toString(), 23 | 'roles' => $account->getRoles()->toArray(), 24 | 'created_at' => $account->getCreatedAt()->toAtomString(), 25 | ], 26 | status: Response::HTTP_OK, 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Application/MessageHandler/GetAccountsByCriteria/GetAccountsByCriteriaHandler.php: -------------------------------------------------------------------------------- 1 | authorizationTokenManager->checkPermission(access: Role::Admin->toString())) { 27 | throw AuthorizationForbiddenException::create(); 28 | } 29 | 30 | $searchPagination = new SearchPagination($request->page, $request->limit); 31 | $searchCriteria = new AccountSearchCriteria($request->email, $request->status, $searchPagination); 32 | 33 | $accountSearchResult = $this->accountEntityRepository->findByCriteria($searchCriteria); 34 | 35 | return GetAccountsByCriteriaResult::success($accountSearchResult, $searchPagination); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Application/MessageHandler/GetAccountsByCriteria/GetAccountsByCriteriaRequest.php: -------------------------------------------------------------------------------- 1 | [ 22 | 'current_page' => $searchPagination->getPage(), 23 | 'items_per_page' => $searchPagination->getLimit(), 24 | 'total_items' => $accountSearchResult->getTotalResultCount(), 25 | ], 26 | 'data' => array_map( 27 | callback: static fn(Account $account): array => [ 28 | 'id' => $account->getId()->toString(), 29 | 'email' => $account->getEmail()->toString(), 30 | 'locale' => $account->getLocale()->toString(), 31 | 'status' => $account->getStatus()->toString(), 32 | 'roles' => $account->getRoles()->toArray(), 33 | 'created_at' => $account->getCreatedAt()->toAtomString(), 34 | ], 35 | array: $accountSearchResult->getAccounts(), 36 | ), 37 | ], 38 | status: Response::HTTP_OK, 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Application/MessageHandler/GetHealthStatus/GetHealthStatusHandler.php: -------------------------------------------------------------------------------- 1 | $health->getStatus()->toString(), 20 | ], 21 | status: Response::HTTP_OK, 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Application/MessageHandler/GetSigninAccount/GetSigninAccountHandler.php: -------------------------------------------------------------------------------- 1 | authorizationTokenManager->getUserIdentifier(); 24 | 25 | $account = $this->accountEntityRepository->findOneById($userIdentifier) 26 | ?? throw AccountNotFoundException::create(); 27 | 28 | return GetSigninAccountResult::success($account); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Application/MessageHandler/GetSigninAccount/GetSigninAccountRequest.php: -------------------------------------------------------------------------------- 1 | $account->getId()->toString(), 20 | 'email' => $account->getEmail()->toString(), 21 | 'locale' => $account->getLocale()->toString(), 22 | 'status' => $account->getStatus()->toString(), 23 | 'roles' => $account->getRoles()->toArray(), 24 | 'created_at' => $account->getCreatedAt()->toAtomString(), 25 | ], 26 | status: Response::HTTP_OK, 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Application/MessageHandler/SigninIntoAccount/SigninIntoAccountHandler.php: -------------------------------------------------------------------------------- 1 | authorizationTokenManager->getUserIdentifier(); 26 | 27 | $account = $this->accountEntityRepository->findOneById($userIdentifier) 28 | ?? throw AccountNotFoundException::create(); 29 | 30 | $accessToken = $this->jwtTokenManager->createAccessToken( 31 | userIdentifier: $account->getId()->toString(), 32 | userRoles: $account->getRoles()->toArray(), 33 | ); 34 | 35 | return SigninIntoAccountResult::success($accessToken); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Application/MessageHandler/SigninIntoAccount/SigninIntoAccountRequest.php: -------------------------------------------------------------------------------- 1 | $accessToken, 19 | ], 20 | status: Response::HTTP_OK, 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Application/MessageHandler/SignupNewAccount/SignupNewAccountHandler.php: -------------------------------------------------------------------------------- 1 | accountEntityRepository->findOneByEmail($request->email)) { 27 | throw AccountAlreadyExistsException::create(); 28 | } 29 | 30 | $hashedPassword = $this->authenticationPasswordHasher->hash($request->password); 31 | $account = Account::create($request->email, $hashedPassword, $request->locale); 32 | 33 | $this->accountWorkflowManager->register($account); 34 | $this->accountWorkflowManager->activate($account); 35 | $this->accountEntityRepository->save($account); 36 | 37 | return SignupNewAccountResult::success(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Application/MessageHandler/SignupNewAccount/SignupNewAccountRequest.php: -------------------------------------------------------------------------------- 1 | value, 28 | ) { 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Application/MessageHandler/SignupNewAccount/SignupNewAccountResult.php: -------------------------------------------------------------------------------- 1 | authorizationTokenManager->checkPermission(access: Role::Admin->toString())) { 28 | throw AuthorizationForbiddenException::create(); 29 | } 30 | 31 | $account = $this->accountEntityRepository->findOneById($request->id) 32 | ?? throw AccountNotFoundException::create(); 33 | 34 | $this->accountWorkflowManager->unblock($account); 35 | $this->accountEntityRepository->save($account); 36 | 37 | return UnblockAccountByIdResult::success(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Application/MessageHandler/UnblockAccountById/UnblockAccountByIdRequest.php: -------------------------------------------------------------------------------- 1 | to($recipient->getEmail()); 34 | $email->locale($this->account->getLocale()->toString()); 35 | $email->subject($this->translator->trans($this->subject, locale: $this->account->getLocale()->toString())); 36 | $email->htmlTemplate(template: '@emails/account.registered.html.twig'); 37 | $email->context([ 38 | 'locale' => $this->account->getLocale()->toString(), 39 | 'account' => ['email' => $this->account->getEmail()->toString()], 40 | ]); 41 | 42 | return new EmailMessage($email); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Application/Service/AccountWorkflowManager.php: -------------------------------------------------------------------------------- 1 | apply($account, action: AccountAction::Activate); 25 | } 26 | 27 | #[Override] 28 | public function block(Account $account): void 29 | { 30 | $this->apply($account, action: AccountAction::Block); 31 | } 32 | 33 | #[Override] 34 | public function register(Account $account): void 35 | { 36 | $this->apply($account, action: AccountAction::Register); 37 | } 38 | 39 | #[Override] 40 | public function unblock(Account $account): void 41 | { 42 | $this->apply($account, action: AccountAction::Unblock); 43 | } 44 | 45 | private function apply(Account $account, AccountAction $action): void 46 | { 47 | if (!$this->accountStateMachine->can($account, $action->toString())) { 48 | throw AccountActionInvalidException::create(); 49 | } 50 | 51 | $this->accountStateMachine->apply($account, $action->toString()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Application/Service/AuthenticationPasswordHasher.php: -------------------------------------------------------------------------------- 1 | getPasswordHasher()->hash($plainPassword); 25 | } 26 | 27 | private function getPasswordHasher(): PasswordHasherInterface 28 | { 29 | return $this->passwordHasherFactory->getPasswordHasher( 30 | user: PasswordAuthenticatedUserInterface::class, 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Application/Service/AuthorizationTokenManager.php: -------------------------------------------------------------------------------- 1 | tokenStorage->getToken()?->getUserIdentifier(); 25 | 26 | if (!is_string($userIdentifier)) { 27 | throw AuthorizationRequiredException::create(); 28 | } 29 | 30 | return $userIdentifier; 31 | } 32 | 33 | #[Override] 34 | public function checkPermission(string $access, mixed $subject = null): bool 35 | { 36 | if (!$this->tokenStorage->getToken()?->getUserIdentifier()) { 37 | throw AuthorizationRequiredException::create(); 38 | } 39 | 40 | return $this->authorizationChecker->isGranted($access, $subject); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Application/Service/InMemoryRequestIdStorage.php: -------------------------------------------------------------------------------- 1 | requestId = $requestId; 19 | } 20 | 21 | #[Override] 22 | public function getRequestId(): ?string 23 | { 24 | return $this->requestId; 25 | } 26 | 27 | #[Override] 28 | public function reset(): void 29 | { 30 | $this->requestId = null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Application/Service/RequestDataPrivacyProtector.php: -------------------------------------------------------------------------------- 1 | */ 13 | private array $templates = [ 14 | 'email' => '/(?<=.).(?=.*.{1}@)/u', 15 | 'password' => '/./u', 16 | ]; 17 | 18 | #[Override] 19 | public function protect(array $data): array 20 | { 21 | foreach ($data as $key => $value) { 22 | if (array_key_exists($key, $this->templates) && is_string($value)) { 23 | $data[$key] = preg_replace($this->templates[$key], replacement: '*', subject: $value); 24 | } elseif (is_array($value)) { 25 | /** @var array $value */ 26 | $data[$key] = $this->protect($value); 27 | } 28 | } 29 | 30 | return $data; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Application/Service/UuidV4RequestIdGenerator.php: -------------------------------------------------------------------------------- 1 | toString(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Domain/Contract/Account/AccountEntityRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | 13 | * @throws HttpbinResponderException 14 | */ 15 | public function getJson(): array; 16 | } 17 | -------------------------------------------------------------------------------- /src/Domain/Contract/Integration/JwtTokenManagerInterface.php: -------------------------------------------------------------------------------- 1 | $context 13 | * @throws TemplateRendererException 14 | */ 15 | public function render(string $name, array $context = []): string; 16 | } 17 | -------------------------------------------------------------------------------- /src/Domain/Contract/Protection/PrivacyProtectorInterface.php: -------------------------------------------------------------------------------- 1 | $data 11 | * @return array 12 | */ 13 | public function protect(array $data): array; 14 | } 15 | -------------------------------------------------------------------------------- /src/Domain/Event/AccountRegisteredEvent.php: -------------------------------------------------------------------------------- 1 | account; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Domain/Exception/Account/AccountActionInvalidException.php: -------------------------------------------------------------------------------- 1 | new ConstraintViolation( 25 | message: 'This field was not expected.', 26 | messageTemplate: null, 27 | parameters: [], 28 | root: $root, 29 | propertyPath: $attribute, 30 | invalidValue: null, 31 | ), 32 | array: $extraAttributes, 33 | ), 34 | ); 35 | 36 | return new self($violations); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Domain/Exception/Validation/RequestParamTypeException.php: -------------------------------------------------------------------------------- 1 | implode(separator: ', ', array: $expected)], 31 | root: $root, 32 | propertyPath: $path, 33 | invalidValue: null, 34 | ), 35 | ]); 36 | 37 | return new self($constraint); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Domain/Exception/Validation/ValidationFailedException.php: -------------------------------------------------------------------------------- 1 | violations; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Domain/Model/AccountAction.php: -------------------------------------------------------------------------------- 1 | value; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Domain/Model/AccountIdentifier.php: -------------------------------------------------------------------------------- 1 | $role->toString(), $this->roles); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Domain/Model/AccountSearchCriteria.php: -------------------------------------------------------------------------------- 1 | email; 22 | } 23 | 24 | public function getStatus(): ?string 25 | { 26 | return $this->status; 27 | } 28 | 29 | public function getPagination(): ?SearchPagination 30 | { 31 | return $this->pagination; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Domain/Model/AccountSearchResult.php: -------------------------------------------------------------------------------- 1 | $totalResultCount 15 | */ 16 | public function __construct( 17 | private readonly array $accounts, 18 | private readonly int $totalResultCount, 19 | ) { 20 | } 21 | 22 | /** 23 | * @return Account[] 24 | */ 25 | public function getAccounts(): array 26 | { 27 | return $this->accounts; 28 | } 29 | 30 | /** 31 | * @return int<0, max> 32 | */ 33 | public function getTotalResultCount(): int 34 | { 35 | return $this->totalResultCount; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Domain/Model/AccountStatus.php: -------------------------------------------------------------------------------- 1 | $item->value, self::cases()); 28 | } 29 | 30 | public function toString(): string 31 | { 32 | return $this->value; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Domain/Model/AuthorizationToken.php: -------------------------------------------------------------------------------- 1 | userIdentifier; 24 | } 25 | 26 | /** 27 | * @return string[] 28 | */ 29 | public function getUserRoles(): array 30 | { 31 | return $this->userRoles; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Domain/Model/Common/AbstractUuidIdentifier.php: -------------------------------------------------------------------------------- 1 | toString()); 21 | } 22 | 23 | public static function fromString(string $uuid): static 24 | { 25 | return new static($uuid); 26 | } 27 | 28 | public function toString(): string 29 | { 30 | return $this->uuid; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Domain/Model/Common/DateTimeUtc.php: -------------------------------------------------------------------------------- 1 | setTimezone(new DateTimeZone(self::TIMEZONE))); 25 | } 26 | 27 | public static function fromImmutable(DateTimeImmutable $datetime): self 28 | { 29 | return new self($datetime->setTimezone(new DateTimeZone(self::TIMEZONE))); 30 | } 31 | 32 | public function toImmutable(): DateTimeImmutable 33 | { 34 | return $this->datetime; 35 | } 36 | 37 | public function toAtomString(): string 38 | { 39 | return $this->datetime->format(DateTimeInterface::ATOM); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Domain/Model/Common/EmailAddress.php: -------------------------------------------------------------------------------- 1 | email) === strtolower($emailAddress->email); 25 | } 26 | 27 | public function toString(): string 28 | { 29 | return $this->email; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Domain/Model/Common/HashedPassword.php: -------------------------------------------------------------------------------- 1 | passwordHash; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Domain/Model/Health.php: -------------------------------------------------------------------------------- 1 | status; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Domain/Model/HealthStatus.php: -------------------------------------------------------------------------------- 1 | value; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Domain/Model/HttpSpecification.php: -------------------------------------------------------------------------------- 1 | $item->value, self::cases()); 26 | } 27 | 28 | public function toString(): string 29 | { 30 | return $this->value; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Domain/Model/Role.php: -------------------------------------------------------------------------------- 1 | value; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Domain/Model/SearchPagination.php: -------------------------------------------------------------------------------- 1 | page; 21 | } 22 | 23 | public function getLimit(): int 24 | { 25 | return $this->limit; 26 | } 27 | 28 | public function getOffset(): int 29 | { 30 | return ($this->page - 1) * $this->limit; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Infrastructure/Adapter/Kennethreitz/KennethreitzHttpbinAdapter.php: -------------------------------------------------------------------------------- 1 | */ 29 | return $this->httpClient->withOptions([ 30 | 'base_uri' => $this->apiUrl, 31 | 'headers' => [ 32 | 'Accept' => 'application/json', 33 | ], 34 | ])->request(method: 'GET', url: 'json')->toArray(); 35 | } catch (ExceptionInterface $e) { 36 | throw HttpbinResponderException::fromException($e); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Infrastructure/Adapter/Sensiolabs/SensiolabsTwigAdapter.php: -------------------------------------------------------------------------------- 1 | environment->render($name, $context); 25 | } catch (Error $e) { 26 | throw TemplateRendererException::fromException($e); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Infrastructure/Doctrine/Mapping/AccountEntity.php: -------------------------------------------------------------------------------- 1 | 320])] 26 | public string $email = '', 27 | 28 | #[ORM\Column(name: 'password', type: Types::STRING, options: ['length' => 60])] 29 | public string $password = '', 30 | 31 | #[ORM\Column(name: 'locale', type: Types::STRING, options: ['length' => 5])] 32 | public string $locale = '', 33 | 34 | /** @var string[] $roles */ 35 | #[ORM\Column(name: 'roles', type: Types::JSON)] 36 | public array $roles = [], 37 | 38 | #[ORM\Column(name: 'status', type: Types::STRING, options: ['length' => 24])] 39 | public string $status = '', 40 | ) { 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Infrastructure/Doctrine/Migration/Version20200101000000.php: -------------------------------------------------------------------------------- 1 | addSql(<<<'SQL' 17 | CREATE TABLE account (id UUID NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, email VARCHAR(320) NOT NULL, password VARCHAR(60) NOT NULL, locale VARCHAR(5) NOT NULL, roles JSON NOT NULL, status VARCHAR(24) NOT NULL, PRIMARY KEY(id)) 18 | SQL); 19 | $this->addSql(<<<'SQL' 20 | CREATE UNIQUE INDEX UNIQ_7D3656A4E7927C74 ON account (email) 21 | SQL); 22 | } 23 | 24 | #[Override] 25 | public function down(Schema $schema): void 26 | { 27 | $this->addSql(<<<'SQL' 28 | DROP TABLE account 29 | SQL); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Infrastructure/Doctrine/Repository/Account/AccountEntityMapper.php: -------------------------------------------------------------------------------- 1 | id), 25 | createdAt: DateTimeUtc::fromImmutable($entity->createdAt), 26 | email: EmailAddress::fromString($entity->email), 27 | password: HashedPassword::fromString($entity->password), 28 | locale: LocaleCode::fromString($entity->locale), 29 | roles: AccountRoles::fromStrings(...$entity->roles), 30 | status: AccountStatus::fromString($entity->status), 31 | ); 32 | } 33 | 34 | /** 35 | * @return Account[] 36 | */ 37 | public static function mapAll(AccountEntity ...$entities): array 38 | { 39 | return array_map(self::map(...), $entities); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Infrastructure/HttpClient/RequestIdHttpClient.php: -------------------------------------------------------------------------------- 1 | client = $client ?? HttpClient::create(); 30 | } 31 | 32 | /** 33 | * @param array $options 34 | * @throws TransportExceptionInterface 35 | */ 36 | public function request(string $method, string $url, array $options = []): ResponseInterface 37 | { 38 | $requestId = $this->requestIdStorage->getRequestId(); 39 | $options['headers'] ??= []; 40 | 41 | if ($requestId !== null && is_array($options['headers'])) { 42 | $options['headers'][HttpSpecification::HEADER_X_REQUEST_ID] = $requestId; 43 | } 44 | 45 | return $this->client->request($method, $url, $options); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Infrastructure/HttpKernel/RequestPayloadValueResolver.php: -------------------------------------------------------------------------------- 1 | normalizer->normalize($request); 38 | $context = [AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false]; 39 | $type = $argument->getType() ?? ''; 40 | 41 | try { 42 | /** @var object[] */ 43 | return [$this->denormalizer->denormalize($payload, $type, context: $context)]; 44 | } catch (ExtraAttributesException $e) { 45 | throw RequestExtraParamsException::create($e->getExtraAttributes(), $type); 46 | } catch (NotNormalizableValueException $e) { 47 | throw RequestParamTypeException::create($e->getExpectedTypes(), $e->getPath(), $type); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Infrastructure/Messenger/Middleware/RequestIdMiddleware.php: -------------------------------------------------------------------------------- 1 | requestIdStorage->getRequestId(); 25 | $requestIdStamp = $envelope->last(stampFqcn: RequestIdStamp::class); 26 | 27 | if ($requestIdStamp instanceof RequestIdStamp) { 28 | $this->requestIdStorage->setRequestId($requestIdStamp->getRequestId()); 29 | } elseif ($requestId !== null) { 30 | $envelope = $envelope->with(new RequestIdStamp($requestId)); 31 | } 32 | 33 | return $stack->next()->handle($envelope, $stack); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Infrastructure/Messenger/Middleware/ValidationMiddleware.php: -------------------------------------------------------------------------------- 1 | validator->validate($envelope->getMessage()); 25 | 26 | if ($violations->count()) { 27 | throw ValidationFailedException::fromViolations($violations); 28 | } 29 | 30 | return $stack->next()->handle($envelope, $stack); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Infrastructure/Messenger/Stamp/RequestIdStamp.php: -------------------------------------------------------------------------------- 1 | requestId; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Infrastructure/Monolog/AuthorizationProcessor.php: -------------------------------------------------------------------------------- 1 | tokenStorage->getToken(); 23 | 24 | if ($token instanceof TokenInterface) { 25 | $record->extra['authorization'] = [ 26 | 'user' => $token->getUserIdentifier(), 27 | 'roles' => $token->getRoleNames(), 28 | ]; 29 | } 30 | 31 | return $record; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Infrastructure/Monolog/RequestIdProcessor.php: -------------------------------------------------------------------------------- 1 | requestIdStorage->getRequestId(); 22 | 23 | if ($requestId !== null) { 24 | $record->extra['identifier'] = $requestId; 25 | } 26 | 27 | return $record; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Infrastructure/Security/DatabaseUserProvider.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class DatabaseUserProvider implements UserProviderInterface 19 | { 20 | public function __construct( 21 | private readonly AccountEntityRepositoryInterface $accountEntityRepository, 22 | ) { 23 | } 24 | 25 | #[Override] 26 | public function loadUserByIdentifier(string $identifier): UserInterface 27 | { 28 | $account = $this->accountEntityRepository->findOneByEmail($identifier); 29 | 30 | if (!$account instanceof Account) { 31 | throw new UserNotFoundException(); 32 | } 33 | 34 | return new PasswordAuthenticatedUser( 35 | userIdentifier: $account->getId()->toString(), 36 | password: $account->getPassword()->toString(), 37 | roles: $account->getRoles()->toArray(), 38 | enabled: $account->isActive(), 39 | ); 40 | } 41 | 42 | #[Override] 43 | public function refreshUser(UserInterface $user): UserInterface 44 | { 45 | throw new UnsupportedUserException(); 46 | } 47 | 48 | #[Override] 49 | public function supportsClass(string $class): bool 50 | { 51 | return is_a($class, class: PasswordAuthenticatedUser::class, allow_string: true); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Infrastructure/Security/JsonLoginAuthenticator.php: -------------------------------------------------------------------------------- 1 | getPayload(); 24 | $userBadge = new UserBadge($payload->getString(key: 'email')); 25 | $credentials = new PasswordCredentials($payload->getString(key: 'password')); 26 | 27 | return new Passport($userBadge, $credentials); 28 | } 29 | 30 | #[Override] 31 | public function createToken(Passport $passport, string $firewallName): TokenInterface 32 | { 33 | return new UsernamePasswordToken($passport->getUser(), $firewallName, $passport->getUser()->getRoles()); 34 | } 35 | 36 | #[Override] 37 | public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response 38 | { 39 | throw $exception; 40 | } 41 | 42 | #[Override] 43 | public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response 44 | { 45 | return null; 46 | } 47 | 48 | #[Override] 49 | public function supports(Request $request): bool 50 | { 51 | return $request->getContentTypeFormat() === 'json'; 52 | } 53 | 54 | #[Override] 55 | public function isInteractive(): bool 56 | { 57 | return true; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Infrastructure/Security/JwtAccessTokenHandler.php: -------------------------------------------------------------------------------- 1 | jwtTokenManager->decodeAccessToken($accessToken); 24 | $userLoader = static fn(): TokenAuthenticatedUser => new TokenAuthenticatedUser( 25 | userIdentifier: $authorizationToken->getUserIdentifier(), 26 | roles: $authorizationToken->getUserRoles(), 27 | ); 28 | 29 | return new UserBadge($authorizationToken->getUserIdentifier(), $userLoader); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Infrastructure/Security/PasswordAuthenticatedUser.php: -------------------------------------------------------------------------------- 1 | userIdentifier; 31 | } 32 | 33 | #[Override] 34 | public function getPassword(): string 35 | { 36 | return $this->password; 37 | } 38 | 39 | #[Override] 40 | public function getRoles(): array 41 | { 42 | return $this->roles; 43 | } 44 | 45 | #[Override] 46 | public function eraseCredentials(): void 47 | { 48 | // Nothing to do 49 | } 50 | 51 | public function isEnabled(): bool 52 | { 53 | return $this->enabled; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Infrastructure/Security/PasswordAuthenticatedUserChecker.php: -------------------------------------------------------------------------------- 1 | isEnabled()) { 18 | throw new CustomUserMessageAccountStatusException( 19 | message: 'The presented account is not activated.', 20 | ); 21 | } 22 | } 23 | 24 | #[Override] 25 | public function checkPreAuth(UserInterface $user): void 26 | { 27 | // Nothing to do 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Infrastructure/Security/TokenAuthenticatedUser.php: -------------------------------------------------------------------------------- 1 | userIdentifier; 28 | } 29 | 30 | #[Override] 31 | public function getRoles(): array 32 | { 33 | return $this->roles; 34 | } 35 | 36 | #[Override] 37 | public function eraseCredentials(): void 38 | { 39 | // Nothing to do 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Infrastructure/Workflow/AccountMarkingStore.php: -------------------------------------------------------------------------------- 1 | getStatus()->toString() => 1]); 24 | } 25 | 26 | /** 27 | * @param array $context 28 | */ 29 | #[Override] 30 | public function setMarking(object $subject, Marking $marking, array $context = []): void 31 | { 32 | if (!$subject instanceof Account) { 33 | throw new InvalidArgumentException(message: 'Subject expected to be a valid account.'); 34 | } 35 | 36 | $subject->updateStatus(AccountStatus::fromString((string) array_key_first($marking->getPlaces()))); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Kernel.php: -------------------------------------------------------------------------------- 1 | addOption( 31 | name: 'delay', 32 | mode: InputOption::VALUE_OPTIONAL, 33 | description: 'Delay in seconds between iterations.', 34 | default: 1, 35 | ); 36 | } 37 | 38 | #[Override] 39 | protected function execute(InputInterface $input, OutputInterface $output): int 40 | { 41 | $console = new SymfonyStyle($input, $output); 42 | $console->title($this->getDescription()); 43 | 44 | $delay = is_string($input->getOption(name: 'delay')) ? (int) $input->getOption(name: 'delay') : 0; 45 | 46 | $iterableItems = [$this->httpbinResponder->getJson()]; 47 | 48 | foreach ($console->progressIterate($iterableItems) as $item) { 49 | $this->clock->sleep($delay); 50 | } 51 | 52 | $console->success('Success'); 53 | 54 | return self::SUCCESS; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Presentation/Controller/AbstractController.php: -------------------------------------------------------------------------------- 1 | messageBus->dispatch($message); 22 | $handledStamps = $envelope->all(stampFqcn: HandledStamp::class); 23 | $handledResult = $handledStamps[0]->getResult(); 24 | 25 | if (count($handledStamps) !== 1) { 26 | $exceptionMessage = 'Message of type "%s" was handled multiple times, but only one handler is expected.'; 27 | throw new LogicException(sprintf($exceptionMessage, get_debug_type($envelope->getMessage()))); 28 | } 29 | 30 | if (!$handledResult instanceof Response) { 31 | $exceptionMessage = 'Message handler for type "%s" must return valid Response object.'; 32 | throw new LogicException(sprintf($exceptionMessage, get_debug_type($envelope->getMessage()))); 33 | } 34 | 35 | return $handledResult; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Presentation/Controller/Health/GetHealthStatusController.php: -------------------------------------------------------------------------------- 1 | getHandledResult($request); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Presentation/Resource/Template/bundles/DoctrineMigrationsBundle/migration.php.twig: -------------------------------------------------------------------------------- 1 | ; 6 | 7 | use Doctrine\DBAL\Schema\Schema; 8 | use Doctrine\Migrations\AbstractMigration; 9 | use Override; 10 | 11 | final class extends AbstractMigration 12 | { 13 | #[Override] 14 | public function up(Schema $schema): void 15 | { 16 | 17 | } 18 | 19 | #[Override] 20 | public function down(Schema $schema): void 21 | { 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Presentation/Resource/Template/emails/account.registered.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "@emails/default/body.html.twig" %} 2 | 3 | {% block content %} 4 | {% trans with {'{email}': account.email} %} 5 | Account with email {email} was successfully created. 6 | {% endtrans %} 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /src/Presentation/Resource/Template/emails/default/body.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% block content %}{% endblock %} 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/Presentation/Resource/Template/views/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opifex/symfony/3346d3fd526c5912c09081d4849d9f3e4a96cc07/src/Presentation/Resource/Template/views/.gitignore -------------------------------------------------------------------------------- /src/Presentation/Resource/Translation/exceptions+intl-icu.uk-UA.yaml: -------------------------------------------------------------------------------- 1 | 'Account with provided identifier not found.': 'Обліковий запис із вказаним ідентифікатором не знайдено.' 2 | 'Authorization required to perform this action.': 'Для виконання цієї дії потрібна авторизація.' 3 | 'Authorization token have invalid structure.': 'Токен авторизації має недопустиму структуру.' 4 | 'Authorization token is invalid or expired.': 'Токен авторизації недійсний або термін дії минув.' 5 | 'Bad credentials.': 'Обліковий запис з вказаною електронною поштою не знайдено.' 6 | 'Could not decode request body.': 'Не вдалося декодувати тіло запиту.' 7 | 'Email address is already associated with another account.': 'Електронна пошта належить іншому обліковому запису.' 8 | 'Error while decoding authorization token.': 'Не вдалось декодувати токен авторизації.' 9 | 'Full authentication is required to access this resource.': 'Для доступу до цього ресурсу необхідно авторизуватись.' 10 | 'No authentication provider found to support the authentication token.': 'Даний тип авторизації не підтримується.' 11 | 'No privileges for the provided action.': 'Недостатньо привілеїв для виконання цієї дії.' 12 | 'Parameters validation failed.': 'Помилка під час перевірки параметрів запиту.' 13 | 'Provided action cannot be applied to account.': 'Зазначену дію не можливо застосувати до даного облікового запису.' 14 | 'The presented account is not activated.': 'Обліковий запис не активовано.' 15 | 'The presented password cannot be empty.': 'Пароль не може бути порожнім.' 16 | 'The presented password is invalid.': 'Вказаний пароль для авторизації недійсний.' 17 | -------------------------------------------------------------------------------- /src/Presentation/Resource/Translation/messages+intl-icu.uk-UA.yaml: -------------------------------------------------------------------------------- 1 | 'Account with email {email} was successfully created.': 'Обліковий запис з електронною поштою {email} успішно створено.' 2 | 'Thank you for registration': 'Ваш обліковий запис зареєстровано' 3 | -------------------------------------------------------------------------------- /src/Presentation/Resource/Translation/validators+intl-icu.uk-UA.yaml: -------------------------------------------------------------------------------- 1 | 'The password strength is too low. Please use a stronger password.': 'Вказаний пароль не є надійним.' 2 | 'The value you selected is not a valid choice.': 'Вказане значення недопустиме.' 3 | 'This field was not expected.': 'Вказаний параметр недопустимий.' 4 | 'This value is not a valid locale.': 'Це некоректна локалізація.' 5 | 'This value should be of type {type}.': 'Вказаний параметр повинен бути типом {type}.' 6 | -------------------------------------------------------------------------------- /src/Presentation/Scheduler/SymfonyCronTask.php: -------------------------------------------------------------------------------- 1 | logger->info('Symfony cron task processed.'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Acceptance.suite.yml: -------------------------------------------------------------------------------- 1 | actor: AcceptanceTester 2 | modules: 3 | enabled: 4 | - PhpBrowser: 5 | url: https://localhost:8000 6 | step_decorators: 7 | - Codeception\Step\ConditionalAssertion 8 | - Codeception\Step\TryTo 9 | - Codeception\Step\Retry 10 | -------------------------------------------------------------------------------- /tests/Acceptance/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opifex/symfony/3346d3fd526c5912c09081d4849d9f3e4a96cc07/tests/Acceptance/.gitignore -------------------------------------------------------------------------------- /tests/Functional.suite.yml: -------------------------------------------------------------------------------- 1 | actor: FunctionalTester 2 | modules: 3 | enabled: 4 | - Cli: 5 | - REST: 6 | depends: Symfony 7 | - Symfony: 8 | app_path: 'src' 9 | environment: 'test' 10 | - Doctrine: 11 | depends: Symfony 12 | cleanup: true 13 | transaction: false 14 | -------------------------------------------------------------------------------- /tests/Functional/AppSymfonyRunCest.php: -------------------------------------------------------------------------------- 1 | haveCleanMockServer(); 17 | $I->haveMockResponse( 18 | request: Request::create(uri: getenv(name: 'HTTPBIN_URL') . 'json'), 19 | response: new JsonResponse( 20 | data: $I->getResponseContent(filename: 'HttpbinResponderGetJsonResponse.json'), 21 | json: true, 22 | ), 23 | ); 24 | $I->runSymfonyConsoleCommand( 25 | command: 'app:symfony:run', 26 | parameters: ['--delay' => 0], 27 | expectedExitCode: Command::SUCCESS, 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Functional/GetAccountsByCriteriaCest.php: -------------------------------------------------------------------------------- 1 | loadFixtures(fixtures: AccountActivatedAdminFixture::class); 18 | $I->haveHttpHeaderApplicationJson(); 19 | $I->haveHttpHeaderAuthorization(email: 'admin@example.com', password: 'password4#account'); 20 | $I->sendGet( 21 | url: '/api/account', 22 | params: [ 23 | 'email' => 'admin@example.com', 24 | 'status' => AccountStatus::Activated->toString(), 25 | ], 26 | ); 27 | $I->seeResponseCodeIs(code: HttpCode::OK); 28 | $I->seeRequestTimeIsLessThan(expectedMilliseconds: 300); 29 | $I->seeResponseIsJson(); 30 | $I->seeResponseContainsJson(['email' => 'admin@example.com']); 31 | $I->seeResponseIsValidOnJsonSchema($I->getSchemaPath(filename: 'GetAccountsByCriteriaSchema.json')); 32 | } 33 | 34 | public function tryToGetAccountsByCriteriaWithoutPermission(FunctionalTester $I): void 35 | { 36 | $I->loadFixtures(fixtures: AccountActivatedJamesFixture::class); 37 | $I->haveHttpHeaderApplicationJson(); 38 | $I->haveHttpHeaderAuthorization(email: 'james@example.com', password: 'password4#account'); 39 | $I->sendGet( 40 | url: '/api/account', 41 | params: [ 42 | 'email' => 'admin@example.com', 43 | 'status' => AccountStatus::Activated->toString(), 44 | ], 45 | ); 46 | $I->seeResponseCodeIs(code: HttpCode::FORBIDDEN); 47 | $I->seeRequestTimeIsLessThan(expectedMilliseconds: 300); 48 | $I->seeResponseIsValidOnJsonSchema($I->getSchemaPath(filename: 'ApplicationExceptionSchema.json')); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/Functional/GetHealthStatusCest.php: -------------------------------------------------------------------------------- 1 | haveHttpHeaderApplicationJson(); 16 | $I->sendGet(url: '/api/health'); 17 | $I->seeResponseCodeIs(code: HttpCode::OK); 18 | $I->seeRequestTimeIsLessThan(expectedMilliseconds: 300); 19 | $I->seeResponseIsJson(); 20 | $I->seeResponseContainsJson(['status' => HealthStatus::Ok->toString()]); 21 | $I->seeResponseIsValidOnJsonSchema($I->getSchemaPath(filename: 'GetHealthStatusSchema.json')); 22 | } 23 | 24 | public function tryToGetHealthWithInvalidMethod(FunctionalTester $I): void 25 | { 26 | $I->haveHttpHeaderApplicationJson(); 27 | $I->sendPost(url: '/api/health'); 28 | $I->seeResponseCodeIs(code: HttpCode::METHOD_NOT_ALLOWED); 29 | $I->seeRequestTimeIsLessThan(expectedMilliseconds: 300); 30 | $I->seeResponseIsValidOnJsonSchema($I->getSchemaPath(filename: 'ApplicationExceptionSchema.json')); 31 | } 32 | 33 | public function tryToGetHealthWithInvalidRoute(FunctionalTester $I): void 34 | { 35 | $I->haveHttpHeaderApplicationJson(); 36 | $I->sendGet(url: '/api/invalid'); 37 | $I->seeResponseCodeIs(code: HttpCode::NOT_FOUND); 38 | $I->seeRequestTimeIsLessThan(expectedMilliseconds: 300); 39 | $I->seeResponseIsValidOnJsonSchema($I->getSchemaPath(filename: 'ApplicationExceptionSchema.json')); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Support/AcceptanceTester.php: -------------------------------------------------------------------------------- 1 | unique()->uuid(), 26 | createdAt: DateTimeImmutable::createFromMutable($faker->dateTime()), 27 | email: $faker->unique()->bothify(string: 'admin@example.com'), 28 | password: 'password4#account', 29 | locale: LocaleCode::EnUs->toString(), 30 | roles: [Role::Admin->toString()], 31 | status: AccountStatus::Activated->toString(), 32 | ); 33 | $manager->persist($account); 34 | $this->addReference(name: 'account:activated:admin', object: $account); 35 | $manager->flush(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Support/Data/Fixture/AccountActivatedEmmaFixture.php: -------------------------------------------------------------------------------- 1 | unique()->uuid(), 26 | createdAt: DateTimeImmutable::createFromMutable($faker->dateTime()), 27 | email: $faker->unique()->bothify(string: 'emma@example.com'), 28 | password: 'password4#account', 29 | locale: LocaleCode::EnUs->toString(), 30 | roles: [Role::User->toString()], 31 | status: AccountStatus::Activated->toString(), 32 | ); 33 | $manager->persist($account); 34 | $this->addReference(name: 'account:activated:emma', object: $account); 35 | $manager->flush(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Support/Data/Fixture/AccountActivatedJamesFixture.php: -------------------------------------------------------------------------------- 1 | unique()->uuid(), 26 | createdAt: DateTimeImmutable::createFromMutable($faker->dateTime()), 27 | email: $faker->unique()->bothify(string: 'james@example.com'), 28 | password: 'password4#account', 29 | locale: LocaleCode::EnUs->toString(), 30 | roles: [Role::User->toString()], 31 | status: AccountStatus::Activated->toString(), 32 | ); 33 | $manager->persist($account); 34 | $this->addReference(name: 'account:activated:james', object: $account); 35 | $manager->flush(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Support/Data/Fixture/AccountBlockedHenryFixture.php: -------------------------------------------------------------------------------- 1 | unique()->uuid(), 26 | createdAt: DateTimeImmutable::createFromMutable($faker->dateTime()), 27 | email: $faker->unique()->bothify(string: 'henry@example.com'), 28 | password: 'password4#account', 29 | locale: LocaleCode::EnUs->toString(), 30 | roles: [Role::User->toString()], 31 | status: AccountStatus::Blocked->toString(), 32 | ); 33 | $manager->persist($account); 34 | $this->addReference(name: 'account:blocked:henry', object: $account); 35 | $manager->flush(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Support/Data/Fixture/AccountRegisteredOliviaFixture.php: -------------------------------------------------------------------------------- 1 | unique()->uuid(), 26 | createdAt: DateTimeImmutable::createFromMutable($faker->dateTime()), 27 | email: $faker->unique()->bothify(string: 'olivia@example.com'), 28 | password: 'password4#account', 29 | locale: LocaleCode::EnUs->toString(), 30 | roles: [Role::User->toString()], 31 | status: AccountStatus::Registered->toString(), 32 | ); 33 | $manager->persist($account); 34 | $this->addReference(name: 'account:registered:olivia', object: $account); 35 | $manager->flush(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Support/Data/Response/HttpbinResponderGetJsonResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "slideshow": { 3 | "author": "Yours Truly", 4 | "date": "date of publication", 5 | "slides": [ 6 | { 7 | "title": "Wake up to WonderWidgets!", 8 | "type": "all" 9 | }, 10 | { 11 | "items": [ 12 | "Why WonderWidgets are great", 13 | "Who buys WonderWidgets" 14 | ], 15 | "title": "Overview", 16 | "type": "all" 17 | } 18 | ], 19 | "title": "Sample Slide Show" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Support/Data/Schema/ApplicationExceptionSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "code": { 5 | "type": "string", 6 | "format": "uuid" 7 | }, 8 | "error": { 9 | "type": "string" 10 | }, 11 | "violations": { 12 | "type": "array", 13 | "items": { 14 | "type": "object", 15 | "properties": { 16 | "name": { 17 | "type": "string" 18 | }, 19 | "reason": { 20 | "type": "string" 21 | }, 22 | "object": { 23 | "type": "string" 24 | }, 25 | "value": { 26 | "type": [ 27 | "string", 28 | "null" 29 | ] 30 | } 31 | }, 32 | "required": [ 33 | "name", 34 | "reason", 35 | "object" 36 | ], 37 | "additionalProperties": false 38 | } 39 | }, 40 | "trace": { 41 | "type": "array", 42 | "items": { 43 | "type": "object", 44 | "properties": { 45 | "file": { 46 | "type": "string" 47 | }, 48 | "type": { 49 | "type": "string" 50 | }, 51 | "line": { 52 | "type": "integer" 53 | } 54 | }, 55 | "required": [ 56 | "file", 57 | "type", 58 | "line" 59 | ], 60 | "additionalProperties": false 61 | } 62 | } 63 | }, 64 | "required": [ 65 | "code", 66 | "error" 67 | ], 68 | "additionalProperties": false 69 | } 70 | -------------------------------------------------------------------------------- /tests/Support/Data/Schema/CreateNewAccountSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "id": { 5 | "type": "string", 6 | "format": "uuid" 7 | } 8 | }, 9 | "required": [ 10 | "id" 11 | ], 12 | "additionalProperties": false 13 | } 14 | -------------------------------------------------------------------------------- /tests/Support/Data/Schema/GetAccountByIdSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "id": { 5 | "type": "string", 6 | "format": "uuid" 7 | }, 8 | "email": { 9 | "type": "string", 10 | "format": "email" 11 | }, 12 | "locale": { 13 | "type": "string", 14 | "pattern": "^[a-z]{2}-[A-Z]{2}$" 15 | }, 16 | "status": { 17 | "type": "string", 18 | "enum": [ 19 | "activated", 20 | "blocked", 21 | "created", 22 | "registered" 23 | ] 24 | }, 25 | "roles": { 26 | "type": "array", 27 | "items": { 28 | "type": "string", 29 | "pattern": "^ROLE_[A-Z_]+$" 30 | }, 31 | "minItems": 1 32 | }, 33 | "created_at": { 34 | "type": "string", 35 | "format": "date-time" 36 | } 37 | }, 38 | "required": [ 39 | "id", 40 | "email", 41 | "locale", 42 | "status", 43 | "roles", 44 | "created_at" 45 | ], 46 | "additionalProperties": false 47 | } 48 | -------------------------------------------------------------------------------- /tests/Support/Data/Schema/GetHealthStatusSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "status": { 5 | "type": "string", 6 | "enum": [ 7 | "ok" 8 | ] 9 | } 10 | }, 11 | "required": [ 12 | "status" 13 | ], 14 | "additionalProperties": false 15 | } 16 | -------------------------------------------------------------------------------- /tests/Support/Data/Schema/GetSigninAccountSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "id": { 5 | "type": "string", 6 | "format": "uuid" 7 | }, 8 | "email": { 9 | "type": "string", 10 | "format": "email" 11 | }, 12 | "locale": { 13 | "type": "string", 14 | "pattern": "^[a-z]{2}-[A-Z]{2}$" 15 | }, 16 | "status": { 17 | "type": "string", 18 | "enum": [ 19 | "activated", 20 | "blocked", 21 | "created", 22 | "registered" 23 | ] 24 | }, 25 | "roles": { 26 | "type": "array", 27 | "items": { 28 | "type": "string", 29 | "pattern": "^ROLE_[A-Z_]+$" 30 | }, 31 | "minItems": 1 32 | }, 33 | "created_at": { 34 | "type": "string", 35 | "format": "date-time" 36 | } 37 | }, 38 | "required": [ 39 | "id", 40 | "email", 41 | "locale", 42 | "status", 43 | "roles", 44 | "created_at" 45 | ], 46 | "additionalProperties": false 47 | } 48 | -------------------------------------------------------------------------------- /tests/Support/Data/Schema/SigninIntoAccountSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "access_token": { 5 | "type": "string", 6 | "pattern": "^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+$" 7 | } 8 | }, 9 | "required": [ 10 | "access_token" 11 | ], 12 | "additionalProperties": false 13 | } 14 | -------------------------------------------------------------------------------- /tests/Support/Helper/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opifex/symfony/3346d3fd526c5912c09081d4849d9f3e4a96cc07/tests/Support/Helper/.gitignore -------------------------------------------------------------------------------- /tests/Support/UnitTester.php: -------------------------------------------------------------------------------- 1 | accountEntityRepository = $this->createMock(type: AccountEntityRepositoryInterface::class); 28 | } 29 | 30 | public function testGetMarkingThrowsExceptionWithInvalidObject(): void 31 | { 32 | $accountMarkingStore = new AccountMarkingStore($this->accountEntityRepository); 33 | 34 | $this->expectException(InvalidArgumentException::class); 35 | 36 | $accountMarkingStore->getMarking(new stdClass()); 37 | } 38 | 39 | public function testSetMarkingThrowsExceptionWithInvalidObject(): void 40 | { 41 | $accountMarkingStore = new AccountMarkingStore($this->accountEntityRepository); 42 | 43 | $this->expectException(InvalidArgumentException::class); 44 | 45 | $accountMarkingStore->setMarking(new stdClass(), new Marking()); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Unit/AccountWorkflowEventListenerTest.php: -------------------------------------------------------------------------------- 1 | eventDispatcher = $this->createMock(type: EventDispatcherInterface::class); 29 | } 30 | 31 | public function testOnWorkflowAccountCompletedRegister(): void 32 | { 33 | $accountWorkflowEventListener = new AccountWorkflowEventListener( 34 | eventDispatcher: $this->eventDispatcher, 35 | ); 36 | 37 | $this->expectException(InvalidArgumentException::class); 38 | 39 | $accountWorkflowEventListener->onWorkflowAccountCompletedRegister( 40 | event: new CompletedEvent(new stdClass(), new Marking()), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Unit/ExceptionNormalizerTest.php: -------------------------------------------------------------------------------- 1 | kernel = $this->createMock(type: KernelInterface::class); 28 | $this->translator = $this->createMock(type: TranslatorInterface::class); 29 | } 30 | 31 | public function testNormalizeThrowsInvalidArgumentException(): void 32 | { 33 | $exceptionNormalizer = new ExceptionNormalizer($this->kernel, $this->translator); 34 | 35 | $this->translator 36 | ->expects($this->once()) 37 | ->method(constraint: 'trans') 38 | ->with(arguments: 'Object expected to be a valid exception type.') 39 | ->willReturn(value: 'Object expected to be a valid exception type.'); 40 | 41 | $normalized = $exceptionNormalizer->normalize(data: null); 42 | 43 | $this->assertArrayHasKey(key: 'error', array: $normalized); 44 | $this->assertEquals(expected: 'Object expected to be a valid exception type.', actual: $normalized['error']); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Unit/GetSigninAccountHandlerTest.php: -------------------------------------------------------------------------------- 1 | accountEntityRepository = $this->createMock(type: AccountEntityRepositoryInterface::class); 30 | $this->authorizationTokenManager = $this->createMock(type: AuthorizationTokenManagerInterface::class); 31 | } 32 | 33 | public function testInvokeThrowsExceptionWhenAccountNotFound(): void 34 | { 35 | $handler = new GetSigninAccountHandler( 36 | accountEntityRepository: $this->accountEntityRepository, 37 | authorizationTokenManager: $this->authorizationTokenManager, 38 | ); 39 | 40 | $this->accountEntityRepository 41 | ->expects($this->once()) 42 | ->method(constraint: 'findOneById') 43 | ->willReturn(value: null); 44 | 45 | $this->expectException(exception: AccountNotFoundException::class); 46 | 47 | $handler(new GetSigninAccountRequest()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/Unit/InMemoryRequestIdStorageTest.php: -------------------------------------------------------------------------------- 1 | setRequestId($requestId); 18 | 19 | $this->assertEquals($requestId, $inMemoryRequestIdStorage->getRequestId()); 20 | 21 | $inMemoryRequestIdStorage->reset(); 22 | 23 | $this->assertEmpty($inMemoryRequestIdStorage->getRequestId()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Unit/KennethreitzHttpbinAdapterTest.php: -------------------------------------------------------------------------------- 1 | getJson(); 25 | 26 | $this->assertSame($json, $response); 27 | } 28 | 29 | public function testGetJsonThrowsExceptionOnHttpError(): void 30 | { 31 | $mockResponse = new MockResponse(); 32 | $mockResponse->cancel(); 33 | 34 | $apiUrl = 'https://api.example.com'; 35 | $mockHttpClient = new MockHttpClient($mockResponse); 36 | $kennethreitzHttpbinAdapter = new KennethreitzHttpbinAdapter($apiUrl, $mockHttpClient); 37 | 38 | $this->expectException(HttpbinResponderException::class); 39 | 40 | $kennethreitzHttpbinAdapter->getJson(); 41 | } 42 | 43 | protected function httpbinResponseProvider(): array 44 | { 45 | return [ 46 | [['slideshow' => ['author' => 'Yours Truly', 'title' => 'Sample Slide Show']]], 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/Unit/KernelExceptionEventListenerTest.php: -------------------------------------------------------------------------------- 1 | kernel = $this->createMock(type: KernelInterface::class); 40 | $this->logger = $this->createMock(type: LoggerInterface::class); 41 | $this->normalizer = $this->createMock(type: NormalizerInterface::class); 42 | $this->privacyProtector = $this->createMock(type: PrivacyProtectorInterface::class); 43 | } 44 | 45 | /** 46 | * @throws ExceptionInterface 47 | * @throws ReflectionException 48 | */ 49 | public function testInvokeWithLogicException(): void 50 | { 51 | $event = new ExceptionEvent( 52 | kernel: $this->kernel, 53 | request: new Request(), 54 | requestType: HttpKernelInterface::MAIN_REQUEST, 55 | e: new LogicException(), 56 | ); 57 | 58 | new KernelExceptionEventListener($this->logger, $this->normalizer, $this->privacyProtector)($event); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/Unit/PasswordAuthenticatedUserCheckerTest.php: -------------------------------------------------------------------------------- 1 | user = $this->createMock(type: UserInterface::class); 29 | } 30 | 31 | public function testCheckPostAuthWithBlockedAccount(): void 32 | { 33 | $accountUserChecker = new PasswordAuthenticatedUserChecker(); 34 | $accountUser = new PasswordAuthenticatedUser( 35 | userIdentifier: Uuid::v7()->hash(), 36 | password: 'password4#account', 37 | roles: [Role::User], 38 | enabled: false, 39 | ); 40 | 41 | $this->expectException(CustomUserMessageAccountStatusException::class); 42 | 43 | $accountUserChecker->checkPostAuth($accountUser); 44 | } 45 | 46 | public function testCheckPostAuthWithNonAccountUser(): void 47 | { 48 | $accountUserChecker = new PasswordAuthenticatedUserChecker(); 49 | $accountUserChecker->checkPostAuth($this->user); 50 | 51 | $this->expectNotToPerformAssertions(); 52 | } 53 | 54 | public function testCheckPostAuthWithVerifiedAccount(): void 55 | { 56 | $accountUserChecker = new PasswordAuthenticatedUserChecker(); 57 | $accountUser = new PasswordAuthenticatedUser( 58 | userIdentifier: Uuid::v7()->hash(), 59 | password: 'password4#account', 60 | roles: [Role::User], 61 | enabled: true, 62 | ); 63 | $accountUserChecker->checkPostAuth($accountUser); 64 | 65 | $this->expectNotToPerformAssertions(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/Unit/PasswordAuthenticatedUserTest.php: -------------------------------------------------------------------------------- 1 | eraseCredentials(); 19 | 20 | $this->expectNotToPerformAssertions(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Unit/RequestDataPrivacyProtectorTest.php: -------------------------------------------------------------------------------- 1 | protect($value); 18 | 19 | $this->assertSame($expected, $protectedMessage); 20 | } 21 | 22 | protected function requestDataProvider(): array 23 | { 24 | return [ 25 | ['value' => ['email' => 'admin@example.com'], 'expected' => ['email' => 'a***n@example.com']], 26 | ['value' => ['password' => 'password4#account'], 'expected' => ['password' => '*****************']], 27 | ['value' => [['email' => 'admin@example.com']], 'expected' => [['email' => 'a***n@example.com']]], 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Unit/RequestIdHttpClientTest.php: -------------------------------------------------------------------------------- 1 | httpClient = $this->createMock(type: HttpClientInterface::class); 32 | $this->requestIdStorage = $this->createMock(type: RequestIdStorageInterface::class); 33 | $this->response = $this->createMock(type: ResponseInterface::class); 34 | 35 | $this->requestIdStorage 36 | ->expects($this->any()) 37 | ->method(constraint: 'getRequestId') 38 | ->willReturn(value: '00000000-0000-6000-8000-000000000000'); 39 | } 40 | 41 | /** 42 | * @throws TransportExceptionInterface 43 | */ 44 | public function testPassingRequestIdHeader(): void 45 | { 46 | $requestIdHttpClient = new RequestIdHttpClient($this->requestIdStorage, $this->httpClient); 47 | 48 | $this->httpClient 49 | ->expects($this->once()) 50 | ->method(constraint: 'request') 51 | ->willReturn($this->response); 52 | 53 | $requestIdHttpClient->request(method: 'GET', url: 'https://api.example.com'); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Unit/RequestIdMiddlewareTest.php: -------------------------------------------------------------------------------- 1 | middleware = $this->createMock(type: MiddlewareInterface::class); 34 | $this->requestIdStorage = $this->createMock(type: RequestIdStorageInterface::class); 35 | $this->stack = $this->createMock(type: StackInterface::class); 36 | } 37 | 38 | public function testHandleEnvelopeWithRequestIdStamp(): void 39 | { 40 | $middleware = new RequestIdMiddleware($this->requestIdStorage); 41 | 42 | $requestIdStamp = new RequestIdStamp(requestId: '00000000-0000-6000-8000-000000000000'); 43 | $envelope = new Envelope(new stdClass(), [$requestIdStamp]); 44 | 45 | $this->stack 46 | ->expects($this->once()) 47 | ->method(constraint: 'next') 48 | ->willReturn($this->middleware); 49 | 50 | $this->middleware 51 | ->expects($this->once()) 52 | ->method(constraint: 'handle') 53 | ->with($envelope, $this->stack) 54 | ->willReturn($envelope); 55 | 56 | $middleware->handle($envelope, $this->stack); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Unit/SensiolabsTwigAdapterTest.php: -------------------------------------------------------------------------------- 1 | environment = $this->createMock(type: Environment::class); 27 | } 28 | 29 | public function testRenderExistedTemplate(): void 30 | { 31 | $sensiolabsTwigAdapter = new SensiolabsTwigAdapter($this->environment); 32 | $content = 'content'; 33 | 34 | $this->environment 35 | ->expects($this->once()) 36 | ->method(constraint: 'render') 37 | ->willReturn($content); 38 | 39 | $rendered = $sensiolabsTwigAdapter->render(name: 'example.html.twig'); 40 | 41 | $this->assertSame($content, $rendered); 42 | } 43 | 44 | public function testRenderThrowsExceptionOnTwigError(): void 45 | { 46 | $sensiolabsTwigAdapter = new SensiolabsTwigAdapter($this->environment); 47 | 48 | $this->environment 49 | ->expects($this->once()) 50 | ->method(constraint: 'render') 51 | ->willThrowException(new Error(message: '')); 52 | 53 | $this->expectException(TemplateRendererException::class); 54 | 55 | $sensiolabsTwigAdapter->render(name: 'example.html.twig'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Unit/SigninIntoAccountHandlerTest.php: -------------------------------------------------------------------------------- 1 | accountEntityRepository = $this->createMock(type: AccountEntityRepositoryInterface::class); 33 | $this->authorizationTokenManager = $this->createMock(type: AuthorizationTokenManagerInterface::class); 34 | $this->jwtTokenManager = $this->createMock(type: JwtTokenManagerInterface::class); 35 | } 36 | 37 | public function testInvokeThrowsExceptionWhenAccountNotFound(): void 38 | { 39 | $handler = new SigninIntoAccountHandler( 40 | accountEntityRepository: $this->accountEntityRepository, 41 | authorizationTokenManager: $this->authorizationTokenManager, 42 | jwtTokenManager: $this->jwtTokenManager, 43 | ); 44 | 45 | $this->accountEntityRepository 46 | ->expects($this->once()) 47 | ->method(constraint: 'findOneById') 48 | ->willReturn(value: null); 49 | 50 | $this->expectException(exception: AccountNotFoundException::class); 51 | 52 | $handler(new SigninIntoAccountRequest()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Unit/SymfonyCronTaskTest.php: -------------------------------------------------------------------------------- 1 | logger = $this->createMock(type: LoggerInterface::class); 25 | } 26 | 27 | public function testInvokeCronTask(): void 28 | { 29 | new SymfonyCronTask($this->logger)(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Unit/SymfonyRunCommandTest.php: -------------------------------------------------------------------------------- 1 | application = new Application(); 31 | $this->httpbinResponder = $this->createMock(type: HttpbinResponderInterface::class); 32 | $this->application->add( 33 | new SymfonyRunCommand( 34 | clock: new MockClock(), 35 | httpbinResponder: $this->httpbinResponder, 36 | ), 37 | ); 38 | } 39 | 40 | public function testExecuteWithSuccessResult(): void 41 | { 42 | $this->httpbinResponder 43 | ->expects($this->once()) 44 | ->method(constraint: 'getJson') 45 | ->willReturn(['slideshow' => ['title' => 'Sample Slide Show']]); 46 | 47 | $commandTester = new CommandTester($this->application->get('app:symfony:run')); 48 | $commandTester->execute(['--delay' => 0]); 49 | 50 | $this->assertSame(expected: Command::SUCCESS, actual: $commandTester->getStatusCode()); 51 | $this->assertStringContainsString(needle: '[OK] Success', haystack: $commandTester->getDisplay()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Unit/TokenAuthenticatedUserTest.php: -------------------------------------------------------------------------------- 1 | eraseCredentials(); 18 | 19 | $this->expectNotToPerformAssertions(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/_output/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | --------------------------------------------------------------------------------