├── .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 |
--------------------------------------------------------------------------------