├── .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 | 
30 | - Next you'll need to configure phpunit in your IDE to have something like:
31 | 
32 | - Now you'll be able to run any php test file or phpunit config.
33 | 
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 | 
10 |
11 | **When** user click in Sign Up button
12 |
13 | **Then** should be redirected to Sign Up page
14 |
15 | 
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 | 
23 |
24 | **Then** it should be registered
25 |
26 | **And** display de user information
27 |
28 | 
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 | 
35 |
36 | **Given** All user events happened in UI
37 |
38 | **And** published in rabbit
39 |
40 | 
41 |
42 | **Then** it should be consumed to be stored in elastic
43 |
44 | **And** visible in Kibana
45 |
46 | 
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 |
7 |
8 | {{ text }}
9 |
10 |
--------------------------------------------------------------------------------
/src/UI/Http/Web/templates/components/header/menu.html.twig:
--------------------------------------------------------------------------------
1 |
2 |
12 |
45 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
--------------------------------------------------------------------------------