├── .env.dist ├── .gitignore ├── LICENSE ├── LICENSE.txt ├── README.md ├── bin ├── console └── phpunit ├── composer.json ├── composer.lock ├── config ├── bundles.php ├── graphql │ ├── schema │ │ ├── Mutation.yml │ │ └── Query.yml │ └── types │ │ ├── Book.graphql │ │ ├── Category.graphql │ │ └── Reader.graphql ├── packages │ ├── dev │ │ └── routing.yaml │ ├── doctrine.yaml │ ├── doctrine_migrations.yaml │ ├── framework.yaml │ ├── graphql.yaml │ ├── league_tactician.yaml │ ├── prod │ │ └── doctrine.yaml │ ├── routing.yaml │ ├── test │ │ └── framework.yaml │ └── twig.yaml ├── routes.yaml ├── routes │ ├── annotations.yaml │ ├── dev │ │ ├── graphiql.yaml │ │ └── twig.yaml │ └── graphql.yaml ├── services.yaml └── services_test.yaml ├── phpunit.xml.dist ├── public └── index.php ├── src ├── Entity │ ├── Book.php │ └── Reader.php ├── Event │ ├── AbstractEvent.php │ ├── EventRecorder.php │ └── ReaderBookRegisteredEvent.php ├── EventSubscriber │ └── ReaderSubscriber.php ├── Exception │ ├── BookNotFoundException.php │ ├── ReaderAlreadyHasBookException.php │ ├── ReaderNotFoundException.php │ └── UnknownCategoryException.php ├── Kernel.php ├── Middleware │ ├── GraphQLMiddleware.php │ ├── QueueMiddleware.php │ └── ReleaseRecordedEventsMiddleware.php ├── Migrations │ ├── .gitignore │ ├── Version20180221021751.php │ ├── Version20180221022807.php │ ├── Version20180224062434.php │ └── Version20180224074548.php ├── Queue │ └── AbstractQueueableCommand.php ├── Repository │ ├── AbstractRepository.php │ ├── BookRepository.php │ └── ReaderRepository.php └── Service │ ├── CatalogBook.php │ ├── CatalogBookHandler.php │ ├── HandlerInterface.php │ ├── NotifyTradeOpportunities.php │ ├── NotifyTradeOpportunitiesHandler.php │ ├── RegisterReader.php │ ├── RegisterReaderBook.php │ ├── RegisterReaderBookHandler.php │ ├── RegisterReaderHandler.php │ ├── RegisterReaderWish.php │ └── RegisterReaderWishHandler.php ├── symfony.lock ├── templates └── base.html.twig └── tests ├── .gitignore ├── Integration ├── AbstractIntegrationTestCase.php └── Service │ ├── CatalogBookHandlerTest.php │ ├── RegisterReaderBookHandlerTest.php │ ├── RegisterReaderHandlerTest.php │ └── RegisterReaderWishHandlerTest.php └── Unit ├── AbstractUnitTestCase.php ├── Entity └── ReaderTest.php ├── Event └── EventRecorderTest.php ├── Middleware └── ReleaseRecordedEventsMiddlewareTest.php └── Service ├── CatalogBookHandlerTest.php ├── RegisterReaderBookHandlerTest.php ├── RegisterReaderHandlerTest.php └── RegisterReaderWishHandlerTest.php /.env.dist: -------------------------------------------------------------------------------- 1 | # This file is a "template" of which env vars need to be defined for your application 2 | # Copy this file to .env file for development, create environment variables when deploying to production 3 | # https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration 4 | 5 | ###> symfony/framework-bundle ### 6 | APP_ENV=dev 7 | APP_SECRET=eb6672dc009c15c02b0da8f4a15a781d 8 | #TRUSTED_PROXIES=127.0.0.1,127.0.0.2 9 | #TRUSTED_HOSTS=localhost,example.com 10 | ###< symfony/framework-bundle ### 11 | 12 | ###> doctrine/doctrine-bundle ### 13 | # Format described at http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url 14 | # For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db" 15 | # Configure your db driver and server_version in config/packages/doctrine.yaml 16 | DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name 17 | ###< doctrine/doctrine-bundle ### 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | ###> symfony/framework-bundle ### 4 | .env 5 | /public/bundles/ 6 | /var/ 7 | /vendor/ 8 | ###< symfony/framework-bundle ### 9 | 10 | ###> symfony/phpunit-bridge ### 11 | .phpunit 12 | /phpunit.xml 13 | ###< symfony/phpunit-bridge ### 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Bruno Neves 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Bruno Neves 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Expressive Architecture 2 | 3 | This repository was created to show some concepts from "Projetando uma arquitetura expressiva" talk. Slides [here](https://www.slideshare.net/brunonm/projetando-uma-arquitetura-expressiva). 4 | 5 | ## That Book 6 | 7 | Platform to find opportunities for book trade among readers. 8 | 9 | ### Features 10 | 11 | - Symfony 4 12 | - GraphQL 13 | - Command Bus by Tactician 14 | - DDDish 15 | 16 | ### Setup 17 | 18 | ```bash 19 | $ composer install 20 | $ php bin/console doctrine:database:create 21 | $ php bin/console doctrine:migrations:migrate 22 | ``` 23 | 24 | ### Tests 25 | 26 | ```bash 27 | $ bin/phpunit 28 | ``` 29 | 30 | ### License 31 | 32 | Code licensed under MIT 33 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | load(__DIR__.'/../.env'); 23 | } 24 | 25 | $input = new ArgvInput(); 26 | $env = $input->getParameterOption(['--env', '-e'], $_SERVER['APP_ENV'] ?? 'dev', true); 27 | $debug = ($_SERVER['APP_DEBUG'] ?? ('prod' !== $env)) && !$input->hasParameterOption('--no-debug', true); 28 | 29 | if ($debug) { 30 | umask(0000); 31 | 32 | if (class_exists(Debug::class)) { 33 | Debug::enable(); 34 | } 35 | } 36 | 37 | $kernel = new Kernel($env, $debug); 38 | $application = new Application($kernel); 39 | $application->run($input); 40 | -------------------------------------------------------------------------------- /bin/phpunit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | ['all' => true], 5 | Doctrine\Bundle\DoctrineCacheBundle\DoctrineCacheBundle::class => ['all' => true], 6 | Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], 7 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 8 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 9 | League\Tactician\Bundle\TacticianBundle::class => ['all' => true], 10 | Overblog\GraphQLBundle\OverblogGraphQLBundle::class => ['all' => true], 11 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], 12 | Overblog\GraphiQLBundle\OverblogGraphiQLBundle::class => ['dev' => true], 13 | ]; 14 | -------------------------------------------------------------------------------- /config/graphql/schema/Mutation.yml: -------------------------------------------------------------------------------- 1 | Mutation: 2 | type: object 3 | config: 4 | fields: 5 | registerReaderBook: 6 | type: Boolean 7 | args: 8 | bookId: 9 | type: String! 10 | readerId: 11 | type: String! 12 | resolve: > 13 | @=service("tactician.commandbus.mutation") 14 | .handle( 15 | newObject("ThatBook\\Service\\RegisterReaderBook", [ 16 | args["bookId"], 17 | args["readerId"] 18 | ]) 19 | ) 20 | 21 | registerReaderWish: 22 | type: Boolean 23 | args: 24 | bookId: 25 | type: String! 26 | readerId: 27 | type: String! 28 | resolve: > 29 | @=service("tactician.commandbus.mutation") 30 | .handle( 31 | newObject("ThatBook\\Service\\RegisterReaderWish", [ 32 | args["bookId"], 33 | args["readerId"] 34 | ]) 35 | ) 36 | 37 | catalogBook: 38 | type: Book 39 | args: 40 | title: 41 | type: String! 42 | publisher: 43 | type: String! 44 | category: 45 | type: Category! 46 | resolve: > 47 | @=service("tactician.commandbus.mutation") 48 | .handle( 49 | newObject("ThatBook\\Service\\CatalogBook", [ 50 | args["title"], 51 | args["publisher"], 52 | args["category"] 53 | ]) 54 | ) 55 | 56 | registerReader: 57 | type: Reader 58 | args: 59 | name: 60 | type: String! 61 | resolve: > 62 | @=service("tactician.commandbus.mutation") 63 | .handle( 64 | newObject("ThatBook\\Service\\RegisterReader", [ 65 | args["name"] 66 | ]) 67 | ) -------------------------------------------------------------------------------- /config/graphql/schema/Query.yml: -------------------------------------------------------------------------------- 1 | Query: 2 | type: object 3 | config: 4 | fields: 5 | books: 6 | type: "[Book]" 7 | resolve: '@=service("ThatBook\\Repository\\BookRepository").findAll()' 8 | readers: 9 | type: "[Reader]" 10 | resolve: '@=service("ThatBook\\Repository\\ReaderRepository").findAll()' 11 | -------------------------------------------------------------------------------- /config/graphql/types/Book.graphql: -------------------------------------------------------------------------------- 1 | type Book { 2 | id: ID! 3 | title: String! 4 | publisher: String! 5 | category: Category! 6 | } -------------------------------------------------------------------------------- /config/graphql/types/Category.graphql: -------------------------------------------------------------------------------- 1 | enum Category { 2 | ACTION 3 | ADVENTURE 4 | BIOGRAPHY 5 | CHILDREN 6 | COMEDY 7 | FICTION 8 | HISTORY 9 | ROMANCE 10 | THRILLER 11 | HORROR 12 | } -------------------------------------------------------------------------------- /config/graphql/types/Reader.graphql: -------------------------------------------------------------------------------- 1 | type Reader { 2 | id: ID! 3 | name: String! 4 | books: [Book] 5 | wishlist: [Book] 6 | } -------------------------------------------------------------------------------- /config/packages/dev/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: true 4 | -------------------------------------------------------------------------------- /config/packages/doctrine.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | # Adds a fallback DATABASE_URL if the env var is not set. 3 | # This allows you to run cache:warmup even if your 4 | # environment variables are not available yet. 5 | # You should not need to change this value. 6 | env(DATABASE_URL): '' 7 | 8 | doctrine: 9 | dbal: 10 | # configure these for your database server 11 | driver: 'pdo_mysql' 12 | server_version: '5.7' 13 | charset: utf8mb4 14 | 15 | # With Symfony 3.3, remove the `resolve:` prefix 16 | url: '%env(resolve:DATABASE_URL)%' 17 | orm: 18 | auto_generate_proxy_classes: '%kernel.debug%' 19 | naming_strategy: doctrine.orm.naming_strategy.underscore 20 | auto_mapping: true 21 | mappings: 22 | ThatBook: 23 | is_bundle: false 24 | type: annotation 25 | dir: '%kernel.project_dir%/src/Entity' 26 | prefix: 'ThatBook\Entity' 27 | alias: ThatBook 28 | -------------------------------------------------------------------------------- /config/packages/doctrine_migrations.yaml: -------------------------------------------------------------------------------- 1 | doctrine_migrations: 2 | dir_name: '%kernel.project_dir%/src/Migrations' 3 | # namespace is arbitrary but should be different from App\Migrations 4 | # as migrations classes should NOT be autoloaded 5 | namespace: DoctrineMigrations 6 | -------------------------------------------------------------------------------- /config/packages/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | secret: '%env(APP_SECRET)%' 3 | #default_locale: en 4 | #csrf_protection: true 5 | #http_method_override: true 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: ~ 11 | 12 | #esi: true 13 | #fragments: true 14 | php_errors: 15 | log: true 16 | 17 | cache: 18 | # Put the unique name of your app here: the prefix seed 19 | # is used to compute stable namespaces for cache keys. 20 | #prefix_seed: your_vendor_name/app_name 21 | 22 | # The app cache caches to the filesystem by default. 23 | # Other options include: 24 | 25 | # Redis 26 | #app: cache.adapter.redis 27 | #default_redis_provider: redis://localhost 28 | 29 | # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) 30 | #app: cache.adapter.apcu 31 | -------------------------------------------------------------------------------- /config/packages/graphql.yaml: -------------------------------------------------------------------------------- 1 | overblog_graphql: 2 | definitions: 3 | schema: 4 | query: Query 5 | mutation: Mutation 6 | mappings: 7 | auto_discover: false 8 | types: 9 | - type: graphql 10 | dir: "%kernel.project_dir%/config/graphql/types" 11 | suffix: ~ 12 | - type: yaml 13 | dir: "%kernel.project_dir%/config/graphql/schema" 14 | suffix: ~ 15 | -------------------------------------------------------------------------------- /config/packages/league_tactician.yaml: -------------------------------------------------------------------------------- 1 | # Library documentation: http://tactician.thephpleague.com/ 2 | # Bundle documentation: https://github.com/thephpleague/tactician-bundle/blob/v1.0/README.md 3 | tactician: 4 | commandbus: 5 | default: 6 | middleware: 7 | - ThatBook\Middleware\QueueMiddleware 8 | - tactician.middleware.locking 9 | - ThatBook\Middleware\ReleaseRecordedEventsMiddleware 10 | - tactician.middleware.doctrine 11 | - tactician.middleware.command_handler 12 | mutation: 13 | middleware: 14 | - ThatBook\Middleware\QueueMiddleware 15 | - ThatBook\Middleware\GraphQLMiddleware 16 | - tactician.middleware.locking 17 | - ThatBook\Middleware\ReleaseRecordedEventsMiddleware 18 | - tactician.middleware.doctrine 19 | - tactician.middleware.command_handler -------------------------------------------------------------------------------- /config/packages/prod/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | orm: 3 | metadata_cache_driver: 4 | type: service 5 | id: doctrine.system_cache_provider 6 | query_cache_driver: 7 | type: service 8 | id: doctrine.system_cache_provider 9 | result_cache_driver: 10 | type: service 11 | id: doctrine.result_cache_provider 12 | 13 | services: 14 | doctrine.result_cache_provider: 15 | class: Symfony\Component\Cache\DoctrineProvider 16 | public: false 17 | arguments: 18 | - '@doctrine.result_cache_pool' 19 | doctrine.system_cache_provider: 20 | class: Symfony\Component\Cache\DoctrineProvider 21 | public: false 22 | arguments: 23 | - '@doctrine.system_cache_pool' 24 | 25 | framework: 26 | cache: 27 | pools: 28 | doctrine.result_cache_pool: 29 | adapter: cache.app 30 | doctrine.system_cache_pool: 31 | adapter: cache.system 32 | -------------------------------------------------------------------------------- /config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: ~ 4 | -------------------------------------------------------------------------------- /config/packages/test/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | test: true 3 | session: 4 | storage_id: session.storage.mock_file 5 | -------------------------------------------------------------------------------- /config/packages/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | paths: ['%kernel.project_dir%/templates'] 3 | debug: '%kernel.debug%' 4 | strict_variables: '%kernel.debug%' 5 | -------------------------------------------------------------------------------- /config/routes.yaml: -------------------------------------------------------------------------------- 1 | #index: 2 | # path: / 3 | # defaults: { _controller: 'App\Controller\DefaultController::index' } 4 | -------------------------------------------------------------------------------- /config/routes/annotations.yaml: -------------------------------------------------------------------------------- 1 | #controllers: 2 | # resource: ../../src/Controller/ 3 | # type: annotation 4 | -------------------------------------------------------------------------------- /config/routes/dev/graphiql.yaml: -------------------------------------------------------------------------------- 1 | overblog_graphiql: 2 | resource: "@OverblogGraphiQLBundle/Resources/config/routing.xml" 3 | -------------------------------------------------------------------------------- /config/routes/dev/twig.yaml: -------------------------------------------------------------------------------- 1 | _errors: 2 | resource: '@TwigBundle/Resources/config/routing/errors.xml' 3 | prefix: /_error 4 | -------------------------------------------------------------------------------- /config/routes/graphql.yaml: -------------------------------------------------------------------------------- 1 | overblog_graphql_endpoint: 2 | resource: "@OverblogGraphQLBundle/Resources/config/routing/graphql.yml" 3 | -------------------------------------------------------------------------------- /config/services.yaml: -------------------------------------------------------------------------------- 1 | # Put parameters here that don't need to change on each machine where the app is deployed 2 | # https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration 3 | parameters: 4 | 5 | services: 6 | # default configuration for services in *this* file 7 | _defaults: 8 | autowire: true # Automatically injects dependencies in your services. 9 | autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. 10 | public: false # Allows optimizing the container by removing unused services; this also means 11 | # fetching services directly from the container via $container->get() won't work. 12 | # The best practice is to be explicit about your dependencies anyway. 13 | 14 | # makes classes in src/ available to be used as services 15 | # this creates a service per class whose id is the fully-qualified class name 16 | ThatBook\: 17 | resource: '../src/*' 18 | exclude: '../src/{Entity,Event,Repository,Migrations,Tests,Kernel.php}' 19 | 20 | ThatBook\Repository\: 21 | resource: '../src/Repository' 22 | public: true 23 | 24 | event_recorder: 25 | class: ThatBook\Event\EventRecorder 26 | autowire: false 27 | 28 | ThatBook\Event\EventRecorder: '@event_recorder' 29 | 30 | League\Tactician\CommandBus: '@tactician.commandbus.default' -------------------------------------------------------------------------------- /config/services_test.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | _defaults: 3 | public: true 4 | 5 | # If you need to access services in a test, create an alias 6 | # and then fetch that alias from the container. As a convention, 7 | # aliases are prefixed with test. For example: 8 | # 9 | # test.App\Service\MyService: '@App\Service\MyService' 10 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | tests/ 38 | 39 | 40 | 41 | 42 | 43 | ./src/ 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | load(__DIR__.'/../.env'); 16 | } 17 | 18 | $env = $_SERVER['APP_ENV'] ?? 'dev'; 19 | $debug = $_SERVER['APP_DEBUG'] ?? ('prod' !== $env); 20 | 21 | if ($debug) { 22 | umask(0000); 23 | 24 | Debug::enable(); 25 | } 26 | 27 | if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? false) { 28 | Request::setTrustedProxies(explode(',', $trustedProxies), Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST); 29 | } 30 | 31 | if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? false) { 32 | Request::setTrustedHosts(explode(',', $trustedHosts)); 33 | } 34 | 35 | $kernel = new Kernel($env, $debug); 36 | $request = Request::createFromGlobals(); 37 | $response = $kernel->handle($request); 38 | $response->send(); 39 | $kernel->terminate($request, $response); 40 | -------------------------------------------------------------------------------- /src/Entity/Book.php: -------------------------------------------------------------------------------- 1 | title = $title; 60 | $this->publisher = $publisher; 61 | $this->category = $category; 62 | } 63 | 64 | public function getId(): string 65 | { 66 | return $this->id; 67 | } 68 | 69 | public function getTitle(): string 70 | { 71 | return $this->title; 72 | } 73 | 74 | public function getPublisher(): string 75 | { 76 | return $this->publisher; 77 | } 78 | 79 | public function getCategory(): string 80 | { 81 | return $this->category; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Entity/Reader.php: -------------------------------------------------------------------------------- 1 | name = $name; 55 | $this->books = new ArrayCollection(); 56 | $this->wishlist = new ArrayCollection(); 57 | } 58 | 59 | public function getId(): string 60 | { 61 | return $this->id; 62 | } 63 | 64 | public function getName(): string 65 | { 66 | return $this->name; 67 | } 68 | 69 | public function registerBook(Book $book): self 70 | { 71 | if (!$this->books->contains($book)) { 72 | $this->books[] = $book; 73 | } 74 | 75 | if ($this->wishlist->contains($book)) { 76 | $this->wishlist->removeElement($book); 77 | } 78 | 79 | return $this; 80 | } 81 | 82 | public function registerWish(Book $book): self 83 | { 84 | if ($this->books->contains($book)) { 85 | throw new ReaderAlreadyHasBookException(); 86 | } 87 | 88 | if (!$this->wishlist->contains($book)) { 89 | $this->wishlist[] = $book; 90 | } 91 | 92 | return $this; 93 | } 94 | 95 | public function getBooks(): Collection 96 | { 97 | return $this->books; 98 | } 99 | 100 | public function getWishlist(): Collection 101 | { 102 | return $this->wishlist; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Event/AbstractEvent.php: -------------------------------------------------------------------------------- 1 | recordedEvents; 16 | $this->eraseEvents(); 17 | return $events; 18 | } 19 | 20 | public function eraseEvents() 21 | { 22 | $this->recordedEvents = []; 23 | } 24 | 25 | public function record(AbstractEvent $event) 26 | { 27 | $this->recordedEvents[] = $event; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Event/ReaderBookRegisteredEvent.php: -------------------------------------------------------------------------------- 1 | book = $book; 29 | $this->reader = $reader; 30 | $this->createdAt = new \DateTime(); 31 | } 32 | 33 | public function getBook(): Book 34 | { 35 | return $this->book; 36 | } 37 | 38 | public function getReader(): Reader 39 | { 40 | return $this->reader; 41 | } 42 | 43 | public function getCreatedAt(): \DateTime 44 | { 45 | return $this->createdAt; 46 | } 47 | 48 | public static function getName(): string 49 | { 50 | return 'reader_book.registered'; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/EventSubscriber/ReaderSubscriber.php: -------------------------------------------------------------------------------- 1 | serviceBus = $serviceBus; 21 | } 22 | 23 | public static function getSubscribedEvents(): array 24 | { 25 | return [ 26 | ReaderBookRegisteredEvent::getName() => 'whenReaderBookRegistered' 27 | ]; 28 | } 29 | 30 | public function whenReaderBookRegistered(ReaderBookRegisteredEvent $event) 31 | { 32 | $command = new NotifyTradeOpportunities(); 33 | $this->serviceBus->handle($command); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Exception/BookNotFoundException.php: -------------------------------------------------------------------------------- 1 | getProjectDir().'/var/cache/'.$this->environment; 21 | } 22 | 23 | public function getLogDir() 24 | { 25 | return $this->getProjectDir().'/var/log'; 26 | } 27 | 28 | public function registerBundles() 29 | { 30 | $contents = require $this->getProjectDir().'/config/bundles.php'; 31 | foreach ($contents as $class => $envs) { 32 | if (isset($envs['all']) || isset($envs[$this->environment])) { 33 | yield new $class(); 34 | } 35 | } 36 | } 37 | 38 | protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader) 39 | { 40 | $container->setParameter('container.autowiring.strict_mode', true); 41 | $container->setParameter('container.dumper.inline_class_loader', true); 42 | $confDir = $this->getProjectDir().'/config'; 43 | 44 | $loader->load($confDir.'/{packages}/*'.self::CONFIG_EXTS, 'glob'); 45 | $loader->load($confDir.'/{packages}/'.$this->environment.'/**/*'.self::CONFIG_EXTS, 'glob'); 46 | $loader->load($confDir.'/{services}'.self::CONFIG_EXTS, 'glob'); 47 | $loader->load($confDir.'/{services}_'.$this->environment.self::CONFIG_EXTS, 'glob'); 48 | 49 | $handlerDefinition = $container->registerForAutoconfiguration(HandlerInterface::class); 50 | $handlerDefinition->addTag('tactician.handler', ['typehints' => true]); 51 | } 52 | 53 | protected function configureRoutes(RouteCollectionBuilder $routes) 54 | { 55 | $confDir = $this->getProjectDir().'/config'; 56 | 57 | $routes->import($confDir.'/{routes}/*'.self::CONFIG_EXTS, '/', 'glob'); 58 | $routes->import($confDir.'/{routes}/'.$this->environment.'/**/*'.self::CONFIG_EXTS, '/', 'glob'); 59 | $routes->import($confDir.'/{routes}'.self::CONFIG_EXTS, '/', 'glob'); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Middleware/GraphQLMiddleware.php: -------------------------------------------------------------------------------- 1 | producer->publish( 26 | // $command->publisherServiceName(), 27 | // $command->toMessage(), 28 | // $command->routingKey() 29 | // ); 30 | 31 | } catch (\Exception | \Throwable $exception) { 32 | throw $exception; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Middleware/ReleaseRecordedEventsMiddleware.php: -------------------------------------------------------------------------------- 1 | eventRecorder = $eventRecorder; 29 | $this->eventDispatcher = $eventDispatcher; 30 | } 31 | 32 | /** 33 | * @inheritdoc 34 | */ 35 | public function execute($command, callable $next) 36 | { 37 | try { 38 | $result = $next($command); 39 | } catch (\Exception $exception) { 40 | $this->eventRecorder->eraseEvents(); 41 | throw $exception; 42 | } 43 | 44 | $recordedEvents = $this->eventRecorder->releaseEvents(); 45 | 46 | foreach ($recordedEvents as $event) { 47 | $this->eventDispatcher->dispatch($event->getName(), $event); 48 | } 49 | 50 | return $result; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Migrations/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brunonm/expressive-architecture/49585506667e1b2041ca6b10ad09338a476367c3/src/Migrations/.gitignore -------------------------------------------------------------------------------- /src/Migrations/Version20180221021751.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 17 | 18 | $this->addSql('CREATE TABLE category (id CHAR(36) NOT NULL COMMENT \'(DC2Type:guid)\', title VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 19 | $this->addSql('CREATE TABLE book (id CHAR(36) NOT NULL COMMENT \'(DC2Type:guid)\', category_id CHAR(36) DEFAULT NULL COMMENT \'(DC2Type:guid)\', title VARCHAR(255) NOT NULL, publisher VARCHAR(255) NOT NULL, INDEX IDX_CBE5A33112469DE2 (category_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 20 | $this->addSql('CREATE TABLE reader (id CHAR(36) NOT NULL COMMENT \'(DC2Type:guid)\', name VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 21 | $this->addSql('CREATE TABLE reader_book (reader_id CHAR(36) NOT NULL COMMENT \'(DC2Type:guid)\', book_id CHAR(36) NOT NULL COMMENT \'(DC2Type:guid)\', INDEX IDX_2A3845F31717D737 (reader_id), INDEX IDX_2A3845F316A2B381 (book_id), PRIMARY KEY(reader_id, book_id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 22 | $this->addSql('CREATE TABLE wishlist (reader_id CHAR(36) NOT NULL COMMENT \'(DC2Type:guid)\', book_id CHAR(36) NOT NULL COMMENT \'(DC2Type:guid)\', INDEX IDX_9CE12A311717D737 (reader_id), INDEX IDX_9CE12A3116A2B381 (book_id), PRIMARY KEY(reader_id, book_id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 23 | $this->addSql('CREATE TABLE trade (id CHAR(36) NOT NULL COMMENT \'(DC2Type:guid)\', old_reader_id CHAR(36) DEFAULT NULL COMMENT \'(DC2Type:guid)\', new_reader_id CHAR(36) DEFAULT NULL COMMENT \'(DC2Type:guid)\', book_id CHAR(36) DEFAULT NULL COMMENT \'(DC2Type:guid)\', created_at DATETIME NOT NULL, INDEX IDX_7E1A436652A39386 (old_reader_id), INDEX IDX_7E1A43662588D258 (new_reader_id), INDEX IDX_7E1A436616A2B381 (book_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 24 | $this->addSql('ALTER TABLE book ADD CONSTRAINT FK_CBE5A33112469DE2 FOREIGN KEY (category_id) REFERENCES category (id)'); 25 | $this->addSql('ALTER TABLE reader_book ADD CONSTRAINT FK_2A3845F31717D737 FOREIGN KEY (reader_id) REFERENCES reader (id)'); 26 | $this->addSql('ALTER TABLE reader_book ADD CONSTRAINT FK_2A3845F316A2B381 FOREIGN KEY (book_id) REFERENCES book (id)'); 27 | $this->addSql('ALTER TABLE wishlist ADD CONSTRAINT FK_9CE12A311717D737 FOREIGN KEY (reader_id) REFERENCES reader (id)'); 28 | $this->addSql('ALTER TABLE wishlist ADD CONSTRAINT FK_9CE12A3116A2B381 FOREIGN KEY (book_id) REFERENCES book (id)'); 29 | $this->addSql('ALTER TABLE trade ADD CONSTRAINT FK_7E1A436652A39386 FOREIGN KEY (old_reader_id) REFERENCES reader (id)'); 30 | $this->addSql('ALTER TABLE trade ADD CONSTRAINT FK_7E1A43662588D258 FOREIGN KEY (new_reader_id) REFERENCES reader (id)'); 31 | $this->addSql('ALTER TABLE trade ADD CONSTRAINT FK_7E1A436616A2B381 FOREIGN KEY (book_id) REFERENCES book (id)'); 32 | } 33 | 34 | public function down(Schema $schema) 35 | { 36 | // this down() migration is auto-generated, please modify it to your needs 37 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 38 | 39 | $this->addSql('ALTER TABLE book DROP FOREIGN KEY FK_CBE5A33112469DE2'); 40 | $this->addSql('ALTER TABLE reader_book DROP FOREIGN KEY FK_2A3845F316A2B381'); 41 | $this->addSql('ALTER TABLE wishlist DROP FOREIGN KEY FK_9CE12A3116A2B381'); 42 | $this->addSql('ALTER TABLE trade DROP FOREIGN KEY FK_7E1A436616A2B381'); 43 | $this->addSql('ALTER TABLE reader_book DROP FOREIGN KEY FK_2A3845F31717D737'); 44 | $this->addSql('ALTER TABLE wishlist DROP FOREIGN KEY FK_9CE12A311717D737'); 45 | $this->addSql('ALTER TABLE trade DROP FOREIGN KEY FK_7E1A436652A39386'); 46 | $this->addSql('ALTER TABLE trade DROP FOREIGN KEY FK_7E1A43662588D258'); 47 | $this->addSql('DROP TABLE category'); 48 | $this->addSql('DROP TABLE book'); 49 | $this->addSql('DROP TABLE reader'); 50 | $this->addSql('DROP TABLE reader_book'); 51 | $this->addSql('DROP TABLE wishlist'); 52 | $this->addSql('DROP TABLE trade'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Migrations/Version20180221022807.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 17 | 18 | $this->addSql('ALTER TABLE book CHANGE category_id category_id CHAR(36) NOT NULL COMMENT \'(DC2Type:guid)\''); 19 | $this->addSql('ALTER TABLE trade CHANGE old_reader_id old_reader_id CHAR(36) NOT NULL COMMENT \'(DC2Type:guid)\', CHANGE new_reader_id new_reader_id CHAR(36) NOT NULL COMMENT \'(DC2Type:guid)\', CHANGE book_id book_id CHAR(36) NOT NULL COMMENT \'(DC2Type:guid)\''); 20 | } 21 | 22 | public function down(Schema $schema) 23 | { 24 | // this down() migration is auto-generated, please modify it to your needs 25 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 26 | 27 | $this->addSql('ALTER TABLE book CHANGE category_id category_id CHAR(36) DEFAULT NULL COLLATE utf8_unicode_ci COMMENT \'(DC2Type:guid)\''); 28 | $this->addSql('ALTER TABLE trade CHANGE old_reader_id old_reader_id CHAR(36) DEFAULT NULL COLLATE utf8_unicode_ci COMMENT \'(DC2Type:guid)\', CHANGE new_reader_id new_reader_id CHAR(36) DEFAULT NULL COLLATE utf8_unicode_ci COMMENT \'(DC2Type:guid)\', CHANGE book_id book_id CHAR(36) DEFAULT NULL COLLATE utf8_unicode_ci COMMENT \'(DC2Type:guid)\''); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Migrations/Version20180224062434.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 17 | 18 | $this->addSql('DROP TABLE trade'); 19 | } 20 | 21 | public function down(Schema $schema) 22 | { 23 | // this down() migration is auto-generated, please modify it to your needs 24 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 25 | 26 | $this->addSql('CREATE TABLE trade (id CHAR(36) NOT NULL COLLATE utf8_unicode_ci COMMENT \'(DC2Type:guid)\', old_reader_id CHAR(36) NOT NULL COLLATE utf8_unicode_ci COMMENT \'(DC2Type:guid)\', new_reader_id CHAR(36) NOT NULL COLLATE utf8_unicode_ci COMMENT \'(DC2Type:guid)\', book_id CHAR(36) NOT NULL COLLATE utf8_unicode_ci COMMENT \'(DC2Type:guid)\', created_at DATETIME NOT NULL, INDEX IDX_7E1A436652A39386 (old_reader_id), INDEX IDX_7E1A43662588D258 (new_reader_id), INDEX IDX_7E1A436616A2B381 (book_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 27 | $this->addSql('ALTER TABLE trade ADD CONSTRAINT FK_7E1A436616A2B381 FOREIGN KEY (book_id) REFERENCES book (id)'); 28 | $this->addSql('ALTER TABLE trade ADD CONSTRAINT FK_7E1A43662588D258 FOREIGN KEY (new_reader_id) REFERENCES reader (id)'); 29 | $this->addSql('ALTER TABLE trade ADD CONSTRAINT FK_7E1A436652A39386 FOREIGN KEY (old_reader_id) REFERENCES reader (id)'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Migrations/Version20180224074548.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 17 | 18 | $this->addSql('ALTER TABLE book DROP FOREIGN KEY FK_CBE5A33112469DE2'); 19 | $this->addSql('DROP TABLE category'); 20 | $this->addSql('DROP INDEX IDX_CBE5A33112469DE2 ON book'); 21 | $this->addSql('ALTER TABLE book ADD category VARCHAR(255) NOT NULL, DROP category_id'); 22 | } 23 | 24 | public function down(Schema $schema) 25 | { 26 | // this down() migration is auto-generated, please modify it to your needs 27 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 28 | 29 | $this->addSql('CREATE TABLE category (id CHAR(36) NOT NULL COLLATE utf8_unicode_ci COMMENT \'(DC2Type:guid)\', title VARCHAR(255) NOT NULL COLLATE utf8_unicode_ci, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 30 | $this->addSql('ALTER TABLE book ADD category_id CHAR(36) NOT NULL COLLATE utf8_unicode_ci COMMENT \'(DC2Type:guid)\', DROP category'); 31 | $this->addSql('ALTER TABLE book ADD CONSTRAINT FK_CBE5A33112469DE2 FOREIGN KEY (category_id) REFERENCES category (id)'); 32 | $this->addSql('CREATE INDEX IDX_CBE5A33112469DE2 ON book (category_id)'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Queue/AbstractQueueableCommand.php: -------------------------------------------------------------------------------- 1 | _em->persist($entity); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Repository/BookRepository.php: -------------------------------------------------------------------------------- 1 | title = $title; 26 | $this->publisher = $publisher; 27 | $this->category = $category; 28 | } 29 | 30 | public function getTitle(): string 31 | { 32 | return $this->title; 33 | } 34 | 35 | public function getPublisher(): string 36 | { 37 | return $this->publisher; 38 | } 39 | 40 | public function getCategory(): string 41 | { 42 | return $this->category; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Service/CatalogBookHandler.php: -------------------------------------------------------------------------------- 1 | bookRepository = $bookRepository; 21 | } 22 | 23 | public function handle(CatalogBook $command): Book 24 | { 25 | $book = new Book($command->getTitle(), $command->getPublisher(), $command->getCategory()); 26 | $this->bookRepository->store($book); 27 | return $book; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Service/HandlerInterface.php: -------------------------------------------------------------------------------- 1 | readerRepository = $readerRepository; 20 | } 21 | 22 | public function handle(NotifyTradeOpportunities $command) 23 | { 24 | // find trade opportunities 25 | // send email 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Service/RegisterReader.php: -------------------------------------------------------------------------------- 1 | name = $name; 16 | } 17 | 18 | public function getName(): string 19 | { 20 | return $this->name; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Service/RegisterReaderBook.php: -------------------------------------------------------------------------------- 1 | bookId = $bookId; 21 | $this->readerId = $readerId; 22 | } 23 | 24 | public function getBookId(): string 25 | { 26 | return $this->bookId; 27 | } 28 | 29 | public function getReaderId(): string 30 | { 31 | return $this->readerId; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Service/RegisterReaderBookHandler.php: -------------------------------------------------------------------------------- 1 | readerRepository = $readerRepository; 38 | $this->bookRepository = $bookRepository; 39 | $this->eventRecorder = $eventRecorder; 40 | } 41 | 42 | public function handle(RegisterReaderBook $command) 43 | { 44 | if (!$reader = $this->readerRepository->find($command->getReaderId())) { 45 | throw new ReaderNotFoundException(); 46 | } 47 | 48 | if (!$book = $this->bookRepository->find($command->getBookId())) { 49 | throw new BookNotFoundException(); 50 | } 51 | 52 | $reader->registerBook($book); 53 | 54 | $this->readerRepository->store($reader); 55 | 56 | $this->eventRecorder->record(new ReaderBookRegisteredEvent($book, $reader)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Service/RegisterReaderHandler.php: -------------------------------------------------------------------------------- 1 | readerRepository = $readerRepository; 21 | } 22 | 23 | public function handle(RegisterReader $command): Reader 24 | { 25 | $reader = new Reader($command->getName()); 26 | $this->readerRepository->store($reader); 27 | return $reader; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Service/RegisterReaderWish.php: -------------------------------------------------------------------------------- 1 | bookId = $bookId; 21 | $this->readerId = $readerId; 22 | } 23 | 24 | public function getBookId(): string 25 | { 26 | return $this->bookId; 27 | } 28 | 29 | public function getReaderId(): string 30 | { 31 | return $this->readerId; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Service/RegisterReaderWishHandler.php: -------------------------------------------------------------------------------- 1 | readerRepository = $readerRepository; 28 | $this->bookRepository = $bookRepository; 29 | } 30 | 31 | public function handle(RegisterReaderWish $command) 32 | { 33 | if (!$reader = $this->readerRepository->find($command->getReaderId())) { 34 | throw new ReaderNotFoundException(); 35 | } 36 | 37 | if (!$book = $this->bookRepository->find($command->getBookId())) { 38 | throw new BookNotFoundException(); 39 | } 40 | 41 | $reader->registerWish($book); 42 | 43 | $this->readerRepository->store($reader); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /symfony.lock: -------------------------------------------------------------------------------- 1 | { 2 | "doctrine/annotations": { 3 | "version": "1.0", 4 | "recipe": { 5 | "repo": "github.com/symfony/recipes", 6 | "branch": "master", 7 | "version": "1.0", 8 | "ref": "cb4152ebcadbe620ea2261da1a1c5a9b8cea7672" 9 | } 10 | }, 11 | "doctrine/cache": { 12 | "version": "v1.7.1" 13 | }, 14 | "doctrine/collections": { 15 | "version": "v1.5.0" 16 | }, 17 | "doctrine/common": { 18 | "version": "v2.8.1" 19 | }, 20 | "doctrine/dbal": { 21 | "version": "v2.6.3" 22 | }, 23 | "doctrine/doctrine-bundle": { 24 | "version": "1.6", 25 | "recipe": { 26 | "repo": "github.com/symfony/recipes", 27 | "branch": "master", 28 | "version": "1.6", 29 | "ref": "c407ab0b5e5a39b242a52d323a5e84e6d3b7e4c0" 30 | } 31 | }, 32 | "doctrine/doctrine-cache-bundle": { 33 | "version": "1.3.2" 34 | }, 35 | "doctrine/doctrine-migrations-bundle": { 36 | "version": "1.2", 37 | "recipe": { 38 | "repo": "github.com/symfony/recipes", 39 | "branch": "master", 40 | "version": "1.2", 41 | "ref": "c1431086fec31f17fbcfe6d6d7e92059458facc1" 42 | } 43 | }, 44 | "doctrine/inflector": { 45 | "version": "v1.3.0" 46 | }, 47 | "doctrine/instantiator": { 48 | "version": "1.1.0" 49 | }, 50 | "doctrine/lexer": { 51 | "version": "v1.0.1" 52 | }, 53 | "doctrine/migrations": { 54 | "version": "v1.6.2" 55 | }, 56 | "doctrine/orm": { 57 | "version": "v2.6.0" 58 | }, 59 | "hamcrest/hamcrest-php": { 60 | "version": "v2.0.0" 61 | }, 62 | "jdorn/sql-formatter": { 63 | "version": "v1.2.17" 64 | }, 65 | "league/tactician": { 66 | "version": "v1.0.3" 67 | }, 68 | "league/tactician-bundle": { 69 | "version": "1.0", 70 | "recipe": { 71 | "repo": "github.com/symfony/recipes-contrib", 72 | "branch": "master", 73 | "version": "1.0", 74 | "ref": "222c3d39d38378bc6a9790a0b5baf841ba6679b9" 75 | } 76 | }, 77 | "league/tactician-container": { 78 | "version": "2.0.0" 79 | }, 80 | "league/tactician-doctrine": { 81 | "version": "v1.1" 82 | }, 83 | "mockery/mockery": { 84 | "version": "1.0" 85 | }, 86 | "ocramius/package-versions": { 87 | "version": "1.3.0" 88 | }, 89 | "ocramius/proxy-manager": { 90 | "version": "2.2.0" 91 | }, 92 | "overblog/graphiql-bundle": { 93 | "version": "0.1", 94 | "recipe": { 95 | "repo": "github.com/symfony/recipes-contrib", 96 | "branch": "master", 97 | "version": "0.1", 98 | "ref": "fe8d172f2480efc598f5a8be0e732656d3594cec" 99 | } 100 | }, 101 | "overblog/graphql-bundle": { 102 | "version": "0.11", 103 | "recipe": { 104 | "repo": "github.com/symfony/recipes-contrib", 105 | "branch": "master", 106 | "version": "0.11", 107 | "ref": "cb22b949fa6a4b01f6955e78a58444605f062616" 108 | } 109 | }, 110 | "overblog/graphql-php-generator": { 111 | "version": "v0.7.2" 112 | }, 113 | "psr/cache": { 114 | "version": "1.0.1" 115 | }, 116 | "psr/container": { 117 | "version": "1.0.0" 118 | }, 119 | "psr/log": { 120 | "version": "1.0.2" 121 | }, 122 | "psr/simple-cache": { 123 | "version": "1.0.0" 124 | }, 125 | "symfony/cache": { 126 | "version": "v4.0.4" 127 | }, 128 | "symfony/config": { 129 | "version": "v4.0.4" 130 | }, 131 | "symfony/console": { 132 | "version": "3.3", 133 | "recipe": { 134 | "repo": "github.com/symfony/recipes", 135 | "branch": "master", 136 | "version": "3.3", 137 | "ref": "c646e4b71af082e94b5014daca36ef6812bad076" 138 | } 139 | }, 140 | "symfony/debug": { 141 | "version": "v4.0.4" 142 | }, 143 | "symfony/dependency-injection": { 144 | "version": "v4.0.4" 145 | }, 146 | "symfony/doctrine-bridge": { 147 | "version": "v4.0.4" 148 | }, 149 | "symfony/dotenv": { 150 | "version": "v4.0.4" 151 | }, 152 | "symfony/event-dispatcher": { 153 | "version": "v4.0.4" 154 | }, 155 | "symfony/expression-language": { 156 | "version": "v4.0.5" 157 | }, 158 | "symfony/filesystem": { 159 | "version": "v4.0.4" 160 | }, 161 | "symfony/finder": { 162 | "version": "v4.0.4" 163 | }, 164 | "symfony/flex": { 165 | "version": "1.0", 166 | "recipe": { 167 | "repo": "github.com/symfony/recipes", 168 | "branch": "master", 169 | "version": "1.0", 170 | "ref": "cc1afd81841db36fbef982fe56b48ade6716fac4" 171 | } 172 | }, 173 | "symfony/framework-bundle": { 174 | "version": "3.3", 175 | "recipe": { 176 | "repo": "github.com/symfony/recipes", 177 | "branch": "master", 178 | "version": "3.3", 179 | "ref": "b9f462a47f7fd28d56c61f59c027fd7ad8e1aac8" 180 | } 181 | }, 182 | "symfony/http-foundation": { 183 | "version": "v4.0.4" 184 | }, 185 | "symfony/http-kernel": { 186 | "version": "v4.0.4" 187 | }, 188 | "symfony/inflector": { 189 | "version": "v4.0.5" 190 | }, 191 | "symfony/lts": { 192 | "version": "4-dev" 193 | }, 194 | "symfony/maker-bundle": { 195 | "version": "1.0", 196 | "recipe": { 197 | "repo": "github.com/symfony/recipes", 198 | "branch": "master", 199 | "version": "1.0", 200 | "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" 201 | } 202 | }, 203 | "symfony/options-resolver": { 204 | "version": "v4.0.5" 205 | }, 206 | "symfony/orm-pack": { 207 | "version": "v1.0.5" 208 | }, 209 | "symfony/phpunit-bridge": { 210 | "version": "3.3", 211 | "recipe": { 212 | "repo": "github.com/symfony/recipes", 213 | "branch": "master", 214 | "version": "3.3", 215 | "ref": "179470cb6492db92dffee208cfdb436f175c93b4" 216 | } 217 | }, 218 | "symfony/polyfill-mbstring": { 219 | "version": "v1.7.0" 220 | }, 221 | "symfony/property-access": { 222 | "version": "v4.0.5" 223 | }, 224 | "symfony/routing": { 225 | "version": "4.0", 226 | "recipe": { 227 | "repo": "github.com/symfony/recipes", 228 | "branch": "master", 229 | "version": "4.0", 230 | "ref": "cda8b550123383d25827705d05a42acf6819fe4e" 231 | } 232 | }, 233 | "symfony/twig-bridge": { 234 | "version": "v4.0.5" 235 | }, 236 | "symfony/twig-bundle": { 237 | "version": "3.3", 238 | "recipe": { 239 | "repo": "github.com/symfony/recipes", 240 | "branch": "master", 241 | "version": "3.3", 242 | "ref": "f75ac166398e107796ca94cc57fa1edaa06ec47f" 243 | } 244 | }, 245 | "symfony/yaml": { 246 | "version": "v4.0.4" 247 | }, 248 | "twig/twig": { 249 | "version": "v2.4.4" 250 | }, 251 | "webonyx/graphql-php": { 252 | "version": "v0.11.5" 253 | }, 254 | "zendframework/zend-code": { 255 | "version": "3.3.0" 256 | }, 257 | "zendframework/zend-eventmanager": { 258 | "version": "3.2.0" 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /templates/base.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}Welcome!{% endblock %} 6 | {% block stylesheets %}{% endblock %} 7 | 8 | 9 | {% block body %}{% endblock %} 10 | {% block javascripts %}{% endblock %} 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brunonm/expressive-architecture/49585506667e1b2041ca6b10ad09338a476367c3/tests/.gitignore -------------------------------------------------------------------------------- /tests/Integration/AbstractIntegrationTestCase.php: -------------------------------------------------------------------------------- 1 | container = static::$kernel->getContainer(); 38 | $this->em = $this->container->get('doctrine.orm.entity_manager'); 39 | $this->databaseFile = $this->em->getConnection()->getDatabase(); 40 | $this->databaseFileBackup = $this->databaseFile . '.bak'; 41 | $this->createDatabase(); 42 | } 43 | 44 | protected function getServiceBus(): CommandBus 45 | { 46 | return $this->container->get('tactician.commandbus.default'); 47 | } 48 | 49 | protected function createDatabase() 50 | { 51 | if (!file_exists($this->databaseFile)) { 52 | 53 | $application = new Application(static::$kernel); 54 | $application->setAutoExit(false); 55 | 56 | $schemaCreate = [ 57 | 'command' => 'doctrine:schema:create', 58 | '--quiet' => true, 59 | '--env' => 'test' 60 | ]; 61 | 62 | $application->run(new ArrayInput($schemaCreate)); 63 | 64 | $this->backupDatabase(); 65 | } 66 | } 67 | 68 | protected function backupDatabase() 69 | { 70 | copy($this->databaseFile, $this->databaseFileBackup); 71 | } 72 | 73 | protected function restoreDatabase() 74 | { 75 | unlink($this->databaseFile); 76 | copy($this->databaseFileBackup, $this->databaseFile); 77 | } 78 | 79 | protected function tearDown() 80 | { 81 | parent::tearDown(); 82 | $this->em->close(); 83 | $this->em = null; 84 | $this->restoreDatabase(); 85 | } 86 | } -------------------------------------------------------------------------------- /tests/Integration/Service/CatalogBookHandlerTest.php: -------------------------------------------------------------------------------- 1 | getServiceBus()->handle(new CatalogBook('Pequeni princípe', 'Tenante', 'CHILDREN')); 15 | 16 | $books = $this->em->getRepository(Book::class)->findAll(); 17 | $this->assertCount(1, $books); 18 | $this->assertEquals('Pequeni princípe', $books[0]->getTitle()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Integration/Service/RegisterReaderBookHandlerTest.php: -------------------------------------------------------------------------------- 1 | em->persist($reader); 17 | 18 | $book = new Book('Pequeni princípe', 'Tenante', 'CHILDREN'); 19 | $this->em->persist($book); 20 | 21 | $this->em->flush(); 22 | 23 | $this->getServiceBus()->handle(new RegisterReaderBook($book->getId(), $reader->getId())); 24 | 25 | $this->assertCount(1, $reader->getBooks()); 26 | $this->assertEquals($book, $reader->getBooks()->first()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Integration/Service/RegisterReaderHandlerTest.php: -------------------------------------------------------------------------------- 1 | getServiceBus()->handle(new RegisterReader('Bruno')); 15 | 16 | $repo = $this->em->getRepository(Reader::class); 17 | 18 | $this->assertCount(1, $repo->findAll()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Integration/Service/RegisterReaderWishHandlerTest.php: -------------------------------------------------------------------------------- 1 | em->persist($reader); 17 | 18 | $book = new Book('Pequeni princípe', 'Tenante', 'CHILDREN'); 19 | $this->em->persist($book); 20 | 21 | $this->em->flush(); 22 | 23 | $this->getServiceBus()->handle(new RegisterReaderWish($book->getId(), $reader->getId())); 24 | 25 | $this->assertCount(1, $reader->getWishlist()); 26 | $this->assertEquals($book, $reader->getWishlist()->first()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Unit/AbstractUnitTestCase.php: -------------------------------------------------------------------------------- 1 | registerBook($book); 19 | 20 | $this->assertCount(1, $reader->getBooks()); 21 | } 22 | 23 | public function testShouldNotRegisterReaderBookTwice() 24 | { 25 | $book = M::mock(Book::class); 26 | 27 | $reader = new Reader('Bruno'); 28 | $reader->registerBook($book); 29 | $reader->registerBook($book); 30 | 31 | $this->assertCount(1, $reader->getBooks()); 32 | } 33 | 34 | public function testShouldRemoveWishWhenRegisterTheBook() 35 | { 36 | $book = M::mock(Book::class); 37 | 38 | $reader = new Reader('Bruno'); 39 | $reader->registerWish($book); 40 | $reader->registerBook($book); 41 | 42 | $this->assertCount(0, $reader->getWishlist()); 43 | } 44 | 45 | public function testShouldRegisterAReaderWish() 46 | { 47 | $book = M::mock(Book::class); 48 | 49 | $reader = new Reader('Bruno'); 50 | $reader->registerBook($book); 51 | 52 | $this->assertCount(1, $reader->getBooks()); 53 | } 54 | 55 | /** 56 | * @expectedException \ThatBook\Exception\ReaderAlreadyHasBookException 57 | */ 58 | public function testShouldThrowExceptionIfReaderAlreadyHasBookWhenRegisterWish() 59 | { 60 | $book = M::mock(Book::class); 61 | 62 | $reader = new Reader('Bruno'); 63 | $reader->registerBook($book); 64 | $reader->registerWish($book); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/Unit/Event/EventRecorderTest.php: -------------------------------------------------------------------------------- 1 | record($event); 20 | $this->assertCount(1, $recorder->releaseEvents()); 21 | } 22 | 23 | public function testShouldEraseRecordedEvents() 24 | { 25 | $event = new ReaderBookRegisteredEvent(M::mock(Book::class), M::mock(Reader::class)); 26 | $recorder = new EventRecorder(); 27 | $recorder->record($event); 28 | $recorder->eraseEvents(); 29 | $this->assertCount(0, $recorder->releaseEvents()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Unit/Middleware/ReleaseRecordedEventsMiddlewareTest.php: -------------------------------------------------------------------------------- 1 | makePartial(); 18 | 19 | $recorderMock = M::mock(EventRecorder::class); 20 | $recorderMock->shouldReceive('releaseEvents')->andReturn([$eventMock]); 21 | 22 | $dispatcherMock = M::mock(EventDispatcherInterface::class); 23 | $dispatcherMock->shouldReceive('dispatch')->once(); 24 | 25 | $middleware = new ReleaseRecordedEventsMiddleware($recorderMock, $dispatcherMock); 26 | 27 | $middleware->execute('command', function () {}); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Unit/Service/CatalogBookHandlerTest.php: -------------------------------------------------------------------------------- 1 | bookRepoMock = M::mock(BookRepository::class); 21 | $this->handler = new CatalogBookHandler($this->bookRepoMock); 22 | } 23 | 24 | public function testShouldCatalogABook() 25 | { 26 | $this->bookRepoMock->shouldReceive('store')->once(); 27 | 28 | $command = new CatalogBook('Contra', 'Alta books', 'ACTION'); 29 | 30 | $this->handler->handle($command); 31 | } 32 | 33 | /** 34 | * @expectedException ThatBook\Exception\UnknownCategoryException 35 | */ 36 | public function testShouldThrowExceptionIfCategoryNotFound() 37 | { 38 | $command = new CatalogBook('Contra', 'Alta books', 'XPTO'); 39 | 40 | $this->handler->handle($command); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Unit/Service/RegisterReaderBookHandlerTest.php: -------------------------------------------------------------------------------- 1 | readerRepoMock = M::mock(ReaderRepository::class); 29 | $this->bookRepoMock = M::mock(BookRepository::class); 30 | $this->recorderMock = M::mock(EventRecorder::class)->makePartial(); 31 | 32 | $this->handler = new RegisterReaderBookHandler( 33 | $this->readerRepoMock, 34 | $this->bookRepoMock, 35 | $this->recorderMock 36 | ); 37 | } 38 | 39 | public function testShouldRegisterABook() 40 | { 41 | $readerMock = M::mock(Reader::class); 42 | $readerMock->shouldReceive('registerBook')->once(); 43 | 44 | $this->readerRepoMock 45 | ->shouldReceive('find') 46 | ->andReturn($readerMock); 47 | 48 | $this->readerRepoMock 49 | ->shouldReceive('store') 50 | ->once(); 51 | 52 | $this->bookRepoMock 53 | ->shouldReceive('find') 54 | ->andReturn(M::mock(Book::class)); 55 | 56 | $command = new RegisterReaderBook('bookId', 'readerId'); 57 | 58 | $this->handler->handle($command); 59 | } 60 | 61 | public function testShouldRecordAnEvent() 62 | { 63 | $readerMock = M::mock(Reader::class); 64 | $readerMock->shouldReceive('registerBook'); 65 | 66 | $this->readerRepoMock 67 | ->shouldReceive('find') 68 | ->andReturn($readerMock); 69 | 70 | $this->readerRepoMock->shouldReceive('store'); 71 | 72 | $this->bookRepoMock 73 | ->shouldReceive('find') 74 | ->andReturn(M::mock(Book::class)); 75 | 76 | $this->recorderMock->shouldReceive('record')->once(); 77 | 78 | $command = new RegisterReaderBook('bookId', 'readerId'); 79 | 80 | $this->handler->handle($command); 81 | } 82 | 83 | /** 84 | * @expectedException ThatBook\Exception\BookNotFoundException 85 | */ 86 | public function testShouldThrowExceptionIfBookNotFound() 87 | { 88 | $this->readerRepoMock->shouldReceive('find')->andReturn(M::mock(Reader::class)); 89 | $this->bookRepoMock->shouldReceive('find')->andReturn(null); 90 | $command = new RegisterReaderBook('bookId', 'readerId'); 91 | $this->handler->handle($command); 92 | } 93 | 94 | /** 95 | * @expectedException ThatBook\Exception\ReaderNotFoundException 96 | */ 97 | public function testShouldThrowExceptionIfReaderNotFound() 98 | { 99 | $this->readerRepoMock->shouldReceive('find')->andReturn(null); 100 | $this->bookRepoMock->shouldReceive('find')->andReturn(M::mock(Book::class)); 101 | $command = new RegisterReaderBook('bookId', 'readerId'); 102 | $this->handler->handle($command); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/Unit/Service/RegisterReaderHandlerTest.php: -------------------------------------------------------------------------------- 1 | readerRepoMock = M::mock(ReaderRepository::class); 21 | $this->handler = new RegisterReaderHandler($this->readerRepoMock); 22 | } 23 | 24 | public function testShouldRCatalogAReader() 25 | { 26 | $this->readerRepoMock->shouldReceive('store')->once(); 27 | 28 | $command = new RegisterReader('Alice'); 29 | 30 | $this->handler->handle($command); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Unit/Service/RegisterReaderWishHandlerTest.php: -------------------------------------------------------------------------------- 1 | readerRepoMock = M::mock(ReaderRepository::class); 26 | $this->bookRepoMock = M::mock(BookRepository::class); 27 | $this->handler = new RegisterReaderWishHandler($this->readerRepoMock, $this->bookRepoMock); 28 | } 29 | 30 | public function testShouldRegisterAWish() 31 | { 32 | $readerMock = M::mock(Reader::class); 33 | $readerMock->shouldReceive('registerWish')->once(); 34 | 35 | $this->readerRepoMock 36 | ->shouldReceive('find') 37 | ->andReturn($readerMock); 38 | 39 | $this->readerRepoMock 40 | ->shouldReceive('store') 41 | ->once(); 42 | 43 | $this->bookRepoMock 44 | ->shouldReceive('find') 45 | ->andReturn(M::mock(Book::class)); 46 | 47 | $command = new RegisterReaderWish('bookId', 'readerId'); 48 | 49 | $this->handler->handle($command); 50 | } 51 | 52 | /** 53 | * @expectedException ThatBook\Exception\BookNotFoundException 54 | */ 55 | public function testShouldThrowExceptionIfBookNotFound() 56 | { 57 | $this->readerRepoMock->shouldReceive('find')->andReturn(M::mock(Reader::class)); 58 | $this->bookRepoMock->shouldReceive('find')->andReturn(null); 59 | $command = new RegisterReaderWish('bookId', 'readerId'); 60 | $this->handler->handle($command); 61 | } 62 | 63 | /** 64 | * @expectedException ThatBook\Exception\ReaderNotFoundException 65 | */ 66 | public function testShouldThrowExceptionIfReaderNotFound() 67 | { 68 | $this->readerRepoMock->shouldReceive('find')->andReturn(null); 69 | $this->bookRepoMock->shouldReceive('find')->andReturn(M::mock(Book::class)); 70 | $command = new RegisterReaderWish('bookId', 'readerId'); 71 | $this->handler->handle($command); 72 | } 73 | } 74 | --------------------------------------------------------------------------------