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