├── .DS_Store ├── .all-contributorsrc ├── .coveralls.yml ├── .dockerignore ├── .env.dist ├── .github └── workflows │ ├── pr.yml │ └── push.yml ├── .gitignore ├── .styleci.yml ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── bin ├── console └── deptrac.phar ├── composer.json ├── composer.lock ├── config ├── bundles.php ├── packages │ ├── broadway.yaml │ ├── dev │ │ ├── monolog.yaml │ │ ├── routing.yaml │ │ └── web_profiler.yaml │ ├── doctrine.yaml │ ├── doctrine │ │ └── mapping │ │ │ └── user │ │ │ ├── Domain.ValueObject.Auth.Credentials.orm.xml │ │ │ └── Infrastructure.ReadModel.UserView.orm.xml │ ├── doctrine_migrations.yaml │ ├── framework.yaml │ ├── framework_extra.yml │ ├── jwt │ │ ├── private.pem │ │ └── public.pem │ ├── lexik_jwt_authentication.yaml │ ├── messenger.yaml │ ├── nelmio_api_doc.yaml │ ├── prod │ │ ├── doctrine.yaml │ │ └── monolog.yaml │ ├── security.yaml │ ├── test │ │ ├── dama_doctrine_test_bundle.yaml │ │ ├── framework.yaml │ │ ├── monolog.yaml │ │ └── web_profiler.yaml │ └── twig.yaml ├── routes.yaml ├── routes │ ├── dev │ │ ├── twig.yaml │ │ └── web_profiler.yaml │ └── nelmio_api_doc.yaml ├── services.yaml └── services_test.yaml ├── deptrac.yaml ├── doc ├── Deployment.md ├── GetStarted │ ├── Async.md │ ├── Buses.md │ ├── Projections.md │ ├── UseCases.md │ └── Xdebug.md ├── Workflow.md ├── docker-php-interpreter.png ├── xdebug-activation.png └── xdebug-mapping.png ├── docker-compose.ci.yml ├── docker-compose.yml ├── easy-coding-standard.yml ├── etc ├── artifact │ ├── Dockerfile │ ├── chart │ │ ├── .helmignore │ │ ├── Chart.lock │ │ ├── Chart.yaml │ │ ├── config │ │ │ ├── mysql.url │ │ │ └── parameters.yaml │ │ ├── templates │ │ │ ├── NOTES.txt │ │ │ ├── _helpers.tpl │ │ │ ├── deployment-worker.yaml │ │ │ ├── deployment.yaml │ │ │ ├── hpa.yaml │ │ │ ├── ingress.yaml │ │ │ ├── migrations.yaml │ │ │ ├── secrets │ │ │ │ ├── app-secret.yaml │ │ │ │ ├── jwt.yaml │ │ │ │ ├── mysql.yaml │ │ │ │ └── rabbitmq.yaml │ │ │ └── service.yaml │ │ └── values.yaml │ ├── docker-compose.yml │ └── nginx │ │ └── nginx.conf ├── ci │ ├── docker-compose.yml │ └── mysql │ │ └── custom.cnf ├── dev │ ├── docker-compose.windows.yml │ ├── docker-compose.yml │ ├── kibana │ │ └── config │ └── nginx │ │ └── nginx.conf └── prod │ ├── docker-compose.windows.yml │ ├── docker-compose.yml │ ├── kibana │ └── config │ └── nginx │ └── nginx.conf ├── makefile ├── phpstan.neon ├── phpunit.xml.dist ├── psalm.xml ├── public └── index.php ├── rector.php ├── src ├── App │ ├── Shared │ │ ├── Application │ │ │ ├── Command │ │ │ │ ├── CommandBusInterface.php │ │ │ │ ├── CommandHandlerInterface.php │ │ │ │ └── CommandInterface.php │ │ │ └── Query │ │ │ │ ├── Collection.php │ │ │ │ ├── Event │ │ │ │ └── GetEvents │ │ │ │ │ ├── GetEventsHandler.php │ │ │ │ │ └── GetEventsQuery.php │ │ │ │ ├── Item.php │ │ │ │ ├── QueryBusInterface.php │ │ │ │ ├── QueryHandlerInterface.php │ │ │ │ └── QueryInterface.php │ │ ├── Domain │ │ │ ├── Exception │ │ │ │ └── DateTimeException.php │ │ │ ├── Specification │ │ │ │ └── AbstractSpecification.php │ │ │ └── ValueObject │ │ │ │ └── DateTime.php │ │ └── Infrastructure │ │ │ ├── Bus │ │ │ ├── AsyncEvent │ │ │ │ ├── AsyncEventHandlerInterface.php │ │ │ │ └── MessengerAsyncEventBus.php │ │ │ ├── Command │ │ │ │ └── MessengerCommandBus.php │ │ │ ├── MessageBusExceptionTrait.php │ │ │ └── Query │ │ │ │ └── MessengerQueryBus.php │ │ │ ├── Event │ │ │ ├── Consumer │ │ │ │ └── SendEventsToElasticConsumer.php │ │ │ ├── Publisher │ │ │ │ └── AsyncEventPublisher.php │ │ │ └── ReadModel │ │ │ │ └── ElasticSearchEventRepository.php │ │ │ ├── Kernel.php │ │ │ └── Persistence │ │ │ ├── Doctrine │ │ │ ├── Migrations │ │ │ │ ├── Version20180102233829.php │ │ │ │ └── Version20200727170306.php │ │ │ ├── MigrationsFactory │ │ │ │ └── ContainerAwareFactory.php │ │ │ └── Types │ │ │ │ ├── DateTimeType.php │ │ │ │ ├── EmailType.php │ │ │ │ └── HashedPasswordType.php │ │ │ └── ReadModel │ │ │ ├── Exception │ │ │ └── NotFoundException.php │ │ │ └── Repository │ │ │ ├── ElasticSearchRepository.php │ │ │ └── MysqlRepository.php │ └── User │ │ ├── Application │ │ ├── Command │ │ │ ├── ChangeEmail │ │ │ │ ├── ChangeEmailCommand.php │ │ │ │ └── ChangeEmailHandler.php │ │ │ ├── SignIn │ │ │ │ ├── SignInCommand.php │ │ │ │ └── SignInHandler.php │ │ │ └── SignUp │ │ │ │ ├── SignUpCommand.php │ │ │ │ └── SignUpHandler.php │ │ └── Query │ │ │ ├── Auth │ │ │ ├── GetAuthUserByEmail │ │ │ │ ├── GetAuthUserByEmailHandler.php │ │ │ │ └── GetAuthUserByEmailQuery.php │ │ │ └── GetToken │ │ │ │ ├── GetTokenHandler.php │ │ │ │ └── GetTokenQuery.php │ │ │ └── User │ │ │ └── FindByEmail │ │ │ ├── FindByEmailHandler.php │ │ │ └── FindByEmailQuery.php │ │ ├── Domain │ │ ├── Event │ │ │ ├── UserEmailChanged.php │ │ │ ├── UserSignedIn.php │ │ │ └── UserWasCreated.php │ │ ├── Exception │ │ │ ├── EmailAlreadyExistException.php │ │ │ ├── ForbiddenException.php │ │ │ └── InvalidCredentialsException.php │ │ ├── Repository │ │ │ ├── CheckUserByEmailInterface.php │ │ │ ├── GetUserCredentialsByEmailInterface.php │ │ │ └── UserRepositoryInterface.php │ │ ├── Specification │ │ │ └── UniqueEmailSpecificationInterface.php │ │ ├── User.php │ │ └── ValueObject │ │ │ ├── Auth │ │ │ ├── Credentials.php │ │ │ └── HashedPassword.php │ │ │ └── Email.php │ │ └── Infrastructure │ │ ├── Auth │ │ ├── Auth.php │ │ ├── AuthProvider.php │ │ ├── AuthenticationProvider.php │ │ ├── Guard │ │ │ └── LoginAuthenticator.php │ │ └── PasswordHasher.php │ │ ├── ReadModel │ │ ├── Mysql │ │ │ └── MysqlReadModelUserRepository.php │ │ ├── Projections │ │ │ ├── ConsoleProjectionFactory.php │ │ │ └── UserProjectionFactory.php │ │ └── UserView.php │ │ ├── Repository │ │ └── UserStore.php │ │ └── Specification │ │ └── UniqueEmailSpecification.php └── UI │ ├── Cli │ └── Command │ │ └── CreateUserCommand.php │ └── Http │ ├── Rest │ ├── Controller │ │ ├── Auth │ │ │ └── CheckController.php │ │ ├── CommandController.php │ │ ├── CommandQueryController.php │ │ ├── Event │ │ │ └── GetEventsController.php │ │ ├── Healthz │ │ │ └── HealthzController.php │ │ ├── QueryController.php │ │ └── User │ │ │ ├── GetUserByEmailController.php │ │ │ ├── SignUpController.php │ │ │ └── UserChangeEmailController.php │ ├── EventSubscriber │ │ ├── ExceptionSubscriber.php │ │ └── JsonBodyParserSubscriber.php │ └── Response │ │ └── OpenApi.php │ ├── Session.php │ └── Web │ ├── Controller │ ├── AbstractRenderController.php │ ├── HomeController.php │ ├── ProfileController.php │ ├── SecurityController.php │ └── SignUpController.php │ └── templates │ ├── base.html.twig │ ├── components │ ├── card.html.twig │ └── header │ │ └── menu.html.twig │ ├── home │ └── index.html.twig │ ├── profile │ └── index.html.twig │ ├── signin │ └── login.html.twig │ └── signup │ ├── index.html.twig │ └── user_created.html.twig ├── symfony.lock └── tests ├── App ├── Shared │ ├── Application │ │ ├── ApplicationTestCase.php │ │ └── Query │ │ │ ├── CollectionTest.php │ │ │ └── Event │ │ │ └── GetEvents │ │ │ └── GetEventsTest.php │ ├── Domain │ │ └── ValueObject │ │ │ └── DateTimeTest.php │ └── Infrastructure │ │ ├── Event │ │ ├── EventCollectorListener.php │ │ ├── Publisher │ │ │ └── EventPublisherTest.php │ │ └── Query │ │ │ └── EventElasticRepositoryTest.php │ │ └── Persistence │ │ └── Doctrine │ │ └── DateTimeTypeTest.php └── User │ ├── Application │ ├── Command │ │ ├── ChangeEmail │ │ │ └── ChangeEmailHandlerTest.php │ │ ├── SignIn │ │ │ └── SignInTest.php │ │ └── SignUp │ │ │ └── SignUpHandlerTest.php │ └── Query │ │ └── FindByEmail │ │ └── FindByEmailHandlerTest.php │ └── Domain │ ├── Event │ ├── UserEmailChangedTest.php │ └── UserSignedInTest.php │ ├── UserTest.php │ └── ValueObject │ ├── Auth │ └── HashedPasswordTest.php │ └── EmailTest.php ├── TidierListener.php └── UI ├── Cli ├── AbstractConsoleTestCase.php └── Command │ └── CreateUserCommandTest.php └── Http ├── Rest ├── Controller │ ├── Auth │ │ └── CheckControllerTest.php │ ├── Events │ │ └── GetEventsControllerTest.php │ ├── Healthz │ │ └── HealthzControllerTest.php │ ├── JsonApiTestCase.php │ └── User │ │ ├── ChangeEmailControllerTest.php │ │ ├── GetUserByEmailControllerTest.php │ │ └── SignUpControllerTest.php ├── EventSubscriber │ └── JsonBodyParserSubscriberTest.php └── Response │ └── OpenApiResponseTest.php └── Web └── Controller ├── CreateUserTrait.php ├── HomeControllerTest.php ├── ProfileControllerTest.php ├── SecurityControllerTest.php └── SignUpControllerTest.php /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorge07/symfony-6-es-cqrs-boilerplate/3a1ff15c90b90ac423addb13b51caeff9496283e/.DS_Store -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "Lutacon", 10 | "name": "Luis", 11 | "avatar_url": "https://avatars2.githubusercontent.com/u/2017676?v=4", 12 | "profile": "http://tacon.eu", 13 | "contributions": [ 14 | "code" 15 | ] 16 | }, 17 | { 18 | "login": "cv65kr", 19 | "name": "Kajetan", 20 | "avatar_url": "https://avatars0.githubusercontent.com/u/9404962?v=4", 21 | "profile": "https://github.com/cv65kr", 22 | "contributions": [ 23 | "code" 24 | ] 25 | }, 26 | { 27 | "login": "kowalk", 28 | "name": "Krzysztof Kowalski", 29 | "avatar_url": "https://avatars0.githubusercontent.com/u/2781079?v=4", 30 | "profile": "https://coderslab.pl", 31 | "contributions": [ 32 | "code" 33 | ] 34 | }, 35 | { 36 | "login": "patrykwozinski", 37 | "name": "Patryk Woziński", 38 | "avatar_url": "https://avatars3.githubusercontent.com/u/17459288?v=4", 39 | "profile": "http://patryk.it", 40 | "contributions": [ 41 | "code" 42 | ] 43 | }, 44 | { 45 | "login": "jon-ht", 46 | "name": "jon-ht", 47 | "avatar_url": "https://avatars3.githubusercontent.com/u/17051512?v=4", 48 | "profile": "https://github.com/jon-ht", 49 | "contributions": [ 50 | "code" 51 | ] 52 | } 53 | ], 54 | "contributorsPerLine": 7, 55 | "projectName": "symfony-5-es-cqrs-boilerplate", 56 | "projectOwner": "jorge07", 57 | "repoType": "github", 58 | "repoHost": "https://github.com", 59 | "skipCi": true 60 | } 61 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: mXFIXUnfdHtX1j3aBrUdU3nBeypTdwzOm 2 | coverage_clover: build/logs/clover.xml 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | report/ 2 | doc/ 3 | vendor/ 4 | test/ 5 | etc/* 6 | !etc/artifact/* 7 | .covertalls.yml 8 | .env 9 | .env.local.php 10 | .styleci.yml 11 | README.md 12 | phpstan.neon 13 | docker-compose* 14 | .git/ 15 | .php_cs* 16 | .travis.yml 17 | depfile.yaml 18 | .idea 19 | phpunit.xml.dist 20 | makefile 21 | -------------------------------------------------------------------------------- /.env.dist: -------------------------------------------------------------------------------- 1 | # This file is a "template" of which env vars need to be defined for your application 2 | # Copy this file to .env file for development, create environment variables when deploying to production 3 | # https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration 4 | 5 | ###> symfony/framework-bundle ### 6 | APP_ENV=dev 7 | APP_DEBUG=1 8 | APP_SECRET=c7375619ac26c8671e52279c31c7f157 9 | #TRUSTED_PROXIES=127.0.0.1,127.0.0.2 10 | #TRUSTED_HOSTS=localhost,example.com 11 | ###< symfony/framework-bundle ### 12 | ###> lexik/jwt-authentication-bundle ### 13 | JWT_SECRET_KEY=%kernel.project_dir%/config/packages/jwt/private.pem 14 | JWT_PUBLIC_KEY=%kernel.project_dir%/config/packages/jwt/public.pem 15 | JWT_PASSPHRASE=development 16 | JWT_TTL=604800 17 | ###< lexik/jwt-authentication-bundle ### 18 | 19 | ###> symfony/messenger ### 20 | MESSENGER_TRANSPORT_DSN=amqp://guest:guest@rmq:5672 21 | ###< symfony/messenger ### 22 | ###> doctrine/doctrine-bundle### 23 | DATABASE_URL=mysql://root:api@mysql:3306/api?serverVersion=8.0 24 | ###< doctrine/doctrine-bundle ### -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: pr 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - symfony-6 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | name: Code build, lint and tests 12 | steps: 13 | - uses: actions/checkout@v1 14 | 15 | - name: Platform 16 | run: uname -m 17 | 18 | - name: Build 19 | run: make env=ci start;make env=ci xoff 20 | 21 | - name: DDD LAYERS 22 | run: make env=ci layer 23 | 24 | - name: DB Schema validation 25 | run: make env=ci schema-validate 26 | 27 | - name: Static tools (analyzers) 28 | run: make env=ci cs-check;make env=ci phpstan;make env=ci layer;make env=ci psalm 29 | 30 | - name: Tests 31 | run: make env=ci xon;make env=ci conf="--coverage-clover build/logs/clover.xml" phpunit 32 | 33 | - name: ARTIFACT 34 | run: make env=ci artifact 35 | 36 | - name: Coveralls 37 | run: make env=ci coverage 38 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: push 2 | 3 | on: 4 | push: 5 | branches: 6 | - symfony-6 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | name: Code build, lint and tests 12 | steps: 13 | - uses: actions/checkout@v1 14 | with: 15 | ref: symfony-5 16 | 17 | - name: Reattach HEAD to Head Ref 18 | run: git checkout "$(echo ${{ github.head_ref }} | sed -E 's|refs/[a-zA-Z]+/||')" 19 | if: github.head_ref != '' 20 | 21 | - name: Reattach HEAD to Ref 22 | run: git checkout "$(echo ${{ github.ref }} | sed -E 's|refs/[a-zA-Z]+/||')" 23 | if: github.head_ref == '' 24 | 25 | - name: Build 26 | run: make env=ci start;make env=ci xoff 27 | 28 | - name: DDD LAYERS 29 | run: make env=ci layer 30 | 31 | - name: DB Schema validation 32 | run: make env=ci schema-validate 33 | 34 | - name: Static tools (analyzers) 35 | run: make env=ci cs-check;make env=ci phpstan;make env=ci layer;make env=ci psalm 36 | 37 | - name: Tests 38 | run: make env=ci xon;make env=ci conf="--coverage-clover build/logs/clover.xml" phpunit 39 | 40 | - name: ARTIFACT 41 | run: make env=ci artifact 42 | 43 | - name: Coveralls 44 | run: make env=ci coverage 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ###> symfony/framework-bundle ### 2 | .env 3 | .env.blackfire 4 | /public/bundles/ 5 | /var/ 6 | /vendor/ 7 | ###< symfony/framework-bundle ### 8 | 9 | /.idea/ 10 | .deptrac.cache 11 | 12 | ###> symfony/phpunit-bridge ### 13 | .phpunit 14 | /phpunit.xml 15 | ###< symfony/phpunit-bridge ### 16 | 17 | ###> phpunit/phpunit ### 18 | /phpunit.xml 19 | ###< phpunit/phpunit ### 20 | 21 | /report/ 22 | build/ 23 | 24 | ###> friendsofphp/php-cs-fixer ### 25 | /.php_cs 26 | /.php_cs.cache 27 | /.phpunit.result.cache 28 | ###< friendsofphp/php-cs-fixer ### 29 | etc/artifact/chart/charts/ -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: symfony 2 | 3 | enabled: 4 | - concat_with_spaces 5 | - align_double_arrow 6 | 7 | disabled: 8 | - concat_without_spaces 9 | - unalign_double_arrow -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Listen for XDebug", 9 | "type": "php", 10 | "request": "launch", 11 | "port": 9003, 12 | "pathMappings": { 13 | "/app": "${workspaceRoot}" 14 | }, 15 | "xdebugSettings": { 16 | "max_data": 65535, 17 | "show_hidden": 1, 18 | "max_children": 100, 19 | "max_depth": 5 20 | } 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jorge Arco 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 | load(__DIR__.'/../.env'); 23 | } 24 | 25 | $input = new ArgvInput(); 26 | $env = $input->getParameterOption(['--env', '-e'], $_SERVER['APP_ENV'] ?? 'dev'); 27 | $debug = ($_SERVER['APP_DEBUG'] ?? ('prod' !== $env)) && !$input->hasParameterOption(['--no-debug', '']); 28 | 29 | if ($debug) { 30 | umask(0000); 31 | 32 | if (class_exists(Debug::class)) { 33 | Debug::enable(); 34 | } 35 | } 36 | 37 | $kernel = new Kernel($env, $debug); 38 | $application = new Application($kernel); 39 | $application->run($input); 40 | -------------------------------------------------------------------------------- /bin/deptrac.phar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorge07/symfony-6-es-cqrs-boilerplate/3a1ff15c90b90ac423addb13b51caeff9496283e/bin/deptrac.phar -------------------------------------------------------------------------------- /config/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 5 | Broadway\Bundle\BroadwayBundle\BroadwayBundle::class => ['all' => true], 6 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 7 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 8 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], 9 | Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], 10 | DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true], 11 | Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], 12 | Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true], 13 | Nelmio\ApiDocBundle\NelmioApiDocBundle::class => ['all' => true], 14 | Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], 15 | ]; 16 | -------------------------------------------------------------------------------- /config/packages/broadway.yaml: -------------------------------------------------------------------------------- 1 | broadway: 2 | event_store: 'Broadway\EventStore\Dbal\DBALEventStore' 3 | -------------------------------------------------------------------------------- /config/packages/dev/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | channels: ['elasticsearch'] 3 | handlers: 4 | elasticsearch: 5 | type: stream 6 | path: "%kernel.logs_dir%/elasticsearch_%kernel.environment%.log" 7 | level: debug 8 | channels: ["elasticsearch"] 9 | main: 10 | type: stream 11 | path: "%kernel.logs_dir%/%kernel.environment%.log" 12 | level: debug 13 | channels: ["!event", "!elasticsearch"] 14 | # uncomment to get logging in your browser 15 | # you may have to allow bigger header sizes in your Web server configuration 16 | #firephp: 17 | # type: firephp 18 | # level: info 19 | #chromephp: 20 | # type: chromephp 21 | # level: info 22 | console: 23 | type: console 24 | process_psr_3_messages: false 25 | channels: ["!event", "!doctrine", "!console"] 26 | -------------------------------------------------------------------------------- /config/packages/dev/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: true 4 | -------------------------------------------------------------------------------- /config/packages/dev/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler: 2 | toolbar: true 3 | intercept_redirects: false 4 | 5 | framework: 6 | profiler: { only_exceptions: false } 7 | -------------------------------------------------------------------------------- /config/packages/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | dbal: 3 | schema_filter: ~^(?!event)~ # this will ignore broadway event store table 4 | url: '%env(resolve:DATABASE_URL)%' 5 | driver: pdo_mysql 6 | charset: utf8mb4 7 | server_version: '8.0.33' 8 | types: 9 | uuid_binary: Ramsey\Uuid\Doctrine\UuidBinaryType 10 | datetime_immutable: App\Shared\Infrastructure\Persistence\Doctrine\Types\DateTimeType 11 | email: App\Shared\Infrastructure\Persistence\Doctrine\Types\EmailType 12 | hashed_password: App\Shared\Infrastructure\Persistence\Doctrine\Types\HashedPasswordType 13 | mapping_types: 14 | uuid_binary: binary 15 | orm: 16 | auto_generate_proxy_classes: '%kernel.debug%' 17 | naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware 18 | auto_mapping: true 19 | mappings: 20 | User: 21 | is_bundle: false 22 | type: xml 23 | dir: '%kernel.project_dir%/config/packages/doctrine/mapping/user' 24 | prefix: 'App\User' 25 | alias: App 26 | -------------------------------------------------------------------------------- /config/packages/doctrine/mapping/user/Domain.ValueObject.Auth.Credentials.orm.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /config/packages/doctrine/mapping/user/Infrastructure.ReadModel.UserView.orm.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /config/packages/doctrine_migrations.yaml: -------------------------------------------------------------------------------- 1 | doctrine_migrations: 2 | services: 3 | Doctrine\Migrations\Version\MigrationFactory: 'App\Shared\Infrastructure\Persistence\Doctrine\MigrationsFactory\ContainerAwareFactory' 4 | migrations_paths: 5 | App\Shared\Infrastructure\Persistence\Doctrine\Migrations: '%kernel.project_dir%/src/App/Shared/Infrastructure/Persistence/Doctrine/Migrations' 6 | -------------------------------------------------------------------------------- /config/packages/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | secret: '%env(APP_SECRET)%' 3 | default_locale: en 4 | csrf_protection: true 5 | http_method_override: false 6 | 7 | # Enables session support. Note that the session will ONLY be started if you read or write from it. 8 | # Remove or comment this section to explicitly disable session support. 9 | session: 10 | handler_id: null 11 | cookie_secure: auto 12 | cookie_samesite: lax 13 | storage_factory_id: session.storage.factory.native 14 | esi: true 15 | fragments: true 16 | php_errors: 17 | log: true 18 | router: 19 | strict_requirements: ~ 20 | utf8: true 21 | -------------------------------------------------------------------------------- /config/packages/framework_extra.yml: -------------------------------------------------------------------------------- 1 | sensio_framework_extra: 2 | router: 3 | annotations: false -------------------------------------------------------------------------------- /config/packages/jwt/public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArBmtoOEVG2GQ9ttdVxhc 3 | ZbLPrUADHQv1xVqV5H61mVcgfbtoRGdFUFuHD4DVopuSR2FhXMDRz0ukOTgwODOd 4 | ri6+aG2+3Dir1oS4VS63OmvCE+ylS9+EbgJkAJMGRM1/pDg61l89+Xc7Wab2/Io3 5 | Q0o39obFVPLnvOSAKng7Ekm3+fC8bRH3EvN2eYaFmvcxn97gEtym+8x+8PXYIICw 6 | WGRp0Tt62ldNhoWkWiY7c1c4+M3A9gIasoNYyxSU7v2oze+MIStWtkDnzhVtYwDf 7 | 3IOVvGxEBAzqYQ9ICI2eUQjf8edcbTIpkI4wBJ1Iz1wi9WJD+c5OP/RMrcKPyO2p 8 | WeHuGbhasevkBeJi1fGClhjb/FwswGeEpcIVN59dafsPJ6U2Oj/RQJQQydEgnNfl 9 | +rxCd/yIl3/Mo0gORL7j2WBPND5k/kVeEFAAA5S6KvwDfvVCJ7ka8IuAsZd5QZCC 10 | OzhpRjCT4JgBa9lJS/ShGguE3/0A0JnqpaqbB+u3qMzRktnVqjTW0htjnmsCB8UH 11 | JTqqSKRUAygJ9MbxQ+o252xEg+S0VHLDHA79ONiOVRZczbVtVjvZLLIVmeJYXTSc 12 | dq97Dy43x+mYSs+RL74Xg+vf4LGoEgBEV++DBg1ELonDoFwdVJwVqUgRk7awf8ZT 13 | gZV/5L0JJAEb4srJ9VrDntkCAwEAAQ== 14 | -----END PUBLIC KEY----- 15 | -------------------------------------------------------------------------------- /config/packages/lexik_jwt_authentication.yaml: -------------------------------------------------------------------------------- 1 | lexik_jwt_authentication: 2 | secret_key: '%env(resolve:JWT_SECRET_KEY)%' 3 | public_key: '%env(resolve:JWT_PUBLIC_KEY)%' 4 | pass_phrase: '%env(JWT_PASSPHRASE)%' 5 | token_ttl: '%env(JWT_TTL)%' -------------------------------------------------------------------------------- /config/packages/messenger.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | messenger: 3 | default_bus: messenger.bus.command 4 | failure_transport: failed 5 | buses: 6 | messenger.bus.command: 7 | default_middleware: false 8 | middleware: 9 | - handle_message 10 | 11 | messenger.bus.query: 12 | default_middleware: false 13 | middleware: 14 | - handle_message 15 | 16 | messenger.bus.event.async: 17 | default_middleware: allow_no_handlers 18 | 19 | transports: 20 | events: 21 | dsn: "%env(MESSENGER_TRANSPORT_DSN)%" 22 | retry_strategy: 23 | delay: 2000 24 | max_retries: 5 25 | multiplier: 2 26 | max_delay: 0 27 | options: 28 | exchange: 29 | type: topic 30 | name: events 31 | queues: 32 | events: 33 | binding_keys: ['#'] 34 | 35 | users: 36 | dsn: "%env(MESSENGER_TRANSPORT_DSN)%" 37 | retry_strategy: 38 | delay: 2000 39 | max_retries: 5 40 | multiplier: 2 41 | max_delay: 0 42 | options: 43 | exchange: 44 | type: topic 45 | name: events 46 | queues: 47 | users: 48 | binding_keys: ['#.User.#'] 49 | failed: 50 | dsn: "%env(MESSENGER_TRANSPORT_DSN)%" 51 | options: 52 | queues: 53 | failed: 54 | binding_keys: ['#'] 55 | 56 | routing: 57 | 'Broadway\Domain\DomainMessage': events 58 | -------------------------------------------------------------------------------- /config/packages/prod/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | orm: 3 | metadata_cache_driver: 4 | type: pool 5 | pool: doctrine.system_cache_pool 6 | query_cache_driver: 7 | type: pool 8 | pool: doctrine.system_cache_pool 9 | result_cache_driver: 10 | type: pool 11 | pool: doctrine.result_cache_pool 12 | 13 | framework: 14 | cache: 15 | pools: 16 | doctrine.result_cache_pool: 17 | adapter: cache.app 18 | doctrine.system_cache_pool: 19 | adapter: cache.system 20 | -------------------------------------------------------------------------------- /config/packages/prod/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: fingers_crossed 5 | action_level: error 6 | handler: nested 7 | excluded_http_codes: [404, 405] 8 | buffer_size: 50 9 | nested: 10 | type: stream 11 | path: "php://stdout" 12 | level: debug 13 | formatter: monolog.formatter.json 14 | console: 15 | type: console 16 | process_psr_3_messages: false 17 | channels: ["!event", "!doctrine"] 18 | -------------------------------------------------------------------------------- /config/packages/security.yaml: -------------------------------------------------------------------------------- 1 | security: 2 | password_hashers: 3 | hasher: 4 | id: App\User\Infrastructure\Auth\PasswordHasher 5 | 6 | providers: 7 | users: 8 | id: 'App\User\Infrastructure\Auth\AuthProvider' 9 | 10 | firewalls: 11 | dev: 12 | pattern: ^/(_(profiler|wdt)|css|images|js)/ 13 | security: false 14 | 15 | api_healthz: 16 | pattern: ^/api/healthz 17 | stateless: true 18 | 19 | api_doc: 20 | pattern: ^/api/doc 21 | stateless: true 22 | 23 | api_auth: 24 | pattern: ^/api/auth 25 | stateless: true 26 | 27 | api_signup: 28 | pattern: ^/api/signup 29 | stateless: true 30 | 31 | api_secured: 32 | pattern: ^/api 33 | provider: users 34 | stateless: true 35 | jwt: ~ 36 | 37 | secured_area: 38 | pattern: ^/ 39 | provider: users 40 | custom_authenticators: 41 | - 'App\User\Infrastructure\Auth\Guard\LoginAuthenticator' 42 | form_login: 43 | login_path: /sign-in 44 | check_path: sign-in 45 | entry_point: form_login 46 | logout: 47 | path: /logout 48 | target: / 49 | 50 | access_control: 51 | - { path: ^/profile, roles: ROLE_USER } 52 | - { path: ^/api/healthz, roles: PUBLIC_ACCESS } 53 | - { path: ^/api/auth, roles: PUBLIC_ACCESS } 54 | - { path: ^/api/signup, roles: PUBLIC_ACCESS } 55 | - { path: ^/api/doc, roles: PUBLIC_ACCESS } 56 | - { path: ^/api/, roles: IS_AUTHENTICATED_FULLY } 57 | - { path: ^/, roles: PUBLIC_ACCESS } 58 | -------------------------------------------------------------------------------- /config/packages/test/dama_doctrine_test_bundle.yaml: -------------------------------------------------------------------------------- 1 | dama_doctrine_test: 2 | enable_static_connection: true 3 | enable_static_meta_data_cache: true 4 | enable_static_query_cache: true 5 | -------------------------------------------------------------------------------- /config/packages/test/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | test: true 3 | session: 4 | storage_factory_id: session.storage.factory.mock_file 5 | messenger: 6 | transports: 7 | events: 'in-memory://' 8 | users: 'in-memory://' 9 | failed: 'in-memory://' -------------------------------------------------------------------------------- /config/packages/test/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: fingers_crossed 5 | action_level: error 6 | handler: nested 7 | excluded_http_codes: [404, 405] 8 | channels: ["!event"] 9 | nested: 10 | type: stream 11 | path: "%kernel.logs_dir%/%kernel.environment%.log" 12 | level: debug 13 | -------------------------------------------------------------------------------- /config/packages/test/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler: 2 | toolbar: false 3 | intercept_redirects: false 4 | 5 | framework: 6 | profiler: { collect: false } 7 | -------------------------------------------------------------------------------- /config/packages/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | paths: ['%kernel.project_dir%/src/UI/Http/Web/templates'] 3 | debug: '%kernel.debug%' 4 | strict_variables: '%kernel.debug%' -------------------------------------------------------------------------------- /config/routes.yaml: -------------------------------------------------------------------------------- 1 | api_controllers: 2 | resource: '../src/UI/Http/Rest/Controller/' 3 | type: annotation 4 | prefix: /api 5 | 6 | web_controllers: 7 | resource: '../src/UI/Http/Web/Controller/' 8 | type: annotation 9 | -------------------------------------------------------------------------------- /config/routes/dev/twig.yaml: -------------------------------------------------------------------------------- 1 | _errors: 2 | resource: '@FrameworkBundle/Resources/config/routing/errors.xml' 3 | prefix: /_error 4 | -------------------------------------------------------------------------------- /config/routes/dev/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler_wdt: 2 | resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' 3 | prefix: /_wdt 4 | 5 | web_profiler_profiler: 6 | resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml' 7 | prefix: /_profiler 8 | -------------------------------------------------------------------------------- /config/routes/nelmio_api_doc.yaml: -------------------------------------------------------------------------------- 1 | # Expose your documentation as JSON swagger compliant 2 | app.swagger: 3 | path: /api/doc.json 4 | methods: GET 5 | defaults: { _controller: nelmio_api_doc.controller.swagger } 6 | 7 | ## Requires the Asset component and the Twig bundle 8 | ## $ composer require twig asset 9 | app.swagger_ui: 10 | path: /api/doc 11 | methods: GET 12 | defaults: { _controller: nelmio_api_doc.controller.swagger_ui } 13 | -------------------------------------------------------------------------------- /config/services_test.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | _defaults: {} 3 | 4 | Tests\App\Shared\Infrastructure\Event\EventCollectorListener: 5 | public: true 6 | tags: 7 | - { name: broadway.domain.event_listener } 8 | -------------------------------------------------------------------------------- /deptrac.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | formatters: 3 | graphviz: 4 | groups: 5 | App: 6 | - Application 7 | - Domain 8 | - Infrastructure 9 | - Auth 10 | - UI 11 | - UIHealth 12 | paths: 13 | - ./src 14 | exclude_files: 15 | layers: 16 | - name: Domain 17 | collectors: 18 | - type: className 19 | regex: .*\\Domain\\.* 20 | - name: Application 21 | collectors: 22 | - type: className 23 | regex: .*Application\\.* 24 | - name: Infrastructure 25 | collectors: 26 | - type: bool 27 | must: 28 | - type: className 29 | regex: .*Infrastructure\\.* 30 | must_not: 31 | - type: className 32 | regex: .*User\\Infrastructure\\Auth\\Auth 33 | - name: Auth 34 | collectors: 35 | - type: className 36 | regex: .*User\\Infrastructure\\Auth\\Auth; 37 | - name: UI 38 | collectors: 39 | - type: bool 40 | must: 41 | - type: className 42 | regex: UI\\.* 43 | must_not: 44 | - type: className 45 | regex: UI\\Http\\Rest\\Controller\\Healthz\\.* 46 | - name: UIHealth 47 | collectors: 48 | - type: className 49 | regex: UI\\Http\\Rest\\Controller\\Healthz\\.* 50 | ruleset: 51 | Domain: 52 | Auth: 53 | - Domain 54 | - Infrastructure 55 | Application: 56 | - Domain 57 | - Infrastructure 58 | - Auth 59 | Infrastructure: 60 | - Domain 61 | - Application 62 | - Auth 63 | UI: 64 | - Auth 65 | - Domain 66 | - Application 67 | UIHealth: # Allow access to infra for health checks 68 | - Infrastructure 69 | - UI 70 | -------------------------------------------------------------------------------- /doc/Deployment.md: -------------------------------------------------------------------------------- 1 | ## Kubernetes Deployment 2 | 3 | `make minikube` 4 | 5 | ### Update 6 | 7 | `helm upgrade cqrs -f {YOUR CUSTOM YAML FILE} etc/deploy/chart` 8 | 9 | ### Recommendations 10 | 11 | - Use your own chart registry. i.e: https://github.com/helm/chartmuseum 12 | - Use separated `values.yaml` per environment and concat in deployments. i.e: `helm upgrade cqrs -f production.yaml etc/deploy/chart` 13 | - Use helm secrets plugin: https://github.com/futuresimple/helm-secrets 14 | -------------------------------------------------------------------------------- /doc/GetStarted/Buses.md: -------------------------------------------------------------------------------- 1 | # Command Bus, Query Bus and Async Event Bus 2 | 3 | ### Symfony Messenger Component 4 | 5 | [Symfony Messenger](https://symfony.com/doc/current/messenger.html) is what we use to distribute messages synchronous and asynchronously. 6 | 7 | We've 3 different type of bus: 8 | 9 | - Command: `public function handle(CommandInterface $command): void` 10 | - Query: `public function handle(QueryInterface $query): Item|Collection|string|int|null` 11 | - Async Event: `public function handle(EventInterface $event): void` 12 | 13 | To define a new use case just implement the required interfaces. 14 | 15 | Use `./bin/console debug:messenger` to check the configuration. 16 | -------------------------------------------------------------------------------- /doc/GetStarted/UseCases.md: -------------------------------------------------------------------------------- 1 | # Creating a new Use Case 2 | 3 | A use case represents an action in the system. These use cases orchestrate the flow of data to and from the entities, and direct those entities to use their enterprise wide business rules to achieve the goals of the use case. 4 | It can be a mutation of the state or a query but not both in a CQRS project. 5 | 6 | Let's create a Use Case that just do: `echo "LOOL"`. 7 | 8 | ### The Command 9 | 10 | ```php 11 | setName('app:echo') 68 | ->setDescription('just an echo') 69 | ; 70 | } 71 | 72 | protected function execute(InputInterface $input, OutputInterface $output) 73 | { 74 | $echoCommand = new EchoCommand(); 75 | $this->commandBus->handle($echoCommand); 76 | } 77 | 78 | public function __construct(MessengerCommandBus $commandBus) 79 | { 80 | parent::__construct(); 81 | $this->commandBus = $commandBus; 82 | } 83 | 84 | /** 85 | * @var MessengerCommandBus 86 | */ 87 | private $commandBus; 88 | } 89 | ``` 90 | 91 | ### Let's test it 92 | 93 | Enter in the docker container: 94 | 95 | `docker-compose exec php sh -l` 96 | 97 | Execute: 98 | 99 | `./bin/console app:echo` 100 | 101 | You should see: `LOOL` 102 | 103 | And that's all with 0 config thanks to Symfony 4|5! 104 | -------------------------------------------------------------------------------- /doc/GetStarted/Xdebug.md: -------------------------------------------------------------------------------- 1 | # Using Xdebug 2 | 3 | ### Requirements: 4 | 5 | - `docker` 6 | - `docker-compose` 7 | 8 | ### Recommendations: 9 | 10 | - When in OSX, due to heavy performance issues with docker4mac I **strongly** recommend [dinghy](https://github.com/codekitchen/dinghy). 11 | 12 | ## Setup 13 | 14 | - Start the project using `make start`. It will start the containers and setup the project. You'll end seeing something like that: 15 | 16 | ```bash 17 | ➜ docker-compose ps 18 | Name Command State Ports 19 | --------------------------------------------------------------------------------------------------------------------------------------------------------- 20 | api_elasticsearch_1 /usr/local/bin/docker-entr ... Up 0.0.0.0:9200->9200/tcp, 9300/tcp 21 | api_kibana_1 /usr/local/bin/kibana-docker Up 0.0.0.0:5601->5601/tcp 22 | api_mysql_1 docker-entrypoint.sh mysqld Up 0.0.0.0:3306->3306/tcp 23 | api_nginx_1 nginx -g daemon off; Up 443/tcp, 0.0.0.0:80->80/tcp 24 | api_php_1 /sbin/tini -- supervisord ... Up 0.0.0.0:2323->22/tcp, 9000/tcp 25 | api_rmq_1 docker-entrypoint.sh rabbi ... Up 15671/tcp, 0.0.0.0:15672->15672/tcp, 25672/tcp, 4369/tcp, 5671/tcp, 0.0.0.0:5672->5672/tcp 26 | api_workers_1 /sbin/tini -- /app/bin/con ... Up 22/tcp, 9000/tcp 27 | ``` 28 | - The php container for development has ssh installed and forwarding the `2323` port. We'll use this to connects to the container as a remote interpreter in our favourite IDE. In PHPStorm, i.e., go to `Languajes & Frameworks > PHP` and configure the connection to have something like: 29 | ![debugger](https://i.imgur.com/oTXsPlZ.png) 30 | - Next you'll need to configure phpunit in your IDE to have something like: 31 | ![phpunit](https://i.imgur.com/AzFTN9k.png) 32 | - Now you'll be able to run any php test file or phpunit config. 33 | ![run test](https://i.imgur.com/PCYXr1U.png) 34 | -------------------------------------------------------------------------------- /doc/Workflow.md: -------------------------------------------------------------------------------- 1 | ## Workflow 2 | 3 | **Given** An annon user 4 | 5 | **When** enters in homepage 6 | 7 | **Then** should be able to see the Sign Up button 8 | 9 | ![Home](https://imgur.com/ykxHf1d.png) 10 | 11 | **When** user click in Sign Up button 12 | 13 | **Then** should be redirected to Sign Up page 14 | 15 | ![Sign up](https://imgur.com/qZs8iIP.png) 16 | 17 | 18 | **Given** a user in Sign Up page 19 | 20 | **When** enter in the form with a valid email and password 21 | 22 | ![invalid email](https://imgur.com/w9Z1w8d.png) 23 | 24 | **Then** it should be registered 25 | 26 | **And** display de user information 27 | 28 | ![signed up](https://imgur.com/XbRtfUh.png) 29 | 30 | **Then** when user enter in Sign In page should be able to Sign In 31 | 32 | **And** open a new session 33 | 34 | ![Signed in](https://imgur.com/ZjmTDYU.png) 35 | 36 | **Given** All user events happened in UI 37 | 38 |  **And** published in rabbit 39 | 40 | ![rmq](https://imgur.com/XobqV9j.png) 41 | 42 |  **Then** it should be consumed to be stored in elastic 43 | 44 | **And** visible in Kibana 45 | 46 | ![kibana](https://imgur.com/VMRLSDJ.png) 47 | -------------------------------------------------------------------------------- /doc/docker-php-interpreter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorge07/symfony-6-es-cqrs-boilerplate/3a1ff15c90b90ac423addb13b51caeff9496283e/doc/docker-php-interpreter.png -------------------------------------------------------------------------------- /doc/xdebug-activation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorge07/symfony-6-es-cqrs-boilerplate/3a1ff15c90b90ac423addb13b51caeff9496283e/doc/xdebug-activation.png -------------------------------------------------------------------------------- /doc/xdebug-mapping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorge07/symfony-6-es-cqrs-boilerplate/3a1ff15c90b90ac423addb13b51caeff9496283e/doc/xdebug-mapping.png -------------------------------------------------------------------------------- /docker-compose.ci.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorge07/symfony-6-es-cqrs-boilerplate/3a1ff15c90b90ac423addb13b51caeff9496283e/docker-compose.ci.yml -------------------------------------------------------------------------------- /easy-coding-standard.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: 'vendor/sylius-labs/coding-standard/easy-coding-standard.yml' } 3 | 4 | services: 5 | PhpCsFixer\Fixer\FunctionNotation\NativeFunctionInvocationFixer: ~ 6 | 7 | parameters: 8 | exclude_files: 9 | - '**/var/*' -------------------------------------------------------------------------------- /etc/artifact/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PHP_VERSION=8.2.11 2 | 3 | 4 | FROM jorge07/alpine-php:${PHP_VERSION} as php-base 5 | # List your dependencies here 6 | ARG PHP_MODULES="php82-pdo php82-pdo_mysql php82-mysqlnd php82-pecl-amqp php82-tokenizer php82-posix php82-simplexml php82-xmlwriter" 7 | RUN apk add -U --no-cache ${PHP_MODULES} 8 | 9 | FROM jorge07/alpine-php:${PHP_VERSION}-dev as php-dev 10 | 11 | ARG PHP_MODULES="php82-pdo php82-pdo_mysql php82-mysqlnd php82-pecl-amqp php82-tokenizer php82-posix php82-simplexml php82-xmlwriter" 12 | ARG PHP_DEV_MODULES="" 13 | 14 | RUN apk add -U --no-cache ${PHP_MODULES} ${PHP_DEV_MODULES} 15 | 16 | ENV PHP_INI_DIR /etc/php82 17 | 18 | RUN apk add -U sqlite \ 19 | && version=$(php -r "echo PHP_MAJOR_VERSION.PHP_MINOR_VERSION;") \ 20 | && curl -A "Docker" -o /tmp/blackfire-probe.tar.gz -D - -L -s https://blackfire.io/api/v1/releases/probe/php/alpine/amd64/$version \ 21 | && mkdir -p /tmp/blackfire \ 22 | && tar zxpf /tmp/blackfire-probe.tar.gz -C /tmp/blackfire \ 23 | && mv /tmp/blackfire/blackfire-*.so $(php -r "echo ini_get ('extension_dir');")/blackfire.so \ 24 | && printf "extension=blackfire.so\nblackfire.agent_socket=tcp://blackfire:8707\n" > $PHP_INI_DIR/conf.d/blackfire.ini \ 25 | && rm -rf /tmp/blackfire /tmp/blackfire-probe.tar.gz \ 26 | && mkdir -p /tmp/blackfire \ 27 | && curl -A "Docker" -L https://blackfire.io/api/v1/releases/client/linux_static/amd64 | tar zxp -C /tmp/blackfire \ 28 | && mv /tmp/blackfire/blackfire /usr/bin/blackfire \ 29 | && rm -Rf /tmp/blackfire 30 | 31 | FROM php-dev as builder 32 | 33 | WORKDIR /app 34 | 35 | ENV APP_ENV prod 36 | ENV APP_SECRET default-secret 37 | 38 | COPY composer.json /app 39 | COPY composer.lock /app 40 | COPY symfony.lock /app 41 | 42 | RUN composer install --no-ansi --no-scripts --no-dev --no-interaction --no-progress --optimize-autoloader 43 | 44 | COPY bin /app/bin 45 | COPY config /app/config 46 | COPY src /app/src 47 | COPY public /app/public 48 | 49 | RUN composer run-script post-install-cmd 50 | 51 | FROM php-base as php 52 | 53 | ENV APP_ENV prod 54 | 55 | WORKDIR /app 56 | 57 | COPY --from=builder /app /app 58 | 59 | FROM nginx:1.17-alpine as nginx 60 | 61 | ENV APP_ENV prod 62 | 63 | WORKDIR /app 64 | 65 | COPY etc/artifact/nginx/nginx.conf /etc/nginx/conf.d/default.conf 66 | 67 | COPY --from=builder /app/public /app/public 68 | -------------------------------------------------------------------------------- /etc/artifact/chart/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .tmpcharts/ -------------------------------------------------------------------------------- /etc/artifact/chart/Chart.lock: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - name: rabbitmq 3 | repository: https://charts.bitnami.com/bitnami 4 | version: 6.25.2 5 | - name: traefik 6 | repository: https://kubernetes-charts.storage.googleapis.com/ 7 | version: 1.86.2 8 | - name: mysql 9 | repository: https://kubernetes-charts.storage.googleapis.com/ 10 | version: 1.6.3 11 | - name: elasticsearch 12 | repository: https://helm.elastic.co 13 | version: 7.6.2 14 | digest: sha256:4036a27f33dad876c6e4a0628e6d00b8d7746dba9bc406561d4fe399ff769ae6 15 | generated: "2020-05-08T11:19:24.259329+02:00" 16 | -------------------------------------------------------------------------------- /etc/artifact/chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | appVersion: "1.0" 3 | description: DDD CQRS boilerplate 4 | name: symfony-5-es-cqrs-boilerplate 5 | version: 0.1.0 6 | type: application 7 | dependencies: 8 | - name: rabbitmq 9 | version: 6.25.2 10 | repository: https://charts.bitnami.com/bitnami 11 | conditions: rabbitmq.selfHosted 12 | - name: traefik 13 | version: 1.86.2 14 | repository: https://kubernetes-charts.storage.googleapis.com/ 15 | conditions: traefik.selfHosted 16 | - name: mysql 17 | version: 1.6.3 18 | repository: https://kubernetes-charts.storage.googleapis.com/ 19 | conditions: mysql.selfHosted 20 | - name: elasticsearch 21 | version: 7.6.2 22 | repository: https://helm.elastic.co 23 | conditions: elasticsearch.selfHosted -------------------------------------------------------------------------------- /etc/artifact/chart/config/mysql.url: -------------------------------------------------------------------------------- 1 | mysql://{{ $.Values.parameters.mysql.user }}:{{ $.Values.parameters.mysql.pass }}@{{ $.Values.parameters.mysql.host }}:{{ $.Values.parameters.mysql.port }}/{{ $.Values.parameters.mysql.database }}?serverVersion=8.0 -------------------------------------------------------------------------------- /etc/artifact/chart/config/parameters.yaml: -------------------------------------------------------------------------------- 1 | - name: APP_ENV 2 | value: {{ .Values.parameters.app.env | quote }} 3 | - name: APP_SECRET 4 | valueFrom: 5 | secretKeyRef: 6 | name: {{ $.Chart.Name }}-secret 7 | key: secret 8 | {{- if $.Values.parameters.app.proxy }} 9 | - name: TRUSTED_PROXIES 10 | value: {{ $.Values.parameters.app.proxy | quote }} 11 | {{- end }} 12 | {{- if $.Values.parameters.app.hosts }} 13 | - name: TRUSTED_HOSTS 14 | value: {{ $.Values.parameters.app.hosts | quote }} 15 | {{- end }} 16 | - name: MESSENGER_TRANSPORT_DSN 17 | valueFrom: 18 | secretKeyRef: 19 | name: {{ $.Chart.Name }}-rabbitmq 20 | key: host 21 | - name: DATABASE_URL 22 | valueFrom: 23 | secretKeyRef: 24 | name: {{ $.Chart.Name }}-mysql 25 | key: url 26 | - name: JWT_SECRET_KEY 27 | value: {{ $.Values.parameters.jwt.hostPath }}/private.pem 28 | - name: JWT_PUBLIC_KEY 29 | value: {{ $.Values.parameters.jwt.hostPath }}/public.pem 30 | - name: JWT_PASSPHRASE 31 | valueFrom: 32 | secretKeyRef: 33 | name: {{ $.Chart.Name }}-jwt 34 | key: passphrase 35 | - name: JWT_TTL 36 | value: {{ $.Values.parameters.jwt.ttl | quote }} 37 | - name: ELASTIC_HOST 38 | value: {{- if $.Values.elasticsearch.selfHosted }} {{ template "elasticHost" . }} {{- else }} {{ $.Values.parameters.elastic.host | quote }} {{- end }} -------------------------------------------------------------------------------- /etc/artifact/chart/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range .Values.ingress.hosts }} 4 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }} 5 | {{- end }} 6 | {{- else if contains "NodePort" .Values.service.type }} 7 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "chart.fullname" . }}) 8 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 9 | echo http://$NODE_IP:$NODE_PORT 10 | {{- else if contains "LoadBalancer" .Values.service.type }} 11 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 12 | You can watch the status of by running 'kubectl get svc -w {{ include "chart.fullname" . }}' 13 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "chart.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') 14 | echo http://$SERVICE_IP:{{ .Values.service.port }} 15 | {{- else if contains "ClusterIP" .Values.service.type }} 16 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ include "chart.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 17 | echo "Visit http://127.0.0.1:8080 to use your application" 18 | kubectl port-forward $POD_NAME 8080:80 19 | {{- end }} 20 | -------------------------------------------------------------------------------- /etc/artifact/chart/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "chart.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "chart.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "chart.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | 34 | {{/* 35 | Rabbitmq self hosted hostname 36 | */}} 37 | {{- define "rabbitHost" -}} 38 | {{- printf "amqp://%s:%s@%s-rabbitmq:5672" .Values.rabbitmq.rabbitmq.username .Values.rabbitmq.rabbitmq.password .Chart.Name | b64enc | quote -}} 39 | {{- end -}} 40 | 41 | {{/* 42 | MySQL self hosted hostname 43 | */}} 44 | {{- define "mysqlHost" -}} 45 | {{- printf "%s-mysql" .Chart.Name | b64enc | quote -}} 46 | {{- end -}} 47 | 48 | {{/* 49 | Elastic self hosted hostname 50 | */}} 51 | {{- define "elasticHost" -}} 52 | {{- printf "%s-elasticsearch-client:9200" .Chart.Name }} 53 | {{- end -}} 54 | -------------------------------------------------------------------------------- /etc/artifact/chart/templates/deployment-worker.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "chart.fullname" . }}-workers 5 | labels: 6 | app: {{ include "chart.name" . }}-workers 7 | chart: {{ include "chart.chart" . }} 8 | release: {{ .Release.Name }} 9 | heritage: {{ .Release.Service }} 10 | annotations: 11 | checksum/secrets-rmq: {{ include (print $.Template.BasePath "/secrets/rabbitmq.yaml") . | sha256sum }} 12 | checksum/secrets-app: {{ include (print $.Template.BasePath "/secrets/app-secret.yaml") . | sha256sum }} 13 | checksum/secrets-jwt: {{ include (print $.Template.BasePath "/secrets/jwt.yaml") . | sha256sum }} 14 | checksum/secrets-mysql: {{ include (print $.Template.BasePath "/secrets/mysql.yaml") . | sha256sum }} 15 | spec: 16 | replicas: {{ .Values.replicaCount }} 17 | selector: 18 | matchLabels: 19 | app: {{ include "chart.name" . }}-workers 20 | release: {{ .Release.Name }} 21 | template: 22 | metadata: 23 | labels: 24 | app: {{ include "chart.name" . }}-workers 25 | release: {{ .Release.Name }} 26 | spec: 27 | volumes: 28 | - name: jwt 29 | secret: 30 | secretName: {{ .Chart.Name }}-jwt 31 | containers: 32 | - name: {{ .Chart.Name }} 33 | image: "{{ .Values.image.php.repository }}:{{ .Values.image.php.tag }}" 34 | command: [ '/app/bin/console', 'messenger:consume', 'events', '-vv' ] 35 | imagePullPolicy: {{ .Values.image.pullPolicy }} 36 | volumeMounts: 37 | - name: jwt 38 | mountPath: {{ .Values.parameters.jwt.hostPath }} 39 | readOnly: true 40 | env: 41 | {{ tpl (.Files.Get "config/parameters.yaml") . | indent 12 }} 42 | ports: 43 | - name: fast-cgi 44 | containerPort: 9000 45 | protocol: TCP 46 | resources: 47 | {{ toYaml .Values.resources | indent 12 }} 48 | {{- with .Values.nodeSelector }} 49 | nodeSelector: 50 | {{ toYaml . | indent 8 }} 51 | {{- end }} 52 | {{- with .Values.affinity }} 53 | affinity: 54 | {{ toYaml . | indent 8 }} 55 | {{- end }} 56 | {{- with .Values.tolerations }} 57 | tolerations: 58 | {{ toYaml . | indent 8 }} 59 | {{- end }} 60 | -------------------------------------------------------------------------------- /etc/artifact/chart/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "chart.fullname" . }} 5 | labels: 6 | app: {{ include "chart.name" . }} 7 | chart: {{ include "chart.chart" . }} 8 | release: {{ .Release.Name }} 9 | heritage: {{ .Release.Service }} 10 | annotations: 11 | checksum/secrets-rmq: {{ include (print $.Template.BasePath "/secrets/rabbitmq.yaml") . | sha256sum }} 12 | checksum/secrets-app: {{ include (print $.Template.BasePath "/secrets/app-secret.yaml") . | sha256sum }} 13 | checksum/secrets-jwt: {{ include (print $.Template.BasePath "/secrets/jwt.yaml") . | sha256sum }} 14 | checksum/secrets-mysql: {{ include (print $.Template.BasePath "/secrets/mysql.yaml") . | sha256sum }} 15 | spec: 16 | replicas: {{ .Values.replicaCount }} 17 | selector: 18 | matchLabels: 19 | app: {{ include "chart.name" . }} 20 | release: {{ .Release.Name }} 21 | template: 22 | metadata: 23 | labels: 24 | app: {{ include "chart.name" . }} 25 | release: {{ .Release.Name }} 26 | spec: 27 | volumes: 28 | - name: jwt 29 | secret: 30 | secretName: {{ .Chart.Name }}-jwt 31 | containers: 32 | - name: {{ .Chart.Name }} 33 | image: "{{ .Values.image.php.repository }}:{{ .Values.image.php.tag }}" 34 | imagePullPolicy: {{ .Values.image.pullPolicy }} 35 | volumeMounts: 36 | - name: jwt 37 | mountPath: {{ .Values.parameters.jwt.hostPath }} 38 | readOnly: true 39 | env: 40 | {{ tpl (.Files.Get "config/parameters.yaml") . | indent 12 }} 41 | ports: 42 | - name: fast-cgi 43 | containerPort: 9000 44 | protocol: TCP 45 | - name: {{ .Chart.Name }}-nginx 46 | image: "{{ .Values.image.nginx.repository }}:{{ .Values.image.nginx.tag }}" 47 | imagePullPolicy: {{ .Values.image.pullPolicy }} 48 | ports: 49 | - name: http 50 | containerPort: 80 51 | protocol: TCP 52 | livenessProbe: 53 | httpGet: 54 | path: /api/healthz 55 | port: http 56 | readinessProbe: 57 | httpGet: 58 | path: /api/healthz 59 | port: http 60 | resources: 61 | {{ toYaml .Values.resources | indent 12 }} 62 | {{- with .Values.nodeSelector }} 63 | nodeSelector: 64 | {{ toYaml . | indent 8 }} 65 | {{- end }} 66 | {{- with .Values.affinity }} 67 | affinity: 68 | {{ toYaml . | indent 8 }} 69 | {{- end }} 70 | {{- with .Values.tolerations }} 71 | tolerations: 72 | {{ toYaml . | indent 8 }} 73 | {{- end }} 74 | -------------------------------------------------------------------------------- /etc/artifact/chart/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling }} 2 | apiVersion: autoscaling/v1 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ .Release.Name }} 6 | labels: 7 | app: {{ include "chart.name" . }} 8 | chart: {{ include "chart.chart" . }} 9 | release: {{ .Release.Name }} 10 | heritage: {{ .Release.Service }} 11 | spec: 12 | scaleTargetRef: 13 | apiVersion: apps/v1 14 | kind: Deployment 15 | name: {{ .Release.Name }} 16 | minReplicas: {{ .Values.autoscaling.minReplicas }} 17 | maxReplicas: {{ .Values.autoscaling.maxReplicas }} 18 | targetCPUUtilizationPercentage: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 19 | {{- end }} -------------------------------------------------------------------------------- /etc/artifact/chart/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "chart.fullname" . -}} 3 | {{- $ingressPath := .Values.ingress.path -}} 4 | apiVersion: networking.k8s.io/v1beta1 5 | kind: Ingress 6 | metadata: 7 | name: {{ $fullName }} 8 | labels: 9 | app: {{ include "chart.name" . }} 10 | chart: {{ include "chart.chart" . }} 11 | release: {{ .Release.Name }} 12 | heritage: {{ .Release.Service }} 13 | {{- with .Values.ingress.annotations }} 14 | annotations: 15 | {{ toYaml . | indent 4 }} 16 | {{- end }} 17 | spec: 18 | {{- if .Values.ingress.tls }} 19 | tls: 20 | {{- range .Values.ingress.tls }} 21 | - hosts: 22 | {{- range .hosts }} 23 | - {{ . | quote }} 24 | {{- end }} 25 | secretName: {{ .secretName }} 26 | {{- end }} 27 | {{- end }} 28 | rules: 29 | {{- range .Values.ingress.hosts }} 30 | - host: {{ . | quote }} 31 | http: 32 | paths: 33 | - path: {{ $ingressPath }} 34 | backend: 35 | serviceName: {{ $fullName }} 36 | servicePort: http 37 | {{- end }} 38 | {{- end }} 39 | -------------------------------------------------------------------------------- /etc/artifact/chart/templates/migrations.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | name: {{ include "chart.fullname" . }}-migrations-{{ date "20060102150405" .Release.Time }} 5 | labels: 6 | app: {{ include "chart.name" . }}-migrations 7 | chart: {{ include "chart.chart" . }} 8 | release: {{ .Release.Name }} 9 | heritage: {{ .Release.Service }} 10 | app.kubernetes.io/component: init-migrations 11 | annotations: 12 | checksum/secrets-rmq: {{ include (print $.Template.BasePath "/secrets/rabbitmq.yaml") . | sha256sum }} 13 | checksum/secrets-app: {{ include (print $.Template.BasePath "/secrets/app-secret.yaml") . | sha256sum }} 14 | checksum/secrets-jwt: {{ include (print $.Template.BasePath "/secrets/jwt.yaml") . | sha256sum }} 15 | checksum/secrets-mysql: {{ include (print $.Template.BasePath "/secrets/mysql.yaml") . | sha256sum }} 16 | helm.sh/hook: "post-install,pre-upgrade" 17 | helm.sh/hook-weight: "-5" 18 | helm.sh/hook-delete-policy: hook-succeeded 19 | spec: 20 | activeDeadlineSeconds: 120 21 | template: 22 | metadata: 23 | labels: 24 | app: {{ include "chart.name" . }}-migrations 25 | release: {{ .Release.Name }} 26 | spec: 27 | restartPolicy: Never 28 | volumes: 29 | - name: jwt 30 | secret: 31 | secretName: {{ .Chart.Name }}-jwt 32 | containers: 33 | - name: {{ .Chart.Name }} 34 | image: "{{ .Values.image.php.repository }}:{{ .Values.image.php.tag }}" 35 | command: [ '/app/bin/console', 'd:migrations:migrate', '-n' ] 36 | imagePullPolicy: {{ .Values.image.pullPolicy }} 37 | volumeMounts: 38 | - name: jwt 39 | mountPath: {{ .Values.parameters.jwt.hostPath }} 40 | readOnly: true 41 | env: 42 | {{ tpl (.Files.Get "config/parameters.yaml") . | indent 12 }} 43 | resources: 44 | {{ toYaml .Values.resources | indent 12 }} 45 | {{- with .Values.nodeSelector }} 46 | nodeSelector: 47 | {{ toYaml . | indent 8 }} 48 | {{- end }} 49 | {{- with .Values.affinity }} 50 | affinity: 51 | {{ toYaml . | indent 8 }} 52 | {{- end }} 53 | {{- with .Values.tolerations }} 54 | tolerations: 55 | {{ toYaml . | indent 8 }} 56 | {{- end }} 57 | -------------------------------------------------------------------------------- /etc/artifact/chart/templates/secrets/app-secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: {{ .Chart.Name }}-secret 5 | labels: 6 | app: {{ include "chart.name" . }} 7 | chart: {{ include "chart.chart" . }} 8 | release: {{ .Release.Name }} 9 | heritage: {{ .Release.Service }} 10 | type: Opaque 11 | data: 12 | secret: {{ .Values.parameters.app.secret | b64enc | quote }} 13 | -------------------------------------------------------------------------------- /etc/artifact/chart/templates/secrets/jwt.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: {{ .Chart.Name }}-jwt 5 | labels: 6 | app: {{ include "chart.name" . }} 7 | chart: {{ include "chart.chart" . }} 8 | release: {{ .Release.Name }} 9 | heritage: {{ .Release.Service }} 10 | type: Opaque 11 | data: 12 | private.pem: {{ .Values.parameters.jwt.secretKey | b64enc | quote }} 13 | public.pem: {{ .Values.parameters.jwt.publicKey | b64enc | quote }} 14 | passphrase: {{ .Values.parameters.jwt.passphrase | b64enc | quote }} 15 | -------------------------------------------------------------------------------- /etc/artifact/chart/templates/secrets/mysql.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: {{ .Chart.Name }}-mysql 5 | labels: 6 | app: {{ include "chart.name" . }} 7 | chart: {{ include "chart.chart" . }} 8 | release: {{ .Release.Name }} 9 | heritage: {{ .Release.Service }} 10 | type: Opaque 11 | data: 12 | url: {{ tpl (.Files.Get "config/mysql.url") . | b64enc }} 13 | -------------------------------------------------------------------------------- /etc/artifact/chart/templates/secrets/rabbitmq.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: {{ .Chart.Name }}-rabbitmq 5 | labels: 6 | app: {{ include "chart.name" . }} 7 | chart: {{ include "chart.chart" . }} 8 | release: {{ .Release.Name }} 9 | heritage: {{ .Release.Service }} 10 | type: Opaque 11 | data: 12 | host: {{ .Values.parameters.rabbit.host | b64enc | quote }} 13 | -------------------------------------------------------------------------------- /etc/artifact/chart/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "chart.fullname" . }} 5 | labels: 6 | app: {{ include "chart.name" . }} 7 | chart: {{ include "chart.chart" . }} 8 | release: {{ .Release.Name }} 9 | heritage: {{ .Release.Service }} 10 | spec: 11 | type: {{ .Values.service.type }} 12 | ports: 13 | - port: {{ .Values.service.port }} 14 | targetPort: http 15 | protocol: TCP 16 | name: http 17 | selector: 18 | app: {{ include "chart.name" . }} 19 | release: {{ .Release.Name }} 20 | -------------------------------------------------------------------------------- /etc/artifact/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | php: 5 | build: 6 | dockerfile: etc/artifact/Dockerfile 7 | context: ../.. 8 | target: php 9 | image: jorge07/cqrs:latest 10 | nginx: 11 | build: 12 | dockerfile: etc/artifact/Dockerfile 13 | context: ../.. 14 | target: nginx 15 | image: jorge07/cqrs:nginx-latest 16 | -------------------------------------------------------------------------------- /etc/artifact/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | root /app/public; 3 | 4 | location / { 5 | try_files $uri /index.php$is_args$args; 6 | } 7 | 8 | location ~ ^/index\.php(/|$) { 9 | fastcgi_pass 127.0.0.1:9000; 10 | fastcgi_split_path_info ^(.+\.php)(/.*)$; 11 | include fastcgi_params; 12 | fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; 13 | fastcgi_param DOCUMENT_ROOT $realpath_root; 14 | } 15 | } -------------------------------------------------------------------------------- /etc/ci/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | -------------------------------------------------------------------------------- /etc/ci/mysql/custom.cnf: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | bind-address = 0.0.0.0 3 | 4 | innodb_flush_log_at_trx_commit = 2 5 | innodb_lock_wait_timeout = 50 6 | 7 | max_connect_errors = 1000000 8 | max_connections = 900 9 | 10 | character-set-server = utf8 11 | sql_mode = "" 12 | innodb = FORCE 13 | default-storage-engine = InnoDB 14 | max_allowed_packet = 256M 15 | -------------------------------------------------------------------------------- /etc/dev/docker-compose.windows.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | 5 | php: 6 | environment: 7 | XDEBUG_CONFIG: remote_connect_back=0 remote_host=host.docker.internal remote_port=9000 8 | -------------------------------------------------------------------------------- /etc/dev/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | 5 | nginx: 6 | ports: 7 | - "80:80" 8 | 9 | php: 10 | ports: 11 | - "2323:22" 12 | # - "9003:9003" 13 | # Allows to debug php script run inside PHP container 14 | environment: 15 | PHP_IDE_CONFIG: serverName=es.local 16 | blackfire: 17 | image: blackfire/blackfire 18 | env_file: 19 | - .env.blackfire 20 | environment: 21 | BLACKFIRE_LOG_LEVEL: 4 22 | 23 | mysql: 24 | ports: 25 | - "3306:3306" 26 | 27 | rmq: 28 | ports: 29 | - "15672:15672" 30 | - "5672:5672" 31 | 32 | kibana: 33 | image: docker.elastic.co/kibana/kibana:7.11.0 34 | ports: 35 | - 5601:5601 36 | volumes: 37 | - "./etc/dev/kibana/config:/usr/share/kibana/config/kibana.yml" 38 | 39 | elasticsearch: 40 | ports: 41 | - "9200:9200" 42 | 43 | volumes: 44 | db_data: 45 | -------------------------------------------------------------------------------- /etc/dev/kibana/config: -------------------------------------------------------------------------------- 1 | --- 2 | ## Default Kibana configuration from kibana-docker. 3 | ## from https://github.com/elastic/kibana/blob/master/config/kibana.yml 4 | # 5 | server.name: kibana 6 | server.host: "0" 7 | elasticsearch.hosts: http://elasticsearch:9200 8 | -------------------------------------------------------------------------------- /etc/dev/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | server_name es.local; 3 | 4 | root /app/public; 5 | 6 | location / { 7 | # try to serve file directly, fallback to index.php 8 | try_files $uri /index.php$is_args$args; 9 | } 10 | 11 | location ~ \.php$ { 12 | fastcgi_pass php:9000; 13 | fastcgi_index index.php; 14 | fastcgi_split_path_info ^(.+\.php)(/.*)$; 15 | include fastcgi_params; 16 | fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; 17 | fastcgi_param DOCUMENT_ROOT $realpath_root; 18 | fastcgi_param PATH_INFO $fastcgi_path_info; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /etc/prod/docker-compose.windows.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | 5 | php: 6 | environment: 7 | XDEBUG_CONFIG: remote_connect_back=0 remote_host=host.docker.internal remote_port=9000 8 | -------------------------------------------------------------------------------- /etc/prod/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | 5 | nginx: 6 | ports: 7 | - "80:80" 8 | 9 | php: 10 | ports: 11 | - "2323:22" 12 | # Allows to debug php script run inside PHP container 13 | environment: 14 | PHP_IDE_CONFIG: serverName=es.local 15 | # Prod mode doesn't load .env file, remember to add here your new required env vars! 16 | APP_ENV: "prod" 17 | APP_DEBUG: 0 18 | APP_SECRET: c7375619ac26c8671e52279c31c7f157 19 | JWT_SECRET_KEY: '%kernel.project_dir%/config/packages/jwt/private.pem' 20 | JWT_PUBLIC_KEY: '%kernel.project_dir%/config/packages/jwt/public.pem' 21 | JWT_PASSPHRASE: productionfakesecret 22 | JWT_TTL: 604800 23 | MESSENGER_TRANSPORT_DSN: amqp://guest:guest@rmq:5672/%2f/messages 24 | 25 | workers: 26 | environment: 27 | PHP_IDE_CONFIG: serverName=es.local 28 | APP_ENV: "prod" 29 | APP_DEBUG: 0 30 | APP_SECRET: c7375619ac26c8671e52279c31c7f157 31 | JWT_SECRET_KEY: '%kernel.project_dir%/config/packages/jwt/private.pem' 32 | JWT_PUBLIC_KEY: '%kernel.project_dir%/config/packages/jwt/public.pem' 33 | JWT_PASSPHRASE: productionfakesecret 34 | JWT_TTL: 604800 35 | MESSENGER_TRANSPORT_DSN: amqp://guest:guest@rmq:5672/%2f/messages 36 | 37 | mysql: 38 | ports: 39 | - "3306:3306" 40 | 41 | rmq: 42 | ports: 43 | - "15672:15672" 44 | - "5672:5672" 45 | 46 | kibana: 47 | image: docker.elastic.co/kibana/kibana:7.9.1 48 | ports: 49 | - 5601:5601 50 | volumes: 51 | - "./etc/dev/kibana/config:/usr/share/kibana/config/kibana.yml" 52 | 53 | elasticsearch: 54 | ports: 55 | - "9200:9200" 56 | 57 | volumes: 58 | db_data: 59 | -------------------------------------------------------------------------------- /etc/prod/kibana/config: -------------------------------------------------------------------------------- 1 | --- 2 | ## Default Kibana configuration from kibana-docker. 3 | ## from https://github.com/elastic/kibana/blob/master/config/kibana.yml 4 | # 5 | server.name: kibana 6 | server.host: "0" 7 | elasticsearch.hosts: http://elasticsearch:9200 8 | -------------------------------------------------------------------------------- /etc/prod/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | server_name es.local; 3 | 4 | root /app/public; 5 | 6 | location / { 7 | # try to serve file directly, fallback to index.php 8 | try_files $uri /index.php$is_args$args; 9 | } 10 | 11 | location ~ \.php$ { 12 | fastcgi_pass php:9000; 13 | fastcgi_index index.php; 14 | fastcgi_split_path_info ^(.+\.php)(/.*)$; 15 | include fastcgi_params; 16 | fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; 17 | fastcgi_param DOCUMENT_ROOT $realpath_root; 18 | fastcgi_param PATH_INFO $fastcgi_path_info; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | checkMissingIterableValueType: false 3 | checkGenericClassInNonGenericObjectType: false 4 | excludePaths: 5 | - src/Infrastructure/Shared/Persistence/Doctrine/Migrations/* 6 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./src/ 7 | 8 | 9 | ./src/Infrastructure/Shared/Persistence/Doctrine/Migrations 10 | ./src/Infrastructure/Kernel.php 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | tests/ 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | load(__DIR__ . '/../.env'); 18 | } 19 | 20 | if ($_SERVER['APP_DEBUG'] ?? ('prod' !== ($_SERVER['APP_ENV'] ?? 'dev'))) { 21 | umask(0000); 22 | 23 | Debug::enable(); 24 | } 25 | 26 | if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? false) { 27 | Request::setTrustedProxies(explode(',', (string) $trustedProxies), Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST); 28 | } 29 | 30 | if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? false) { 31 | Request::setTrustedHosts(explode(',', (string) $trustedHosts)); 32 | } 33 | 34 | $kernel = new Kernel($_SERVER['APP_ENV'] ?? 'dev', (bool) $_SERVER['APP_DEBUG']) ?? false; 35 | $request = Request::createFromGlobals(); 36 | $response = $kernel->handle($request); 37 | $response->send(); 38 | $kernel->terminate($request, $response); 39 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | symfonyContainerXml(__DIR__ . '/var/cache/dev/App_KernelDevDebugContainer.xml'); 15 | $rectorConfig->paths([ 16 | __DIR__ . '/config', 17 | __DIR__ . '/public', 18 | __DIR__ . '/src', 19 | __DIR__ . '/tests', 20 | ]); 21 | // register a single rule 22 | $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class); 23 | // define sets of rules 24 | $rectorConfig->sets([ 25 | // LevelSetList::UP_TO_PHP_81, 26 | // SetList::DEAD_CODE, 27 | // SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION, 28 | // SymfonySetList::SYMFONY_CODE_QUALITY, 29 | // SymfonySetList::SYMFONY_62, 30 | // SymfonySetList::ANNOTATIONS_TO_ATTRIBUTES, 31 | // DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES, 32 | // SensiolabsSetList::FRAMEWORK_EXTRA_61, 33 | ]); 34 | }; 35 | -------------------------------------------------------------------------------- /src/App/Shared/Application/Command/CommandBusInterface.php: -------------------------------------------------------------------------------- 1 | exists($page, $limit, $total); 21 | } 22 | 23 | /** 24 | * @throws NotFoundException 25 | */ 26 | private function exists(int $page, int $limit, int $total): void 27 | { 28 | if (($limit * ($page - 1)) >= $total) { 29 | throw new NotFoundException(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/App/Shared/Application/Query/Event/GetEvents/GetEventsHandler.php: -------------------------------------------------------------------------------- 1 | eventRepository->page($query->page, $query->limit); 26 | 27 | return new Collection($query->page, $query->limit, $result['total']['value'], $result['data']); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/App/Shared/Application/Query/Event/GetEvents/GetEventsQuery.php: -------------------------------------------------------------------------------- 1 | getId(), 31 | self::type($serializableReadModel), 32 | $serializableReadModel->serialize(), 33 | $relations 34 | ); 35 | } 36 | 37 | public static function fromPayload(string $id, string $type, array $payload, array $relations = []): self 38 | { 39 | return new self( 40 | $id, 41 | $type, 42 | $payload, 43 | $relations 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/App/Shared/Application/Query/QueryBusInterface.php: -------------------------------------------------------------------------------- 1 | getMessage(), (int) $e->getCode(), $e)); 42 | } 43 | } 44 | 45 | public function toString(): string 46 | { 47 | return $this->format(self::FORMAT); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/App/Shared/Infrastructure/Bus/AsyncEvent/AsyncEventHandlerInterface.php: -------------------------------------------------------------------------------- 1 | messageBus->dispatch($command, [ 29 | new AmqpStamp($command->getType()), 30 | ]); 31 | } catch (HandlerFailedException $error) { 32 | $this->throwException($error); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/App/Shared/Infrastructure/Bus/Command/MessengerCommandBus.php: -------------------------------------------------------------------------------- 1 | messageBus->dispatch($command); 29 | } catch (HandlerFailedException $e) { 30 | $this->throwException($e); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/App/Shared/Infrastructure/Bus/MessageBusExceptionTrait.php: -------------------------------------------------------------------------------- 1 | getPrevious(); 20 | } 21 | 22 | throw $exception; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/App/Shared/Infrastructure/Bus/Query/MessengerQueryBus.php: -------------------------------------------------------------------------------- 1 | messageBus->dispatch($query); 34 | 35 | /** @var HandledStamp $stamp */ 36 | $stamp = $envelope->last(HandledStamp::class); 37 | 38 | return $stamp->getResult(); 39 | } catch (HandlerFailedException $e) { 40 | $this->throwException($e); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/App/Shared/Infrastructure/Event/Consumer/SendEventsToElasticConsumer.php: -------------------------------------------------------------------------------- 1 | eventElasticRepository->store($event); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/App/Shared/Infrastructure/Event/Publisher/AsyncEventPublisher.php: -------------------------------------------------------------------------------- 1 | messages[] = $domainMessage; 27 | } 28 | 29 | public static function getSubscribedEvents(): array 30 | { 31 | return [ 32 | KernelEvents::TERMINATE => 'publish', 33 | ConsoleEvents::TERMINATE => 'publish', 34 | ]; 35 | } 36 | 37 | /** 38 | * @throws Throwable 39 | */ 40 | public function publish(): void 41 | { 42 | if (empty($this->messages)) { 43 | return; 44 | } 45 | 46 | foreach ($this->messages as $message) { 47 | $this->bus->handle($message); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/App/Shared/Infrastructure/Event/ReadModel/ElasticSearchEventRepository.php: -------------------------------------------------------------------------------- 1 | $message->getType(), 23 | 'payload' => $message->getPayload()->serialize(), 24 | 'occurred_on' => $message->getRecordedOn()->toString(), 25 | ]; 26 | 27 | $this->add($document); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/App/Shared/Infrastructure/Kernel.php: -------------------------------------------------------------------------------- 1 | getProjectDir() . '/var/cache/' . $this->environment; 21 | } 22 | 23 | public function getLogDir(): string 24 | { 25 | return $this->getProjectDir() . '/var/log'; 26 | } 27 | 28 | public function registerBundles(): iterable 29 | { 30 | $contents = require $this->getProjectDir() . '/config/bundles.php'; 31 | foreach ($contents as $class => $envs) { 32 | if (isset($envs['all']) || isset($envs[$this->environment])) { 33 | yield new $class(); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/App/Shared/Infrastructure/Persistence/Doctrine/Migrations/Version20180102233829.php: -------------------------------------------------------------------------------- 1 | get(DBALEventStore::class); 34 | $this->eventStore = $eventStore; 35 | 36 | /** @var EntityManagerInterface $em */ 37 | $em = $container->get('doctrine.orm.entity_manager'); 38 | $this->em = $em; 39 | } 40 | 41 | public function up(Schema $schema): void 42 | { 43 | $this->eventStore->configureSchema($schema); 44 | 45 | $this->em->flush(); 46 | } 47 | 48 | public function down(Schema $schema): void 49 | { 50 | $schema->dropTable('api.events'); 51 | 52 | $this->em->flush(); 53 | } 54 | 55 | public function isTransactional(): bool 56 | { 57 | return false; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/App/Shared/Infrastructure/Persistence/Doctrine/Migrations/Version20200727170306.php: -------------------------------------------------------------------------------- 1 | abortIf(get_class($this->connection->getDatabasePlatform()) !== MySQL80Platform::class, 'Migration can only be executed safely on \'mysql\'.'); 25 | 26 | $this->addSql('CREATE TABLE users (uuid BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid_binary)\', created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', updated_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', credentials_email VARCHAR(255) NOT NULL COMMENT \'(DC2Type:email)\', credentials_password VARCHAR(255) NOT NULL COMMENT \'(DC2Type:hashed_password)\', UNIQUE INDEX UNIQ_1483A5E9299C9369 (credentials_email), PRIMARY KEY(uuid)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); 27 | } 28 | 29 | public function down(Schema $schema): void 30 | { 31 | // this down() migration is auto-generated, please modify it to your needs 32 | $this->abortIf(get_class($this->connection->getDatabasePlatform()) !== MySQL80Platform::class, 'Migration can only be executed safely on \'mysql\'.'); 33 | 34 | $this->addSql('DROP TABLE users'); 35 | } 36 | 37 | /** 38 | * https://github.com/doctrine/migrations/issues/1104 39 | */ 40 | public function isTransactional(): bool 41 | { 42 | return false; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/App/Shared/Infrastructure/Persistence/Doctrine/MigrationsFactory/ContainerAwareFactory.php: -------------------------------------------------------------------------------- 1 | connection, 29 | $this->logger 30 | ); 31 | 32 | if ($instance instanceof ContainerAwareInterface) { 33 | $instance->setContainer($this->container); 34 | } 35 | 36 | return $instance; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/App/Shared/Infrastructure/Persistence/Doctrine/Types/DateTimeType.php: -------------------------------------------------------------------------------- 1 | getDateTimeTypeDeclarationSQL($column); 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | * 29 | * @throws ConversionException 30 | * 31 | * @param T $value 32 | * 33 | * @return (T is null ? null : string) 34 | * 35 | * @template T 36 | **/ 37 | public function convertToDatabaseValue($value, AbstractPlatform $platform) 38 | { 39 | if (null === $value) { 40 | return null; 41 | } 42 | 43 | if ($value instanceof DateTime) { 44 | return $value->format($platform->getDateTimeFormatString()); 45 | } 46 | 47 | if ($value instanceof DateTimeImmutable) { 48 | return $value->format($platform->getDateTimeFormatString()); 49 | } 50 | 51 | throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', DateTime::class]); 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | * 57 | * @throws ConversionException 58 | * 59 | * @param T $value 60 | * 61 | * @return (T is null ? null : DateTimeImmutable) 62 | * 63 | * @template T 64 | */ 65 | public function convertToPHPValue($value, AbstractPlatform $platform) 66 | { 67 | if (null === $value || $value instanceof DateTime) { 68 | return $value; 69 | } 70 | 71 | try { 72 | $dateTime = DateTime::fromString($value); 73 | } catch (DateTimeException) { 74 | throw ConversionException::conversionFailedFormat($value, $this->getName(), $platform->getDateTimeFormatString()); 75 | } 76 | 77 | return $dateTime; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/App/Shared/Infrastructure/Persistence/Doctrine/Types/EmailType.php: -------------------------------------------------------------------------------- 1 | getName(), ['null', Email::class]); 32 | } 33 | 34 | return $value->toString(); 35 | } 36 | 37 | /** 38 | * @param Email|string|null $value 39 | * 40 | * @return Email|null 41 | * 42 | * @throws ConversionException 43 | */ 44 | public function convertToPHPValue($value, AbstractPlatform $platform) 45 | { 46 | if (null === $value || $value instanceof Email) { 47 | return $value; 48 | } 49 | 50 | try { 51 | $email = Email::fromString($value); 52 | } catch (Throwable) { 53 | throw ConversionException::conversionFailedFormat($value, $this->getName(), $platform->getDateTimeFormatString()); 54 | } 55 | 56 | return $email; 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | public function requiresSQLCommentHint(AbstractPlatform $platform) 63 | { 64 | return true; 65 | } 66 | 67 | /** 68 | * @return string 69 | */ 70 | public function getName() 71 | { 72 | return self::TYPE; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/App/Shared/Infrastructure/Persistence/Doctrine/Types/HashedPasswordType.php: -------------------------------------------------------------------------------- 1 | getName(), ['null', HashedPassword::class]); 32 | } 33 | 34 | return $value->toString(); 35 | } 36 | 37 | /** 38 | * @param HashedPassword|string|null $value 39 | * 40 | * @return HashedPassword|null 41 | * 42 | * @throws ConversionException 43 | */ 44 | public function convertToPHPValue($value, AbstractPlatform $platform) 45 | { 46 | if (null === $value || $value instanceof HashedPassword) { 47 | return $value; 48 | } 49 | 50 | try { 51 | $hashedPassword = HashedPassword::fromHash($value); 52 | } catch (Throwable) { 53 | throw ConversionException::conversionFailedFormat($value, $this->getName(), $platform->getDateTimeFormatString()); 54 | } 55 | 56 | return $hashedPassword; 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | public function requiresSQLCommentHint(AbstractPlatform $platform) 63 | { 64 | return true; 65 | } 66 | 67 | /** 68 | * @return string 69 | */ 70 | public function getName() 71 | { 72 | return self::TYPE; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/App/Shared/Infrastructure/Persistence/ReadModel/Exception/NotFoundException.php: -------------------------------------------------------------------------------- 1 | userUuid = Uuid::fromString($userUuid); 27 | $this->email = Email::fromString($email); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/App/User/Application/Command/ChangeEmail/ChangeEmailHandler.php: -------------------------------------------------------------------------------- 1 | userRepository->get($command->userUuid); 20 | 21 | $user->changeEmail($command->email, $this->uniqueEmailSpecification); 22 | 23 | $this->userRepository->store($user); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/App/User/Application/Command/SignIn/SignInCommand.php: -------------------------------------------------------------------------------- 1 | email = Email::fromString($email); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/App/User/Application/Command/SignIn/SignInHandler.php: -------------------------------------------------------------------------------- 1 | uuidFromEmail($command->email); 23 | 24 | $user = $this->userStore->get($uuid); 25 | 26 | $user->signIn($command->plainPassword); 27 | 28 | $this->userStore->store($user); 29 | } 30 | 31 | private function uuidFromEmail(Email $email): UuidInterface 32 | { 33 | $uuid = $this->userCollection->existsEmail($email); 34 | 35 | if (null === $uuid) { 36 | throw new InvalidCredentialsException(); 37 | } 38 | 39 | return $uuid; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/App/User/Application/Command/SignUp/SignUpCommand.php: -------------------------------------------------------------------------------- 1 | uuid = Uuid::fromString($uuid); 29 | $this->credentials = new Credentials(Email::fromString($email), HashedPassword::encode($plainPassword)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/App/User/Application/Command/SignUp/SignUpHandler.php: -------------------------------------------------------------------------------- 1 | uuid, $command->credentials, $this->uniqueEmailSpecification); 25 | 26 | $this->userRepository->store($user); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/App/User/Application/Query/Auth/GetAuthUserByEmail/GetAuthUserByEmailHandler.php: -------------------------------------------------------------------------------- 1 | userCredentialsByEmail->getCredentialsByEmail($query->email); 20 | 21 | return Auth::create($uuid, $email, $hashedPassword); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/App/User/Application/Query/Auth/GetAuthUserByEmail/GetAuthUserByEmailQuery.php: -------------------------------------------------------------------------------- 1 | email = Email::fromString($email); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/App/User/Application/Query/Auth/GetToken/GetTokenHandler.php: -------------------------------------------------------------------------------- 1 | userCredentialsByEmail->getCredentialsByEmail($query->email); 20 | 21 | return $this->authenticationProvider->generateToken($uuid, $email, $hashedPassword); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/App/User/Application/Query/Auth/GetToken/GetTokenQuery.php: -------------------------------------------------------------------------------- 1 | email = Email::fromString($email); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/App/User/Application/Query/User/FindByEmail/FindByEmailHandler.php: -------------------------------------------------------------------------------- 1 | repository->oneByEmailAsArray($query->email); 27 | 28 | return Item::fromPayload($userView['uuid']->toString(), UserView::TYPE, $userView); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/App/User/Application/Query/User/FindByEmail/FindByEmailQuery.php: -------------------------------------------------------------------------------- 1 | email = Email::fromString($email); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/App/User/Domain/Event/UserEmailChanged.php: -------------------------------------------------------------------------------- 1 | $this->uuid->toString(), 42 | 'email' => $this->email->toString(), 43 | 'updated_at' => $this->updatedAt->toString(), 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/App/User/Domain/Event/UserSignedIn.php: -------------------------------------------------------------------------------- 1 | $this->uuid->toString(), 38 | 'email' => $this->email->toString(), 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/App/User/Domain/Event/UserWasCreated.php: -------------------------------------------------------------------------------- 1 | $this->uuid->toString(), 47 | 'credentials' => [ 48 | 'email' => $this->credentials->email->toString(), 49 | 'password' => $this->credentials->password->toString(), 50 | ], 51 | 'created_at' => $this->createdAt->toString(), 52 | ]; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/App/User/Domain/Exception/EmailAlreadyExistException.php: -------------------------------------------------------------------------------- 1 | hashedPassword); 37 | } 38 | 39 | /** 40 | * @throws AssertionFailedException 41 | */ 42 | private static function hash(string $plainPassword): string 43 | { 44 | Assertion::minLength($plainPassword, 6, 'Min 6 characters password'); 45 | 46 | /** @var string|bool|null $hashedPassword */ 47 | $hashedPassword = \password_hash($plainPassword, PASSWORD_BCRYPT, ['cost' => self::COST]); 48 | 49 | if (\is_bool($hashedPassword)) { 50 | throw new RuntimeException('Server error hashing password'); 51 | } 52 | 53 | return (string) $hashedPassword; 54 | } 55 | 56 | public function toString(): string 57 | { 58 | return $this->hashedPassword; 59 | } 60 | 61 | public function __toString(): string 62 | { 63 | return $this->hashedPassword; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/App/User/Domain/ValueObject/Email.php: -------------------------------------------------------------------------------- 1 | email; 30 | } 31 | 32 | public function __toString(): string 33 | { 34 | return $this->email; 35 | } 36 | 37 | public function jsonSerialize(): string 38 | { 39 | return $this->toString(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/App/User/Infrastructure/Auth/Auth.php: -------------------------------------------------------------------------------- 1 | email->toString(); 28 | } 29 | 30 | public function getUsername(): string 31 | { 32 | return $this->email->toString(); 33 | } 34 | 35 | public function getPassword(): string 36 | { 37 | return $this->hashedPassword->toString(); 38 | } 39 | 40 | public function getRoles(): array 41 | { 42 | return [ 43 | 'ROLE_USER', 44 | ]; 45 | } 46 | 47 | public function getSalt(): ?string 48 | { 49 | return null; 50 | } 51 | 52 | public function eraseCredentials(): void 53 | { 54 | // noop 55 | } 56 | 57 | public function getPasswordHasherName(): string 58 | { 59 | return 'hasher'; 60 | } 61 | 62 | public function uuid(): UuidInterface 63 | { 64 | return $this->uuid; 65 | } 66 | 67 | public function __toString(): string 68 | { 69 | return $this->email->toString(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/App/User/Infrastructure/Auth/AuthProvider.php: -------------------------------------------------------------------------------- 1 | userReadModelRepository->getCredentialsByEmail( 26 | Email::fromString($identifier) 27 | ); 28 | 29 | return Auth::create($uuid, $email, $hashedPassword); 30 | } catch (NotFoundException) { 31 | throw new UserNotFoundException(); 32 | } 33 | 34 | } 35 | /** 36 | * @throws NotFoundException 37 | * @throws AssertionFailedException 38 | * @throws NonUniqueResultException 39 | * @throws \Throwable 40 | * 41 | * @return Auth|UserInterface 42 | */ 43 | public function loadUserByUsername(string $email): \App\User\Infrastructure\Auth\Auth|\Symfony\Component\Security\Core\User\UserInterface 44 | { 45 | [$uuid, $email, $hashedPassword] = $this->userReadModelRepository->getCredentialsByEmail( 46 | Email::fromString($email) 47 | ); 48 | 49 | return Auth::create($uuid, $email, $hashedPassword); 50 | } 51 | 52 | /** 53 | * @throws NotFoundException 54 | * @throws AssertionFailedException 55 | * @throws NonUniqueResultException 56 | */ 57 | public function refreshUser(UserInterface $user): UserInterface 58 | { 59 | return $this->loadUserByUsername($user->getUserIdentifier()); 60 | } 61 | 62 | public function supportsClass(string $class): bool 63 | { 64 | return Auth::class === $class; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/App/User/Infrastructure/Auth/AuthenticationProvider.php: -------------------------------------------------------------------------------- 1 | JWTManager->create($auth); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/App/User/Infrastructure/Auth/PasswordHasher.php: -------------------------------------------------------------------------------- 1 | hasher::encode($plainPassword)->toString(); 17 | } 18 | 19 | public function verify(string $hashedPassword, string $plainPassword): bool 20 | { 21 | return $this->hasher::fromHash($hashedPassword)->match($plainPassword); 22 | } 23 | 24 | public function needsRehash(string $hashedPassword): bool 25 | { 26 | return false; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/App/User/Infrastructure/ReadModel/Projections/ConsoleProjectionFactory.php: -------------------------------------------------------------------------------- 1 | getPayload() instanceof UserSignedIn) { 41 | return; 42 | } 43 | 44 | $this->onUserSignedIn($message->getPayload()); 45 | } 46 | 47 | private function onUserSignedIn(UserSignedIn $event): void 48 | { 49 | $this->logger->info('Welcome to the jungle ' . $event->email->toString()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/App/User/Infrastructure/ReadModel/Projections/UserProjectionFactory.php: -------------------------------------------------------------------------------- 1 | repository->add($userReadModel); 32 | } 33 | 34 | /** 35 | * @throws NotFoundException 36 | * @throws NonUniqueResultException 37 | */ 38 | protected function applyUserEmailChanged(UserEmailChanged $emailChanged): void 39 | { 40 | $userReadModel = $this->repository->oneByUuid($emailChanged->uuid); 41 | 42 | $userReadModel->changeEmail($emailChanged->email); 43 | $userReadModel->changeUpdatedAt($emailChanged->updatedAt); 44 | 45 | $this->repository->apply(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/App/User/Infrastructure/ReadModel/UserView.php: -------------------------------------------------------------------------------- 1 | serialize()); 36 | } 37 | 38 | /** 39 | * @throws DateTimeException 40 | * @throws AssertionFailedException 41 | */ 42 | public static function deserialize(array $data): self 43 | { 44 | return new self( 45 | Uuid::fromString($data['uuid']), 46 | new Credentials( 47 | Email::fromString($data['credentials']['email']), 48 | HashedPassword::fromHash($data['credentials']['password'] ?? '') 49 | ), 50 | DateTime::fromString($data['created_at']), 51 | isset($data['updated_at']) ? DateTime::fromString($data['updated_at']) : null 52 | ); 53 | } 54 | 55 | public function serialize(): array 56 | { 57 | return [ 58 | 'uuid' => $this->getId(), 59 | 'credentials' => [ 60 | 'email' => (string) $this->credentials->email, 61 | ], 62 | ]; 63 | } 64 | 65 | public function uuid(): UuidInterface 66 | { 67 | return $this->uuid; 68 | } 69 | 70 | public function email(): string 71 | { 72 | return (string) $this->credentials->email; 73 | } 74 | 75 | public function changeEmail(Email $email): void 76 | { 77 | $this->credentials->email = $email; 78 | } 79 | 80 | public function changeUpdatedAt(DateTime $updatedAt): void 81 | { 82 | $this->updatedAt = $updatedAt; 83 | } 84 | 85 | public function getId(): string 86 | { 87 | return $this->uuid->toString(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/App/User/Infrastructure/Repository/UserStore.php: -------------------------------------------------------------------------------- 1 | save($user); 34 | } 35 | 36 | public function get(UuidInterface $uuid): User 37 | { 38 | /** @var User $user */ 39 | $user = $this->load($uuid->toString()); 40 | 41 | return $user; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/App/User/Infrastructure/Specification/UniqueEmailSpecification.php: -------------------------------------------------------------------------------- 1 | isSatisfiedBy($email); 26 | } 27 | 28 | /** 29 | * @param Email $value 30 | * @psalm-suppress MoreSpecificImplementedParamType 31 | */ 32 | public function isSatisfiedBy($value): bool 33 | { 34 | try { 35 | if ($this->checkUserByEmail->existsEmail($value)) { 36 | throw new EmailAlreadyExistException(); 37 | } 38 | } catch (NonUniqueResultException) { 39 | throw new EmailAlreadyExistException(); 40 | } 41 | 42 | return true; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/UI/Cli/Command/CreateUserCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Given a uuid and email, generates a new user.') 30 | ->addArgument('email', InputArgument::REQUIRED, 'User email') 31 | ->addArgument('password', InputArgument::REQUIRED, 'User password') 32 | ->addArgument('uuid', InputArgument::OPTIONAL, 'User Uuid') 33 | ; 34 | } 35 | 36 | /** 37 | * @throws Exception 38 | * @throws AssertionFailedException 39 | * @throws Throwable 40 | */ 41 | protected function execute(InputInterface $input, OutputInterface $output): int 42 | { 43 | /** @var string $uuid */ 44 | $uuid = $input->getArgument('uuid') ?: Uuid::uuid4()->toString(); 45 | /** @var string $email */ 46 | $email = $input->getArgument('email'); 47 | /** @var string $password */ 48 | $password = $input->getArgument('password'); 49 | 50 | $command = new CreateUser($uuid, $email, $password); 51 | 52 | $this->commandBus->handle($command); 53 | 54 | $output->writeln('User Created: '); 55 | $output->writeln(''); 56 | $output->writeln("Uuid: $uuid"); 57 | $output->writeln("Email: $email"); 58 | 59 | return 1; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/UI/Http/Rest/Controller/Auth/CheckController.php: -------------------------------------------------------------------------------- 1 | '\w+', '_password' => '\w+'], methods: [Request::METHOD_POST])] 56 | public function __invoke(Request $request): OpenApi 57 | { 58 | $username = (string) $request->request->get('_username'); 59 | 60 | Assertion::notEmpty($username, 'Username cant\'t be empty'); 61 | 62 | $signInCommand = new SignInCommand( 63 | $username, 64 | (string) $request->request->get('_password') 65 | ); 66 | 67 | $this->handle($signInCommand); 68 | 69 | return OpenApi::fromPayload( 70 | [ 71 | 'token' => $this->ask(new GetTokenQuery($username)), 72 | ], 73 | OpenApi::HTTP_OK 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/UI/Http/Rest/Controller/CommandController.php: -------------------------------------------------------------------------------- 1 | commandBus->handle($command); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/UI/Http/Rest/Controller/CommandQueryController.php: -------------------------------------------------------------------------------- 1 | commandBus->handle($command); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/UI/Http/Rest/Controller/Event/GetEventsController.php: -------------------------------------------------------------------------------- 1 | query->get('page', 1); 48 | $limit = $request->query->get('limit', 50); 49 | 50 | Assertion::numeric($page, 'Page number must be an integer'); 51 | Assertion::numeric($limit, 'Limit results must be an integer'); 52 | 53 | $query = new GetEventsQuery((int) $page, (int) $limit); 54 | 55 | /** @var Collection $response */ 56 | $response = $this->ask($query); 57 | 58 | return $this->jsonCollection($response, 200, true); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/UI/Http/Rest/Controller/Healthz/HealthzController.php: -------------------------------------------------------------------------------- 1 | elasticSearchEventRepository->isHealthly() && 40 | true === $mysql = $this->mysqlReadModelUserRepository->isHealthy() 41 | ) { 42 | return OpenApi::empty(200); 43 | } 44 | 45 | return OpenApi::fromPayload( 46 | [ 47 | 'Healthy services' => [ 48 | 'Elastic' => $elastic, 49 | 'MySQL' => $mysql, 50 | ], 51 | ], 52 | 500 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/UI/Http/Rest/Controller/QueryController.php: -------------------------------------------------------------------------------- 1 | queryBus->ask($query); 33 | } 34 | 35 | protected function jsonCollection(Collection $collection, int $status = OpenApi::HTTP_OK, bool $isImmutable = false): OpenApi 36 | { 37 | $response = OpenApi::collection($collection, $status); 38 | 39 | $this->decorateWithCache($response, $collection, $isImmutable); 40 | 41 | return $response; 42 | } 43 | 44 | protected function json(Item $resource, int $status = OpenApi::HTTP_OK): OpenApi 45 | { 46 | return OpenApi::one($resource, $status); 47 | } 48 | 49 | protected function route(string $name, array $params = []): string 50 | { 51 | return $this->router->generate($name, $params); 52 | } 53 | 54 | private function decorateWithCache(OpenApi $response, Collection $collection, bool $isImmutable): void 55 | { 56 | if ($isImmutable && $collection->limit === \count($collection->data)) { 57 | $response 58 | ->setMaxAge(self::CACHE_MAX_AGE) 59 | ->setSharedMaxAge(self::CACHE_MAX_AGE); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/UI/Http/Rest/Controller/User/GetUserByEmailController.php: -------------------------------------------------------------------------------- 1 | ask($query); 50 | 51 | return $this->json($user); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/UI/Http/Rest/Controller/User/SignUpController.php: -------------------------------------------------------------------------------- 1 | request->get('uuid'); 51 | $email = (string) $request->request->get('email'); 52 | $plainPassword = (string) $request->request->get('password'); 53 | 54 | Assertion::notEmpty($uuid, "Uuid can\'t be empty"); 55 | Assertion::notEmpty($email, "Email can\'t be empty"); 56 | Assertion::notEmpty($plainPassword, "Password can\'t be empty"); 57 | 58 | $commandRequest = new SignUpCommand($uuid, $email, $plainPassword); 59 | 60 | $this->handle($commandRequest); 61 | 62 | return OpenApi::created("/user/$email"); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/UI/Http/Rest/Controller/User/UserChangeEmailController.php: -------------------------------------------------------------------------------- 1 | validateUuid($uuid); 65 | 66 | $email = (string) $request->request->get('email'); 67 | 68 | Assertion::notEmpty($email, "Email can\'t be empty"); 69 | 70 | $command = new ChangeEmailCommand($uuid, $email); 71 | 72 | $this->handle($command); 73 | 74 | return new JsonResponse(); 75 | } 76 | 77 | private function validateUuid(string $uuid): void 78 | { 79 | if (!$this->session->get()->uuid()->equals(Uuid::fromString($uuid))) { 80 | throw new ForbiddenException(); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/UI/Http/Rest/EventSubscriber/JsonBodyParserSubscriber.php: -------------------------------------------------------------------------------- 1 | 'onKernelRequest', 21 | ]; 22 | } 23 | 24 | public function onKernelRequest(RequestEvent $event): void 25 | { 26 | $request = $event->getRequest(); 27 | if (!$this->isJsonRequest($request)) { 28 | return; 29 | } 30 | 31 | $content = $request->getContent(); 32 | if (empty($content)) { 33 | return; 34 | } 35 | 36 | if (!$this->transformJsonBody($request)) { 37 | $response = new Response('Unable to parse json request.', Response::HTTP_BAD_REQUEST); 38 | $event->setResponse($response); 39 | } 40 | } 41 | 42 | private function isJsonRequest(Request $request): bool 43 | { 44 | return 'json' === $request->getContentTypeFormat(); 45 | } 46 | 47 | private function transformJsonBody(Request $request): bool 48 | { 49 | try { 50 | $data = \json_decode( 51 | $request->getContent(), 52 | true, 53 | 512, 54 | JSON_THROW_ON_ERROR 55 | ); 56 | } catch (Throwable) { 57 | return false; 58 | } 59 | 60 | if (null === $data) { 61 | return true; 62 | } 63 | 64 | $request->request->replace($data); 65 | 66 | return true; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/UI/Http/Session.php: -------------------------------------------------------------------------------- 1 | tokenStorage->getToken(); 20 | 21 | if (!$token) { 22 | throw new InvalidCredentialsException(); 23 | } 24 | 25 | $user = $token->getUser(); 26 | 27 | if (!$user instanceof Auth) { 28 | throw new InvalidCredentialsException(); 29 | } 30 | 31 | return $user; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/UI/Http/Web/Controller/AbstractRenderController.php: -------------------------------------------------------------------------------- 1 | template->render($view, $parameters); 31 | 32 | return new Response($content, $code); 33 | } 34 | 35 | /** 36 | * @throws Throwable 37 | */ 38 | protected function handle(CommandInterface $command): void 39 | { 40 | $this->commandBus->handle($command); 41 | } 42 | 43 | /**\ 44 | * @throws Throwable 45 | */ 46 | protected function ask(QueryInterface $query): mixed 47 | { 48 | return $this->queryBus->ask($query); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/UI/Http/Web/Controller/HomeController.php: -------------------------------------------------------------------------------- 1 | render('home/index.html.twig'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/UI/Http/Web/Controller/ProfileController.php: -------------------------------------------------------------------------------- 1 | render('profile/index.html.twig'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/UI/Http/Web/Controller/SecurityController.php: -------------------------------------------------------------------------------- 1 | render('signin/login.html.twig', [ 27 | 'last_username' => $authUtils->getLastUsername(), 28 | 'error' => $authUtils->getLastAuthenticationError(), 29 | ]); 30 | } 31 | 32 | #[Route(path: '/logout', name: 'logout')] 33 | public function logout(): never 34 | { 35 | throw new AuthenticationException('I shouldn\'t be here..'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/UI/Http/Web/Controller/SignUpController.php: -------------------------------------------------------------------------------- 1 | toString(); 33 | 34 | return $this->render('signup/index.html.twig', ['uuid' => $uuid]); 35 | } 36 | 37 | /** 38 | * 39 | * @throws AssertionFailedException 40 | * @throws Throwable 41 | * @throws LoaderError 42 | * @throws RuntimeError 43 | * @throws SyntaxError 44 | */ 45 | #[Route(path: '/sign-up', name: 'sign-up-post', methods: ['POST'])] 46 | public function post(Request $request): Response 47 | { 48 | $errorHTTPStatusCode = null; 49 | $afterErrorUuid = Uuid::uuid4()->toString(); 50 | 51 | 52 | $uuid = $request->request->get('uuid'); 53 | $email = $request->request->get('email'); 54 | $password = $request->request->get('password'); 55 | 56 | try { 57 | Assertion::notNull($uuid, 'Missing uuid'); 58 | Assertion::notNull($email, 'Email can\'t be null'); 59 | Assertion::notNull($password, 'Password can\'t be null'); 60 | 61 | $this->handle(new SignUpCommand((string) $uuid, (string) $email, (string) $password)); 62 | 63 | return $this->render('signup/user_created.html.twig', ['uuid' => $uuid, 'email' => $email]); 64 | } catch (EmailAlreadyExistException $exception) { 65 | $errorHTTPStatusCode = Response::HTTP_CONFLICT; 66 | 67 | return $this->render('signup/index.html.twig', ['uuid' => $afterErrorUuid, 'error' => $exception->getMessage()], $errorHTTPStatusCode); 68 | } catch (InvalidArgumentException $exception) { 69 | $errorHTTPStatusCode= Response::HTTP_BAD_REQUEST; 70 | 71 | return $this->render('signup/index.html.twig', ['uuid' => $afterErrorUuid, 'error' => $exception->getMessage()], $errorHTTPStatusCode); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/UI/Http/Web/templates/base.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}Hey!{% endblock %} 6 | 7 | {% block stylesheets %} 8 | 9 | {% endblock %} 10 | 11 | 12 | {% include 'components/header/menu.html.twig' only %} 13 |
14 |
15 | {% block body %}{% endblock %} 16 |
17 | {% block javascripts %}{% endblock %} 18 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/UI/Http/Web/templates/components/card.html.twig: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | {{ title }} 5 |

6 |
7 |
8 | {{ text }} 9 |
10 |
-------------------------------------------------------------------------------- /src/UI/Http/Web/templates/components/header/menu.html.twig: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/UI/Http/Web/templates/home/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block body %} 4 |
5 |
6 |
7 | {% include 'components/card.html.twig' with { 'title': 'Hello!', 'text': 'Lorem ipsum hamend awer fiti carem conde moren. Fistro pecadorem de las praderum.'} only %} 8 |
9 |
10 |
11 | {% endblock body %} 12 | -------------------------------------------------------------------------------- /src/UI/Http/Web/templates/profile/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block body %} 4 |
5 |
6 |
7 |

Hi

{{ app.user.username }} 8 |
9 |
10 |
11 | {% endblock body %} 12 | -------------------------------------------------------------------------------- /src/UI/Http/Web/templates/signin/login.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block body %} 4 |
5 |
6 |
7 | {% if error %} 8 |
{{ error.messageKey|trans(error.messageData, 'security') }}
9 | {% endif %} 10 |
11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 |
20 | 21 |
22 |
23 |
24 |
25 | 26 |
27 |
28 |
29 |
30 |
31 |
32 | {% endblock body %} 33 | -------------------------------------------------------------------------------- /src/UI/Http/Web/templates/signup/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block body %} 4 |
5 |
6 |
7 | {% if error is defined %} 8 |
{{ error }}
9 | {% endif %} 10 |
11 |
12 | 13 |
14 |
15 | 16 |
17 | 18 |
19 |
20 |
21 | 22 |
23 | 24 |
25 |
26 |
27 |
28 | 29 |
30 |
31 |
32 |
33 |
34 |
35 | {% endblock body %} 36 | -------------------------------------------------------------------------------- /src/UI/Http/Web/templates/signup/user_created.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block body %} 4 |
5 |
6 |
7 |

Hello {{ email }}

8 | Your id is {{ uuid }} 9 |
10 |
11 |
12 | {% endblock body %} 13 | -------------------------------------------------------------------------------- /tests/App/Shared/Application/ApplicationTestCase.php: -------------------------------------------------------------------------------- 1 | commandBus = $this->service(CommandBusInterface::class); 30 | $this->queryBus = $this->service(QueryBusInterface::class); 31 | } 32 | 33 | /** 34 | * @return mixed 35 | * 36 | * @throws Throwable 37 | */ 38 | protected function ask(QueryInterface $query) 39 | { 40 | return $this->queryBus->ask($query); 41 | } 42 | 43 | /** 44 | * @throws Throwable 45 | */ 46 | protected function handle(CommandInterface $command): void 47 | { 48 | $this->commandBus->handle($command); 49 | } 50 | 51 | /** 52 | * @return object|null 53 | */ 54 | protected function service(string $serviceId) 55 | { 56 | return $this->getContainer()->get($serviceId); 57 | } 58 | 59 | protected function fireTerminateEvent(): void 60 | { 61 | /** @var EventDispatcher $dispatcher */ 62 | $dispatcher = $this->service('event_dispatcher'); 63 | 64 | $dispatcher->dispatch( 65 | new TerminateEvent( 66 | static::$kernel, 67 | Request::create('/'), 68 | new Response() 69 | ), 70 | KernelEvents::TERMINATE 71 | ); 72 | } 73 | 74 | protected function tearDown(): void 75 | { 76 | parent::tearDown(); 77 | $this->commandBus = null; 78 | $this->queryBus = null; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tests/App/Shared/Application/Query/CollectionTest.php: -------------------------------------------------------------------------------- 1 | expectException(NotFoundException::class); 23 | 24 | new Collection(2, 10, 2, []); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/App/Shared/Domain/ValueObject/DateTimeTest.php: -------------------------------------------------------------------------------- 1 | expectException(DateTimeException::class); 25 | DateTime::fromString(self::BAD_DATETIME); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/App/Shared/Infrastructure/Event/EventCollectorListener.php: -------------------------------------------------------------------------------- 1 | publishedEvents[] = $domainMessage; 15 | } 16 | 17 | public function popEvents(): array 18 | { 19 | $events = $this->publishedEvents; 20 | 21 | $this->publishedEvents = []; 22 | 23 | return $events; 24 | } 25 | 26 | private array $publishedEvents = []; 27 | } 28 | -------------------------------------------------------------------------------- /tests/App/Shared/Infrastructure/Event/Publisher/EventPublisherTest.php: -------------------------------------------------------------------------------- 1 | publisher = $this->service(AsyncEventPublisher::class); 30 | $this->transport = $this->service('messenger.transport.events'); 31 | } 32 | 33 | /** 34 | * @test 35 | * 36 | * @group integration 37 | * 38 | * @throws AssertionFailedException 39 | * @throws DateTimeException 40 | * @throws Throwable 41 | */ 42 | public function events_are_consumed(): void 43 | { 44 | $current = DomainDateTime::now(); 45 | 46 | $data = [ 47 | 'uuid' => $uuid = Uuid::uuid4()->toString(), 48 | 'credentials' => [ 49 | 'email' => 'lol@lol.com', 50 | 'password' => 'lkasjbdalsjdbalsdbaljsdhbalsjbhd987', 51 | ], 52 | 'created_at' => $current->toString(), 53 | ]; 54 | 55 | $this->publisher->handle(DomainMessage::recordNow($uuid, 1, new Metadata(), UserWasCreated::deserialize($data))); 56 | 57 | $this->publisher->publish(); 58 | 59 | $transportMessages = $this->transport->get(); 60 | self::assertCount(1, $transportMessages); 61 | 62 | $event = $transportMessages[0]->getMessage()->getPayload(); 63 | 64 | self::assertInstanceOf(UserWasCreated::class, $event); 65 | self::assertSame($data, $event->serialize(), 'Check that its the same event'); 66 | } 67 | 68 | protected function tearDown(): void 69 | { 70 | $this->publisher = null; 71 | $this->transport = null; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/App/Shared/Infrastructure/Event/Query/EventElasticRepositoryTest.php: -------------------------------------------------------------------------------- 1 | repository = new ElasticSearchEventRepository( 23 | [ 24 | 'hosts' => [ 25 | 'elasticsearch', 26 | ], 27 | ] 28 | ); 29 | $this->repository->reboot(); 30 | $this->repository->refresh(); 31 | } 32 | 33 | /** 34 | * @test 35 | * 36 | * @group integration 37 | * 38 | * @throws AssertionFailedException 39 | * @throws DateTimeException 40 | */ 41 | public function an_event_should_be_stored_in_elastic(): void 42 | { 43 | $data = [ 44 | 'uuid' => $uuid = 'e937f793-45d8-41e9-a756-a2bc711e3172', 45 | 'credentials' => [ 46 | 'email' => 'lol@lol.com', 47 | 'password' => 'lkasjbdalsjdbalsdbaljsdhbalsjbhd987', 48 | ], 49 | 'created_at' => DomainDateTime::now()->toString(), 50 | ]; 51 | 52 | $event = DomainMessage::recordNow($uuid, 1, new Metadata(), UserWasCreated::deserialize($data)); 53 | 54 | $this->repository->store($event); 55 | $this->repository->refresh(); 56 | 57 | $result = $this->repository->search([ 58 | 'query' => [ 59 | 'match' => [ 60 | 'type' => $event->getType(), 61 | ], 62 | ], 63 | ]); 64 | 65 | self::assertSame(1, $result['hits']['total']['value']); 66 | } 67 | 68 | protected function tearDown(): void 69 | { 70 | $this->repository->delete(); 71 | $this->repository = null; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/App/User/Application/Command/ChangeEmail/ChangeEmailHandlerTest.php: -------------------------------------------------------------------------------- 1 | toString(), 'asd@asd.asd', 'password'); 33 | 34 | $this->handle($command); 35 | 36 | $email = 'lol@asd.asd'; 37 | 38 | $command = new ChangeEmailCommand($uuid, $email); 39 | 40 | $this->handle($command); 41 | 42 | /** @var EventCollectorListener $eventCollector 43 | */ 44 | $eventCollector = $this->service(EventCollectorListener::class); 45 | 46 | /** @var DomainMessage[] $events */ 47 | $events = $eventCollector->popEvents(); 48 | 49 | self::assertCount(2, $events); 50 | 51 | /** @var UserEmailChanged $emailChangedEmail */ 52 | $emailChangedEmail = $events[1]->getPayload(); 53 | 54 | self::assertInstanceOf(UserEmailChanged::class, $emailChangedEmail); 55 | self::assertSame($email, $emailChangedEmail->email->toString()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/App/User/Application/Command/SignIn/SignInTest.php: -------------------------------------------------------------------------------- 1 | handle($command); 36 | 37 | /** @var EventCollectorListener $eventCollector */ 38 | $eventCollector = $this->service(EventCollectorListener::class); 39 | /** @var DomainMessage[] $events */ 40 | $events = $eventCollector->popEvents(); 41 | 42 | self::assertInstanceOf(UserSignedIn::class, $events[1]->getPayload()); 43 | } 44 | 45 | /** 46 | * @test 47 | * 48 | * @group integration 49 | * 50 | * @dataProvider invalidCredentials 51 | * 52 | * @throws AssertionFailedException 53 | * @throws Throwable 54 | */ 55 | public function user_sign_up_with_invalid_credentials_must_throw_domain_exception(string $email, string $pass): void 56 | { 57 | $this->expectException(InvalidCredentialsException::class); 58 | 59 | $command = new SignInCommand($email, $pass); 60 | 61 | $this->handle($command); 62 | } 63 | 64 | public function invalidCredentials(): array 65 | { 66 | return [ 67 | [ 68 | 'email' => 'asd@asd.asd', 69 | 'pass' => 'qwerqwer123', 70 | ], 71 | [ 72 | 'email' => 'asd@asd.com', 73 | 'pass' => 'qwerqwer', 74 | ], 75 | ]; 76 | } 77 | 78 | /** 79 | * @throws Exception 80 | * @throws AssertionFailedException 81 | * @throws Throwable 82 | */ 83 | protected function setUp(): void 84 | { 85 | parent::setUp(); 86 | 87 | $command = new SignUpCommand( 88 | Uuid::uuid4()->toString(), 89 | 'asd@asd.asd', 90 | 'qwerqwer' 91 | ); 92 | 93 | $this->handle($command); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tests/App/User/Application/Command/SignUp/SignUpHandlerTest.php: -------------------------------------------------------------------------------- 1 | toString(), $email, 'password'); 30 | $this 31 | ->handle($command); 32 | 33 | /** @var EventCollectorListener $collector */ 34 | $collector = $this->service(EventCollectorListener::class); 35 | 36 | /** @var DomainMessage[] $events */ 37 | $events = $collector->popEvents(); 38 | 39 | self::assertCount(1, $events); 40 | 41 | /** @var UserWasCreated $userCreatedEvent */ 42 | $userCreatedEvent = $events[0]->getPayload(); 43 | 44 | self::assertInstanceOf(UserWasCreated::class, $userCreatedEvent); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/App/User/Application/Query/FindByEmail/FindByEmailHandlerTest.php: -------------------------------------------------------------------------------- 1 | createUserRead(); 28 | 29 | $this->fireTerminateEvent(); 30 | 31 | /** @var Item $result */ 32 | $result = $this->ask(new FindByEmailQuery($email)); 33 | 34 | self::assertInstanceOf(Item::class, $result); 35 | self::assertSame('UserView', $result->type); 36 | self::assertSame($email, $result->resource['credentials.email']->toString()); 37 | } 38 | 39 | /** 40 | * @throws Throwable 41 | * @throws AssertionFailedException 42 | */ 43 | private function createUserRead(): string 44 | { 45 | $uuid = Uuid::uuid4()->toString(); 46 | $email = 'lol@lol.com'; 47 | 48 | $this->handle(new SignUpCommand($uuid, $email, 'password')); 49 | 50 | return $email; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/App/User/Domain/Event/UserEmailChangedTest.php: -------------------------------------------------------------------------------- 1 | 'eb62dfdc-2086-11e8-b467-0ed5f89f718b', 27 | 'email' => 'asd@asd.asd', 28 | 'updated_at' => DateTime::now()->toString(), 29 | ]); 30 | 31 | self::assertInstanceOf(UserEmailChanged::class, $event); 32 | self::assertSame('eb62dfdc-2086-11e8-b467-0ed5f89f718b', $event->uuid->toString()); 33 | self::assertInstanceOf(Email::class, $event->email); 34 | } 35 | 36 | /** 37 | * @test 38 | * 39 | * @group unit 40 | * 41 | * @throws \App\Shared\Domain\Exception\DateTimeException 42 | * @throws \Assert\AssertionFailedException 43 | * @throws \Throwable 44 | */ 45 | public function event_should_fail_when_deserialize_with_wrong_data(): void 46 | { 47 | $this->expectException(\InvalidArgumentException::class); 48 | 49 | UserEmailChanged::deserialize([ 50 | 'uuids' => 'eb62dfdc-2086-11e8-b467-0ed5f89f718b', 51 | 'emails' => 'asd@asd.asd', 52 | 'updated_at' => DateTime::now()->toString(), 53 | ]); 54 | } 55 | 56 | /** 57 | * @test 58 | * 59 | * @group unit 60 | * 61 | * @throws \App\Shared\Domain\Exception\DateTimeException 62 | * @throws \Assert\AssertionFailedException 63 | * @throws \Throwable 64 | */ 65 | public function event_should_be_serializable(): void 66 | { 67 | $event = UserEmailChanged::deserialize([ 68 | 'uuid' => 'eb62dfdc-2086-11e8-b467-0ed5f89f718b', 69 | 'email' => 'asd@asd.asd', 70 | 'updated_at' => DateTime::now()->toString(), 71 | ]); 72 | 73 | $serialized = $event->serialize(); 74 | 75 | self::assertArrayHasKey('uuid', $serialized); 76 | self::assertArrayHasKey('email', $serialized); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/App/User/Domain/Event/UserSignedInTest.php: -------------------------------------------------------------------------------- 1 | 'eb62dfdc-2086-11e8-b467-0ed5f89f718b', 24 | 'email' => 'an@email.com', 25 | ]); 26 | 27 | self::assertSame('eb62dfdc-2086-11e8-b467-0ed5f89f718b', $event->uuid->toString()); 28 | self::assertInstanceOf(Email::class, $event->email); 29 | } 30 | 31 | /** 32 | * @test 33 | * 34 | * @group unit 35 | * 36 | * @throws \Assert\AssertionFailedException 37 | */ 38 | public function event_shoud_be_serializable(): void 39 | { 40 | $event = UserSignedIn::deserialize([ 41 | 'uuid' => 'eb62dfdc-2086-11e8-b467-0ed5f89f718b', 42 | 'email' => 'an@email.com', 43 | ]); 44 | 45 | $serialized = $event->serialize(); 46 | 47 | self::assertArrayHasKey('uuid', $serialized); 48 | self::assertArrayHasKey('email', $serialized); 49 | } 50 | 51 | /** 52 | * @test 53 | * 54 | * @group unit 55 | * 56 | * @throws \Assert\AssertionFailedException 57 | */ 58 | public function event_should_fail_when_deserialize_with_incorrect_data(): void 59 | { 60 | $this->expectException(\InvalidArgumentException::class); 61 | 62 | UserSignedIn::deserialize([ 63 | 'notAnUuid' => 'eb62dfdc-2086-11e8-b467-0ed5f89f718b', 64 | 'notAnEmail' => 'an@email.com', 65 | ]); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/App/User/Domain/ValueObject/Auth/HashedPasswordTest.php: -------------------------------------------------------------------------------- 1 | match('1234567890')); 22 | } 23 | 24 | /** 25 | * @test 26 | * 27 | * @group unit 28 | */ 29 | public function min_6_password_length(): void 30 | { 31 | $this->expectException(\InvalidArgumentException::class); 32 | 33 | HashedPassword::encode('12345'); 34 | } 35 | 36 | /** 37 | * @test 38 | * 39 | * @group unit 40 | */ 41 | public function from_hash_password_should_still_valid(): void 42 | { 43 | $pass = HashedPassword::fromHash((string) HashedPassword::encode('1234567890')); 44 | 45 | self::assertTrue($pass->match('1234567890')); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/App/User/Domain/ValueObject/EmailTest.php: -------------------------------------------------------------------------------- 1 | expectException(\InvalidArgumentException::class); 22 | 23 | Email::fromString('asd'); 24 | } 25 | 26 | /** 27 | * @test 28 | * 29 | * @group unit 30 | * 31 | * @throws \Assert\AssertionFailedException 32 | */ 33 | public function valid_email_should_be_able_to_convert_to_string(): void 34 | { 35 | $email = Email::fromString('an@email.com'); 36 | 37 | self::assertSame('an@email.com', $email->toString()); 38 | self::assertSame('an@email.com', (string) $email); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/TidierListener.php: -------------------------------------------------------------------------------- 1 | getProperties() as $prop) { 33 | if (!$prop->isStatic() && !str_starts_with($prop->getDeclaringClass()->getName(), 'PHPUnit_')) { 34 | $prop->setAccessible(true); 35 | $prop->setValue($target, null); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/UI/Cli/AbstractConsoleTestCase.php: -------------------------------------------------------------------------------- 1 | add($command); 19 | 20 | $command = $application->find($alias); 21 | 22 | return new CommandTester($command); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/UI/Cli/Command/CreateUserCommandTest.php: -------------------------------------------------------------------------------- 1 | service(CommandBusInterface::class); 32 | $commandTester = $this->app($command = new CreateUserCommand($commandBus), 'app:create-user'); 33 | 34 | $commandTester->execute([ 35 | 'command' => $command->getName(), 36 | 'uuid' => Uuid::uuid4()->toString(), 37 | 'email' => $email, 38 | 'password' => 'jorgepass', 39 | ]); 40 | 41 | $output = $commandTester->getDisplay(); 42 | 43 | $this->assertStringContainsString('User Created:', $output); 44 | $this->assertStringContainsString('Email: jorge.arcoma@gmail.com', $output); 45 | 46 | /** @var Item $result */ 47 | $result = $this->ask(new FindByEmailQuery($email)); 48 | 49 | self::assertInstanceOf(Item::class, $result); 50 | self::assertSame('UserView', $result->type); 51 | self::assertSame($email, $result->resource['credentials.email']->toString()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/UI/Http/Rest/Controller/Auth/CheckControllerTest.php: -------------------------------------------------------------------------------- 1 | post('/api/auth_check', [ 20 | '_username' => 'oze@lol.com', 21 | '_password' => 'qwer', 22 | ]); 23 | 24 | self::assertSame(Response::HTTP_UNAUTHORIZED, $this->cli->getResponse()->getStatusCode()); 25 | } 26 | 27 | /** 28 | * @test 29 | * 30 | * @group e2e 31 | */ 32 | public function email_must_be_valid_or_fail_with_400(): void 33 | { 34 | $this->post('/api/auth_check', [ 35 | '_username' => 'oze@', 36 | '_password' => 'qwer', 37 | ]); 38 | 39 | self::assertSame(Response::HTTP_BAD_REQUEST, $this->cli->getResponse()->getStatusCode()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/UI/Http/Rest/Controller/Healthz/HealthzControllerTest.php: -------------------------------------------------------------------------------- 1 | get('/api/healthz'); 20 | 21 | self::assertSame(Response::HTTP_OK, $this->cli->getResponse()->getStatusCode()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/UI/Http/Rest/EventSubscriber/JsonBodyParserSubscriberTest.php: -------------------------------------------------------------------------------- 1 | headers->set('Content-Type', 'application/json'); 26 | 27 | $requestEvent = new RequestEvent( 28 | $this->createMock(HttpKernelInterface::class), 29 | $request, 30 | HttpKernelInterface::MASTER_REQUEST 31 | ); 32 | 33 | $jsonBodyParserSubscriber->onKernelRequest($requestEvent); 34 | 35 | $response = $requestEvent->getResponse(); 36 | 37 | self::assertEquals('Unable to parse json request.', $response->getContent()); 38 | self::assertEquals(Response::HTTP_BAD_REQUEST, $response->getStatusCode()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/UI/Http/Web/Controller/CreateUserTrait.php: -------------------------------------------------------------------------------- 1 | request('GET', '/sign-up'); 16 | 17 | $form = $crawler->selectButton('Send')->form(); 18 | 19 | $form->get('email')->setValue($email); 20 | $form->get('password')->setValue($password); 21 | 22 | $client->submit($form); 23 | 24 | return $client; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/UI/Http/Web/Controller/HomeControllerTest.php: -------------------------------------------------------------------------------- 1 | request('GET', '/'); 21 | 22 | $this->assertGreaterThan(0, $crawler->filter('html:contains("Hello!")')->count()); 23 | $this->assertGreaterThan(0, $crawler->filter('html:contains("Sign up")')->count()); 24 | } 25 | 26 | /** 27 | * @test 28 | * 29 | * @group e2e 30 | */ 31 | public function sign_up_button_should_redirect_to_sign_up_page(): void 32 | { 33 | $client = self::createClient(); 34 | 35 | $crawler = $client->request('GET', '/'); 36 | 37 | $link = $crawler->selectLink('Sign up')->link(); 38 | 39 | $crawler = $client->click($link); 40 | 41 | $this->assertGreaterThan('/sign-up', $crawler->getUri()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/UI/Http/Web/Controller/ProfileControllerTest.php: -------------------------------------------------------------------------------- 1 | request('GET', '/profile'); 23 | 24 | /** @var RedirectResponse $response */ 25 | $response = $client->getResponse(); 26 | $this->assertSame(Response::HTTP_FOUND, $response->getStatusCode()); 27 | $this->assertStringContainsString('/sign-in', $response->getTargetUrl()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/UI/Http/Web/Controller/SignUpControllerTest.php: -------------------------------------------------------------------------------- 1 | request('GET', '/sign-up'); 22 | 23 | $this->assertSame(1, $crawler->filter('label:contains("Email")')->count()); 24 | $this->assertSame(1, $crawler->selectButton('Send')->count()); 25 | } 26 | 27 | /** 28 | * @test 29 | * 30 | * @group e2e 31 | */ 32 | public function sign_up_form_create_user_success(): void 33 | { 34 | $crawler = $this->createUser($email = 'ads@asd.asd'); 35 | 36 | self::assertSame(1, $crawler->filter('html:contains("Hello ' . $email . '")')->count()); 37 | self::assertSame(1, $crawler->filter('html:contains("Your id is ")')->count()); 38 | } 39 | 40 | /** 41 | * @test 42 | * 43 | * @group e2e 44 | */ 45 | public function sign_up_form_create_user_invalid_email(): void 46 | { 47 | $crawler = $this->createUser('jorge@gmail'); 48 | 49 | self::assertSame(1, $crawler->filter('html:contains("Not a valid email")')->count()); 50 | } 51 | 52 | /** 53 | * @test 54 | * 55 | * @group e2e 56 | */ 57 | public function sign_up_form_create_user_with_email_already_taken(): void 58 | { 59 | $this->createUser('jorge.arcoma@gmail.com'); 60 | $crawler = $this->createUser('jorge.arcoma@gmail.com'); 61 | 62 | self::assertSame(1, $crawler->filter('html:contains("Email already registered.")')->count()); 63 | } 64 | 65 | private function createUser(string $email, string $password = 'crqs-demo'): Crawler 66 | { 67 | self::ensureKernelShutdown(); 68 | $client = self::createClient(); 69 | 70 | $crawler = $client->request('GET', '/sign-up'); 71 | 72 | $form = $crawler->selectButton('Send')->form(); 73 | 74 | $form->get('email')->setValue($email); 75 | $form->get('password')->setValue($password); 76 | 77 | return $client->submit($form); 78 | } 79 | } 80 | --------------------------------------------------------------------------------