├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── app ├── AppKernel.php ├── config │ ├── config.yml │ ├── routing_dev.yml │ ├── routing_prod.yml │ └── routing_test.yml ├── console └── phpunit.xml.dist ├── behat.yml ├── bin ├── behat ├── geotools ├── phpunit └── uuid ├── composer.json ├── features ├── bootstrap │ └── PublisherContext.php └── message_publisher.feature ├── phpunit.xml ├── src └── MessageContext │ ├── Application │ ├── Command │ │ ├── DeleteMessageCommand.php │ │ └── NewMessageInChannelCommand.php │ ├── Exception │ │ ├── AuthorizationNotFoundException.php │ │ ├── ChannelNotFoundException.php │ │ ├── MessageNotFoundException.php │ │ ├── ServiceFailureException.php │ │ ├── ServiceNotAvailableException.php │ │ └── UnableToPerformActionOnChannel.php │ ├── Handler │ │ ├── MessageHandler.php │ │ └── MessageHandlerInterface.php │ ├── Service │ │ ├── ChannelAuthorizationFetcher.php │ │ ├── ChannelFetcher.php │ │ └── PublisherFetcher.php │ └── Tests │ │ └── Service │ │ └── ChannelFetcherTest.php │ ├── Domain │ ├── Event │ │ └── DomainEvents.php │ ├── Exception │ │ ├── ChannelClosedException.php │ │ ├── ExceptionInterface.php │ │ ├── MessageNotOwnedByThePublisherException.php │ │ ├── MicroServiceIntegrationException.php │ │ ├── PublisherIdNotValidException.php │ │ └── PublisherNotAuthorizedException.php │ ├── Message.php │ ├── Publisher.php │ ├── Repository │ │ ├── MessageRepositoryInterface.php │ │ └── PublisherRepositoryInterface.php │ ├── Service │ │ └── Gateway │ │ │ ├── ChannelAuthorizationGatewayInterface.php │ │ │ ├── ChannelGatewayInterface.php │ │ │ └── ServiceIntegrationInterface.php │ ├── Tests │ │ ├── MessageContextDomainUnitTest.php │ │ ├── MessageTest.php │ │ ├── PublisherTest.php │ │ └── ValueObjects │ │ │ └── ChannelIdTest.php │ └── ValueObjects │ │ ├── BodyMessage.php │ │ ├── Channel.php │ │ ├── ChannelAuthorization.php │ │ ├── ChannelId.php │ │ ├── MessageId.php │ │ └── PublisherId.php │ ├── InfrastructureBundle │ ├── CircuitBreaker │ │ ├── CircuitBreaker.php │ │ ├── DoctrineCacheAdapter.php │ │ ├── Factory.php │ │ └── MessageContextCircuitBreakerInterface.php │ ├── DependencyInjection │ │ ├── Configuration.php │ │ └── InfrastructureBundleExtension.php │ ├── Exception │ │ └── UnableToProcessResponseFromService.php │ ├── InfrastructureBundle.php │ ├── Repository │ │ ├── InMemory │ │ │ ├── MessageRepository.php │ │ │ └── PublisherRepository.php │ │ ├── PostRepository.php │ │ └── UserRepository.php │ ├── RequestHandler │ │ ├── Event │ │ │ └── ReceivedResponse.php │ │ ├── Listener │ │ │ └── JsonResponseListener.php │ │ ├── Middleware │ │ │ ├── EventRequestHandler.php │ │ │ └── GuzzleRequestHandler.php │ │ ├── Request.php │ │ ├── RequestHandler.php │ │ └── Response.php │ ├── Resources │ │ └── config │ │ │ ├── circuit_breaker.yml │ │ │ ├── gateways.yml │ │ │ ├── repositories.yml │ │ │ └── request_handlers.yml │ ├── Service │ │ ├── Channel │ │ │ ├── ChannelAdapter.php │ │ │ ├── ChannelGateway.php │ │ │ └── ChannelTranslator.php │ │ └── ChannelAuthorization │ │ │ ├── ChannelAuthorizationAdapter.php │ │ │ ├── ChannelAuthorizationGateway.php │ │ │ └── ChannelAuthorizationTranslator.php │ └── Tests │ │ ├── Resources │ │ ├── MockResponsesLocator.php │ │ ├── channel200AuthorizationResponse.json │ │ └── channel200response.json │ │ └── Service │ │ ├── Channel │ │ ├── ChannelGatewayTest.php │ │ └── ChannelTranslatorTest.php │ │ ├── ChannelAuthorization │ │ ├── ChannelAuthorizationGatewayTest.php │ │ └── ChannelAuthorizationTranslatorTest.php │ │ └── GatewayTrait.php │ └── PresentationBundle │ ├── Adapter │ ├── DeleteMessageAdapter.php │ └── NewMessageCommandAdapter.php │ ├── Controller │ └── MessageController.php │ ├── DependencyInjection │ ├── Configuration.php │ └── PresentationBundleExtension.php │ ├── PresentationBundle.php │ ├── Request │ ├── DeleteMessageRequest.php │ └── NewMessageRequest.php │ ├── Resources │ └── config │ │ ├── application │ │ ├── handlers.yml │ │ └── services.yml │ │ ├── controllers.yml │ │ └── presentation │ │ └── serializer │ │ ├── Message.yml │ │ ├── ValueObjects.ChannelId.yml │ │ └── ValueObjects.PublisherId.yml │ └── Tests │ └── Controller │ └── MessageControllerTest.php └── web ├── .htaccess └── index.php /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | vendor 3 | composer.lock 4 | app/cache 5 | app/logs 6 | .idea 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2004-2014 Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | install: install_vendors cache_warm ci_phpunit 2 | quality: phpcs phpmd 3 | tests: tests_unit tests_functional tests_behat 4 | 5 | install_vendors: 6 | curl -sS https://getcomposer.org/installer | php 7 | php composer.phar install --no-interaction 8 | 9 | # Run by CI server 10 | ci_phpunit: install_vendors tests_unit 11 | 12 | # Run by CI server 13 | ci_phpfunctional: install_vendors tests_functional tests_behat 14 | 15 | tests_unit: 16 | ./bin/phpunit --exclude-group=functional 17 | 18 | tests_functional: 19 | ./bin/phpunit --group=functional 20 | 21 | tests_behat: 22 | ./bin/behat -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Symfony Bounded Context Microservice Stryle 2 | 3 | This is a minimal Symfony distribution 4 | to implement a bounded context inside a micro-service architecture. 5 | 6 | **Plese note**: It's not a DDD proof of concepts, therefore the domain is "poor" and it doesn't reflect a real scenario!! 7 | 8 | ## The Story 9 | 10 | The Development Team have identified the following context inside the application domain: 11 | It only contains the following bundles: 12 | 13 | - PostContext: allow to insert and delete messages into a Channel 14 | - ChannelContext: allow to create and delete Channels 15 | - ChannelAuthorizationContext: allow to maintain the authorizations to write into a Channel 16 | - IdentityAndAccessContext: allow to authenticate a user 17 | - UserContext: allow a user to change the basic profile information 18 | 19 | After started with monolithic solution, the Team have decided to split 20 | the application into 5 "Micro"Service (so far). 21 | 22 | Each Bounder Context will be a separate application, with a separate database and 23 | independently deployable. 24 | 25 | The codebase was entirely developed with Symfony. 26 | The team decided to keep Symfony Framework for the PostContext, UserContext and ChannelAuthorizationContext. 27 | They have also decided to change the programming language for the ChannelContext, the ChannelAuthorizationContext 28 | and the IdentityContext (which are the more "used" services) 29 | 30 | The Team have identified also another service, called API-Proxy, which will be the gateway between the clients and the 31 | rest of the services. 32 | The API-Proxy adds also a security layer, by identifying each request. 33 | 34 | The rest of the services will be into a private network. 35 | 36 | ## The Post Context 37 | A publisher can publish and delete messages on a Channel. 38 | The publisher can publish on a Channel only if he's authorized. 39 | The publisher can delete only own messages. 40 | A message in a channel has a creational date. 41 | The publisher cannot publish or delete messages on a closed Channel. 42 | 43 | ###Microservices consideration 44 | The team have already shipped part of the domain code, but they started wondering about the introduction 45 | of the network. 46 | 47 | - What's happen if the IdentityAndAccessContext it's not available? 48 | - What's happen if the ChannelAuthorizationContext is not available or there is a not expected response? 49 | - What's happen if the ChannelContext is not available or there is a not expected response? 50 | 51 | They started with talk with the product owners and the have identified this scenarios: 52 | 53 | - We don't really need caring about the IdentityAndAccessContext, because the authentication layer is given by 54 | the API-Proxy. We can take the publisherId from the request header, and it's enough. Note that, since the monolithic repo, 55 | in the PostContext we don't care about the user information like username, date of birth or whatever. 56 | All what we need about the Publisher is only the PublisherId. 57 | 58 | - If we're not able to determinate if a user is authorized to publish or not into a channel, then show to the user a message where he's 59 | informed that is not possible to publish on this channel in this moment. 60 | 61 | - If we're not able to determinate if a channel exists and is not closed, then show to the user a message where he's 62 | informed that is not possible to publish on this channel in this moment. 63 | 64 | ###Integrations 65 | 66 | The PostContext should be integrated with: 67 | 68 | - ChannelContext to get the information of the channel 69 | - ChannelAuthorizationContext to determinate if a Publisher can publish or not messages into a Channel. 70 | 71 | ### Future Integrations 72 | The ChannelContext needs to answer a simple question: 73 | - Give me all the messages into a Channel. 74 | 75 | The PostContext needs to give to the ChannelContext this information. 76 | This API should be really fast. To avoid a direct integration between the ChannelContext -> PostContext 77 | the team decides to push into a queue the a message straightway later it has been published. 78 | The ChannelContext can consume this queue and update his ReadModel to perform this query really fast. 79 | 80 | ##Symfony implementation 81 | The implementation of the PostContext hs been done with Symfony. 82 | It has been developed with a Symfony minimal distribution inspired by: 83 | - http://www.whitewashing.de/2014/10/26/symfony_all_the_things_web.html 84 | - https://github.com/beberlei/symfony-minimal-distribution 85 | 86 | ##Installation 87 | 88 | - Cloning the project: 89 | ```bash 90 | git clone git@github.com:danieledangeli/symfony-microservice-bounded-context-example.git 91 | ``` 92 | - Install dependencies 93 | ```bash 94 | composer install 95 | ``` 96 | 97 | - Setting environment variables 98 | 99 | This vars needs to be exported in the environment: 100 | 101 | SYMFONY_ENV=dev 102 | SYMFONY_DEBUG=1 103 | SYMFONY__SECRET=abcdefg 104 | SYMFONY__MONOLOG_ACTION_LEVEL=debug 105 | 106 | Or we can just add these variables in a file called .env in the project root directory 107 | 108 | ##Running the tests 109 | 110 | Phpunit: 111 | ```bash 112 | ./bin/phpunit 113 | ``` 114 | 115 | Behat: 116 | ```bash 117 | ./bin/behat 118 | ``` 119 | 120 | By Makefile: 121 | 122 | ```bash 123 | make tests 124 | ``` 125 | -------------------------------------------------------------------------------- /app/AppKernel.php: -------------------------------------------------------------------------------- 1 | getEnvironment() === "test") { 14 | if(file_exists(__DIR__ . '/../.env')) { 15 | $dotenv = new Dotenv(__DIR__ . '/../'); 16 | $dotenv->overload(); //always overload on test env 17 | } 18 | } 19 | 20 | $bundles = array( 21 | new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), 22 | new Symfony\Bundle\MonologBundle\MonologBundle(), 23 | new JMS\SerializerBundle\JMSSerializerBundle(), 24 | new FOS\RestBundle\FOSRestBundle(), 25 | new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(), 26 | 27 | new \MessageContext\PresentationBundle\PresentationBundle(), 28 | new \MessageContext\InfrastructureBundle\InfrastructureBundle(), 29 | new Symfony\Bundle\TwigBundle\TwigBundle() 30 | ); 31 | 32 | if (in_array($this->getEnvironment(), array('dev', 'test'))) { 33 | $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle(); 34 | } 35 | 36 | return $bundles; 37 | } 38 | 39 | public function registerContainerConfiguration(LoaderInterface $loader) 40 | { 41 | $loader->load(__DIR__ . '/config/config.yml'); 42 | 43 | if (in_array($this->getEnvironment(), array('dev', 'test'))) { 44 | $loader->load(function ($container) { 45 | $container->loadFromExtension('web_profiler', array( 46 | 'toolbar' => true, 47 | )); 48 | $container->loadFromExtension('framework', array( 49 | 'test' => true, 50 | )); 51 | }); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/config/config.yml: -------------------------------------------------------------------------------- 1 | # app/config/config.yml 2 | framework: 3 | secret: %secret% 4 | router: 5 | resource: "%kernel.root_dir%/config/routing_%kernel.environment%.yml" 6 | strict_requirements: %kernel.debug% 7 | templating: 8 | engines: ['twig'] 9 | profiler: 10 | enabled: %kernel.debug% 11 | 12 | monolog: 13 | handlers: 14 | main: 15 | type: fingers_crossed 16 | action_level: %monolog_action_level% 17 | handler: nested 18 | nested: 19 | type: stream 20 | path: "%kernel.logs_dir%/%kernel.environment%.log" 21 | level: debug 22 | 23 | # config.yml 24 | jms_serializer: 25 | property_naming: 26 | separator: _ 27 | lower_case: true 28 | metadata: 29 | auto_detection: true 30 | 31 | directories: 32 | Domain: 33 | namespace_prefix: "MessageContext\\Domain" 34 | path: "@PresentationBundle/Resources/config/presentation/serializer/" 35 | 36 | sensio_framework_extra: 37 | view: { annotations: false } 38 | 39 | fos_rest: 40 | view: 41 | view_response_listener: force 42 | format_listener: 43 | rules: 44 | - { path: ^/, priorities: [ json ], fallback_format: json, prefer_extension: false } 45 | exception: 46 | enabled: true 47 | codes: 48 | 'MessageContext\Application\Exception\MessageNotFoundException': HTTP_FORBIDDEN 49 | 'MessageContext\Application\Exception\UnableToPerformActionOnChannel': HTTP_FORBIDDEN 50 | 'MessageContext\Domain\Exception\PublisherNotAuthorizedException': HTTP_FORBIDDEN 51 | 'MessageContext\Application\Exception\ChannelNotFoundException': HTTP_NOT_FOUND 52 | 'MessageContext\Application\Exception\MessageNotFoundException': HTTP_NOT_FOUND 53 | 'MessageContext\Domain\Exception\ChannelClosedException': HTTP_FORBIDDEN 54 | 'MessageContext\Domain\Exception\MessageNotOwnedByThePublisherException': HTTP_FORBIDDEN 55 | 'Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException': HTTP_BAD_REQUEST 56 | 'Symfony\Component\OptionsResolver\Exception\NoSuchOptionException': HTTP_BAD_REQUEST 57 | 'Symfony\Component\OptionsResolver\Exception\OptionDefinitionException': HTTP_BAD_REQUEST 58 | 'Symfony\Component\OptionsResolver\Exception\MissingOptionsException': HTTP_BAD_REQUEST 59 | 'Symfony\Component\OptionsResolver\Exception\MissingOptionsException': HTTP_BAD_REQUEST 60 | 'MessageContext\Domain\Exception\PublisherIdNotValidException': HTTP_BAD_REQUEST 61 | allowed_methods_listener: true 62 | body_listener: false 63 | routing_loader: 64 | default_format: json 65 | include_format: false -------------------------------------------------------------------------------- /app/config/routing_dev.yml: -------------------------------------------------------------------------------- 1 | _wdt: 2 | resource: "@WebProfilerBundle/Resources/config/routing/wdt.xml" 3 | prefix: /_wdt 4 | 5 | _profiler: 6 | resource: "@WebProfilerBundle/Resources/config/routing/profiler.xml" 7 | prefix: /_profiler 8 | 9 | _main: 10 | resource: routing_prod.yml 11 | -------------------------------------------------------------------------------- /app/config/routing_prod.yml: -------------------------------------------------------------------------------- 1 | post_context: 2 | prefix: /api 3 | type: rest 4 | resource: post_context.controller.create_post -------------------------------------------------------------------------------- /app/config/routing_test.yml: -------------------------------------------------------------------------------- 1 | _main: 2 | resource: routing_prod.yml 3 | -------------------------------------------------------------------------------- /app/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | load(); 16 | 17 | $input = new ArgvInput(); 18 | $kernel = new AppKernel($_SERVER['SYMFONY_ENV'], (bool)$_SERVER['SYMFONY_DEBUG']); 19 | $application = new Application($kernel); 20 | $application->run($input); 21 | -------------------------------------------------------------------------------- /app/phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | 17 | 18 | ../src/Acme/*/Tests 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /behat.yml: -------------------------------------------------------------------------------- 1 | default: 2 | suites: 3 | default: 4 | contexts: 5 | - PublisherContext: 6 | kernel: '@kernel' 7 | 8 | filters: 9 | tags: "~@skipped" 10 | extensions: 11 | Behat\Symfony2Extension: 12 | kernel: 13 | path: app/AppKernel.php 14 | class: AppKernel 15 | -------------------------------------------------------------------------------- /bin/behat: -------------------------------------------------------------------------------- 1 | ../vendor/behat/behat/bin/behat -------------------------------------------------------------------------------- /bin/geotools: -------------------------------------------------------------------------------- 1 | ../vendor/league/geotools/bin/geotools -------------------------------------------------------------------------------- /bin/phpunit: -------------------------------------------------------------------------------- 1 | ../vendor/phpunit/phpunit/phpunit -------------------------------------------------------------------------------- /bin/uuid: -------------------------------------------------------------------------------- 1 | ../vendor/ramsey/uuid/bin/uuid -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "symfony/symfony": "@stable", 4 | "symfony/monolog-bundle": "@stable", 5 | "vlucas/phpdotenv": "~2.0", 6 | "nicolopignatelli/valueobjects": "^3.0", 7 | "jms/serializer-bundle": "^1.0", 8 | "friendsofsymfony/rest-bundle": "^1.7", 9 | "sensio/framework-extra-bundle": "^3.0", 10 | "guzzlehttp/guzzle": "~5.0", 11 | "ejsmont-artur/php-circuit-breaker": "^0.1.0", 12 | "simple-bus/symfony-bridge": "^4.1" 13 | }, 14 | "autoload": { 15 | "psr-0": { "MessageContext": "src/" } 16 | }, 17 | 18 | "require-dev": { 19 | "phpunit/phpunit": "4.6.*", 20 | "behat/behat": "^3.0", 21 | "behat/symfony2-extension": "^2.0", 22 | "behat/mink-browserkit-driver": "^1.3" 23 | }, 24 | 25 | "config": { 26 | "bin-dir": "bin" 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /features/bootstrap/PublisherContext.php: -------------------------------------------------------------------------------- 1 | mock = new Mock(); 41 | } 42 | 43 | /** 44 | * @Given a publisher with id :publisherId 45 | */ 46 | public function aPublisherWithId($publisherId) 47 | { 48 | $this->publisherId = $publisherId; 49 | } 50 | 51 | /** 52 | * @Given exists a :state channel with id :channelId 53 | */ 54 | public function existsAChannelWithId($channelId, $state) 55 | { 56 | $isClosed = $state === "open" ? "false" : "true"; 57 | $this->isClosed = $isClosed; 58 | 59 | $this->channelId = $channelId; 60 | $channelResponseTemplate = MockResponsesLocator::getResponseTemplate("channel200response.json"); 61 | $body = sprintf($channelResponseTemplate, $this->channelId, $this->isClosed); 62 | 63 | $response = new \GuzzleHttp\Message\Response(200, [], Stream::factory($body)); 64 | 65 | $response->addHeader("Content-Type", "application/json"); 66 | $this->mock->addResponse( 67 | $response 68 | ); 69 | } 70 | 71 | /** 72 | * @Given the publisher is authorized to publish message on that channel 73 | */ 74 | public function thePublisherIsAuthorizedToPublishMessageOnTheChannel() 75 | { 76 | $response = new \GuzzleHttp\Message\Response(200,[], Stream::factory( 77 | sprintf("{\"publisher_id\" : \"%s\", \"channel_id\": %s, \"authorized\": %s}", 78 | $this->publisherId, 79 | $this->channelId, 80 | "true") 81 | )); 82 | $response->addHeader("Content-Type", "application/json"); 83 | $this->mock->addResponse($response); 84 | } 85 | 86 | /** 87 | * @When the publisher write the message :message 88 | */ 89 | public function thePublisherWriteTheMessage($message) 90 | { 91 | if($this->mock->count() > 0) { 92 | $container = $this->getClient()->getContainer(); 93 | 94 | /** @var Client $httpClient */ 95 | $httpClient = $container->get('message_context.infrastructure.guzzle_http_client'); 96 | $httpClient->getEmitter()->attach($this->mock); 97 | } 98 | 99 | $this->message = $message; 100 | $bodyRequest = ['publisher_id' => $this->publisherId, 'channel_id' => $this->channelId, "message" => $this->message]; 101 | 102 | $this->getClient()->request( 103 | "POST", 104 | $this->prepareUrl(sprintf("/api/messages")), 105 | array(), 106 | array(), 107 | array(["Accept", "application/json"]), 108 | json_encode($bodyRequest) 109 | ); 110 | } 111 | 112 | /** 113 | * @Then a new message will be created on the channel 114 | */ 115 | public function aNewMessageWillBeCreatedOnTheChannel() 116 | { 117 | $content = json_decode($this->getContent(), true); 118 | 119 | PHPUnit_Framework_Assert::assertEquals($this->message, $content["message"]); 120 | PHPUnit_Framework_Assert::assertEquals($this->publisherId, $content["publisher_id"]); 121 | PHPUnit_Framework_Assert::assertEquals($this->channelId, $content["channel_id"]); 122 | PHPUnit_Framework_Assert::assertEquals(Response::HTTP_CREATED, $this->getResponse()->getStatus()); 123 | } 124 | 125 | /** 126 | * @Given the publisher is not authorized to publish message on that channel 127 | */ 128 | public function thePublisherIsNotAuthorizedToPublishMessageOnThatChannel() 129 | { 130 | $this->mock->addResponse( 131 | new \GuzzleHttp\Message\Response(200, ["Content-Type" => "application/json"], Stream::factory( 132 | sprintf("{\"publisher_id\" : \"%s\", \"channel_id\": %s, \"authorized\": %s}", 133 | $this->publisherId, 134 | $this->channelId, 135 | "false") 136 | ))); 137 | } 138 | 139 | /** 140 | * @Then the publisher is informed that is not authorized 141 | */ 142 | public function thePublisherIsInformedThatIsNotAuthorized() 143 | { 144 | $response = $this->getResponse(); 145 | PHPUnit_Framework_Assert::assertEquals(Response::HTTP_FORBIDDEN, $response->getStatus()); 146 | } 147 | 148 | /** 149 | * @Then no new messages will be added on that channel 150 | */ 151 | public function noNewMessagesWillBeAddedOnThatChannel() 152 | { 153 | /** @var Container $container */ 154 | $container = $this->getClient()->getContainer(); 155 | 156 | /** @var MessageRepositoryInterface $messageRepository */ 157 | $messageRepository = $container->get("message_context.infrastructure.message_repository"); 158 | 159 | $messages = $messageRepository->getAll(); 160 | 161 | foreach($messages as $message) { 162 | PHPUnit_Framework_Assert::assertNotEquals($this->message, $message->getMessage()); 163 | } 164 | } 165 | 166 | /** 167 | * @Given a channel with id :arg1 doesn't exists 168 | */ 169 | public function aChannelWithIdDoesNotExists($channelId) 170 | { 171 | $this->channelId = $channelId; 172 | 173 | $this->mock->addResponse( 174 | new \GuzzleHttp\Message\Response(404, []) 175 | ); 176 | } 177 | 178 | /** 179 | * @Then the publisher is informed that the channel doesn't exists 180 | */ 181 | public function thePublisherIsInformedThatTheChannelDoesNotExists() 182 | { 183 | $response = $this->getResponse(); 184 | PHPUnit_Framework_Assert::assertEquals(Response::HTTP_NOT_FOUND, $response->getStatus()); 185 | } 186 | 187 | /** 188 | * @Given exists message with id :messageId on the channel :channelId 189 | */ 190 | public function aMessageWithIdOnTheChannel($messageId, $channelId) 191 | { 192 | $this->messageId = $messageId; 193 | $this->channelId = $channelId; 194 | } 195 | 196 | /** 197 | * @Given the publisher is the owner of the message 198 | */ 199 | public function thePublisherIsTheOwnerOfTheMessage() 200 | { 201 | $publisherId = new PublisherId($this->publisherId); 202 | $channelId = new ChannelId($this->channelId); 203 | $publisher = new Publisher($publisherId); 204 | 205 | $message = $publisher->publishOnChannel( 206 | new Channel($channelId, false), 207 | new ChannelAuthorization($publisherId, $channelId, true), 208 | new BodyMessage("hello") 209 | ); 210 | 211 | $reflectionClass = new ReflectionClass(Message::class); 212 | $reflectionProperty = $reflectionClass->getProperty('messageId'); 213 | $reflectionProperty->setAccessible(true); 214 | $reflectionProperty->setValue($message, new MessageId($this->messageId)); 215 | 216 | /** @var Container $container */ 217 | $container = $this->getClient()->getContainer(); 218 | 219 | /** @var MessageRepositoryInterface $messageRepository */ 220 | $messageRepository = $container->get("message_context.infrastructure.message_repository"); 221 | $messageRepository->add($message); 222 | 223 | /** @var PublisherRepository $messageRepository */ 224 | $publisherRepository = $container->get("message_context.infrastructure.publisher_repository"); 225 | $publisherRepository->add($publisher); 226 | } 227 | 228 | /** 229 | * @When the publisher delete the message 230 | */ 231 | public function thePublisherDeleteTheMessage() 232 | { 233 | if($this->mock->count() > 0) { 234 | $container = $this->getClient()->getContainer(); 235 | 236 | /** @var Client $httpClient */ 237 | $httpClient = $container->get('message_context.infrastructure.guzzle_http_client'); 238 | $httpClient->getEmitter()->attach($this->mock); 239 | } 240 | 241 | $this->getClient()->request( 242 | "DELETE", 243 | $this->prepareUrl(sprintf("/api/messages/%s", $this->messageId)), 244 | array(), 245 | array(), 246 | array(["Accept", "application/json"]) 247 | ); 248 | } 249 | 250 | /** 251 | * @Then the message will be deleted from the channel 252 | */ 253 | public function theMessageWillBeDeletedFromTheChannel() 254 | { 255 | $container = $this->getClient()->getContainer(); 256 | 257 | $response = $this->getResponse(); 258 | PHPUnit_Framework_Assert::assertEquals(Response::HTTP_NO_CONTENT, $response->getStatus()); 259 | 260 | /** @var MessageRepositoryInterface $messageRepository */ 261 | $messageRepository = $container->get("message_context.infrastructure.message_repository"); 262 | 263 | $collection = $messageRepository->get(new MessageId($this->messageId)); 264 | PHPUnit_Framework_Assert::assertCount(0, $collection); 265 | } 266 | 267 | /** 268 | * @Given the publisher is not the owner of the message 269 | */ 270 | public function thePublisherIsNotTheOwnerOfTheMessage() 271 | { 272 | $message = new Message(new PublisherId("7777"), new ChannelId($this->channelId), new BodyMessage("hello everyone")); 273 | 274 | $reflectionClass = new ReflectionClass(Message::class); 275 | $reflectionProperty = $reflectionClass->getProperty('messageId'); 276 | $reflectionProperty->setAccessible(true); 277 | $reflectionProperty->setValue($message, new MessageId($this->messageId)); 278 | 279 | //create the message with message repository 280 | /** @var Container $container */ 281 | $container = $this->getClient()->getContainer(); 282 | 283 | /** @var MessageRepositoryInterface $messageRepository */ 284 | $messageRepository = $container->get("message_context.infrastructure.message_repository"); 285 | $messageRepository->add($message); 286 | } 287 | 288 | /** 289 | * @Then the publisher is informed that is not the owner of that message 290 | */ 291 | public function theUserIsInformedThatIsNotTheOwnerOfThatMessage() 292 | { 293 | $response = $this->getResponse(); 294 | PHPUnit_Framework_Assert::assertEquals(Response::HTTP_FORBIDDEN, $response->getStatus()); 295 | } 296 | 297 | /** 298 | * @Then the message will not be deleted from the channel 299 | */ 300 | public function theMessageWillNotBeDeletedFromTheChannel() 301 | { 302 | $container = $this->getClient()->getContainer(); 303 | 304 | /** @var MessageRepositoryInterface $messageRepository */ 305 | $messageRepository = $container->get("message_context.infrastructure.message_repository"); 306 | 307 | $collection = $messageRepository->get(new MessageId($this->messageId)); 308 | PHPUnit_Framework_Assert::assertCount(1, $collection); 309 | } 310 | 311 | /** 312 | * @Then the publisher is informed that is not possible to perform action on a closed channel 313 | */ 314 | public function thePublisherIsInformedThatIsNotPossiblePerformActionsOnAClosedChannel() 315 | { 316 | $response = $this->getResponse(); 317 | PHPUnit_Framework_Assert::assertEquals(Response::HTTP_FORBIDDEN, $response->getStatus()); 318 | } 319 | 320 | } -------------------------------------------------------------------------------- /features/message_publisher.feature: -------------------------------------------------------------------------------- 1 | Feature: message publisher 2 | As an Publisher 3 | I need to be able to publish message on channel 4 | 5 | Scenario: A publisher, if authorized can publish a new message on an open channel 6 | Given a publisher with id "899" 7 | And exists a "open" channel with id "3333" 8 | And the publisher is authorized to publish message on that channel 9 | When the publisher write the message "Hi guys" 10 | Then a new message will be created on the channel 11 | 12 | Scenario: A publisher, if not authorized is not able to publish a new message on channel 13 | Given a publisher with id "899" 14 | And exists a "open" channel with id "3333" 15 | And the publisher is not authorized to publish message on that channel 16 | When the publisher write the message "Hi guys" 17 | Then the publisher is informed that is not authorized 18 | And no new messages will be added on that channel 19 | 20 | Scenario: A publisher cannot publish a new message on a not existing channel 21 | Given a publisher with id "899" 22 | And a channel with id "3333" doesn't exists 23 | When the publisher write the message "Hi guys" 24 | Then the publisher is informed that the channel doesn't exists 25 | 26 | Scenario: A publisher can delete his message in an open channel 27 | Given a publisher with id "899" 28 | And exists a "open" channel with id "3333" 29 | And exists message with id "1ca188d6-b1b9-4f4e-9b2d-67a68f409cac" on the channel "3333" 30 | And the publisher is the owner of the message 31 | When the publisher delete the message 32 | Then the message will be deleted from the channel 33 | 34 | Scenario: A publisher cannot delete a message in a channel, if he's not the owner 35 | Given a publisher with id "899" 36 | And exists a "open" channel with id "3333" 37 | And exists message with id "1ca188d6-b1b9-4f4e-9b2d-67a68f409cac" on the channel "3333" 38 | And the publisher is not the owner of the message 39 | And the publisher is authorized to publish message on that channel 40 | When the publisher delete the message 41 | Then the publisher is informed that is not the owner of that message 42 | And the message will not be deleted from the channel 43 | 44 | Scenario: A publisher cannot publish a message into a closed channel 45 | Given a publisher with id "899" 46 | And exists a "closed" channel with id "3333" 47 | And the publisher is authorized to publish message on that channel 48 | When the publisher write the message "Hi guys" 49 | Then the publisher is informed that is not possible to perform action on a closed channel 50 | 51 | Scenario: A publisher cannot delete a message in a closed channel 52 | Given a publisher with id "899" 53 | And exists a "closed" channel with id "3333" 54 | And exists message with id "1ca188d6-b1b9-4f4e-9b2d-67a68f409cac" on the channel "3333" 55 | And the publisher is the owner of the message 56 | When the publisher delete the message 57 | Then the publisher is informed that is not possible to perform action on a closed channel -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | src/MessageContext/*/Tests 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | vendor/ 24 | 25 | 26 | ../src 27 | 28 | src/MessageContext/*Bundle/Resources 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/MessageContext/Application/Command/DeleteMessageCommand.php: -------------------------------------------------------------------------------- 1 | publisherId = $publisherId; 16 | $this->messageId = $messageId; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/MessageContext/Application/Command/NewMessageInChannelCommand.php: -------------------------------------------------------------------------------- 1 | publisherId = $publisherId; 18 | $this->message = $message; 19 | $this->channelId = $channelId; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/MessageContext/Application/Exception/AuthorizationNotFoundException.php: -------------------------------------------------------------------------------- 1 | channelAuthorizationFetcher = $channelAuthorizationFetcher; 27 | $this->channelFetcher = $channelFetcher; 28 | $this->publisherFetcher = $publisherFetcher; 29 | $this->messageRepository = $messageRepository; 30 | } 31 | 32 | public function postNewMessage(NewMessageInChannelCommand $publisherMessageInChannel) 33 | { 34 | $channel = $this->channelFetcher->fetchChannel($publisherMessageInChannel->channelId); 35 | $publisher = $this->publisherFetcher->fetchPublisher($publisherMessageInChannel->publisherId); 36 | 37 | $channelAuthorization = $this->channelAuthorizationFetcher->fetchChannelAuthorization( 38 | $publisherMessageInChannel->publisherId, 39 | $publisherMessageInChannel->channelId 40 | ); 41 | 42 | $message = $publisher->publishOnChannel($channel, $channelAuthorization, $publisherMessageInChannel->message); 43 | 44 | return $this->messageRepository->add($message); 45 | } 46 | 47 | public function deleteMessage(DeleteMessageCommand $deleteMessageCommand) 48 | { 49 | $publisher = $this->publisherFetcher->fetchPublisher($deleteMessageCommand->publisherId); 50 | $message = $this->messageRepository->get($deleteMessageCommand->messageId); 51 | 52 | if ($message->count() > 0) { 53 | $channel = $this->channelFetcher->fetchChannel($message->get(0)->getChannelId()); 54 | $publisher->deleteMessage($channel, $deleteMessageCommand->messageId); 55 | return $this->messageRepository->remove($deleteMessageCommand->messageId); 56 | } 57 | 58 | throw new MessageNotFoundException(sprintf("The message has not been found", $deleteMessageCommand->messageId)); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/MessageContext/Application/Handler/MessageHandlerInterface.php: -------------------------------------------------------------------------------- 1 | channelAuthorizationGateway = $channelAuthorizationGateway; 18 | } 19 | 20 | public function fetchChannelAuthorization(PublisherId $publisherId, ChannelId $channelId) 21 | { 22 | try { 23 | return $this->channelAuthorizationGateway->getChannelAuthorization($publisherId, $channelId); 24 | } catch (MicroServiceIntegrationException $e) { 25 | //the service channel is not available 26 | //A. the channel doesn't exists 27 | //B. the channel exist, but we it's impossible to fetch 28 | //In any case handling a temporary exception 29 | 30 | throw new UnableToPerformActionOnChannel( 31 | sprintf("Impossible to perform any action on the channel at the moment %s caused by %e", 32 | $channelId->toNative(), $e->getMessage()) 33 | ); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/MessageContext/Application/Service/ChannelFetcher.php: -------------------------------------------------------------------------------- 1 | channelGateway = $channelGateway; 15 | } 16 | 17 | public function fetchChannel(ChannelId $channelId) 18 | { 19 | return $this->fetchChannelFromGateway(($channelId)); 20 | } 21 | 22 | private function fetchChannelFromGateway(ChannelId $channelId) 23 | { 24 | try { 25 | $channel = $this->channelGateway->getChannel($channelId); 26 | return $channel; 27 | } catch (MicroServiceIntegrationException $e) { 28 | //the service channel is not available 29 | //A. the channel doesn't exists 30 | //B. the channel exist, but we it's impossible to fetch 31 | //In any case handling a temporary exception 32 | 33 | throw new UnableToPerformActionOnChannel( 34 | sprintf("Impossible to perform any action in the channel at the moment %s caused by %e", 35 | $channelId->toNative(), $e->getMessage()) 36 | ); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/MessageContext/Application/Service/PublisherFetcher.php: -------------------------------------------------------------------------------- 1 | publisherRepository = $publisherRepository; 16 | } 17 | 18 | /** 19 | * @param PublisherId $publisherId 20 | * @return Publisher 21 | */ 22 | public function fetchPublisher(PublisherId $publisherId) 23 | { 24 | $publisherCollection = $this->publisherRepository->get($publisherId); 25 | 26 | if ($publisherCollection->count() === 0) { 27 | $publisherCollection->add($this->createPublisher($publisherId)); 28 | } 29 | 30 | return $publisherCollection->get(0); 31 | } 32 | 33 | private function createPublisher(PublisherId $publisherId) 34 | { 35 | $publisher = new Publisher($publisherId); 36 | $this->publisherRepository->add($publisher); 37 | 38 | return $publisher; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/MessageContext/Application/Tests/Service/ChannelFetcherTest.php: -------------------------------------------------------------------------------- 1 | channelGateway = $this->anyChannelGateway(); 23 | 24 | $this->channelFetcher = new ChannelFetcher($this->channelGateway); 25 | } 26 | 27 | public function testItFetchChannelFromGateway() 28 | { 29 | $channelId = new ChannelId("1243"); 30 | $channelFetchedFromGateway = new Channel($channelId, true); 31 | 32 | $this->channelGateway->expects($this->once()) 33 | ->method("getChannel") 34 | ->with($this->equalTo($channelId)) 35 | ->willReturn($channelFetchedFromGateway); 36 | 37 | $channel = $this->channelFetcher->fetchChannel($channelId); 38 | $this->assertEquals($channelFetchedFromGateway, $channel); 39 | } 40 | 41 | /** 42 | * @dataProvider getFailMicroServicesExceptions 43 | * @expectedException \MessageContext\Application\Exception\UnableToPerformActionOnChannel 44 | */ 45 | public function testItRaiseUnableToCreatePostExceptionOnMicroServiceException($e) 46 | { 47 | $channelId = new ChannelId("1243"); 48 | 49 | $this->channelGateway->expects($this->once()) 50 | ->method("getChannel") 51 | ->willThrowException($e); 52 | 53 | $this->channelFetcher->fetchChannel($channelId); 54 | } 55 | 56 | public function getFailMicroServicesExceptions() 57 | { 58 | return [ 59 | [new ServiceFailureException()], 60 | [new ServiceNotAvailableException()] 61 | ]; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/MessageContext/Domain/Event/DomainEvents.php: -------------------------------------------------------------------------------- 1 | messageId = new MessageId(); 22 | $this->publisherId = $publisherId; 23 | $this->message = $message; 24 | $this->channelId = $channelId; 25 | $this->deleted = false; 26 | $this->createdAt = DateTime::now(); 27 | } 28 | 29 | public function isDeleted() 30 | { 31 | return $this->deleted; 32 | } 33 | 34 | public function getId() 35 | { 36 | return $this->messageId; 37 | } 38 | 39 | public function getMessage() 40 | { 41 | return $this->message; 42 | } 43 | 44 | public function getChannelId() 45 | { 46 | return $this->channelId; 47 | } 48 | 49 | public function getPublisherId() 50 | { 51 | return $this->publisherId; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/MessageContext/Domain/Publisher.php: -------------------------------------------------------------------------------- 1 | pid = $publisherId; 25 | $this->messages = new ArrayCollection(); 26 | } 27 | 28 | /** 29 | * @param Channel $channel 30 | * @param ChannelAuthorization $channelAuthorization 31 | * @param BodyMessage $message 32 | * 33 | * @return Message 34 | * @throws ChannelClosedException 35 | * @throws PublisherNotAuthorizedException 36 | */ 37 | public function publishOnChannel( 38 | Channel $channel, 39 | ChannelAuthorization $channelAuthorization, 40 | BodyMessage $message 41 | ) { 42 | if ($channelAuthorization->canPublisherPublishOnChannel()) { 43 | if (!$channel->isClosed()) { 44 | $message = new Message($this->getId(), $channel->getId(), $message); 45 | $this->messages->add($message); 46 | 47 | return $message; 48 | } 49 | 50 | throw new ChannelClosedException( 51 | sprintf("The channel %s is closed", $channel->getId()) 52 | ); 53 | } 54 | 55 | throw new PublisherNotAuthorizedException( 56 | sprintf("The publisher %s is not authorized to publish on channel %s", 57 | $this->getId(), $channel->getId()) 58 | ); 59 | } 60 | 61 | /** 62 | * @param Channel $channel 63 | * @param MessageId $messageId 64 | * 65 | * @throws ChannelClosedException 66 | * @throws MessageNotOwnedByThePublisherException 67 | */ 68 | public function deleteMessage(Channel $channel, MessageId $messageId) 69 | { 70 | if (!$channel->isClosed()) { 71 | $removed = false; 72 | 73 | foreach ($this->messages as $message) { 74 | if ($message->getId()->sameValueAs($messageId)) { 75 | $removed = $this->messages->removeElement($message); 76 | } 77 | } 78 | 79 | if (!$removed) { 80 | throw new MessageNotOwnedByThePublisherException( 81 | sprintf("The message id: %s is not owned by the publisher id %s", 82 | $messageId, 83 | $this->getId() 84 | ) 85 | ); 86 | } 87 | 88 | return; 89 | } 90 | 91 | throw new ChannelClosedException( 92 | sprintf("The channel %s is closed", $channel->getId()) 93 | ); 94 | } 95 | 96 | public function getId() 97 | { 98 | return $this->pid; 99 | } 100 | 101 | public function getMessages() 102 | { 103 | return $this->messages; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/MessageContext/Domain/Repository/MessageRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | public function getAll(); 21 | 22 | /** 23 | * @param MessageId $messageId 24 | * @return ArrayCollection 25 | */ 26 | public function get(MessageId $messageId); 27 | 28 | /** 29 | * @param MessageId $messageId 30 | * @return Message 31 | */ 32 | public function remove(MessageId $messageId); 33 | } 34 | -------------------------------------------------------------------------------- /src/MessageContext/Domain/Repository/PublisherRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | $publishers 14 | */ 15 | public function get(PublisherId $publisherId); 16 | 17 | /** 18 | * @param Publisher $publisher 19 | * @return void 20 | */ 21 | public function add(Publisher $publisher); 22 | } 23 | -------------------------------------------------------------------------------- /src/MessageContext/Domain/Service/Gateway/ChannelAuthorizationGatewayInterface.php: -------------------------------------------------------------------------------- 1 | getMockBuilder(MessageRepositoryInterface::class) 23 | ->getMock(); 24 | } 25 | 26 | /** 27 | * @return \PHPUnit_Framework_MockObject_MockObject 28 | */ 29 | public function anyPublisherRepository() 30 | { 31 | return $this->getMockBuilder(PublisherRepositoryInterface::class) 32 | ->getMock(); 33 | } 34 | 35 | /** 36 | * @return \PHPUnit_Framework_MockObject_MockObject 37 | */ 38 | public function anyChannelGateway() 39 | { 40 | return $this->getMockBuilder(ChannelGatewayInterface::class) 41 | ->getMock(); 42 | } 43 | 44 | /** 45 | * @return \PHPUnit_Framework_MockObject_MockObject 46 | */ 47 | public function anyAuthorizationGateway() 48 | { 49 | return $this->getMockBuilder(ChannelAuthorizationGatewayInterface::class) 50 | ->getMock(); 51 | } 52 | 53 | /** 54 | * @return \PHPUnit_Framework_MockObject_MockObject 55 | */ 56 | public function anyPublisher() 57 | { 58 | return new Publisher(new PublisherId()); 59 | } 60 | 61 | /** 62 | * @param PublisherId $id 63 | * @return \PHPUnit_Framework_MockObject_MockObject 64 | */ 65 | public function anyPublisherWithId(PublisherId $id) 66 | { 67 | $publisherMock = $this->anyPublisher(); 68 | 69 | $publisherMock->expects($this->any()) 70 | ->method("getId") 71 | ->willReturn($id); 72 | 73 | return $publisherMock; 74 | } 75 | 76 | /** 77 | * @return \PHPUnit_Framework_MockObject_MockObject 78 | */ 79 | public function anyChannel() 80 | { 81 | return $this->anyOpenChannelWithId(new ChannelId("3333")); 82 | } 83 | 84 | /** 85 | * @param ChannelId $id 86 | * @return Channel 87 | */ 88 | public function anyChannelWithId(ChannelId $id) 89 | { 90 | return new Channel($id, false); 91 | } 92 | 93 | public function anyOpenChannelWithId(ChannelId $id) 94 | { 95 | return new Channel($id, false); 96 | } 97 | 98 | public function anyClosedChannelWithId(ChannelId $id) 99 | { 100 | return new Channel($id, true); 101 | } 102 | 103 | 104 | public function anyAuthorizedChannelAuthorization(PublisherId $publisherId, ChannelId $channelId) 105 | { 106 | return new ChannelAuthorization($publisherId, $channelId, true); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/MessageContext/Domain/Tests/MessageTest.php: -------------------------------------------------------------------------------- 1 | messageBody = new BodyMessage("this is a new message in channel"); 19 | $this->message = new Message(new PublisherId("4444"), new ChannelId("222"), $this->messageBody); 20 | } 21 | 22 | public function testItCreatedPostWithUID() 23 | { 24 | $this->assertInstanceOf(MessageId::class, $this->message->getId()); 25 | $this->assertEquals($this->messageBody, $this->message->getMessage()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/MessageContext/Domain/Tests/PublisherTest.php: -------------------------------------------------------------------------------- 1 | channel = $this->anyChannelWithId(new ChannelId("3333")); 27 | 28 | $this->message = new BodyMessage("this is a new message in channel"); 29 | 30 | $this->publisher = new Publisher(new PublisherId("1264328")); 31 | $this->publisherMessages = []; 32 | } 33 | 34 | /** 35 | * @group unit 36 | */ 37 | public function testItPublishMessageInNotClosedChannel() 38 | { 39 | $channelId = new ChannelId("3333"); 40 | $channel = $this->anyOpenChannelWithId($channelId); 41 | $channelAuthorization = $this->anyAuthorizedChannelAuthorization($this->publisher->getId(), $channelId); 42 | 43 | $message = $this->publisher->publishOnChannel($channel, $channelAuthorization, $this->message); 44 | 45 | $this->assertInstanceOf(Message::class, $message); 46 | $this->assertEquals($this->message, $message->getMessage()); 47 | } 48 | 49 | /** 50 | * @group unit 51 | * @expectedException \MessageContext\Domain\Exception\ChannelClosedException 52 | */ 53 | public function testHeIsNotAbleToPublishOnClosedChannel() 54 | { 55 | $channelId = new ChannelId("3333"); 56 | 57 | $channel = $this->anyClosedChannelWithId($channelId); 58 | $authorization = $this->anyAuthorizedChannelAuthorization($this->publisher->getId(), $channelId); 59 | 60 | $this->publisher->publishOnChannel($channel, $authorization, $this->message); 61 | } 62 | 63 | /** 64 | * @group unit 65 | * @expectedException \MessageContext\Domain\Exception\MessageNotOwnedByThePublisherException 66 | */ 67 | public function testHeIsNotAbleToDeleteMessagesNotOwnedByHim() 68 | { 69 | $channelId = new ChannelId("1111"); 70 | $channel = $this->anyChannelWithId($channelId); 71 | $channelAuthorization = $this->anyAuthorizedChannelAuthorization($this->publisher->getId(), $channelId); 72 | 73 | $firstMessage = $this->publisher->publishOnChannel($channel, $channelAuthorization, $this->message); 74 | 75 | $this->publisher->deleteMessage($channel, new MessageId(UUID::generateAsString())); 76 | 77 | $this->assertCount(1, $this->publisher->getMessages()); 78 | $this->assertEquals($firstMessage->getId(), $this->publisher->getMessages()->get(0)->getId()); 79 | } 80 | 81 | /** 82 | * @group unit 83 | * @expectedException \MessageContext\Domain\Exception\ChannelClosedException 84 | */ 85 | public function testItNotDeleteMessagesOnClosedChannel() 86 | { 87 | $channelId = new ChannelId("1111"); 88 | $channel = $this->anyClosedChannelWithId($channelId); 89 | $channelAuthorization = $this->anyAuthorizedChannelAuthorization($this->publisher->getId(), $channelId); 90 | 91 | $publishedMessage = $this->publisher->publishOnChannel($channel, $channelAuthorization, $this->message); 92 | 93 | $this->shouldNotBeDeleted($publishedMessage); 94 | $this->publisher->deleteMessage($this->channel, $publishedMessage->getId()); 95 | } 96 | 97 | private function shouldNotBeDeleted($message) 98 | { 99 | $this->publisherMessages[] = $message; 100 | } 101 | 102 | public function tearDown() 103 | { 104 | if (count($this->publisherMessages) > 0) { 105 | $this->assertEquals($this->publisherMessages, $this->publisher->getMessages()->toArray()); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/MessageContext/Domain/Tests/ValueObjects/ChannelIdTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(ValueObjectInterface::class, $ean); 14 | } 15 | 16 | public function testFromNative() 17 | { 18 | $channel = ChannelId::fromNative("4006381333931"); 19 | $constructedChannel = new ChannelId("4006381333931"); 20 | 21 | $this->assertTrue($channel->sameValueAs($constructedChannel)); 22 | } 23 | 24 | public function testSameValueAs() 25 | { 26 | $channelA = new ChannelId("4006381333931"); 27 | $channelAEquals = new ChannelId("4006381333931"); 28 | $channelDifferent = new ChannelId("512638133393"); 29 | 30 | $this->assertTrue($channelA->sameValueAs($channelAEquals)); 31 | $this->assertFalse($channelA->sameValueAs($channelDifferent)); 32 | } 33 | 34 | public function testToString() 35 | { 36 | $channel = new ChannelId("4006381333931"); 37 | $this->assertSame("4006381333931", $channel->__toString()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/MessageContext/Domain/ValueObjects/BodyMessage.php: -------------------------------------------------------------------------------- 1 | channelId = new ChannelId($channelId); 17 | $this->closed = $closed; 18 | } 19 | 20 | public function getId() 21 | { 22 | return $this->channelId; 23 | } 24 | 25 | public function isClosed() 26 | { 27 | return $this->closed; 28 | } 29 | 30 | /** 31 | * Returns a object taking PHP native value(s) as argument(s). 32 | * 33 | * @return ValueObjectInterface 34 | */ 35 | public static function fromNative() 36 | { 37 | $pId = func_get_arg(0); 38 | $isClosed = func_get_arg(1); 39 | 40 | return new self(new PublisherId($pId), $isClosed); 41 | } 42 | 43 | /** 44 | * Compare two ValueObjectInterface and tells whether they can be considered equal 45 | * 46 | * @param ValueObjectInterface $object 47 | * @return bool 48 | */ 49 | public function sameValueAs(ValueObjectInterface $object) 50 | { 51 | if (get_class($object) !== get_class($this)) { 52 | return false; 53 | } 54 | 55 | return $this->isClosed() === $object->isClosed() && $this->getId() === $object->getId(); 56 | } 57 | 58 | /** 59 | * Returns a string representation of the object 60 | * 61 | * @return string 62 | */ 63 | public function __toString() 64 | { 65 | return sprintf("Channel: %s|Closed: %s", 66 | $this->channelId, 67 | $this->closed 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/MessageContext/Domain/ValueObjects/ChannelAuthorization.php: -------------------------------------------------------------------------------- 1 | channelId = $channelId; 16 | $this->publisherId = $publisherId; 17 | $this->isAuthorized = $isAuthorized; 18 | } 19 | 20 | /** 21 | * @return boolean 22 | */ 23 | public function canPublisherPublishOnChannel() 24 | { 25 | return $this->isAuthorized; 26 | } 27 | 28 | public function getChannelId() 29 | { 30 | return $this->channelId; 31 | } 32 | 33 | /** 34 | * Returns a object taking PHP native value(s) as argument(s). 35 | * 36 | * @return ValueObjectInterface 37 | */ 38 | public static function fromNative() 39 | { 40 | $pId = func_get_arg(0); 41 | $cId = func_get_arg(1); 42 | $isAuth = func_get_arg(2); 43 | 44 | return new self(new PublisherId($pId), new ChannelId($cId), $isAuth); 45 | } 46 | 47 | /** 48 | * Compare two ValueObjectInterface and tells whether they can be considered equal 49 | * 50 | * @param ValueObjectInterface $object 51 | * @return bool 52 | */ 53 | public function sameValueAs(ValueObjectInterface $object) 54 | { 55 | return (string) $this === (string) $object; 56 | } 57 | 58 | /** 59 | * Returns a string representation of the object 60 | * 61 | * @return string 62 | */ 63 | public function __toString() 64 | { 65 | return sprintf("Publisher: %s|Channel: %s|Authorized: %s", 66 | $this->publisherId, 67 | $this->channelId, 68 | $this->isAuthorized 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/MessageContext/Domain/ValueObjects/ChannelId.php: -------------------------------------------------------------------------------- 1 | circuitBreaker = $circuitBreaker; 14 | } 15 | 16 | public function isAvailable($serviceName) 17 | { 18 | return $this->circuitBreaker->isAvailable($serviceName); 19 | } 20 | 21 | public function reportSuccess($serviceName) 22 | { 23 | $this->circuitBreaker->reportSuccess($serviceName); 24 | } 25 | 26 | public function reportFailure($serviceName) 27 | { 28 | $this->circuitBreaker->reportFailure($serviceName); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/MessageContext/InfrastructureBundle/CircuitBreaker/DoctrineCacheAdapter.php: -------------------------------------------------------------------------------- 1 | doctrineCacheInstance = $doctrineCacheInstance; 25 | } 26 | /** 27 | * If you provided instance of doctrine cache we assume that it is ready to go. 28 | * @return boolean 29 | */ 30 | protected function checkExtension() 31 | { 32 | return true; 33 | } 34 | /** 35 | * Loads item by cache key. 36 | * 37 | * @param string $key 38 | * @return mixed 39 | * 40 | * @throws \Ejsmont\CircuitBreaker\Storage\StorageException if storage error occurs, handler can not be used 41 | */ 42 | protected function load($key) 43 | { 44 | return $this->doctrineCacheInstance->fetch($key); 45 | } 46 | /** 47 | * Save item in the cache. 48 | * 49 | * @param string $key 50 | * @param string $value 51 | * @param int $ttl 52 | * @return void 53 | * 54 | * @throws \Ejsmont\CircuitBreaker\Storage\StorageException if storage error occurs, handler can not be used 55 | */ 56 | protected function save($key, $value, $ttl) 57 | { 58 | $this->doctrineCacheInstance->save($key, $value, $ttl); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/MessageContext/InfrastructureBundle/CircuitBreaker/Factory.php: -------------------------------------------------------------------------------- 1 | root('post_context_presentation_bundle'); 22 | 23 | return $treeBuilder; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/MessageContext/InfrastructureBundle/DependencyInjection/InfrastructureBundleExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($configuration, $configs); 25 | 26 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 27 | $loader->load('request_handlers.yml'); 28 | $loader->load('gateways.yml'); 29 | $loader->load('repositories.yml'); 30 | $loader->load('circuit_breaker.yml'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/MessageContext/InfrastructureBundle/Exception/UnableToProcessResponseFromService.php: -------------------------------------------------------------------------------- 1 | response = $response; 15 | } 16 | 17 | public function getResponse() 18 | { 19 | return $this->response; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/MessageContext/InfrastructureBundle/InfrastructureBundle.php: -------------------------------------------------------------------------------- 1 | messages = new ArrayCollection(); 18 | } 19 | /** 20 | * @param Message $message 21 | * @return Message 22 | */ 23 | public function add(Message $message) 24 | { 25 | $this->messages->add($message); 26 | return $message; 27 | } 28 | 29 | /** 30 | * @return ArrayCollection 31 | */ 32 | public function getAll() 33 | { 34 | return $this->messages; 35 | } 36 | 37 | /** 38 | * @param MessageId $messageId 39 | * @return ArrayCollection 40 | */ 41 | public function get(MessageId $messageId) 42 | { 43 | return $this->messages->filter(function (Message $message) use ($messageId) { 44 | return $message->getId()->sameValueAs($messageId); 45 | }); 46 | } 47 | 48 | /** 49 | * @param MessageId $messageId 50 | * @return Message 51 | */ 52 | public function remove(MessageId $messageId) 53 | { 54 | foreach ($this->messages as $message) { 55 | if ($message->getId()->sameValueAs($messageId)) { 56 | $this->messages->removeElement($message); 57 | 58 | return $message; 59 | } 60 | } 61 | 62 | //to fix 63 | return null; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/MessageContext/InfrastructureBundle/Repository/InMemory/PublisherRepository.php: -------------------------------------------------------------------------------- 1 | publishers = new ArrayCollection(); 18 | } 19 | /** 20 | * @param PublisherId $publisherId 21 | * @return ArrayCollection $publishers 22 | */ 23 | public function get(PublisherId $publisherId) 24 | { 25 | return $this->publishers->filter(function (Publisher $p) use ($publisherId) { 26 | return $p->getId()->sameValueAs($publisherId); 27 | }); 28 | } 29 | 30 | /** 31 | * @param Publisher $publisher 32 | * @return Publisher 33 | */ 34 | public function add(Publisher $publisher) 35 | { 36 | $this->publishers->add($publisher); 37 | return $publisher; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/MessageContext/InfrastructureBundle/Repository/PostRepository.php: -------------------------------------------------------------------------------- 1 | response = $response; 15 | } 16 | 17 | public function getResponse() 18 | { 19 | return $this->response; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/MessageContext/InfrastructureBundle/RequestHandler/Listener/JsonResponseListener.php: -------------------------------------------------------------------------------- 1 | getResponse(); 13 | if (false === strpos($response->getHeader('Content-Type'), 'application/json')) { 14 | return; 15 | } 16 | 17 | $body = $response->getBody(); 18 | $json = json_decode($body, true); 19 | 20 | if (json_last_error()) { 21 | throw new Exception("Invalid JSON in response body: $body"); 22 | } 23 | 24 | $response->setBody($json); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/MessageContext/InfrastructureBundle/RequestHandler/Middleware/EventRequestHandler.php: -------------------------------------------------------------------------------- 1 | eventDispatcher = $eventDispatcher; 18 | $this->requestHandler = $requestHandler; 19 | } 20 | 21 | public function handle(Request $request) 22 | { 23 | $response = $this->requestHandler->handle($request); 24 | $this->eventDispatcher->dispatch('request_handler.received_response', new ReceivedResponse($response)); 25 | 26 | return $response; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/MessageContext/InfrastructureBundle/RequestHandler/Middleware/GuzzleRequestHandler.php: -------------------------------------------------------------------------------- 1 | client = $client; 19 | } 20 | 21 | /** 22 | * @param Request $request 23 | * @return Response 24 | */ 25 | public function handle(Request $request) 26 | { 27 | $guzzleRequest = $this->client->createRequest($request->getVerb(), $request->getUri(), array( 28 | 'headers' => $request->getHeaders(), 29 | 'body' => $request->getBody(), 30 | )); 31 | 32 | try { 33 | $guzzleResponse = $this->client->send($guzzleRequest); 34 | $response = new Response($guzzleResponse->getStatusCode()); 35 | $response->setHeaders($guzzleResponse->getHeaders()); 36 | $response->setBody($guzzleResponse->getBody()->__toString()); 37 | 38 | return $response; 39 | } catch (ConnectException $e) { 40 | return $this->handleConnectionException(); 41 | } catch (RequestException $e) { 42 | return $this->handleRequestException($e); 43 | } 44 | } 45 | 46 | private function handleConnectionException() 47 | { 48 | return Response::buildConnectionFailedResponse(); 49 | } 50 | 51 | private function handleRequestException(RequestException $e) 52 | { 53 | if ($e->hasResponse()) { 54 | $guzzleResponse = $e->getResponse(); 55 | 56 | $response = new Response($guzzleResponse->getStatusCode()); 57 | $response->setHeaders($guzzleResponse->getHeaders()); 58 | 59 | if (null !== $guzzleResponse->getBody()) { 60 | $response->setBody($guzzleResponse->getBody()->__toString()); 61 | } 62 | 63 | return $response; 64 | } 65 | 66 | return Response::buildConnectionFailedResponse(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/MessageContext/InfrastructureBundle/RequestHandler/Request.php: -------------------------------------------------------------------------------- 1 | verb = $verb; 15 | $this->uri = $uri; 16 | } 17 | 18 | public function getVerb() 19 | { 20 | return $this->verb; 21 | } 22 | 23 | public function getUri() 24 | { 25 | return $this->uri; 26 | } 27 | 28 | public function addHeader($name, $value) 29 | { 30 | $this->headers[$name] = $value; 31 | } 32 | 33 | public function getHeaders() 34 | { 35 | return $this->headers; 36 | } 37 | 38 | public function setBody($body) 39 | { 40 | $this->body = $body; 41 | } 42 | 43 | public function getBody() 44 | { 45 | return $this->body; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/MessageContext/InfrastructureBundle/RequestHandler/RequestHandler.php: -------------------------------------------------------------------------------- 1 | statusCode = $statusCode; 15 | $this->connectionFailed = false; 16 | } 17 | 18 | public static function buildConnectionFailedResponse() 19 | { 20 | $response = new self(0); 21 | $response->connectionFailed = true; 22 | 23 | return $response; 24 | } 25 | 26 | public function getStatusCode() 27 | { 28 | return $this->statusCode; 29 | } 30 | 31 | public function setHeaders(array $headers) 32 | { 33 | $this->headers = $headers; 34 | } 35 | 36 | public function getHeader($name) 37 | { 38 | $header = isset($this->headers[$name]) ? $this->headers[$name] : null; 39 | if (null !== $header && is_array($header)) { 40 | return $header[0]; 41 | } 42 | 43 | return $header; 44 | } 45 | 46 | public function setBody($body) 47 | { 48 | $this->body = $body; 49 | } 50 | 51 | public function getBody() 52 | { 53 | return $this->body; 54 | } 55 | 56 | public function hasConnectionFailed() 57 | { 58 | return $this->connectionFailed; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/MessageContext/InfrastructureBundle/Resources/config/circuit_breaker.yml: -------------------------------------------------------------------------------- 1 | services: 2 | cache: 3 | class: Doctrine\Common\Cache\FilesystemCache 4 | arguments: [%kernel.cache_dir%] 5 | extension: ".circuit.cache" 6 | 7 | circuit_breaker.filesystem_cache.factory: 8 | class: MessageContext\InfrastructureBundle\CircuitBreaker\Factory 9 | 10 | circuit_breaker_file_system: 11 | class: Ejsmont\CircuitBreakerBundle\Core\CircuitBreaker 12 | factory: ["@circuit_breaker.filesystem_cache.factory", getDoctrineCacheInstance] 13 | arguments: [@cache, 30, 60] 14 | 15 | 16 | message_context.infrastracture.circuit_breaker: 17 | class: MessageContext\InfrastructureBundle\CircuitBreaker\CircuitBreaker 18 | arguments: 19 | - @circuit_breaker_file_system 20 | -------------------------------------------------------------------------------- /src/MessageContext/InfrastructureBundle/Resources/config/gateways.yml: -------------------------------------------------------------------------------- 1 | services: 2 | message_context.infrastructure.channel_gateway: 3 | class: MessageContext\InfrastructureBundle\Service\Channel\ChannelGateway 4 | arguments: 5 | - @message_context.infrastructure.channel_adapter 6 | - @message_context.infrastracture.circuit_breaker 7 | 8 | message_context.infrastructure.channel_authorization_gateway: 9 | class: MessageContext\InfrastructureBundle\Service\ChannelAuthorization\ChannelAuthorizationGateway 10 | arguments: 11 | - @message_context.infrastructure.channel_authorization_adapter 12 | 13 | message_context.infrastructure.channel_adapter: 14 | class: MessageContext\InfrastructureBundle\Service\Channel\ChannelAdapter 15 | arguments: 16 | - @message_context.infrastructure.request_handler 17 | - "http://127.0.0.1:8080" 18 | 19 | message_context.infrastructure.channel_authorization_adapter: 20 | class: MessageContext\InfrastructureBundle\Service\ChannelAuthorization\ChannelAuthorizationAdapter 21 | arguments: 22 | - @message_context.infrastructure.request_handler 23 | - "http://127.0.0.1:8080" 24 | -------------------------------------------------------------------------------- /src/MessageContext/InfrastructureBundle/Resources/config/repositories.yml: -------------------------------------------------------------------------------- 1 | services: 2 | message_context.infrastructure.channel_repository: 3 | class: MessageContext\InfrastructureBundle\Repository\InMemory\ChannelRepository 4 | 5 | message_context.infrastructure.message_repository: 6 | class: MessageContext\InfrastructureBundle\Repository\InMemory\MessageRepository 7 | 8 | message_context.infrastructure.publisher_repository: 9 | class: MessageContext\InfrastructureBundle\Repository\InMemory\PublisherRepository 10 | -------------------------------------------------------------------------------- /src/MessageContext/InfrastructureBundle/Resources/config/request_handlers.yml: -------------------------------------------------------------------------------- 1 | services: 2 | message_context.infrastructure.request_handler: 3 | class: MessageContext\InfrastructureBundle\RequestHandler\Middleware\EventRequestHandler 4 | arguments: 5 | - @event_dispatcher 6 | - @message_context.infrastractrue.guzzle_request_handler 7 | 8 | message_context.infrastractrue.guzzle_request_handler: 9 | class: MessageContext\InfrastructureBundle\RequestHandler\Middleware\GuzzleRequestHandler 10 | arguments: 11 | - @message_context.infrastructure.guzzle_http_client 12 | 13 | message_context.infrastractrue.json_response_listener: 14 | class: MessageContext\InfrastructureBundle\RequestHandler\Listener\JsonResponseListener 15 | tags: 16 | - { name: kernel.event_listener, event: request_handler.received_response, method: onReceivedResponse } 17 | 18 | 19 | message_context.infrastructure.guzzle_http_client: 20 | class: GuzzleHttp\Client 21 | -------------------------------------------------------------------------------- /src/MessageContext/InfrastructureBundle/Service/Channel/ChannelAdapter.php: -------------------------------------------------------------------------------- 1 | requestHandler = $requestHandler; 23 | $this->channelUri = $channelUri; 24 | } 25 | 26 | /** 27 | * @param ChannelId $channelId 28 | * @return \MessageContext\Domain\Channel 29 | * 30 | * @throws \MessageContext\Domain\Exception\ChannelNotFoundException 31 | * @throws \MessageContext\InfrastructureBundle\Exception\UnableToProcessResponseFromService 32 | */ 33 | public function toChannel(ChannelId $channelId) 34 | { 35 | $request = new Request("GET", sprintf("%s/api/channels/%s", $this->channelUri, $channelId)); 36 | $request->addHeader("Accept", "application/json"); 37 | $response = $this->requestHandler->handle($request); 38 | 39 | $channelTranslator = new ChannelTranslator(); 40 | return $channelTranslator->toChannelFromResponse($response); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/MessageContext/InfrastructureBundle/Service/Channel/ChannelGateway.php: -------------------------------------------------------------------------------- 1 | channelAdapter = $channelAdapter; 23 | $this->circuitBreaker = $circuitBreaker; 24 | $this->serviceUniqueName = "channel.service"; 25 | } 26 | 27 | /** 28 | * @param ChannelId $channelId 29 | * @return Channel 30 | * 31 | * @throws ServiceNotAvailableException 32 | */ 33 | public function getChannel(ChannelId $channelId) 34 | { 35 | if ($this->circuitBreaker->isAvailable($this->serviceUniqueName)) { 36 | try { 37 | $channel = $this->channelAdapter->toChannel($channelId); 38 | $this->circuitBreaker->reportSuccess($this->serviceUniqueName); 39 | return $channel; 40 | } catch (UnableToProcessResponseFromService $e) { 41 | $this->handleNotExpectedResponse($e->getResponse()); 42 | } 43 | } 44 | 45 | $this->onServiceNotAvailable("The service is not available at the moment"); 46 | } 47 | 48 | private function handleNotExpectedResponse(Response $response) 49 | { 50 | $this->circuitBreaker->reportFailure($this->serviceUniqueName); 51 | 52 | if ($response->hasConnectionFailed()) { 53 | $this->onServiceNotAvailable("connection failed on channel service"); 54 | } 55 | 56 | $this->onServiceFailure( 57 | sprintf("The service %s has failed with message %s", $this->serviceUniqueName, $response->getBody()) 58 | ); 59 | } 60 | 61 | /** 62 | * @param $message 63 | * @throws ServiceNotAvailableException 64 | */ 65 | public function onServiceNotAvailable($message) 66 | { 67 | throw new ServiceNotAvailableException($message); 68 | } 69 | 70 | /** 71 | * @param $message 72 | * @throws ServiceFailureException 73 | */ 74 | public function onServiceFailure($message) 75 | { 76 | throw new ServiceFailureException($message); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/MessageContext/InfrastructureBundle/Service/Channel/ChannelTranslator.php: -------------------------------------------------------------------------------- 1 | getStatusCode()) { 16 | $contentArray = $this->validateAndGetResponseBodyArray($response); 17 | return new Channel(new ChannelId($contentArray["id"]), $contentArray["closed"]); 18 | } 19 | 20 | if (404 === $response->getStatusCode()) { 21 | throw new ChannelNotFoundException; 22 | } 23 | 24 | throw new UnableToProcessResponseFromService($response); 25 | } 26 | 27 | private function validateAndGetResponseBodyArray(Response $response) 28 | { 29 | $contentArray = $response->getBody(); 30 | 31 | if (isset($contentArray["id"]) && isset($contentArray["closed"])) { 32 | return $contentArray; 33 | } 34 | 35 | throw new UnableToProcessResponseFromService( 36 | $response, 37 | "Unable to process response body from channel service" 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/MessageContext/InfrastructureBundle/Service/ChannelAuthorization/ChannelAuthorizationAdapter.php: -------------------------------------------------------------------------------- 1 | requestHandler = $requestHandler; 19 | $this->channelAuthorizationUri = $channelAuthorizationUri; 20 | } 21 | 22 | /** 23 | * @param PublisherId $publisherId 24 | * @param ChannelId $channelId 25 | * 26 | * @return ChannelAuthorization 27 | */ 28 | public function toChannelAuthorization(PublisherId $publisherId, ChannelId $channelId) 29 | { 30 | $channelAdapter = new ChannelAuthorizationTranslator(); 31 | 32 | $request = new Request("GET", sprintf("%s/api/authorization/channels/%s/users/%s", 33 | $this->channelAuthorizationUri, 34 | $channelId, 35 | $publisherId) 36 | ); 37 | 38 | $request->addHeader("Accept", "application/json"); 39 | $response = $this->requestHandler->handle($request); 40 | 41 | return $channelAdapter->toChannelAuthorizationFromResponse( 42 | $response 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/MessageContext/InfrastructureBundle/Service/ChannelAuthorization/ChannelAuthorizationGateway.php: -------------------------------------------------------------------------------- 1 | channelAuthorizationAdapter = $channelAuthorizationAdapter; 19 | } 20 | 21 | /** 22 | * @param PublisherId $publisherId 23 | * @param ChannelId $channelId 24 | * 25 | * @return \MessageContext\Domain\ValueObjects\ChannelAuthorization 26 | */ 27 | public function getChannelAuthorization(PublisherId $publisherId, ChannelId $channelId) 28 | { 29 | try { 30 | return $this->channelAuthorizationAdapter->toChannelAuthorization($publisherId, $channelId); 31 | } catch (UnableToProcessResponseFromService $e) { 32 | $response = $e->getResponse(); 33 | 34 | if ($response->hasConnectionFailed()) { 35 | $this->onServiceNotAvailable(sprintf("service channel not available")); 36 | } else { 37 | $this->onServiceFailure( 38 | sprintf("The service channel auth failed with status code: %s and body %s", 39 | $response->getStatusCode(), 40 | $response->getBody() 41 | ) 42 | ); 43 | } 44 | } 45 | } 46 | 47 | /** 48 | * @param $message 49 | * @throws ServiceNotAvailableException 50 | */ 51 | public function onServiceNotAvailable($message) 52 | { 53 | throw new ServiceNotAvailableException($message); 54 | } 55 | 56 | /** 57 | * @param $message 58 | * @throws ServiceFailureException 59 | */ 60 | public function onServiceFailure($message) 61 | { 62 | throw new ServiceFailureException($message); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/MessageContext/InfrastructureBundle/Service/ChannelAuthorization/ChannelAuthorizationTranslator.php: -------------------------------------------------------------------------------- 1 | getStatusCode()) { 24 | $responseBodyArray = $this->validateAndGetResponseBodyArray($response); 25 | 26 | return new ChannelAuthorization( 27 | new PublisherId($responseBodyArray["publisher_id"]), 28 | new ChannelId($responseBodyArray["channel_id"]), 29 | $responseBodyArray["authorized"] 30 | ); 31 | } 32 | 33 | if (404 === $response->getStatusCode()) { 34 | throw new AuthorizationNotFoundException; 35 | } 36 | 37 | throw new UnableToProcessResponseFromService($response); 38 | } 39 | 40 | private function validateAndGetResponseBodyArray(Response $response) 41 | { 42 | $contentArray = $response->getBody(); 43 | 44 | if (isset($contentArray["publisher_id"]) && 45 | isset($contentArray["channel_id"]) && 46 | isset($contentArray["authorized"])) { 47 | return $contentArray; 48 | } 49 | 50 | throw new UnableToProcessResponseFromService( 51 | $response, 52 | "Unable to process response body from channel channel authorization" 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/MessageContext/InfrastructureBundle/Tests/Resources/MockResponsesLocator.php: -------------------------------------------------------------------------------- 1 | channelAdapter = $this->getMockBuilder(ChannelAdapter::class) 29 | ->disableOriginalConstructor() 30 | ->getMock(); 31 | 32 | $this->circuitBreaker = $this->getMockBuilder(MessageContextCircuitBreakerInterface::class) 33 | ->disableOriginalConstructor() 34 | ->getMock(); 35 | 36 | $this->channelGateway = new ChannelGateway( 37 | $this->channelAdapter, 38 | $this->circuitBreaker 39 | ); 40 | } 41 | 42 | public function testItGetChannel() 43 | { 44 | $channelId = new ChannelId("1"); 45 | $channelExpected = $this->anyChannel(); 46 | 47 | $this->channelAdapter->expects($this->once()) 48 | ->method("toChannel") 49 | ->with($this->equalTo($channelId)) 50 | ->willReturn($channelExpected); 51 | 52 | $this->theServiceIsAvailable(); 53 | $this->itReportSuccess(); 54 | 55 | $channel = $this->channelGateway->getChannel($channelId); 56 | 57 | $this->assertEquals($channelExpected, $channel); 58 | } 59 | 60 | /** 61 | * @expectedException \MessageContext\Application\Exception\ServiceFailureException 62 | */ 63 | public function testItReturnServiceFailureException() 64 | { 65 | $channelId = new ChannelId("1"); 66 | 67 | $this->channelAdapter->expects($this->once()) 68 | ->method("toChannel") 69 | ->with($this->equalTo($channelId)) 70 | ->willThrowException(new UnableToProcessResponseFromService(new Response(500))); 71 | 72 | $this->theServiceIsAvailable(); 73 | $this->itReportFailure(); 74 | 75 | $this->channelGateway->getChannel($channelId); 76 | } 77 | 78 | /** 79 | * @expectedException \MessageContext\Application\Exception\ServiceNotAvailableException 80 | */ 81 | public function testItReturnServiceNotAvailableException() 82 | { 83 | $channelId = new ChannelId("1"); 84 | 85 | $this->channelAdapter->expects($this->once()) 86 | ->method("toChannel") 87 | ->with($this->equalTo($channelId)) 88 | ->willThrowException(new UnableToProcessResponseFromService(Response::buildConnectionFailedResponse())); 89 | 90 | $this->theServiceIsAvailable(); 91 | $this->itReportFailure(); 92 | 93 | $this->channelGateway->getChannel($channelId); 94 | } 95 | 96 | /** 97 | * @expectedException \MessageContext\Application\Exception\ServiceNotAvailableException 98 | */ 99 | public function testItReturnServiceNotAvailableExceptionIfServiceIsNotAvailable() 100 | { 101 | $channelId = new ChannelId("1"); 102 | 103 | $this->theServiceIsNotAvailable(); 104 | $this->channelAdapter->expects($this->never()) 105 | ->method("toChannel"); 106 | 107 | $this->channelGateway->getChannel($channelId); 108 | } 109 | 110 | private function anyChannel() 111 | { 112 | return new Channel(new ChannelId("3333"), false); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/MessageContext/InfrastructureBundle/Tests/Service/Channel/ChannelTranslatorTest.php: -------------------------------------------------------------------------------- 1 | channelTranslator = new ChannelTranslator(); 17 | } 18 | 19 | public function testItTranslateResponseToChannel() 20 | { 21 | $contents = MockResponsesLocator::getResponseTemplate("channel200response.json"); 22 | $responseMock = sprintf($contents, "3333", "true"); //the channelId 23 | 24 | $response = new Response(200); 25 | $response->setBody(json_decode($responseMock, true)); 26 | 27 | $channel = $this->channelTranslator->toChannelFromResponse($response); 28 | $this->assertEquals("3333", $channel->getId()); 29 | } 30 | 31 | /** 32 | * @expectedException \MessageContext\Application\Exception\ChannelNotFoundException 33 | */ 34 | public function testItRaiseChannelNotFoundException() 35 | { 36 | $response = new Response(404); 37 | $this->channelTranslator->toChannelFromResponse($response); 38 | } 39 | 40 | /** 41 | * @expectedException \MessageContext\InfrastructureBundle\Exception\UnableToProcessResponseFromService 42 | * @dataProvider getNotAcceptableStatusCodes 43 | * @param $statusCode 44 | */ 45 | public function testItRaiseUnableToProcessResponseException($statusCode) 46 | { 47 | $response = new Response($statusCode); 48 | $this->channelTranslator->toChannelFromResponse($response); 49 | } 50 | 51 | /** 52 | * @expectedException \MessageContext\InfrastructureBundle\Exception\UnableToProcessResponseFromService 53 | */ 54 | public function testItRaiseUnableToProcessResponseIfContentIsNotExpected() 55 | { 56 | $response = new Response(200); 57 | $contents = ["c_id" => 1]; 58 | 59 | $response->setBody($contents); 60 | 61 | $this->channelTranslator->toChannelFromResponse($response); 62 | } 63 | 64 | public function getNotAcceptableStatusCodes() 65 | { 66 | return [ 67 | [201], [400], [500], [419], [418], 68 | ]; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/MessageContext/InfrastructureBundle/Tests/Service/ChannelAuthorization/ChannelAuthorizationGatewayTest.php: -------------------------------------------------------------------------------- 1 | channelAuthorizationAdapter = $this->getMockBuilder(ChannelAuthorizationAdapter::class) 24 | ->disableOriginalConstructor() 25 | ->getMock(); 26 | 27 | $this->channelAuthorizationGateway = new ChannelAuthorizationGateway( 28 | $this->channelAuthorizationAdapter 29 | ); 30 | } 31 | 32 | public function testItGetChannelAuthorization() 33 | { 34 | $publisherId = new PublisherId("3333"); 35 | $channelId = new ChannelId("1"); 36 | $channelAuthorizationExpected = new ChannelAuthorization($publisherId, $channelId, true); 37 | 38 | $this->channelAuthorizationAdapter->expects($this->once()) 39 | ->method("toChannelAuthorization") 40 | ->with($this->equalTo($publisherId), $this->equalTo($channelId)) 41 | ->willReturn($channelAuthorizationExpected); 42 | 43 | $channelAuthorization = $this->channelAuthorizationGateway->getChannelAuthorization($publisherId, $channelId); 44 | 45 | $this->assertTrue($channelAuthorization->sameValueAs($channelAuthorizationExpected)); 46 | } 47 | 48 | /** 49 | * @expectedException \MessageContext\Application\Exception\ServiceFailureException 50 | */ 51 | public function testItReturnServiceFailureException() 52 | { 53 | $publisherId = new PublisherId("3333"); 54 | $channelId = new ChannelId("1"); 55 | 56 | $this->channelAuthorizationAdapter->expects($this->once()) 57 | ->method("toChannelAuthorization") 58 | ->with($this->equalTo($publisherId), $this->equalTo($channelId)) 59 | ->willThrowException(new UnableToProcessResponseFromService(new Response(500))); 60 | 61 | $this->channelAuthorizationGateway->getChannelAuthorization($publisherId, $channelId); 62 | } 63 | 64 | /** 65 | * @expectedException \MessageContext\Application\Exception\ServiceNotAvailableException 66 | */ 67 | public function testItReturnServiceNotAvailable() 68 | { 69 | $publisherId = new PublisherId("3333"); 70 | $channelId = new ChannelId("1"); 71 | 72 | $this->channelAuthorizationAdapter->expects($this->once()) 73 | ->method("toChannelAuthorization") 74 | ->with($this->equalTo($publisherId), $this->equalTo($channelId)) 75 | ->willThrowException(new UnableToProcessResponseFromService(Response::buildConnectionFailedResponse())); 76 | 77 | $this->channelAuthorizationGateway->getChannelAuthorization($publisherId, $channelId); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/MessageContext/InfrastructureBundle/Tests/Service/ChannelAuthorization/ChannelAuthorizationTranslatorTest.php: -------------------------------------------------------------------------------- 1 | channelAuthorizationTranslator = new ChannelAuthorizationTranslator(); 20 | } 21 | 22 | public function testItTranslateAuthorizedResponse() 23 | { 24 | $template = MockResponsesLocator::getResponseTemplate("channel200AuthorizationResponse.json"); 25 | $responseContent = sprintf($template, "3333", "1", "true"); 26 | 27 | $response = new Response(200); 28 | $response->setBody(json_decode($responseContent, true)); 29 | 30 | $channelAuthorization = $this->channelAuthorizationTranslator->toChannelAuthorizationFromResponse( 31 | $response 32 | ); 33 | 34 | $channelAuthorizationExpected = new ChannelAuthorization( 35 | new PublisherId("3333"), 36 | new ChannelId("1"), 37 | true 38 | ); 39 | 40 | $this->assertTrue($channelAuthorizationExpected->sameValueAs($channelAuthorization)); 41 | } 42 | 43 | public function testItTranslateNotAuthorizedResponse() 44 | { 45 | $template = MockResponsesLocator::getResponseTemplate("channel200AuthorizationResponse.json"); 46 | $responseContent = sprintf($template, "3333", "1", "false"); 47 | 48 | $response = new Response(200); 49 | $response->setBody(json_decode($responseContent, true)); 50 | 51 | $channelAuthorization = $this->channelAuthorizationTranslator->toChannelAuthorizationFromResponse( 52 | $response 53 | ); 54 | 55 | $channelAuthorizationExpected = new ChannelAuthorization( 56 | new PublisherId("3333"), 57 | new ChannelId("1"), 58 | false 59 | ); 60 | 61 | $this->assertTrue($channelAuthorizationExpected->sameValueAs($channelAuthorization)); 62 | } 63 | 64 | /** 65 | * @expectedException \MessageContext\Application\Exception\AuthorizationNotFoundException 66 | */ 67 | public function testItRaiseAuthorizationNotFoundException() 68 | { 69 | $response = new Response(404); 70 | $this->channelAuthorizationTranslator->toChannelAuthorizationFromResponse($response); 71 | } 72 | 73 | /** 74 | * @expectedException \MessageContext\InfrastructureBundle\Exception\UnableToProcessResponseFromService 75 | * @dataProvider getNotAcceptableStatusCodes 76 | * @param $statusCode 77 | */ 78 | public function testItRaiseUnableToProcessResponseException($statusCode) 79 | { 80 | $response = new Response($statusCode); 81 | $this->channelAuthorizationTranslator->toChannelAuthorizationFromResponse($response); 82 | } 83 | 84 | /** 85 | * @expectedException \MessageContext\InfrastructureBundle\Exception\UnableToProcessResponseFromService 86 | */ 87 | public function testItRaiseUnableToProcessResponseIfContentIsNotExpected() 88 | { 89 | $response = new Response(200); 90 | $contents = ["publisher_id" => 1, "channel" => 2, "authorization:" => false]; 91 | 92 | $response->setBody($contents); 93 | 94 | $this->channelAuthorizationTranslator->toChannelAuthorizationFromResponse($response); 95 | } 96 | 97 | public function getNotAcceptableStatusCodes() 98 | { 99 | return [ 100 | [201], [400], [500], [419], [418], 101 | ]; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/MessageContext/InfrastructureBundle/Tests/Service/GatewayTrait.php: -------------------------------------------------------------------------------- 1 | circuitBreaker->expects($this->once()) 10 | ->method("isAvailable") 11 | ->willReturn(true); 12 | } 13 | 14 | public function theServiceIsNotAvailable() 15 | { 16 | $this->circuitBreaker->expects($this->once()) 17 | ->method("isAvailable") 18 | ->willReturn(false); 19 | } 20 | 21 | public function itReportFailure() 22 | { 23 | $this->circuitBreaker->expects($this->once()) 24 | ->method("reportFailure"); 25 | } 26 | 27 | public function itReportSuccess() 28 | { 29 | $this->circuitBreaker->expects($this->once()) 30 | ->method("reportSuccess"); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/MessageContext/PresentationBundle/Adapter/DeleteMessageAdapter.php: -------------------------------------------------------------------------------- 1 | getRequestParameters(); 15 | 16 | $deleteMessageCommand = new DeleteMessageCommand( 17 | new PublisherId($parameters["publisher_id"]), 18 | new MessageId($parameters["message_id"]) 19 | ); 20 | 21 | return $deleteMessageCommand; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/MessageContext/PresentationBundle/Adapter/NewMessageCommandAdapter.php: -------------------------------------------------------------------------------- 1 | getRequestParameters(); 20 | 21 | $command = new NewMessageInChannelCommand( 22 | new PublisherId($parameters["publisher_id"]), 23 | new ChannelId($parameters["channel_id"]), 24 | new BodyMessage($parameters["message"]) 25 | ); 26 | 27 | return $command; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/MessageContext/PresentationBundle/Controller/MessageController.php: -------------------------------------------------------------------------------- 1 | messageHandler = $messageHandler; 27 | $this->serializer = $serializer; 28 | } 29 | 30 | public function postMessageAction(Request $request) 31 | { 32 | $messageCommandAdapter = new NewMessageCommandAdapter(); 33 | 34 | $newMessageCommand = $messageCommandAdapter->createCommandFromRequest( 35 | new NewMessageRequest(json_decode($request->getContent(), true)) 36 | ); 37 | 38 | $message = $this->messageHandler->postNewMessage( 39 | $newMessageCommand 40 | ); 41 | 42 | return $this->view($message, Response::HTTP_CREATED); 43 | } 44 | 45 | public function deleteMessageAction(Request $request, $messageId) 46 | { 47 | $deleteMessageAdapter = new DeleteMessageAdapter(); 48 | 49 | $deleteMessageCommand = $deleteMessageAdapter->createCommandFromRequest( 50 | new DeleteMessageRequest(["message_id" => $messageId, "publisher_id" => "899"]) 51 | ); 52 | 53 | $this->messageHandler->deleteMessage($deleteMessageCommand); 54 | 55 | return $this->view(null, Response::HTTP_NO_CONTENT); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/MessageContext/PresentationBundle/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | root('post_context_presentation_bundle'); 22 | 23 | return $treeBuilder; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/MessageContext/PresentationBundle/DependencyInjection/PresentationBundleExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($configuration, $configs); 25 | 26 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 27 | $loader->load('controllers.yml'); 28 | $loader->load('application/handlers.yml'); 29 | $loader->load('application/services.yml'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/MessageContext/PresentationBundle/PresentationBundle.php: -------------------------------------------------------------------------------- 1 | setRequired(['publisher_id', 'message_id']); 17 | 18 | $resolver->setAllowedTypes(array( 19 | 'publisher_id' => array('string'), 20 | 'message_id' => array('string') 21 | )); 22 | 23 | $this->requestParameters = $resolver->resolve($options); 24 | } 25 | 26 | public function getRequestParameters() 27 | { 28 | return $this->requestParameters; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/MessageContext/PresentationBundle/Request/NewMessageRequest.php: -------------------------------------------------------------------------------- 1 | setRequired(['publisher_id', 'channel_id', 'message']); 17 | 18 | $resolver->setAllowedTypes(array( 19 | 'publisher_id' => array('string'), 20 | 'channel_id' => array('string'), 21 | 'message' => array('string') 22 | )); 23 | 24 | $this->requestParameters = $resolver->resolve($options); 25 | } 26 | 27 | public function getRequestParameters() 28 | { 29 | return $this->requestParameters; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/MessageContext/PresentationBundle/Resources/config/application/handlers.yml: -------------------------------------------------------------------------------- 1 | services: 2 | message_context.handlers.message_handler: 3 | class: MessageContext\Application\Handler\MessageHandler 4 | arguments: 5 | - @message_context.application.channel_fetcher 6 | - @message_context.application.publisher_fetcher 7 | - @message_context.application.channel_authorization_fetcher 8 | - @message_context.infrastructure.message_repository 9 | -------------------------------------------------------------------------------- /src/MessageContext/PresentationBundle/Resources/config/application/services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | message_context.application.channel_fetcher: 3 | class: MessageContext\Application\Service\ChannelFetcher 4 | arguments: 5 | - @message_context.infrastructure.channel_gateway 6 | 7 | message_context.application.channel_authorization_fetcher: 8 | class: MessageContext\Application\Service\ChannelAuthorizationFetcher 9 | arguments: 10 | - @message_context.infrastructure.channel_authorization_gateway 11 | 12 | message_context.application.publisher_fetcher: 13 | class: MessageContext\Application\Service\PublisherFetcher 14 | arguments: 15 | - @message_context.infrastructure.publisher_repository 16 | -------------------------------------------------------------------------------- /src/MessageContext/PresentationBundle/Resources/config/controllers.yml: -------------------------------------------------------------------------------- 1 | services: 2 | post_context.controller.create_post: 3 | class: MessageContext\PresentationBundle\Controller\MessageController 4 | arguments: 5 | - @message_context.handlers.message_handler 6 | - @jms_serializer 7 | -------------------------------------------------------------------------------- /src/MessageContext/PresentationBundle/Resources/config/presentation/serializer/Message.yml: -------------------------------------------------------------------------------- 1 | MessageContext\Domain\Message: 2 | exclusion_policy: ALL 3 | read_only: false 4 | access_type: property # defaults to property 5 | 6 | properties: 7 | messageId: 8 | expose: true 9 | access_type: property # defaults to property 10 | type: string 11 | serialized_name: message_id 12 | message: 13 | expose: true 14 | access_type: property # defaults to property 15 | type: string 16 | serialized_name: message 17 | channelId: 18 | expose: true 19 | inline: true 20 | publisherId: 21 | expose: true 22 | inline: true 23 | -------------------------------------------------------------------------------- /src/MessageContext/PresentationBundle/Resources/config/presentation/serializer/ValueObjects.ChannelId.yml: -------------------------------------------------------------------------------- 1 | MessageContext\Domain\ValueObjects\ChannelId: 2 | exclusion_policy: ALL 3 | properties: 4 | value: 5 | expose: true 6 | access_type: property # defaults to property 7 | serialized_name: channel_id 8 | -------------------------------------------------------------------------------- /src/MessageContext/PresentationBundle/Resources/config/presentation/serializer/ValueObjects.PublisherId.yml: -------------------------------------------------------------------------------- 1 | MessageContext\Domain\ValueObjects\PublisherId: 2 | exclusion_policy: ALL 3 | properties: 4 | value: 5 | expose: true 6 | access_type: property # defaults to property 7 | serialized_name: publisher_id 8 | -------------------------------------------------------------------------------- /src/MessageContext/PresentationBundle/Tests/Controller/MessageControllerTest.php: -------------------------------------------------------------------------------- 1 | messageHandlerMock = $this->getMockBuilder(MessageHandler::class) 28 | ->disableOriginalConstructor() 29 | ->getMock(); 30 | 31 | $this->serializerMock = $this->getMockBuilder(Serializer::class) 32 | ->disableOriginalConstructor() 33 | ->getMock(); 34 | 35 | $this->messageController = new MessageController($this->messageHandlerMock, $this->serializerMock); 36 | } 37 | 38 | public function testItCreateNewMessage() 39 | { 40 | $requestBody = <<getMockBuilder(Request::class) 45 | ->disableOriginalConstructor() 46 | ->getMock(); 47 | 48 | $request->expects($this->once()) 49 | ->method("getContent") 50 | ->willReturn($requestBody); 51 | 52 | $this->messageHandlerMock->expects($this->once()) 53 | ->method("postNewMessage") 54 | ->with($this->equalTo(new NewMessageInChannelCommand(new PublisherId("3444"), new ChannelId("22222"), new BodyMessage("message")))) 55 | ->willReturn(["id" => 1]); 56 | 57 | $view = $this->messageController->postMessageAction($request); 58 | 59 | $this->assertEquals(201, $view->getStatusCode()); 60 | $this->assertEquals('{"id":1}', json_encode($view->getData())); 61 | } 62 | 63 | /** 64 | * @group test 65 | * @dataProvider getInvalidNewMessageRequestBody 66 | * @expectedException \Symfony\Component\OptionsResolver\Exception\ExceptionInterface 67 | */ 68 | public function testRequiredRequestParameters($requestBody) 69 | { 70 | 71 | $request = $this->getMockBuilder(Request::class) 72 | ->disableOriginalConstructor() 73 | ->getMock(); 74 | 75 | $request->expects($this->once()) 76 | ->method("getContent") 77 | ->willReturn($requestBody); 78 | 79 | $this->messageController->postMessageAction($request); 80 | } 81 | 82 | public function getInvalidNewMessageRequestBody() 83 | { 84 | return [ 85 | ['"channel_id": "22222","message": "message"}'], 86 | ['{"publisher_id": "22222","message": "message"}'], 87 | ['{"channel_id": "22222","publisher_id": "1234"}'] 88 | ]; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /web/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | # turn on rewriting 3 | RewriteEngine On 4 | 5 | # turn empty requests into requests for "index.php", 6 | # keeping the query string intact 7 | RewriteRule ^$ index.php [QSA] 8 | 9 | # for all files not found in the file system, 10 | # reroute to "index.php" bootstrap script, 11 | # keeping the query string intact. 12 | RewriteCond %{REQUEST_FILENAME} !-d 13 | RewriteCond %{REQUEST_FILENAME} !-f 14 | RewriteCond %{REQUEST_FILENAME} !favicon.ico$ 15 | RewriteRule ^(.*)$ index.php [QSA,L] 16 | 17 | -------------------------------------------------------------------------------- /web/index.php: -------------------------------------------------------------------------------- 1 | load(); 12 | 13 | $request = Request::createFromGlobals(); 14 | $kernel = new AppKernel( 15 | $_SERVER['SYMFONY_ENV'], 16 | (bool)$_SERVER['SYMFONY_DEBUG'] 17 | ); 18 | 19 | $response = $kernel->handle($request); 20 | $response->send(); 21 | $kernel->terminate($request, $response); 22 | 23 | 24 | --------------------------------------------------------------------------------