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