├── .env
├── .gitignore
├── .php-cs-fixer.dist.php
├── README.md
├── bin
├── console
└── phpunit
├── composer.json
├── composer.lock
├── config
├── bundles.php
├── packages
│ ├── cache.yaml
│ ├── dev
│ │ ├── debug.yaml
│ │ ├── monolog.yaml
│ │ └── web_profiler.yaml
│ ├── doctrine.yaml
│ ├── doctrine_migrations.yaml
│ ├── framework.yaml
│ ├── mailer.yaml
│ ├── messenger.yaml
│ ├── notifier.yaml
│ ├── prod
│ │ ├── deprecations.yaml
│ │ ├── doctrine.yaml
│ │ └── monolog.yaml
│ ├── routing.yaml
│ ├── security.yaml
│ ├── test
│ │ ├── dama_doctrine_test_bundle.yaml
│ │ ├── doctrine.yaml
│ │ ├── monolog.yaml
│ │ ├── validator.yaml
│ │ └── web_profiler.yaml
│ ├── translation.yaml
│ ├── twig.yaml
│ └── validator.yaml
├── preload.php
├── routes.yaml
├── routes
│ ├── dev
│ │ └── web_profiler.yaml
│ └── framework.yaml
└── services.yaml
├── migrations
├── .gitignore
└── Version20231223153440.php
├── phpunit.xml.dist
├── public
└── index.php
├── src
├── Kernel.php
└── RentCar
│ ├── Application
│ ├── Car
│ │ ├── CreateCarCommand.php
│ │ └── CreateCarCommandHandler.php
│ ├── Customer
│ │ ├── CreateCustomerCommand.php
│ │ └── CreateCustomerCommandHandler.php
│ ├── External
│ │ ├── CarWasCreatedHandler.php
│ │ └── CustomerWasCreatedHandler.php
│ ├── Match
│ │ ├── CreateCarMatchCommand.php
│ │ └── CreateCarMatchCommandHandler.php
│ └── Reservation
│ │ ├── CancelReservationCommand.php
│ │ ├── CancelReservationCommandHandler.php
│ │ ├── CreateReservationCommand.php
│ │ └── CreateReservationCommandHandler.php
│ ├── Domain
│ └── Model
│ │ ├── Car
│ │ ├── Car.php
│ │ ├── CarForCategoryNotFoundException.php
│ │ ├── CarRepository.php
│ │ └── CarWasCreated.php
│ │ ├── Customer
│ │ ├── Customer.php
│ │ ├── CustomerNotFoundException.php
│ │ ├── CustomerRepository.php
│ │ └── CustomerWasCreated.php
│ │ ├── Match
│ │ ├── CarMatch.php
│ │ ├── CarMatchRepository.php
│ │ └── CarWasMatched.php
│ │ └── Reservation
│ │ ├── Reservation.php
│ │ ├── ReservationNotFoundException.php
│ │ ├── ReservationRepository.php
│ │ ├── ReservationWasCancelled.php
│ │ └── ReservationWasCreated.php
│ └── Infrastructure
│ ├── Persistence
│ └── Doctrine
│ │ ├── DoctrineCarMatchRepository.php
│ │ ├── DoctrineCarRepository.php
│ │ ├── DoctrineCustomerRepository.php
│ │ ├── DoctrineReservationRepository.php
│ │ └── Fixtures
│ │ └── AppFixtures.php
│ └── Symfony
│ ├── Console
│ └── RentCarSimulationCommand.php
│ ├── Controller
│ └── CarController.php
│ └── Security
│ └── User.php
├── symfony.lock
├── templates
└── base.html.twig
├── tests
├── RentCar
│ ├── Application
│ │ ├── Car
│ │ │ └── CreateCarCommandHandlerTest.php
│ │ ├── Customer
│ │ │ └── CreateCustomerCommandHandlerTest.php
│ │ ├── Match
│ │ │ └── CreateCarMatchCommandHandlerTest.php
│ │ └── TraceableDomainEventDispatcher.php
│ ├── Domain
│ │ ├── Car
│ │ │ └── CarTest.php
│ │ ├── Customer
│ │ │ └── CustomerTest.php
│ │ ├── Match
│ │ │ └── CarMatchTest.php
│ │ ├── RentCarMother.php
│ │ └── Reservation
│ │ │ └── ReservationTest.php
│ └── Infrastructure
│ │ └── Persistence
│ │ └── InMemory
│ │ ├── InMemoryCarMatchRepository.php
│ │ ├── InMemoryCarRepository.php
│ │ ├── InMemoryCustomerRepository.php
│ │ └── InMemoryReservationRepository.php
└── bootstrap.php
└── translations
└── .gitignore
/.env:
--------------------------------------------------------------------------------
1 | # In all environments, the following files are loaded if they exist,
2 | # the latter taking precedence over the former:
3 | #
4 | # * .env contains default values for the environment variables needed by the app
5 | # * .env.local uncommitted file with local overrides
6 | # * .env.$APP_ENV committed environment-specific defaults
7 | # * .env.$APP_ENV.local uncommitted environment-specific overrides
8 | #
9 | # Real environment variables win over .env files.
10 | #
11 | # DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
12 | #
13 | # Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
14 | # https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
15 |
16 | ###> symfony/framework-bundle ###
17 | APP_ENV=dev
18 | APP_SECRET=719a20cb9fabe94daa4c383100b0ae4c
19 | ###< symfony/framework-bundle ###
20 |
21 | ###> symfony/mailer ###
22 | # MAILER_DSN=smtp://localhost
23 | ###< symfony/mailer ###
24 |
25 | ###> doctrine/doctrine-bundle ###
26 | # Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
27 | # IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
28 | #
29 | DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
30 | # DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7"
31 | # DATABASE_URL="postgresql://symfony:ChangeMe@127.0.0.1:5432/app?serverVersion=13&charset=utf8"
32 | ###< doctrine/doctrine-bundle ###
33 |
34 | ###> symfony/messenger ###
35 | # Choose one of the transports below
36 | MESSENGER_TRANSPORT_DSN=doctrine://default
37 | # MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
38 | # MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
39 | ###< symfony/messenger ###
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ###> symfony/framework-bundle ###
2 | /.env.local
3 | /.env.local.php
4 | /.env.*.local
5 | /config/secrets/prod/prod.decrypt.private.php
6 | /public/bundles/
7 | /var/
8 | /vendor/
9 | ###< symfony/framework-bundle ###
10 |
11 | ###> symfony/phpunit-bridge ###
12 | .phpunit.result.cache
13 | /phpunit.xml
14 | ###< symfony/phpunit-bridge ###
15 | ###> friendsofphp/php-cs-fixer ###
16 | /.php-cs-fixer.php
17 | /.php-cs-fixer.cache
18 | ###< friendsofphp/php-cs-fixer ###
19 |
--------------------------------------------------------------------------------
/.php-cs-fixer.dist.php:
--------------------------------------------------------------------------------
1 | in(__DIR__)
5 | ->exclude('var')
6 | ;
7 |
8 | return (new PhpCsFixer\Config())
9 | ->setRules([
10 | '@Symfony' => true,
11 | ])
12 | ->setFinder($finder)
13 | ;
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SymfonyWorld Winter Edition 2021
2 |
3 |
4 | This project is a demo from the presentation of Hugo Monteiro at SymfonyWorld Winter edition 2021:
5 |
6 | ["Decoupling your application using Symfony Messenger and events"](https://live.symfony.com/2021-world-winter/schedule#session-606)
7 |
8 |
9 | ## Setup the application
10 |
11 | ```
12 | composer install
13 |
14 | bin/console doctrine:migrations:migrate
15 | bin/console doctrine:fixtures:load
16 |
17 | // create a customer, car and reservation and cancel it
18 | bin/console app:rentcar:simulation -vvv
19 |
20 | // get domain events from the database (from the stored_event table) and push to the transport (outbox) - infinite loop
21 | bin/console app:domain:events:publish -vvv
22 |
23 | // consume domain events in another handler using the transport
24 | bin/console messenger:consume async
25 | ```
26 |
27 | _Note: The transport configured is the doctrine one, so the sqlite database will be used by default._
28 |
29 | ## Run unit tests
30 |
31 | ```
32 | bin/phpunit -c phpunit.xml.dist
33 | ```
34 |
35 | ## What should you expect?
36 |
37 | - Domain events in a table "stored_event" after executing the "simulation" command
38 | - Aggregate roots in their tables (e.g. customer, reservation, etc)
39 | - Worker that sends the domain events from the database to the transport configured
40 | - Use Symfony Messenger consume command to consume domain events published
41 |
42 | ## Work in progress
43 |
44 | - API with all the handler actions and API tests
45 | - Create more tests to cover all invariants
46 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | =8.2",
8 | "monteiro/ddd-bundle": "^0.1",
9 | "ext-ctype": "*",
10 | "ext-iconv": "*",
11 | "ext-pcntl": "*",
12 | "beberlei/assert": "^3.3",
13 | "doctrine/doctrine-bundle": "^2.11",
14 | "doctrine/doctrine-migrations-bundle": "^3.3",
15 | "doctrine/orm": "^2.17",
16 | "phpdocumentor/reflection-docblock": "^5.3",
17 | "phpstan/phpdoc-parser": "^1.24",
18 | "symfony/asset": "7.0.*",
19 | "symfony/console": "7.0.*",
20 | "symfony/doctrine-messenger": "7.0.*",
21 | "symfony/dotenv": "7.0.*",
22 | "symfony/expression-language": "7.0.*",
23 | "symfony/flex": "^2",
24 | "symfony/form": "7.0.*",
25 | "symfony/framework-bundle": "7.0.*",
26 | "symfony/http-client": "7.0.*",
27 | "symfony/intl": "7.0.*",
28 | "symfony/mailer": "7.0.*",
29 | "symfony/mime": "7.0.*",
30 | "symfony/monolog-bundle": "^3.0",
31 | "symfony/notifier": "7.0.*",
32 | "symfony/process": "7.0.*",
33 | "symfony/property-access": "7.0.*",
34 | "symfony/property-info": "7.0.*",
35 | "symfony/runtime": "7.0.*",
36 | "symfony/security-bundle": "7.0.*",
37 | "symfony/serializer": "7.0.*",
38 | "symfony/string": "7.0.*",
39 | "symfony/translation": "7.0.*",
40 | "symfony/twig-bundle": "7.0.*",
41 | "symfony/uid": "7.0.*",
42 | "symfony/validator": "7.0.*",
43 | "symfony/web-link": "7.0.*",
44 | "symfony/yaml": "7.0.*",
45 | "twig/extra-bundle": "^2.12|^3.0",
46 | "twig/twig": "^2.12|^3.0"
47 | },
48 | "config": {
49 | "allow-plugins": {
50 | "php-http/discovery": true,
51 | "symfony/flex": true,
52 | "symfony/runtime": true
53 | },
54 | "sort-packages": true
55 | },
56 | "autoload": {
57 | "psr-4": {
58 | "App\\": "src/"
59 | }
60 | },
61 | "autoload-dev": {
62 | "psr-4": {
63 | "App\\Tests\\": "tests/"
64 | }
65 | },
66 | "replace": {
67 | "symfony/polyfill-ctype": "*",
68 | "symfony/polyfill-iconv": "*",
69 | "symfony/polyfill-php72": "*",
70 | "symfony/polyfill-php73": "*",
71 | "symfony/polyfill-php74": "*",
72 | "symfony/polyfill-php80": "*",
73 | "symfony/polyfill-php81": "*"
74 | },
75 | "scripts": {
76 | "auto-scripts": {
77 | "cache:clear": "symfony-cmd",
78 | "assets:install %PUBLIC_DIR%": "symfony-cmd"
79 | },
80 | "post-install-cmd": [
81 | "@auto-scripts"
82 | ],
83 | "post-update-cmd": [
84 | "@auto-scripts"
85 | ]
86 | },
87 | "conflict": {
88 | "symfony/symfony": "*"
89 | },
90 | "extra": {
91 | "symfony": {
92 | "allow-contrib": false,
93 | "require": "7.0.*"
94 | }
95 | },
96 | "require-dev": {
97 | "dama/doctrine-test-bundle": "^8.0",
98 | "doctrine/doctrine-fixtures-bundle": "^3.5",
99 | "symfony/browser-kit": "7.0.*",
100 | "symfony/css-selector": "7.0.*",
101 | "symfony/debug-bundle": "7.0.*",
102 | "symfony/maker-bundle": "^1.0",
103 | "symfony/phpunit-bridge": "^6.3",
104 | "symfony/stopwatch": "7.0.*",
105 | "symfony/web-profiler-bundle": "7.0.*"
106 | },
107 | "repositories": [
108 | {
109 | "type": "vcs",
110 | "url": "https://github.com/monteiro/ddd-bundle"
111 | }
112 | ]
113 | }
114 |
--------------------------------------------------------------------------------
/config/bundles.php:
--------------------------------------------------------------------------------
1 | ['all' => true],
5 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
6 | Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
7 | Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
8 | Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
9 | Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
10 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
11 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
12 | Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
13 | Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
14 | Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
15 | DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
16 | App\DDDBundle\DDDBundle::class => ['all' => true],
17 | ];
18 |
--------------------------------------------------------------------------------
/config/packages/cache.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | cache:
3 | # Unique name of your app: used to compute stable namespaces for cache keys.
4 | #prefix_seed: your_vendor_name/app_name
5 |
6 | # The "app" cache stores to the filesystem by default.
7 | # The data in this cache should persist between deploys.
8 | # Other options include:
9 |
10 | # Redis
11 | #app: cache.adapter.redis
12 | #default_redis_provider: redis://localhost
13 |
14 | # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
15 | #app: cache.adapter.apcu
16 |
17 | # Namespaced pools use the above "app" backend by default
18 | #pools:
19 | #my.dedicated.cache: null
20 |
--------------------------------------------------------------------------------
/config/packages/dev/debug.yaml:
--------------------------------------------------------------------------------
1 | debug:
2 | # Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.
3 | # See the "server:dump" command to start a new server.
4 | dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%"
5 |
--------------------------------------------------------------------------------
/config/packages/dev/monolog.yaml:
--------------------------------------------------------------------------------
1 | monolog:
2 | handlers:
3 | main:
4 | type: stream
5 | path: "%kernel.logs_dir%/%kernel.environment%.log"
6 | level: debug
7 | channels: ["!event"]
8 | # uncomment to get logging in your browser
9 | # you may have to allow bigger header sizes in your Web server configuration
10 | #firephp:
11 | # type: firephp
12 | # level: info
13 | #chromephp:
14 | # type: chromephp
15 | # level: info
16 | console:
17 | type: console
18 | process_psr_3_messages: false
19 | channels: ["!event", "!doctrine", "!console"]
20 |
--------------------------------------------------------------------------------
/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 | url: '%env(resolve:DATABASE_URL)%'
4 |
5 | # IMPORTANT: You MUST configure your server version,
6 | # either here or in the DATABASE_URL env var (see .env file)
7 | #server_version: '13'
8 | orm:
9 | auto_generate_proxy_classes: true
10 | enable_lazy_ghost_objects: true
11 | validate_xml_mapping: true
12 | naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
13 | auto_mapping: true
14 | report_fields_where_declared: true
15 | mappings:
16 | App:
17 | is_bundle: false
18 | type: attribute
19 | dir: '%kernel.project_dir%/src/RentCar/Domain'
20 | prefix: 'App\RentCar\Domain'
21 | alias: App
22 |
--------------------------------------------------------------------------------
/config/packages/doctrine_migrations.yaml:
--------------------------------------------------------------------------------
1 | doctrine_migrations:
2 | migrations_paths:
3 | # namespace is arbitrary but should be different from App\Migrations
4 | # as migrations classes should NOT be autoloaded
5 | 'DoctrineMigrations': '%kernel.project_dir%/migrations'
6 | enable_profiler: '%kernel.debug%'
7 |
--------------------------------------------------------------------------------
/config/packages/framework.yaml:
--------------------------------------------------------------------------------
1 | # see https://symfony.com/doc/current/reference/configuration/framework.html
2 | framework:
3 | secret: '%env(APP_SECRET)%'
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 |
15 | #esi: true
16 | #fragments: true
17 | php_errors:
18 | log: true
19 |
20 | when@test:
21 | framework:
22 | test: true
23 | session:
24 | storage_factory_id: session.storage.factory.mock_file
25 |
--------------------------------------------------------------------------------
/config/packages/mailer.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | mailer:
3 | dsn: '%env(MAILER_DSN)%'
4 |
--------------------------------------------------------------------------------
/config/packages/messenger.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | messenger:
3 | default_bus: command.bus
4 | buses:
5 | command.bus:
6 | middleware:
7 | - doctrine_transaction
8 | event.bus:
9 | # the 'allow_no_handlers' middleware allows to have no handler
10 | default_middleware: allow_no_handlers
11 | transports:
12 | async: '%env(MESSENGER_TRANSPORT_DSN)%'
13 | failed: 'doctrine://default?queue_name=failed'
14 | sync: 'sync://'
15 |
16 | routing:
17 | App\RentCar\Domain\Model\Car\CarWasCreated: async
18 | App\RentCar\Domain\Model\Customer\CustomerWasCreated: async
19 | App\RentCar\Domain\Model\Reservation\ReservationWasCreated: async
20 | App\RentCar\Domain\Model\Reservation\ReservationWasCancelled: async
21 |
--------------------------------------------------------------------------------
/config/packages/notifier.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | notifier:
3 | #chatter_transports:
4 | # slack: '%env(SLACK_DSN)%'
5 | # telegram: '%env(TELEGRAM_DSN)%'
6 | #texter_transports:
7 | # twilio: '%env(TWILIO_DSN)%'
8 | # nexmo: '%env(NEXMO_DSN)%'
9 | channel_policy:
10 | # use chat/slack, chat/telegram, sms/twilio or sms/nexmo
11 | urgent: ['email']
12 | high: ['email']
13 | medium: ['email']
14 | low: ['email']
15 | admin_recipients:
16 | - { email: admin@example.com }
17 |
--------------------------------------------------------------------------------
/config/packages/prod/deprecations.yaml:
--------------------------------------------------------------------------------
1 | # As of Symfony 5.1, deprecations are logged in the dedicated "deprecation" channel when it exists
2 | #monolog:
3 | # channels: [deprecation]
4 | # handlers:
5 | # deprecation:
6 | # type: stream
7 | # channels: [deprecation]
8 | # path: php://stderr
9 |
--------------------------------------------------------------------------------
/config/packages/prod/doctrine.yaml:
--------------------------------------------------------------------------------
1 | doctrine:
2 | orm:
3 | auto_generate_proxy_classes: false
4 | query_cache_driver:
5 | type: pool
6 | pool: doctrine.system_cache_pool
7 | result_cache_driver:
8 | type: pool
9 | pool: doctrine.result_cache_pool
10 |
11 | framework:
12 | cache:
13 | pools:
14 | doctrine.result_cache_pool:
15 | adapter: cache.app
16 | doctrine.system_cache_pool:
17 | adapter: cache.system
18 |
--------------------------------------------------------------------------------
/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 # How many messages should be saved? Prevent memory leaks
9 | nested:
10 | type: stream
11 | path: php://stderr
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/routing.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | router:
3 | utf8: true
4 |
5 | # Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
6 | # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
7 | #default_uri: http://localhost
8 |
9 | when@prod:
10 | framework:
11 | router:
12 | strict_requirements: null
13 |
--------------------------------------------------------------------------------
/config/packages/security.yaml:
--------------------------------------------------------------------------------
1 | security:
2 | # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
3 | password_hashers:
4 | Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
5 | App\RentCar\Infrastructure\Symfony\Security\User:
6 | algorithm: auto
7 |
8 | # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
9 | providers:
10 | # used to reload user from session & other features (e.g. switch_user)
11 | app_user_provider:
12 | entity:
13 | class: App\RentCar\Infrastructure\Symfony\Security
14 | property: email
15 | firewalls:
16 | dev:
17 | pattern: ^/(_(profiler|wdt)|css|images|js)/
18 | security: false
19 | main:
20 | lazy: true
21 | provider: app_user_provider
22 |
23 | # activate different ways to authenticate
24 | # https://symfony.com/doc/current/security.html#the-firewall
25 |
26 | # https://symfony.com/doc/current/security/impersonating_user.html
27 | # switch_user: true
28 |
29 | # Easy way to control access for large sections of your site
30 | # Note: Only the *first* access control that matches will be used
31 | access_control:
32 | # - { path: ^/admin, roles: ROLE_ADMIN }
33 | # - { path: ^/profile, roles: ROLE_USER }
34 |
--------------------------------------------------------------------------------
/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/doctrine.yaml:
--------------------------------------------------------------------------------
1 | doctrine:
2 | dbal:
3 | # "TEST_TOKEN" is typically set by ParaTest
4 | dbname_suffix: '_test%env(default::TEST_TOKEN)%'
5 |
--------------------------------------------------------------------------------
/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/validator.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | validation:
3 | not_compromised_password: false
4 |
--------------------------------------------------------------------------------
/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/translation.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | default_locale: en
3 | translator:
4 | default_path: '%kernel.project_dir%/translations'
5 | fallbacks:
6 | - en
7 | # providers:
8 | # crowdin:
9 | # dsn: '%env(CROWDIN_DSN)%'
10 | # loco:
11 | # dsn: '%env(LOCO_DSN)%'
12 | # lokalise:
13 | # dsn: '%env(LOKALISE_DSN)%'
14 |
--------------------------------------------------------------------------------
/config/packages/twig.yaml:
--------------------------------------------------------------------------------
1 | twig:
2 | default_path: '%kernel.project_dir%/templates'
3 |
4 | when@test:
5 | twig:
6 | strict_variables: true
7 |
--------------------------------------------------------------------------------
/config/packages/validator.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | validation:
3 | email_validation_mode: html5
4 |
5 | # Enables validator auto-mapping support.
6 | # For instance, basic validation constraints will be inferred from Doctrine's metadata.
7 | #auto_mapping:
8 | # App\Entity\: []
9 |
--------------------------------------------------------------------------------
/config/preload.php:
--------------------------------------------------------------------------------
1 | addSql('CREATE TABLE car (id VARCHAR(36) NOT NULL, brand VARCHAR(255) NOT NULL, model VARCHAR(255) NOT NULL, category VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
24 | $this->addSql('CREATE TABLE car_match (id VARCHAR(36) NOT NULL, reservation_id VARCHAR(36) DEFAULT NULL, car_id VARCHAR(36) DEFAULT NULL, PRIMARY KEY(id), CONSTRAINT FK_A5F040D2B83297E7 FOREIGN KEY (reservation_id) REFERENCES reservation (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_A5F040D2C3C6F69F FOREIGN KEY (car_id) REFERENCES car (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
25 | $this->addSql('CREATE UNIQUE INDEX UNIQ_A5F040D2B83297E7 ON car_match (reservation_id)');
26 | $this->addSql('CREATE INDEX IDX_A5F040D2C3C6F69F ON car_match (car_id)');
27 | $this->addSql('CREATE TABLE customer (id VARCHAR(36) NOT NULL, name VARCHAR(255) NOT NULL, address VARCHAR(255) NOT NULL, phone VARCHAR(50) NOT NULL, email VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
28 | $this->addSql('CREATE TABLE event_store (id BLOB NOT NULL --(DC2Type:uuid)
29 | , event_name VARCHAR(255) NOT NULL, event_body CLOB NOT NULL --(DC2Type:json)
30 | , aggregate_root_id VARCHAR(255) NOT NULL, user_id VARCHAR(32) NOT NULL, published BOOLEAN NOT NULL, occurred_on DATETIME NOT NULL --(DC2Type:datetime_immutable)
31 | , PRIMARY KEY(id))');
32 | $this->addSql('CREATE INDEX IDX_BE4CE95B683C6017 ON event_store (published)');
33 | $this->addSql('CREATE TABLE reservation (id VARCHAR(36) NOT NULL, customer_id VARCHAR(36) DEFAULT NULL, location VARCHAR(255) NOT NULL, pick_up_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
34 | , return_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
35 | , category VARCHAR(255) NOT NULL, status VARCHAR(255) NOT NULL, PRIMARY KEY(id), CONSTRAINT FK_42C849559395C3F3 FOREIGN KEY (customer_id) REFERENCES customer (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
36 | $this->addSql('CREATE INDEX IDX_42C849559395C3F3 ON reservation (customer_id)');
37 | $this->addSql('CREATE TABLE messenger_messages (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, body CLOB NOT NULL, headers CLOB NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
38 | , available_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
39 | , delivered_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable)
40 | )');
41 | $this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0 ON messenger_messages (queue_name)');
42 | $this->addSql('CREATE INDEX IDX_75EA56E0E3BD61CE ON messenger_messages (available_at)');
43 | $this->addSql('CREATE INDEX IDX_75EA56E016BA31DB ON messenger_messages (delivered_at)');
44 | }
45 |
46 | public function down(Schema $schema): void
47 | {
48 | // this down() migration is auto-generated, please modify it to your needs
49 | $this->addSql('DROP TABLE car');
50 | $this->addSql('DROP TABLE car_match');
51 | $this->addSql('DROP TABLE customer');
52 | $this->addSql('DROP TABLE event_store');
53 | $this->addSql('DROP TABLE reservation');
54 | $this->addSql('DROP TABLE messenger_messages');
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | tests
23 |
24 |
25 |
26 |
27 |
28 | src
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/index.php:
--------------------------------------------------------------------------------
1 | brand = $brand;
15 | $this->model = $model;
16 | $this->category = $category;
17 | $this->actorId = $actorId;
18 | }
19 |
20 | /**
21 | * @return string
22 | */
23 | public function getBrand(): string
24 | {
25 | return $this->brand;
26 | }
27 |
28 | /**
29 | * @return string
30 | */
31 | public function getModel(): string
32 | {
33 | return $this->model;
34 | }
35 |
36 | /**
37 | * @return string
38 | */
39 | public function getCategory(): string
40 | {
41 | return $this->category;
42 | }
43 |
44 | /**
45 | * @return string
46 | */
47 | public function getActorId(): string
48 | {
49 | return $this->actorId;
50 | }
51 | }
--------------------------------------------------------------------------------
/src/RentCar/Application/Car/CreateCarCommandHandler.php:
--------------------------------------------------------------------------------
1 | carRepository = $carRepository;
17 | $this->domainEventDispatcher = $domainEventDispatcher;
18 | }
19 |
20 | public function __invoke(CreateCarCommand $command): string
21 | {
22 | $newCar = Car::create(
23 | $this->carRepository->nextIdentity(),
24 | $command->getBrand(),
25 | $command->getModel(),
26 | $command->getCategory(),
27 | $command->getActorId()
28 | );
29 |
30 | $this->carRepository->save($newCar);
31 | $this->domainEventDispatcher->dispatchAll($newCar->releaseEvents());
32 |
33 | return $newCar->getId();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/RentCar/Application/Customer/CreateCustomerCommand.php:
--------------------------------------------------------------------------------
1 | name = $name;
16 | $this->address = $address;
17 | $this->phone = $phone;
18 | $this->email = $email;
19 | $this->actorId = $actorId;
20 | }
21 |
22 | /**
23 | * @return string
24 | */
25 | public function getName(): string
26 | {
27 | return $this->name;
28 | }
29 |
30 | /**
31 | * @return string
32 | */
33 | public function getAddress(): string
34 | {
35 | return $this->address;
36 | }
37 |
38 | /**
39 | * @return string
40 | */
41 | public function getPhone(): string
42 | {
43 | return $this->phone;
44 | }
45 |
46 | /**
47 | * @return string
48 | */
49 | public function getEmail(): string
50 | {
51 | return $this->email;
52 | }
53 |
54 | /**
55 | * @return string
56 | */
57 | public function getActorId(): string
58 | {
59 | return $this->actorId;
60 | }
61 |
62 | }
--------------------------------------------------------------------------------
/src/RentCar/Application/Customer/CreateCustomerCommandHandler.php:
--------------------------------------------------------------------------------
1 | customerRepository = $customerRepository;
18 | $this->domainEventDispatcher = $domainEventDispatcher;
19 | }
20 |
21 | /**
22 | * @throws AssertionFailedException
23 | */
24 | public function __invoke(CreateCustomerCommand $command)
25 | {
26 | $customer = Customer::create(
27 | $this->customerRepository->nextIdentity(),
28 | $command->getName(),
29 | $command->getAddress(),
30 | $command->getPhone(),
31 | $command->getEmail(),
32 | $command->getActorId()
33 | );
34 |
35 | $this->customerRepository->save($customer);
36 | $this->domainEventDispatcher->dispatchAll($customer->releaseEvents());
37 |
38 | return $customer->getId();
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/RentCar/Application/External/CarWasCreatedHandler.php:
--------------------------------------------------------------------------------
1 | logger = $logger;
18 | }
19 |
20 | public function __invoke(CarWasCreated $carWasCreated)
21 | {
22 | $this->logger->info(
23 | sprintf('the car "%s" has been created. Upgrade list of cars projection?', $carWasCreated->getAggregateRootId())
24 | );
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/RentCar/Application/External/CustomerWasCreatedHandler.php:
--------------------------------------------------------------------------------
1 | logger = $logger;
17 | }
18 |
19 | public function __invoke(CustomerWasCreated $customerWasCreated)
20 | {
21 | $this->logger->info(
22 | sprintf('the customer "%s" has been created. Send a welcoming email', $customerWasCreated->getAggregateRootId())
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/RentCar/Application/Match/CreateCarMatchCommand.php:
--------------------------------------------------------------------------------
1 | reservationId = $reservationId;
12 | }
13 |
14 | public function getReservationId(): string
15 | {
16 | return $this->reservationId;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/RentCar/Application/Match/CreateCarMatchCommandHandler.php:
--------------------------------------------------------------------------------
1 | reservationRepository = $reservationRepository;
22 | $this->carRepository = $carRepository;
23 | $this->carMatchRepository = $carMatchRepository;
24 | }
25 |
26 | public function __invoke(CreateCarMatchCommand $command): string
27 | {
28 | $reservation = $this->reservationRepository->findById($command->getReservationId());
29 | $carMatch = CarMatch::match(
30 | $this->carMatchRepository->nextIdentity(),
31 | $reservation,
32 | $this->carRepository
33 | );
34 |
35 | $this->carMatchRepository->save($carMatch);
36 |
37 | return $carMatch->getId();
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/RentCar/Application/Reservation/CancelReservationCommand.php:
--------------------------------------------------------------------------------
1 | reservationId = $reservationId;
19 | $this->actorId = $actorId;
20 | }
21 |
22 | /**
23 | * @return string
24 | */
25 | public function getReservationId(): string
26 | {
27 | return $this->reservationId;
28 | }
29 |
30 | /**
31 | * @return string
32 | */
33 | public function getActorId(): string
34 | {
35 | return $this->actorId;
36 | }
37 | }
--------------------------------------------------------------------------------
/src/RentCar/Application/Reservation/CancelReservationCommandHandler.php:
--------------------------------------------------------------------------------
1 | reservationRepository = $reservationRepository;
17 | $this->domainEventDispatcher = $domainEventDispatcher;
18 | }
19 |
20 | public function __invoke(CancelReservationCommand $command)
21 | {
22 | $reservation = $this->reservationRepository->findById($command->getReservationId());
23 |
24 | $reservation->cancel($command->getActorId());
25 | $this->reservationRepository->save($reservation);
26 | $this->domainEventDispatcher->dispatchAll($reservation->releaseEvents());
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/RentCar/Application/Reservation/CreateReservationCommand.php:
--------------------------------------------------------------------------------
1 | location = $location;
26 | $this->pickUpAt = $pickUpAt;
27 | $this->returnAt = $returnAt;
28 | $this->category = $category;
29 | $this->customerId = $customerId;
30 | $this->actorId = $actorId;
31 | }
32 |
33 | /**
34 | * @return string
35 | */
36 | public function getLocation(): string
37 | {
38 | return $this->location;
39 | }
40 |
41 | /**
42 | * @return \DateTimeImmutable
43 | */
44 | public function getPickUpAt(): \DateTimeImmutable
45 | {
46 | return $this->pickUpAt;
47 | }
48 |
49 | /**
50 | * @return \DateTimeImmutable
51 | */
52 | public function getReturnAt(): \DateTimeImmutable
53 | {
54 | return $this->returnAt;
55 | }
56 |
57 | /**
58 | * @return string
59 | */
60 | public function getCategory(): string
61 | {
62 | return $this->category;
63 | }
64 |
65 | /**
66 | * @return string
67 | */
68 | public function getCustomerId(): string
69 | {
70 | return $this->customerId;
71 | }
72 |
73 | /**
74 | * @return string
75 | */
76 | public function getActorId(): string
77 | {
78 | return $this->actorId;
79 | }
80 |
81 |
82 |
83 | }
--------------------------------------------------------------------------------
/src/RentCar/Application/Reservation/CreateReservationCommandHandler.php:
--------------------------------------------------------------------------------
1 | reservationRepository = $reservationRepository;
19 | $this->customerRepository = $customerRepository;
20 | }
21 |
22 | public function __invoke(CreateReservationCommand $command)
23 | {
24 | $customer = $this->customerRepository->findById($command->getCustomerId());
25 |
26 | $reservation = Reservation::create(
27 | $this->reservationRepository->nextIdentity(),
28 | $command->getLocation(),
29 | $command->getPickUpAt(),
30 | $command->getReturnAt(),
31 | $command->getCategory(),
32 | $command->getActorId(),
33 | $customer
34 | );
35 |
36 | $this->reservationRepository->save($reservation);
37 |
38 | return $reservation->getId();
39 | }
40 | }
--------------------------------------------------------------------------------
/src/RentCar/Domain/Model/Car/Car.php:
--------------------------------------------------------------------------------
1 | id = $id;
34 | $this->brand = $brand;
35 | $this->model = $model;
36 | $this->category = $category;
37 |
38 | $this->record(new CarWasCreated($id, $actorId));
39 | }
40 |
41 | public function getId(): string
42 | {
43 | return $this->id;
44 | }
45 |
46 | public function getBrand(): ?string
47 | {
48 | return $this->brand;
49 | }
50 |
51 | public function getModel(): ?string
52 | {
53 | return $this->model;
54 | }
55 |
56 | public function getCategory(): ?string
57 | {
58 | return $this->category;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/RentCar/Domain/Model/Car/CarForCategoryNotFoundException.php:
--------------------------------------------------------------------------------
1 | id = $aggregateRootId;
16 | $this->actorId = $actorId;
17 | $this->occurredOn = new \DateTimeImmutable();
18 | }
19 |
20 | public function getAggregateRootId(): string
21 | {
22 | return $this->id;
23 | }
24 |
25 | public function getOccurredOn(): \DateTimeImmutable
26 | {
27 | return $this->occurredOn;
28 | }
29 |
30 | public function getUserId(): ?string
31 | {
32 | return $this->actorId;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/RentCar/Domain/Model/Customer/Customer.php:
--------------------------------------------------------------------------------
1 | id = $id;
52 | $this->name = $name;
53 | $this->address = $address;
54 | $this->phone = $phone;
55 | $this->email = $email;
56 | $this->actorId = $actorId;
57 |
58 | $this->record(new CustomerWasCreated($id, $actorId));
59 | }
60 |
61 | public function getId(): string
62 | {
63 | return $this->id;
64 | }
65 |
66 | public function getName(): ?string
67 | {
68 | return $this->name;
69 | }
70 |
71 | public function getAddress(): ?string
72 | {
73 | return $this->address;
74 | }
75 |
76 | public function getPhone(): ?string
77 | {
78 | return $this->phone;
79 | }
80 |
81 | public function getEmail(): ?string
82 | {
83 | return $this->email;
84 | }
85 |
86 | public function getActorId(): string
87 | {
88 | return $this->actorId;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/RentCar/Domain/Model/Customer/CustomerNotFoundException.php:
--------------------------------------------------------------------------------
1 | id = $aggregateRootId;
16 | $this->actorId = $actorId;
17 | $this->occurredOn = new \DateTimeImmutable();
18 | }
19 |
20 | public function getAggregateRootId(): string
21 | {
22 | return $this->id;
23 | }
24 |
25 | public function getOccurredOn(): \DateTimeImmutable
26 | {
27 | return $this->occurredOn;
28 | }
29 |
30 | public function getUserId(): ?string
31 | {
32 | return $this->actorId;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/RentCar/Domain/Model/Match/CarMatch.php:
--------------------------------------------------------------------------------
1 | findOneByCategory($reservation->getCategory());
30 |
31 | return new self(
32 | $matchId,
33 | $reservation,
34 | $car
35 | );
36 | }
37 |
38 | public function __construct(string $id, Reservation $reservation, Car $car)
39 | {
40 | $this->id = $id;
41 | $this->reservation = $reservation;
42 | $this->car = $car;
43 | $this->record(new CarWasMatched($id));
44 | }
45 |
46 | public function getId(): string
47 | {
48 | return $this->id;
49 | }
50 |
51 | public function getReservation(): Reservation
52 | {
53 | return $this->reservation;
54 | }
55 |
56 | public function getCar(): Car
57 | {
58 | return $this->car;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/RentCar/Domain/Model/Match/CarMatchRepository.php:
--------------------------------------------------------------------------------
1 | id = $aggregateRootId;
14 | $this->occurredOn = new \DateTimeImmutable();
15 | }
16 |
17 | public function getAggregateRootId(): string
18 | {
19 | return $this->id;
20 | }
21 |
22 | public function getOccurredOn(): \DateTimeImmutable
23 | {
24 | return $this->occurredOn;
25 | }
26 |
27 | public function getUserId(): ?string
28 | {
29 | return null;
30 | }
31 | }
--------------------------------------------------------------------------------
/src/RentCar/Domain/Model/Reservation/Reservation.php:
--------------------------------------------------------------------------------
1 | id = $id;
80 | $this->location = $location;
81 | $this->pickUpAt = $pickUpAt;
82 | $this->returnAt = $returnAt;
83 | $this->category = $category;
84 | $this->customer = $customer;
85 | $this->actorId = $actorId;
86 | $this->status = 'pending';
87 |
88 | $this->record(new ReservationWasCreated($id, $actorId));
89 | }
90 |
91 | public function cancel(string $actorId) {
92 | Assertion::uuid($actorId);
93 | if ($this->status === self::STATUS_CANCELLED) {
94 | throw new \InvalidArgumentException(sprintf('The reservation with id "%s" was already cancelled', $this->id));
95 | }
96 |
97 | $this->status = self::STATUS_CANCELLED;
98 |
99 | $this->record(new ReservationWasCancelled($this->id, $actorId));
100 | }
101 |
102 | public function getId(): string
103 | {
104 | return $this->id;
105 | }
106 |
107 | public function getLocation(): string
108 | {
109 | return $this->location;
110 | }
111 |
112 | public function getPickUpAt(): \DateTimeImmutable
113 | {
114 | return $this->pickUpAt;
115 | }
116 |
117 | public function getReturnAt(): \DateTimeImmutable
118 | {
119 | return $this->returnAt;
120 | }
121 |
122 | public function getCategory(): string
123 | {
124 | return $this->category;
125 | }
126 |
127 | public function getStatus(): string
128 | {
129 | return $this->status;
130 | }
131 |
132 | /**
133 | * @return string
134 | */
135 | public function getActorId(): string
136 | {
137 | return $this->actorId;
138 | }
139 |
140 | /**
141 | * @return Customer
142 | */
143 | public function getCustomer(): Customer
144 | {
145 | return $this->customer;
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/RentCar/Domain/Model/Reservation/ReservationNotFoundException.php:
--------------------------------------------------------------------------------
1 | id = $aggregateRootId;
17 | $this->actorId = $actorId;
18 | $this->occurredOn = new \DateTimeImmutable();
19 | }
20 |
21 | public function getAggregateRootId(): string
22 | {
23 | return $this->id;
24 | }
25 |
26 | public function getOccurredOn(): \DateTimeImmutable
27 | {
28 | return $this->occurredOn;
29 | }
30 |
31 | public function getUserId(): ?string
32 | {
33 | return $this->actorId;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/RentCar/Domain/Model/Reservation/ReservationWasCreated.php:
--------------------------------------------------------------------------------
1 | id = $aggregateRootId;
18 | $this->actorId = $actorId;
19 | $this->occurredOn = new \DateTimeImmutable();
20 | }
21 |
22 | public function getAggregateRootId(): string
23 | {
24 | return $this->id;
25 | }
26 |
27 | public function getOccurredOn(): \DateTimeImmutable
28 | {
29 | return $this->occurredOn;
30 | }
31 |
32 | public function getUserId(): ?string
33 | {
34 | return $this->actorId;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/RentCar/Infrastructure/Persistence/Doctrine/DoctrineCarMatchRepository.php:
--------------------------------------------------------------------------------
1 | getEntityManager()->persist($carMatch);
27 | }
28 |
29 | public function nextIdentity(): string
30 | {
31 | return Uuid::v4()->toRfc4122();
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/RentCar/Infrastructure/Persistence/Doctrine/DoctrineCarRepository.php:
--------------------------------------------------------------------------------
1 | createQueryBuilder('c')
32 | ->andWhere('c.exampleField = :val')
33 | ->setParameter('val', $value)
34 | ->orderBy('c.id', 'ASC')
35 | ->setMaxResults(10)
36 | ->getQuery()
37 | ->getResult()
38 | ;
39 | }
40 | */
41 |
42 | /*
43 | public function findOneBySomeField($value): ?Car
44 | {
45 | return $this->createQueryBuilder('c')
46 | ->andWhere('c.exampleField = :val')
47 | ->setParameter('val', $value)
48 | ->getQuery()
49 | ->getOneOrNullResult()
50 | ;
51 | }
52 | */
53 |
54 | public function save(Car $car): void
55 | {
56 | $this->getEntityManager()->persist($car);
57 | }
58 |
59 | public function nextIdentity(): string
60 | {
61 | return Uuid::v4()->toRfc4122();
62 | }
63 |
64 | public function findOneByCategory(string $category): Car
65 | {
66 | $car = $this->findOneBy([
67 | 'category' => $category
68 | ]);
69 |
70 | if (!$car) {
71 | throw CarForCategoryNotFoundException::withCategory($category);
72 | }
73 |
74 | return $car;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/RentCar/Infrastructure/Persistence/Doctrine/DoctrineCustomerRepository.php:
--------------------------------------------------------------------------------
1 | createQueryBuilder('c')
32 | ->andWhere('c.exampleField = :val')
33 | ->setParameter('val', $value)
34 | ->orderBy('c.id', 'ASC')
35 | ->setMaxResults(10)
36 | ->getQuery()
37 | ->getResult()
38 | ;
39 | }
40 | */
41 |
42 | /*
43 | public function findOneBySomeField($value): ?Customer
44 | {
45 | return $this->createQueryBuilder('c')
46 | ->andWhere('c.exampleField = :val')
47 | ->setParameter('val', $value)
48 | ->getQuery()
49 | ->getOneOrNullResult()
50 | ;
51 | }
52 | */
53 | public function save(Customer $customer): void
54 | {
55 | $this->getEntityManager()->persist($customer);
56 | }
57 |
58 | public function nextIdentity(): string
59 | {
60 | return Uuid::v4()->toRfc4122();
61 | }
62 |
63 | public function findById(string $customerId): Customer
64 | {
65 | $customer = $this->findOneBy([
66 | 'id' => $customerId
67 | ]);
68 |
69 | if (!$customer) {
70 | throw CustomerNotFoundException::withId($customerId);
71 | }
72 |
73 | return $customer;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/RentCar/Infrastructure/Persistence/Doctrine/DoctrineReservationRepository.php:
--------------------------------------------------------------------------------
1 | getEntityManager()->persist($reservation);
28 | }
29 |
30 | public function nextIdentity(): string
31 | {
32 | return Uuid::v4()->toRfc4122();
33 | }
34 |
35 | public function findById(string $reservationId): Reservation
36 | {
37 | $reservation = $this->findOneBy(['id' => $reservationId]);
38 | if (!$reservation) {
39 | throw ReservationNotFoundException::withId($reservationId);
40 | }
41 |
42 | return $reservation;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/RentCar/Infrastructure/Persistence/Doctrine/Fixtures/AppFixtures.php:
--------------------------------------------------------------------------------
1 | carRepository = $carRepository;
33 | $this->customerRepository = $customerRepository;
34 | $this->reservationRepository = $reservationRepository;
35 | $this->carMatchRepository = $carMatchRepository;
36 | }
37 |
38 | /**
39 | * @throws AssertionFailedException
40 | */
41 | public function load(ObjectManager $manager): void
42 | {
43 | $systemUser = Uuid::v4()->toRfc4122();
44 | $cars = [
45 | [
46 | 'id' => $this->carRepository->nextIdentity(),
47 | 'brand' => 'mazda',
48 | 'model' => 'z2',
49 | 'category' => 'standard',
50 | 'actorId' => $systemUser
51 | ],
52 | [
53 | 'id' => $this->carRepository->nextIdentity(),
54 | 'brand' => 'bmw',
55 | 'model' => 'x2',
56 | 'category' => 'premium',
57 | 'actorId' => $systemUser
58 | ],
59 | [
60 | 'id' => $this->carRepository->nextIdentity(),
61 | 'brand' => 'citroen',
62 | 'model' => 'c4',
63 | 'category' => 'intermediate',
64 | 'actorId' => $systemUser
65 | ],
66 | ];
67 |
68 | foreach ($cars as $carFixture) {
69 | $this->carRepository->save(
70 | Car::create(
71 | $carFixture['id'],
72 | $carFixture['brand'],
73 | $carFixture['model'],
74 | $carFixture['category'],
75 | $carFixture['actorId']
76 | )
77 | );
78 | }
79 | $manager->flush();
80 |
81 | $customers = [
82 | [
83 | 'id' => $this->customerRepository->nextIdentity(),
84 | 'name' => 'John Due',
85 | 'address' => '57 The Avenue London',
86 | 'phone' => '+447911123456',
87 | 'email' => 'johndue@test.com'
88 | ],
89 | [
90 | 'id' => $this->customerRepository->nextIdentity(),
91 | 'name' => 'Mira Skyer',
92 | 'address' => '61 King Street London',
93 | 'phone' => '+447911123456',
94 | 'email' => 'miraskyer@test.com'
95 | ],
96 | [
97 | 'id' => $this->customerRepository->nextIdentity(),
98 | 'name' => 'Jean Paul',
99 | 'address' => '253 Albert Road London',
100 | 'phone' => '+447911123456',
101 | 'email' => 'jeanpaul@test.com'
102 | ],
103 | ];
104 |
105 | foreach ($customers as $customer) {
106 | $this->customerRepository->save(
107 | Customer::create(
108 | $customer['id'],
109 | $customer['name'],
110 | $customer['address'],
111 | $customer['phone'],
112 | $customer['email'],
113 | $systemUser
114 | )
115 | );
116 | }
117 | $manager->flush();
118 |
119 | $reservations = [
120 | [
121 | 'id' => $this->reservationRepository->nextIdentity(),
122 | 'location' => '40 The Green London',
123 | 'pickUp' => new \DateTimeImmutable('2021-12-01 14:00:00'),
124 | 'returnAt' => new \DateTimeImmutable('2021-12-05 18:00:00'),
125 | 'category' => 'premium',
126 | 'status' => 'pending',
127 | 'customer' => $this->customerRepository->findById($customers[0]['id'])
128 | ],
129 | [
130 | 'id' => $this->reservationRepository->nextIdentity(),
131 | 'location' => '40 The Green London',
132 | 'pickUp' => new \DateTimeImmutable('2021-12-01 14:00:00'),
133 | 'returnAt' => new \DateTimeImmutable('2021-12-05 18:00:00'),
134 | 'category' => 'premium',
135 | 'status' => 'pending',
136 | 'customer' => $this->customerRepository->findById($customers[1]['id'])
137 | ],
138 | [
139 | 'id' => $this->reservationRepository->nextIdentity(),
140 | 'location' => '40 The Green London',
141 | 'pickUp' => new \DateTimeImmutable('2021-12-01 14:00:00'),
142 | 'returnAt' => new \DateTimeImmutable('2021-12-05 18:00:00'),
143 | 'category' => 'standard',
144 | 'status' => 'pending',
145 | 'customer' => $this->customerRepository->findById($customers[2]['id'])
146 | ]
147 | ];
148 |
149 | foreach($reservations as $reservation) {
150 | $this->reservationRepository->save(
151 | Reservation::create(
152 | $reservation['id'],
153 | $reservation['location'],
154 | $reservation['pickUp'],
155 | $reservation['returnAt'],
156 | $reservation['category'],
157 | $systemUser,
158 | $reservation['customer'],
159 | )
160 | );
161 | }
162 | $manager->flush();
163 |
164 | $matches = [
165 | [
166 | 'id' => $this->carMatchRepository->nextIdentity(),
167 | 'reservation' => $this->reservationRepository->findById(
168 | $reservations[0]['id']
169 | ),
170 | 'car' => $cars[0]['id']
171 | ],
172 | [
173 | 'id' => $this->carMatchRepository->nextIdentity(),
174 | 'reservation' => $this->reservationRepository->findById(
175 | $reservations[1]['id']
176 | ),
177 | 'car' => $cars[1]['id']
178 | ],
179 | [
180 | 'id' => $this->carMatchRepository->nextIdentity(),
181 | 'reservation' => $this->reservationRepository->findById(
182 | $reservations[2]['id']
183 | ),
184 | 'car' => $cars[2]['id']
185 | ],
186 | ];
187 |
188 | foreach($matches as $match) {
189 | $this->carMatchRepository->save(
190 | CarMatch::match(
191 | $this->carMatchRepository->nextIdentity(),
192 | $match['reservation'],
193 | $this->carRepository,
194 | )
195 | );
196 | }
197 | $manager->flush();
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/src/RentCar/Infrastructure/Symfony/Console/RentCarSimulationCommand.php:
--------------------------------------------------------------------------------
1 | messageBus = $messageBus;
26 | }
27 |
28 | protected function configure(): void
29 | {
30 | $this->setDescription('simulate rentcar startup');
31 | }
32 |
33 | protected function execute(InputInterface $input, OutputInterface $output): int
34 | {
35 | $systemUser = Uuid::v4()->toRfc4122();
36 |
37 | $output->writeln('creating a car');
38 | $this->messageBus->dispatch(
39 | new CreateCarCommand(
40 | 'mitsubishi',
41 | 'lancer',
42 | 'standard',
43 | $systemUser
44 | )
45 | );
46 |
47 | $output->writeln('creating a customer');
48 | $envelope = $this->messageBus->dispatch(
49 | new CreateCustomerCommand(
50 | 'John Doe',
51 | '44 Grange Road London',
52 | '+4429129891',
53 | 'johndoe@test.com',
54 | $systemUser
55 | )
56 | );
57 |
58 | $handledStamp = $envelope->last(HandledStamp::class);
59 | $customerId = $handledStamp->getResult();
60 |
61 | $output->writeln('creating a reservation');
62 | $envelope = $this->messageBus->dispatch(
63 | new CreateReservationCommand(
64 | '21 Main Road London',
65 | new \DateTimeImmutable('2021-12-01 14:00:00'),
66 | new \DateTimeImmutable('2021-12-05 14:00:00'),
67 | 'premium',
68 | $customerId,
69 | $systemUser
70 | )
71 | );
72 |
73 | $handledStamp = $envelope->last(HandledStamp::class);
74 | $reservationId = $handledStamp->getResult();
75 |
76 | $output->writeln('cancelling a reservation');
77 | $this->messageBus->dispatch(
78 | new CancelReservationCommand(
79 | $reservationId,
80 | $systemUser
81 | )
82 | );
83 |
84 | return Command::SUCCESS;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/RentCar/Infrastructure/Symfony/Controller/CarController.php:
--------------------------------------------------------------------------------
1 | toArray();
23 |
24 | $envelope = $bus->dispatch(
25 | new CreateCarCommand(
26 | $body['brand'] ?? null,
27 | $body['model'] ?? null,
28 | $body['category'] ?? null,
29 | $user->getUserIdentifier()
30 | )
31 | );
32 |
33 | return new JsonResponse([
34 | 'id' => $envelope->last(HandledStamp::class)?->getResult(),
35 | ], Response::HTTP_CREATED);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/RentCar/Infrastructure/Symfony/Security/User.php:
--------------------------------------------------------------------------------
1 | id;
40 | }
41 |
42 | public function getEmail(): ?string
43 | {
44 | return $this->email;
45 | }
46 |
47 | public function setEmail(string $email): self
48 | {
49 | $this->email = $email;
50 |
51 | return $this;
52 | }
53 |
54 | /**
55 | * A visual identifier that represents this user.
56 | *
57 | * @see UserInterface
58 | */
59 | public function getUserIdentifier(): string
60 | {
61 | return (string) $this->email;
62 | }
63 |
64 | /**
65 | * @deprecated since Symfony 5.3, use getUserIdentifier instead
66 | */
67 | public function getUsername(): string
68 | {
69 | return (string) $this->email;
70 | }
71 |
72 | /**
73 | * @see UserInterface
74 | */
75 | public function getRoles(): array
76 | {
77 | $roles = $this->roles;
78 | // guarantee every user at least has ROLE_USER
79 | $roles[] = 'ROLE_USER';
80 |
81 | return array_unique($roles);
82 | }
83 |
84 | public function setRoles(array $roles): self
85 | {
86 | $this->roles = $roles;
87 |
88 | return $this;
89 | }
90 |
91 | /**
92 | * @see PasswordAuthenticatedUserInterface
93 | */
94 | public function getPassword(): string
95 | {
96 | return $this->password;
97 | }
98 |
99 | public function setPassword(string $password): self
100 | {
101 | $this->password = $password;
102 |
103 | return $this;
104 | }
105 |
106 | /**
107 | * Returning a salt is only needed, if you are not using a modern
108 | * hashing algorithm (e.g. bcrypt or sodium) in your security.yaml.
109 | *
110 | * @see UserInterface
111 | */
112 | public function getSalt(): ?string
113 | {
114 | return null;
115 | }
116 |
117 | /**
118 | * @see UserInterface
119 | */
120 | public function eraseCredentials(): void
121 | {
122 | // If you store any temporary, sensitive data on the user, clear it here
123 | // $this->plainPassword = null;
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/symfony.lock:
--------------------------------------------------------------------------------
1 | {
2 | "beberlei/assert": {
3 | "version": "v3.3.1"
4 | },
5 | "composer/package-versions-deprecated": {
6 | "version": "1.11.99.4"
7 | },
8 | "composer/pcre": {
9 | "version": "1.0.0"
10 | },
11 | "composer/xdebug-handler": {
12 | "version": "2.0.3"
13 | },
14 | "dama/doctrine-test-bundle": {
15 | "version": "8.0",
16 | "recipe": {
17 | "repo": "github.com/symfony/recipes-contrib",
18 | "branch": "main",
19 | "version": "4.0",
20 | "ref": "2c920f73a217f30bd4a37833c91071f4d3dc1ecd"
21 | },
22 | "files": [
23 | "config/packages/test/dama_doctrine_test_bundle.yaml"
24 | ]
25 | },
26 | "doctrine/annotations": {
27 | "version": "1.0",
28 | "recipe": {
29 | "repo": "github.com/symfony/recipes",
30 | "branch": "master",
31 | "version": "1.0",
32 | "ref": "a2759dd6123694c8d901d0ec80006e044c2e6457"
33 | },
34 | "files": [
35 | "config/routes/annotations.yaml"
36 | ]
37 | },
38 | "doctrine/cache": {
39 | "version": "2.1.1"
40 | },
41 | "doctrine/collections": {
42 | "version": "1.6.8"
43 | },
44 | "doctrine/common": {
45 | "version": "3.2.0"
46 | },
47 | "doctrine/data-fixtures": {
48 | "version": "1.5.1"
49 | },
50 | "doctrine/dbal": {
51 | "version": "3.1.3"
52 | },
53 | "doctrine/deprecations": {
54 | "version": "v0.5.3"
55 | },
56 | "doctrine/doctrine-bundle": {
57 | "version": "2.4",
58 | "recipe": {
59 | "repo": "github.com/symfony/recipes",
60 | "branch": "master",
61 | "version": "2.4",
62 | "ref": "032f52ed50a27762b78ca6a2aaf432958c473553"
63 | },
64 | "files": [
65 | "config/packages/doctrine.yaml",
66 | "config/packages/prod/doctrine.yaml",
67 | "config/packages/test/doctrine.yaml",
68 | "src/Entity/.gitignore",
69 | "src/Repository/.gitignore"
70 | ]
71 | },
72 | "doctrine/doctrine-fixtures-bundle": {
73 | "version": "3.4",
74 | "recipe": {
75 | "repo": "github.com/symfony/recipes",
76 | "branch": "master",
77 | "version": "3.0",
78 | "ref": "1f5514cfa15b947298df4d771e694e578d4c204d"
79 | },
80 | "files": [
81 | "src/DataFixtures/AppFixtures.php"
82 | ]
83 | },
84 | "doctrine/doctrine-migrations-bundle": {
85 | "version": "3.1",
86 | "recipe": {
87 | "repo": "github.com/symfony/recipes",
88 | "branch": "master",
89 | "version": "3.1",
90 | "ref": "ee609429c9ee23e22d6fa5728211768f51ed2818"
91 | },
92 | "files": [
93 | "config/packages/doctrine_migrations.yaml",
94 | "migrations/.gitignore"
95 | ]
96 | },
97 | "doctrine/event-manager": {
98 | "version": "1.1.1"
99 | },
100 | "doctrine/inflector": {
101 | "version": "2.0.4"
102 | },
103 | "doctrine/instantiator": {
104 | "version": "1.4.0"
105 | },
106 | "doctrine/lexer": {
107 | "version": "1.2.1"
108 | },
109 | "doctrine/migrations": {
110 | "version": "3.3.0"
111 | },
112 | "doctrine/orm": {
113 | "version": "2.10.2"
114 | },
115 | "doctrine/persistence": {
116 | "version": "2.2.3"
117 | },
118 | "doctrine/sql-formatter": {
119 | "version": "1.1.1"
120 | },
121 | "egulias/email-validator": {
122 | "version": "3.1.2"
123 | },
124 | "friendsofphp/php-cs-fixer": {
125 | "version": "3.3",
126 | "recipe": {
127 | "repo": "github.com/symfony/recipes",
128 | "branch": "master",
129 | "version": "3.0",
130 | "ref": "be2103eb4a20942e28a6dd87736669b757132435"
131 | },
132 | "files": [
133 | ".php-cs-fixer.dist.php"
134 | ]
135 | },
136 | "friendsofphp/proxy-manager-lts": {
137 | "version": "v1.0.5"
138 | },
139 | "laminas/laminas-code": {
140 | "version": "4.4.3"
141 | },
142 | "monolog/monolog": {
143 | "version": "2.3.5"
144 | },
145 | "monteiro/ddd-bundle": {
146 | "version": "dev-main"
147 | },
148 | "nikic/php-parser": {
149 | "version": "v4.13.1"
150 | },
151 | "php-cs-fixer/diff": {
152 | "version": "v2.0.2"
153 | },
154 | "phpdocumentor/reflection-common": {
155 | "version": "2.2.0"
156 | },
157 | "phpdocumentor/reflection-docblock": {
158 | "version": "5.3.0"
159 | },
160 | "phpdocumentor/type-resolver": {
161 | "version": "1.5.1"
162 | },
163 | "phpspec/prophecy": {
164 | "version": "1.14.0"
165 | },
166 | "psr/cache": {
167 | "version": "1.0.1"
168 | },
169 | "psr/container": {
170 | "version": "1.1.1"
171 | },
172 | "psr/event-dispatcher": {
173 | "version": "1.0.0"
174 | },
175 | "psr/link": {
176 | "version": "1.0.0"
177 | },
178 | "psr/log": {
179 | "version": "1.1.4"
180 | },
181 | "sensio/framework-extra-bundle": {
182 | "version": "5.2",
183 | "recipe": {
184 | "repo": "github.com/symfony/recipes",
185 | "branch": "master",
186 | "version": "5.2",
187 | "ref": "fb7e19da7f013d0d422fa9bce16f5c510e27609b"
188 | },
189 | "files": [
190 | "config/packages/sensio_framework_extra.yaml"
191 | ]
192 | },
193 | "symfony/amqp-messenger": {
194 | "version": "v5.3.7"
195 | },
196 | "symfony/asset": {
197 | "version": "v5.3.4"
198 | },
199 | "symfony/browser-kit": {
200 | "version": "v5.3.4"
201 | },
202 | "symfony/cache": {
203 | "version": "v5.3.10"
204 | },
205 | "symfony/cache-contracts": {
206 | "version": "v2.4.0"
207 | },
208 | "symfony/config": {
209 | "version": "v5.3.10"
210 | },
211 | "symfony/console": {
212 | "version": "5.3",
213 | "recipe": {
214 | "repo": "github.com/symfony/recipes",
215 | "branch": "master",
216 | "version": "5.3",
217 | "ref": "da0c8be8157600ad34f10ff0c9cc91232522e047"
218 | },
219 | "files": [
220 | "bin/console"
221 | ]
222 | },
223 | "symfony/css-selector": {
224 | "version": "v5.3.4"
225 | },
226 | "symfony/debug-bundle": {
227 | "version": "4.1",
228 | "recipe": {
229 | "repo": "github.com/symfony/recipes",
230 | "branch": "master",
231 | "version": "4.1",
232 | "ref": "0ce7a032d344fb7b661cd25d31914cd703ad445b"
233 | },
234 | "files": [
235 | "config/packages/dev/debug.yaml"
236 | ]
237 | },
238 | "symfony/debug-pack": {
239 | "version": "v1.0.10"
240 | },
241 | "symfony/dependency-injection": {
242 | "version": "v5.3.10"
243 | },
244 | "symfony/deprecation-contracts": {
245 | "version": "v2.4.0"
246 | },
247 | "symfony/doctrine-bridge": {
248 | "version": "v5.3.8"
249 | },
250 | "symfony/doctrine-messenger": {
251 | "version": "v5.3.10"
252 | },
253 | "symfony/dom-crawler": {
254 | "version": "v5.3.7"
255 | },
256 | "symfony/dotenv": {
257 | "version": "v5.3.10"
258 | },
259 | "symfony/error-handler": {
260 | "version": "v5.3.7"
261 | },
262 | "symfony/event-dispatcher": {
263 | "version": "v5.3.7"
264 | },
265 | "symfony/event-dispatcher-contracts": {
266 | "version": "v2.4.0"
267 | },
268 | "symfony/expression-language": {
269 | "version": "v5.3.7"
270 | },
271 | "symfony/filesystem": {
272 | "version": "v5.3.4"
273 | },
274 | "symfony/finder": {
275 | "version": "v5.3.7"
276 | },
277 | "symfony/flex": {
278 | "version": "1.0",
279 | "recipe": {
280 | "repo": "github.com/symfony/recipes",
281 | "branch": "master",
282 | "version": "1.0",
283 | "ref": "c0eeb50665f0f77226616b6038a9b06c03752d8e"
284 | },
285 | "files": [
286 | ".env"
287 | ]
288 | },
289 | "symfony/form": {
290 | "version": "v5.3.10"
291 | },
292 | "symfony/framework-bundle": {
293 | "version": "5.3",
294 | "recipe": {
295 | "repo": "github.com/symfony/recipes",
296 | "branch": "master",
297 | "version": "5.3",
298 | "ref": "414ba00ad43fa71be42c7906a551f1831716b03c"
299 | },
300 | "files": [
301 | "config/packages/cache.yaml",
302 | "config/packages/framework.yaml",
303 | "config/preload.php",
304 | "config/routes/framework.yaml",
305 | "config/services.yaml",
306 | "public/index.php",
307 | "src/Controller/.gitignore",
308 | "src/Kernel.php"
309 | ]
310 | },
311 | "symfony/http-client": {
312 | "version": "v5.3.10"
313 | },
314 | "symfony/http-client-contracts": {
315 | "version": "v2.4.0"
316 | },
317 | "symfony/http-foundation": {
318 | "version": "v5.3.10"
319 | },
320 | "symfony/http-kernel": {
321 | "version": "v5.3.10"
322 | },
323 | "symfony/intl": {
324 | "version": "v5.3.8"
325 | },
326 | "symfony/mailer": {
327 | "version": "4.3",
328 | "recipe": {
329 | "repo": "github.com/symfony/recipes",
330 | "branch": "master",
331 | "version": "4.3",
332 | "ref": "bbfc7e27257d3a3f12a6fb0a42540a42d9623a37"
333 | },
334 | "files": [
335 | "config/packages/mailer.yaml"
336 | ]
337 | },
338 | "symfony/maker-bundle": {
339 | "version": "1.0",
340 | "recipe": {
341 | "repo": "github.com/symfony/recipes",
342 | "branch": "master",
343 | "version": "1.0",
344 | "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
345 | }
346 | },
347 | "symfony/messenger": {
348 | "version": "5.3",
349 | "recipe": {
350 | "repo": "github.com/symfony/recipes",
351 | "branch": "master",
352 | "version": "4.3",
353 | "ref": "25e3c964d3aee480b3acc3114ffb7940c89edfed"
354 | },
355 | "files": [
356 | "config/packages/messenger.yaml"
357 | ]
358 | },
359 | "symfony/mime": {
360 | "version": "v5.3.8"
361 | },
362 | "symfony/monolog-bridge": {
363 | "version": "v5.3.7"
364 | },
365 | "symfony/monolog-bundle": {
366 | "version": "3.7",
367 | "recipe": {
368 | "repo": "github.com/symfony/recipes",
369 | "branch": "master",
370 | "version": "3.7",
371 | "ref": "a7bace7dbc5a7ed5608dbe2165e0774c87175fe6"
372 | },
373 | "files": [
374 | "config/packages/dev/monolog.yaml",
375 | "config/packages/prod/deprecations.yaml",
376 | "config/packages/prod/monolog.yaml",
377 | "config/packages/test/monolog.yaml"
378 | ]
379 | },
380 | "symfony/notifier": {
381 | "version": "5.0",
382 | "recipe": {
383 | "repo": "github.com/symfony/recipes",
384 | "branch": "master",
385 | "version": "5.0",
386 | "ref": "c31585e252b32fe0e1f30b1f256af553f4a06eb9"
387 | },
388 | "files": [
389 | "config/packages/notifier.yaml"
390 | ]
391 | },
392 | "symfony/options-resolver": {
393 | "version": "v5.3.7"
394 | },
395 | "symfony/password-hasher": {
396 | "version": "v5.3.8"
397 | },
398 | "symfony/phpunit-bridge": {
399 | "version": "5.3",
400 | "recipe": {
401 | "repo": "github.com/symfony/recipes",
402 | "branch": "master",
403 | "version": "5.3",
404 | "ref": "97cb3dc7b0f39c7cfc4b7553504c9d7b7795de96"
405 | },
406 | "files": [
407 | ".env.test",
408 | "bin/phpunit",
409 | "phpunit.xml.dist",
410 | "tests/bootstrap.php"
411 | ]
412 | },
413 | "symfony/polyfill-intl-grapheme": {
414 | "version": "v1.23.1"
415 | },
416 | "symfony/polyfill-intl-icu": {
417 | "version": "v1.23.0"
418 | },
419 | "symfony/polyfill-intl-idn": {
420 | "version": "v1.23.0"
421 | },
422 | "symfony/polyfill-intl-normalizer": {
423 | "version": "v1.23.0"
424 | },
425 | "symfony/polyfill-mbstring": {
426 | "version": "v1.23.1"
427 | },
428 | "symfony/polyfill-php73": {
429 | "version": "v1.23.0"
430 | },
431 | "symfony/polyfill-php80": {
432 | "version": "v1.23.1"
433 | },
434 | "symfony/polyfill-php81": {
435 | "version": "v1.23.0"
436 | },
437 | "symfony/polyfill-uuid": {
438 | "version": "v1.23.0"
439 | },
440 | "symfony/process": {
441 | "version": "v5.3.7"
442 | },
443 | "symfony/profiler-pack": {
444 | "version": "v1.0.6"
445 | },
446 | "symfony/property-access": {
447 | "version": "v5.3.8"
448 | },
449 | "symfony/property-info": {
450 | "version": "v5.3.8"
451 | },
452 | "symfony/proxy-manager-bridge": {
453 | "version": "v5.3.4"
454 | },
455 | "symfony/redis-messenger": {
456 | "version": "v5.3.10"
457 | },
458 | "symfony/routing": {
459 | "version": "5.3",
460 | "recipe": {
461 | "repo": "github.com/symfony/recipes",
462 | "branch": "master",
463 | "version": "5.3",
464 | "ref": "44633353926a0382d7dfb0530922c5c0b30fae11"
465 | },
466 | "files": [
467 | "config/packages/routing.yaml",
468 | "config/routes.yaml"
469 | ]
470 | },
471 | "symfony/runtime": {
472 | "version": "v5.3.10"
473 | },
474 | "symfony/security-bundle": {
475 | "version": "5.3",
476 | "recipe": {
477 | "repo": "github.com/symfony/recipes",
478 | "branch": "master",
479 | "version": "5.3",
480 | "ref": "3307d76caa2d12fb10ade57975beb3d8975df396"
481 | },
482 | "files": [
483 | "config/packages/security.yaml"
484 | ]
485 | },
486 | "symfony/security-core": {
487 | "version": "v5.3.10"
488 | },
489 | "symfony/security-csrf": {
490 | "version": "v5.3.4"
491 | },
492 | "symfony/security-guard": {
493 | "version": "v5.3.7"
494 | },
495 | "symfony/security-http": {
496 | "version": "v5.3.10"
497 | },
498 | "symfony/serializer": {
499 | "version": "v5.3.10"
500 | },
501 | "symfony/service-contracts": {
502 | "version": "v2.4.0"
503 | },
504 | "symfony/stopwatch": {
505 | "version": "v5.3.4"
506 | },
507 | "symfony/string": {
508 | "version": "v5.3.10"
509 | },
510 | "symfony/test-pack": {
511 | "version": "v1.0.9"
512 | },
513 | "symfony/translation": {
514 | "version": "5.3",
515 | "recipe": {
516 | "repo": "github.com/symfony/recipes",
517 | "branch": "master",
518 | "version": "5.3",
519 | "ref": "da64f5a2b6d96f5dc24914517c0350a5f91dee43"
520 | },
521 | "files": [
522 | "config/packages/translation.yaml",
523 | "translations/.gitignore"
524 | ]
525 | },
526 | "symfony/translation-contracts": {
527 | "version": "v2.4.0"
528 | },
529 | "symfony/twig-bridge": {
530 | "version": "v5.3.7"
531 | },
532 | "symfony/twig-bundle": {
533 | "version": "5.3",
534 | "recipe": {
535 | "repo": "github.com/symfony/recipes",
536 | "branch": "master",
537 | "version": "5.3",
538 | "ref": "3dd530739a4284e3272274c128dbb7a8140a66f1"
539 | },
540 | "files": [
541 | "config/packages/twig.yaml",
542 | "templates/base.html.twig"
543 | ]
544 | },
545 | "symfony/twig-pack": {
546 | "version": "v1.0.1"
547 | },
548 | "symfony/uid": {
549 | "version": "v5.3.10"
550 | },
551 | "symfony/validator": {
552 | "version": "4.3",
553 | "recipe": {
554 | "repo": "github.com/symfony/recipes",
555 | "branch": "master",
556 | "version": "4.3",
557 | "ref": "3eb8df139ec05414489d55b97603c5f6ca0c44cb"
558 | },
559 | "files": [
560 | "config/packages/test/validator.yaml",
561 | "config/packages/validator.yaml"
562 | ]
563 | },
564 | "symfony/var-dumper": {
565 | "version": "v5.3.10"
566 | },
567 | "symfony/var-exporter": {
568 | "version": "v5.3.8"
569 | },
570 | "symfony/web-link": {
571 | "version": "v5.3.4"
572 | },
573 | "symfony/web-profiler-bundle": {
574 | "version": "3.3",
575 | "recipe": {
576 | "repo": "github.com/symfony/recipes",
577 | "branch": "master",
578 | "version": "3.3",
579 | "ref": "6bdfa1a95f6b2e677ab985cd1af2eae35d62e0f6"
580 | },
581 | "files": [
582 | "config/packages/dev/web_profiler.yaml",
583 | "config/packages/test/web_profiler.yaml",
584 | "config/routes/dev/web_profiler.yaml"
585 | ]
586 | },
587 | "symfony/yaml": {
588 | "version": "v5.3.6"
589 | },
590 | "twig/extra-bundle": {
591 | "version": "v3.3.3"
592 | },
593 | "twig/twig": {
594 | "version": "v3.3.3"
595 | },
596 | "webmozart/assert": {
597 | "version": "1.10.0"
598 | }
599 | }
600 |
--------------------------------------------------------------------------------
/templates/base.html.twig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {% block title %}Welcome!{% endblock %}
6 | {# Run `composer require symfony/webpack-encore-bundle`
7 | and uncomment the following Encore helpers to start using Symfony UX #}
8 | {% block stylesheets %}
9 | {#{{ encore_entry_link_tags('app') }}#}
10 | {% endblock %}
11 |
12 | {% block javascripts %}
13 | {#{{ encore_entry_script_tags('app') }}#}
14 | {% endblock %}
15 |
16 |
17 | {% block body %}{% endblock %}
18 |
19 |
20 |
--------------------------------------------------------------------------------
/tests/RentCar/Application/Car/CreateCarCommandHandlerTest.php:
--------------------------------------------------------------------------------
1 | carRepository = new InMemoryCarRepository(self::CAR_ID);
20 | $this->domainEventDispatcher = new TraceableDomainEventDispatcher();
21 | }
22 |
23 | /**
24 | * @test
25 | */
26 | public function itShouldCreateCar(): void
27 | {
28 | // given
29 | $brand = 'mazda';
30 | $model = 'z2';
31 | $category = 'intermediate';
32 | $actorId = 'f6774efa-e40a-4e75-a26f-6aed8e08e2f5';
33 | $command = new CreateCarCommand($brand, $model, $category, $actorId);
34 | $handler = new CreateCarCommandHandler(
35 | $this->carRepository,
36 | $this->domainEventDispatcher
37 | );
38 |
39 | // when
40 | $handler($command);
41 |
42 | // then
43 | $this->assertCount(1, $this->carRepository->getCars());
44 | $this->assertCount(1, $this->domainEventDispatcher->getEvents());
45 | $carWasCreated = $this->domainEventDispatcher->getEvent();
46 | $this->assertEquals(self::CAR_ID, $carWasCreated->getAggregateRootId());
47 | $this->assertEquals($actorId, $carWasCreated->getActorId());
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/tests/RentCar/Application/Customer/CreateCustomerCommandHandlerTest.php:
--------------------------------------------------------------------------------
1 | customerRepository = new InMemoryCustomerRepository(self::CUSTOMER_ID);
20 | $this->domainEventDispatcher = new TraceableDomainEventDispatcher();
21 | }
22 |
23 | /**
24 | * @test
25 | */
26 | public function itShouldNotCreateCustomerWrongEmailFormat(): void
27 | {
28 | $this->expectException(\InvalidArgumentException::class);
29 |
30 | $actorId = 'f6774efa-e40a-4e75-a26f-6aed8e08e2f5';
31 | $command = new CreateCustomerCommand('John Doe', 'Rue Paris, 122', '+44210902191', 'WRONG_EMAIL_FORMAT', $actorId);
32 | $handler = new CreateCustomerCommandHandler(
33 | $this->customerRepository,
34 | $this->domainEventDispatcher
35 | );
36 |
37 | $handler($command);
38 | }
39 |
40 | /**
41 | * @test
42 | */
43 | public function itShouldCreateCustomer(): void
44 | {
45 | // given
46 | $actorId = 'f6774efa-e40a-4e75-a26f-6aed8e08e2f5';
47 | $command = new CreateCustomerCommand('John Doe', 'Rue Paris, 122', '+44210902191', 'johndoe@test.com', $actorId);
48 | $handler = new CreateCustomerCommandHandler(
49 | $this->customerRepository,
50 | $this->domainEventDispatcher
51 | );
52 |
53 | // when
54 | $handler($command);
55 |
56 | // then
57 | $this->assertCount(1, $this->customerRepository->getCustomers());
58 | $this->assertCount(1, $this->domainEventDispatcher->getEvents());
59 | $carWasCreated = $this->domainEventDispatcher->getEvent();
60 | $this->assertEquals(self::CUSTOMER_ID, $carWasCreated->getAggregateRootId());
61 | $this->assertEquals($actorId, $carWasCreated->getActorId());
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/tests/RentCar/Application/Match/CreateCarMatchCommandHandlerTest.php:
--------------------------------------------------------------------------------
1 | carMatchRepository = new InMemoryCarMatchRepository();
26 | $this->carRepository = new InMemoryCarRepository(self::CAR_ID);
27 | $this->reservationRepository = new InMemoryReservationRepository();
28 | $this->domainEventDispatcher = new TraceableDomainEventDispatcher();
29 | }
30 |
31 | /**
32 | * @test
33 | */
34 | public function itShouldCreateCarMatch(): void
35 | {
36 | // given
37 | $car = RentCarMother::aCar('premium');
38 | $this->carRepository->save($car);
39 | $customer = RentCarMother::aCustomer();
40 |
41 | $reservation = RentCarMother::aReservation($customer);
42 | $this->reservationRepository->save($reservation);
43 |
44 | $command = new CreateCarMatchCommand($reservation->getId());
45 | $handler = new CreateCarMatchCommandHandler(
46 | $this->reservationRepository,
47 | $this->carRepository,
48 | $this->carMatchRepository
49 | );
50 |
51 | // when
52 | $carMatchId = $handler($command);
53 |
54 | // then
55 | $this->assertNotNull($carMatchId);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/tests/RentCar/Application/TraceableDomainEventDispatcher.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | private array $eventsToDispatch;
16 |
17 | public function dispatchAll(array $domainEvents): void
18 | {
19 | $this->eventsToDispatch = $domainEvents;
20 | }
21 |
22 | public function getEvent(): ?DomainEvent
23 | {
24 | return $this->eventsToDispatch[0] ?? null;
25 | }
26 |
27 | /**
28 | * @return DomainEvent[]
29 | */
30 | public function getEvents(): array
31 | {
32 | return $this->eventsToDispatch;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tests/RentCar/Domain/Car/CarTest.php:
--------------------------------------------------------------------------------
1 | releaseEvents();
25 | $this->assertCount(1, $events);
26 |
27 | $carWasCreated = $events[0];
28 | $this->assertSame($carWasCreated->getAggregateRootId(), $carId);
29 | $this->assertSame($carWasCreated->getActorId(), $actorId);
30 |
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/tests/RentCar/Domain/Customer/CustomerTest.php:
--------------------------------------------------------------------------------
1 | releaseEvents();
32 | $this->assertCount(1, $events);
33 |
34 | $customerWasCreated = $events[0];
35 | $this->assertSame($customerWasCreated->getAggregateRootId(), $customerId);
36 | $this->assertSame($customerWasCreated->getActorId(), $actorId);
37 |
38 | $this->assertSame($customerId, $customer->getId());
39 | $this->assertSame($name, $customer->getName());
40 | $this->assertSame($address, $customer->getAddress());
41 | $this->assertSame($phone, $customer->getPhone());
42 | $this->assertSame($email, $customer->getEmail());
43 | $this->assertSame($actorId, $customer->getActorId());
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tests/RentCar/Domain/Match/CarMatchTest.php:
--------------------------------------------------------------------------------
1 | carRepository = new InMemoryCarRepository();
20 | }
21 |
22 | /**
23 | * @test
24 | */
25 | public function itShouldMatchCar(): void
26 | {
27 | $car = RentCarMother::aCar('premium');
28 | $this->carRepository->save($car);
29 |
30 | $reservation = RentCarMother::aReservation(RentCarMother::aCustomer());
31 | $carMatch = CarMatch::match(
32 | self::MATCH_ID,
33 | $reservation,
34 | $this->carRepository
35 | );
36 |
37 | $this->assertSame(self::MATCH_ID, $carMatch->getId());
38 | $this->assertSame($reservation, $carMatch->getReservation());
39 | $this->assertSame($car, $carMatch->getCar());
40 |
41 | $events = $carMatch->releaseEvents();
42 | $this->assertCount(1, $events);
43 |
44 | $carWasMatched = $events[0];
45 | $this->assertInstanceOf(CarWasMatched::class, $carWasMatched);
46 |
47 | $this->assertSame($carMatch->getId(), $carWasMatched->getAggregateRootId());
48 | $this->assertNull($carWasMatched->getActorId());
49 | }
50 |
51 | /**
52 | * @test
53 | */
54 | public function itShouldNotMatchIfCarNotFound(): void
55 | {
56 | $this->expectException(CarForCategoryNotFoundException::class);
57 | CarMatch::match(
58 | self::MATCH_ID,
59 | RentCarMother::aReservation(RentCarMother::aCustomer()),
60 | $this->carRepository
61 | );
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/tests/RentCar/Domain/RentCarMother.php:
--------------------------------------------------------------------------------
1 | releaseEvents();
44 | $this->assertCount(1, $events);
45 | $this->assertInstanceOf(ReservationWasCreated::class, $events[0]);
46 |
47 | $reservationWasCreated = $events[0];
48 | $this->assertSame($reservationWasCreated->getAggregateRootId(), $reservationId);
49 | $this->assertSame($reservationWasCreated->getActorId(), $actorId);
50 |
51 | $this->assertSame('pending', $reservation->getStatus());
52 | $this->assertSame($actorId, $reservation->getActorId());
53 | $this->assertSame($reservationId, $reservation->getId());
54 | $this->assertSame($category, $reservation->getCategory());
55 | $this->assertSame($location, $reservation->getLocation());
56 | $this->assertSame($pickUpAt, $reservation->getPickUpAt());
57 | $this->assertSame($returnAt, $reservation->getReturnAt());
58 | }
59 |
60 | /**
61 | * @test
62 | */
63 | public function itShouldCancelReservation(): void
64 | {
65 | $actorId = 'c807779b-ad5e-42e8-954e-c0f193fe6375';
66 |
67 | $customer = Customer::create(
68 | '4923038f-28e4-4921-a7f7-cf71a7e44883',
69 | 'Hugo Monteiro',
70 | 'Rua do Ouro, Lisboa',
71 | '+35196271231',
72 | 'hugo@test.com',
73 | $actorId
74 | );
75 |
76 | $reservation = Reservation::create(
77 | '0e3d7e91-3b09-4dd9-ae20-5fe6acc4c2d2',
78 | 'Rua da Prata, 22',
79 | new \DateTimeImmutable('2021-12-09 14:00:00'),
80 | new \DateTimeImmutable('2021-12-12 14:00:00'),
81 | 'premium',
82 | 'd0e7c215-f9d5-4818-bba4-c00d416fafe5',
83 | $customer
84 | );
85 | $reservation->releaseEvents();
86 |
87 | $reservation->cancel($actorId);
88 | $this->assertSame('cancelled', $reservation->getStatus());
89 |
90 | $events = $reservation->releaseEvents();
91 | $this->assertCount(1, $events);
92 | $this->assertInstanceOf(ReservationWasCancelled::class, $events[0]);
93 |
94 | $reservationWasCancelled = $events[0];
95 | $this->assertSame($reservationWasCancelled->getAggregateRootId(), '0e3d7e91-3b09-4dd9-ae20-5fe6acc4c2d2');
96 | $this->assertSame($reservationWasCancelled->getActorId(), $actorId);
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/tests/RentCar/Infrastructure/Persistence/InMemory/InMemoryCarMatchRepository.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | private array $carMatches = [];
14 | private ?string $uuid;
15 |
16 | public function __construct(?string $uuid = null)
17 | {
18 | $this->uuid = $uuid;
19 | }
20 |
21 | public function save(CarMatch $carMatch): void
22 | {
23 | $this->carMatches[$carMatch->getId()] = $carMatch;
24 | }
25 |
26 | public function nextIdentity(): string
27 | {
28 | if ($this->uuid) {
29 | return $this->uuid;
30 | }
31 |
32 | return '0970424c-d5a1-4af7-b2dd-872c0edf5f3f';
33 | }
34 |
35 | public function getCarMatches(): array
36 | {
37 | return $this->carMatches;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/tests/RentCar/Infrastructure/Persistence/InMemory/InMemoryCarRepository.php:
--------------------------------------------------------------------------------
1 |
15 | */
16 | private array $cars = [];
17 | private ?string $uuid;
18 |
19 | public function __construct(?string $uuid = null) {
20 |
21 | $this->uuid = $uuid;
22 | }
23 |
24 | public function save(Car $car): void
25 | {
26 | $this->cars[$car->getId()] = $car;
27 | }
28 |
29 | public function findOneByCategory(string $category): Car
30 | {
31 | foreach($this->cars as $car) {
32 | if ($car->getCategory() === $category) {
33 | return $car;
34 | }
35 | }
36 |
37 | throw CarForCategoryNotFoundException::withCategory($category);
38 | }
39 |
40 | public function nextIdentity(): string
41 | {
42 | if ($this->uuid) {
43 | return $this->uuid;
44 | }
45 |
46 | return '0970424c-d5a1-4af7-b2dd-872c0edf5f3f';
47 | }
48 |
49 | public function getCars(): array
50 | {
51 | return $this->cars;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/tests/RentCar/Infrastructure/Persistence/InMemory/InMemoryCustomerRepository.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | private array $customers = [];
15 | private ?string $uuid;
16 |
17 | public function __construct(?string $uuid = null)
18 | {
19 | $this->uuid = $uuid;
20 | }
21 |
22 | public function save(Customer $customer): void
23 | {
24 | $this->customers[$customer->getId()] = $customer;
25 | }
26 |
27 | public function nextIdentity(): string
28 | {
29 | if ($this->uuid) {
30 | return $this->uuid;
31 | }
32 |
33 | return 'ee5e93a7-6880-4fcb-9de1-59e575d691f5';
34 | }
35 |
36 | public function getCustomers(): array
37 | {
38 | return $this->customers;
39 | }
40 |
41 | public function findById(string $customerId): Customer
42 | {
43 | if (isset($this->customers[$customerId])) {
44 | return $this->customers[$customerId];
45 | }
46 |
47 | throw CustomerNotFoundException::withId($customerId);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/tests/RentCar/Infrastructure/Persistence/InMemory/InMemoryReservationRepository.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | private array $reservations = [];
15 | private ?string $uuid;
16 |
17 | public function __construct(?string $uuid = null)
18 | {
19 | $this->uuid = $uuid;
20 | }
21 |
22 | public function save(Reservation $reservation): void
23 | {
24 | $this->reservations[$reservation->getId()] = $reservation;
25 | }
26 |
27 | public function nextIdentity(): string
28 | {
29 | if ($this->uuid) {
30 | return $this->uuid;
31 | }
32 |
33 | return '0970424c-d5a1-4af7-b2dd-872c0edf5f3f';
34 | }
35 |
36 | public function getReservations(): array
37 | {
38 | return $this->reservations;
39 | }
40 |
41 | public function findById(string $reservationId): Reservation
42 | {
43 | if (isset($this->reservations[$reservationId])) {
44 | return $this->reservations[$reservationId];
45 | }
46 |
47 | throw ReservationNotFoundException::withId($reservationId);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | bootEnv(dirname(__DIR__).'/.env');
11 | }
12 |
--------------------------------------------------------------------------------
/translations/.gitignore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monteiro/symfonycon-winter-2021/fd72df6153b1417059c243cf5e5007d036b8239b/translations/.gitignore
--------------------------------------------------------------------------------