symfony in microservice architecture
TRANSCRIPT
SYMFONYSYMFONYCOMPONENTS INCOMPONENTS INMICROSERVICESMICROSERVICESARCHITECTUREARCHITECTURE
ABOUT MEABOUT ME
Daniele D'Angeli@dangelidanielehttp://danieledangeli.com
http://www.sainsburys.co.uk/London
Born and raised in Rome
WHAT WE'RE DOINGWHAT WE'RE DOING
A "brownfield" project 8 "micro" service so farAPI gateway written in GoLangPython, PHP, Symfony, Django, GoLang
TECHNOLOGIESTECHNOLOGIES
... LET ME START... LET ME STARTFROMFROM
CONCLUSIONSCONCLUSIONS
IS SYMFONY SUITABLE FORA MICROSERVICESARCHITECTURE?
UNDER CERTAINUNDER CERTAIN
CONDITIONSCONDITIONS
YESYES
.What they are
.Drawbacks
.When Symfony can fail?
.When Symfony can success?
MICROSERVICESMICROSERVICES
WHAT THEY AREWHAT THEY ARE
BUZZWORD WINNINGBUZZWORD WINNINGAWARD 2015AWARD 2015
...EMERGED FROM...EMERGED FROM
#CONTINUOS#CONTINUOSDELIVERYDELIVERY
#DOMAIN DRIVEN#DOMAIN DRIVENDESIGNDESIGN
#ON DEMAND#ON DEMANDVIRTUALIZATIONVIRTUALIZATION
#INFRASTRACTURE#INFRASTRACTUREAUTOMATIONAUTOMATION
SMALL, AUTONOMOUSSMALL, AUTONOMOUSSERVICES THAT WORKSERVICES THAT WORK
TOGETHERTOGETHER
small and focused on doing one thing well
"Gather together those things that change for the same reason,
and separate those things that change for different reasons"
Robert C. Martin
HOW SMALL IS SMALL?HOW SMALL IS SMALL?
AUTONOMOUSAUTONOMOUS
Each microservice can be able to change independentlywithout requiring consumer to change
IDEALLY!!!IDEALLY!!!
DRAWBACKSDRAWBACKS
TESTINGTESTING
REPRODUCE THE ENTIREREPRODUCE THE ENTIRESTACK ON A LOCAL ENVSTACK ON A LOCAL ENV
WHEN SYMFONY CANWHEN SYMFONY CANFAILFAIL
SYMFONY IS NOT ASYMFONY IS NOT AMICROMICRO FRAMEWORK FRAMEWORK
IT CAN BEIT CAN BECONSIDERED MORECONSIDERED MORE
A A "FULL-STACK""FULL-STACK"FRAMEWORKFRAMEWORK
...IF THEY ARE SO...IF THEY ARE SOSMALLSMALL
TOO MANY TOOLS INTOO MANY TOOLS INOUR "FRAMEWORK" OUR "FRAMEWORK"
SYMFONY IS HARDSYMFONY IS HARDTO LEARNTO LEARN
MICROSERVICESMICROSERVICES
====
ETEREGENOUS TEAMETEREGENOUS TEAM
WHAT ABOUT IFWHAT ABOUT IFYOU ASK TO AYOU ASK TO APYTHONIST TOPYTHONIST TO
WORK WITHWORK WITHSYMFONY?SYMFONY?
TOO MANY CONFIGURATIONTOO MANY CONFIGURATIONFILESFILES
WHEN SYMFONY CANWHEN SYMFONY CANSUCCESS?SUCCESS?
...THEY ARE SO...THEY ARE SOSMALL?SMALL?
THERE ARE A LOT OFTHERE ARE A LOT OFEXAMPLES WHEREEXAMPLES WHERE
COMPANIES FAILEDCOMPANIES FAILEDWITH MICROSERVICESWITH MICROSERVICES
IT'S NOT ABOUTIT'S NOT ABOUTSYMFONY, OR OTHERSYMFONY, OR OTHERMVC FRAMEWORKS...MVC FRAMEWORKS...
IT'S ABOUTIT'S ABOUTPREMATUREPREMATURE
DECOMPOSITIONDECOMPOSITION
HTTPS://RCLAYTON.SILVRBACK.COM/FAILING-HTTPS://RCLAYTON.SILVRBACK.COM/FAILING-AT-MICROSERVICESAT-MICROSERVICES
"... ANOTHER LESSON I LEARNED WAS"... ANOTHER LESSON I LEARNED WAS
TO NOT GET TOO GRANULAR WITHTO NOT GET TOO GRANULAR WITH
MICROSERVICES AT THE BEGINNINGMICROSERVICES AT THE BEGINNING
OF A PROJECT"OF A PROJECT"
"... INSTEAD OF DOING WHAT WE DID"... INSTEAD OF DOING WHAT WE DID
(STARTING WITH 8 SERVICES), TRY(STARTING WITH 8 SERVICES), TRY
STARTING WITH TWO OR THREESTARTING WITH TWO OR THREE
SERVICES OF LOGICALLY RELATEDSERVICES OF LOGICALLY RELATED
FUNCTIONALITY (THEY WON'T BEFUNCTIONALITY (THEY WON'T BE
MICRO, HOWEVER)"MICRO, HOWEVER)"
HOW FACINGHOW FACINGPREMATUREPREMATURE
DECOMPOSITION?DECOMPOSITION?
FIRST APPROACHFIRST APPROACHMONOLITH FIRSTMONOLITH FIRST
http://martinfowler.com/bliki/MonolithFirst.html
RealityHope vs.
http://martinfowler.com/articles/dont-start-monolith.html
DOMAIN DRIVENDOMAIN DRIVENDESIGN PRINCIPLESDESIGN PRINCIPLES
ARE USEFULARE USEFUL
ERIC EVANS ERIC EVANS
He talked about how microservices boundary enable DDDhttps://skillsmatter.com/skillscasts/6259-ddd-and-microservices-at-last-some-
bounderies
DDD eXchange 2015
THOUGHWORKSTHOUGHWORKS https://www.thoughtworks.com/insights/blog/domain-driven-design-services-
architecture
Domain Driven Design for Services ArchitectureBounded Contexts Designed as Service Applications
ONEONE"MICRO"SERVICE"MICRO"SERVICE
FOR EACH BOUNDEDFOR EACH BOUNDEDCONTEXTCONTEXT
LESS MICRO, MORELESS MICRO, MORE"MACRO""MACRO"
WE NEED TO DESIGNWE NEED TO DESIGNOUR APPLICATIONS OUR APPLICATIONS
PROPERLYPROPERLY
IN SUCHIN SUCHSCENARIOS SCENARIOS
SYMFONY CAN HELPSYMFONY CAN HELP...BUT PROBABLY WE...BUT PROBABLY WE
NEED SOMENEED SOMEEXPEDIENTSEXPEDIENTS
A PRATICAL EXAMPLEA PRATICAL EXAMPLE
.Symfony scaffolding
&minimal configuration
.Testing
.How develop the integrations
LET'SLET'SCONSIDER ANCONSIDER ANAPPLICATIONAPPLICATIONLIKE SLACKLIKE SLACK
DISCOVERDISCOVERBOUNDED CONTEXT (HARD)BOUNDED CONTEXT (HARD)
ChannelContext
MessageContext
AuthorizationContext
AuthenticationContext
User management
Context
MESSAGE CONTEXTMESSAGE CONTEXT
MessagesContext
ALLOWS AALLOWS A PUBLISHER PUBLISHERTO PUBLISH AND DELETETO PUBLISH AND DELETEMESSAGGES ON MESSAGGES ON OPENOPEN
CHANNELSCHANNELS
A A PUBLISHERPUBLISHER MUST BE MUST BEAUTHORIZEDAUTHORIZED TO TO
PUBLISH A MESSAGE ONPUBLISH A MESSAGE ONA CHANNELA CHANNEL
SYMFONYSYMFONYSCAFFOLDINGSCAFFOLDINGAND MINIMALAND MINIMAL
CONFIGURATIONCONFIGURATION
AVOID A CLASSICAVOID A CLASSICSYMFONYSYMFONY
SCAFFOLDINGSCAFFOLDING
USE A MINIMALUSE A MINIMALSYMFONYSYMFONY
CONFIGURATIONCONFIGURATION PLUSPLUS
DDD DIRECTORYDDD DIRECTORYSTRUCTURE STRUCTURE
https://github.com/danieledangeli/symfony-microservice-bounded-context-example
Github repository
MINIMAL SYMFONY CONFMINIMAL SYMFONY CONF
Inspired by:
http://www.whitewashing.de/2014/10/26/symfony_all_the_things_web.html
environment variables.env files
only one config.ymlonly one entry point (index.php)
DotEnv
https://github.com/vlucas/phpdotenv
require_once __DIR__ . "/../vendor/autoload.php";require_once __DIR__ . "/../app/AppKernel.php";
use Symfony\Component\HttpFoundation\Request;use Dotenv\Dotenv;
//load environment variables. It doesn't overwrite existing ones$dotenv = new Dotenv(__DIR__ . '/../');$dotenv->load();
$request = Request::createFromGlobals();
$kernel = new AppKernel( $_SERVER['SYMFONY_ENV'], (bool)$_SERVER['SYMFONY_DEBUG']);
$response = $kernel->handle($request);
$response->send();$kernel->terminate($request, $response);
Only one index.php
Only 1 config.yml
public function registerBundles(){ ...}
public function registerContainerConfiguration(LoaderInterface $loader){ $loader->load(__DIR__ . '/config/config.yml');
if (in_array($this->getEnvironment(), array('dev', 'test'))) { $loader->load(function ($container) { $container->loadFromExtension('web_profiler', array( 'toolbar' => true, )); $container->loadFromExtension('framework', array( 'test' => true, )); }); }}
The result:
START TOSTART TOCODINGCODING
TESTINGTESTING
TEST BEHAVIOURTEST BEHAVIOURFIRSTFIRST
MAKE THE VALUE EXPLICITMAKE THE VALUE EXPLICIT(It's important also for other developers)
Feature: message publisher As a Publisher I need to be able to publish a message on a channel ... Scenario: A publisher, if not authorized is not able to publish a new message on a channel Given I am publisher And exists an "open" channel And I'm not authorized to publish messages on that channel When I write the message "Hi guys" Then I'm informed that I'm not authorized And no new messages will be published on that channel
FUNCTIONAL TESTSFUNCTIONAL TESTS
Stub dependencies
http://martinfowler.com/articles/microservice-testing/
PHPUNIT/PHPSPECPHPUNIT/PHPSPECwith or without
MOCKERYMOCKERY
UNIT TESTSUNIT TESTS
MAKE CLEAR THE TESTMAKE CLEAR THE TESTTYPE: TYPE: FUNCTIONAL,FUNCTIONAL, UNIT,UNIT, INTEGRATIONINTEGRATION
PHPUNIT @GROUPSPHPUNIT @GROUPS
HOW TO HOW TO
DEVELOP THEDEVELOP THEINTEGRATIONSINTEGRATIONS
final class Publisher { public function publishOnChannel( Channel $channel, ChannelAuthorization $channelAuthorization, BodyMessage $body ) { if($channelAuthorization->canPublisherPublishOnChannel()) { if (!$channel->isClosed()) { //create message }
throw new ChannelClosedException( sprintf("The channel %s is closed", $channel ); }
throw new PublisherNotAuthorizedException; }}
Channel { id ChannelId isOpen boolean}
ChannelAuthorization { publisherId PublisherId channelId ChannelId isAuthorized boolean}
MESSAGE CONTEXT MODELSMESSAGE CONTEXT MODELS
Publisher { id PublisherId}
sent within theauthenticatedrequest
taken from services integrations (Channel Context, Channel Authorization Context)
namespace MessageContext\Domain\Service\Gateway;
interface ChannelGatewayInterface{ /** * @param ChannelId $channelId * * @return Channel */ public function getChannel(ChannelId $channelId);}
<?php
namespace MessageContext\InfrastructureBundle\Service\Channel;
class ChannelGateway implements ChannelGatewayInterface{ private $channelAdapter;
...
/** * @param ChannelId $channelId * * @return Channel */ public function getChannel(ChannelId $channelId) { return $this->channelAdapter ->toChannel($channelId); }}
namespace MessageContext\InfrastructureBundle\Service\ChannelAuthorization
class ChannelAdapter{ ...
public function toChannel(ChannelId $channelId) { $request = new Request("GET", sprintf("%s/api/channels/%s", $this->channelContextUri, $channelId ));
$request->addHeader("Accept", "application/json"); $response = $this->requestHandler->handle($request);
return $this->channelTranslator->toChannelFromResponse( $response ); }}
class ChannelTranslator{ public function toChannelFromResponse(Response $response) { if (200 === $response->getStatusCode()) { $contentArray = $this->validateAndGetResponseBodyArray($response); return new Channel(new ChannelId($contentArray["id"]), $contentArray["isOpen"]); } .... }
private function validateAndGetResponseBodyArray(Response $response) { $contentArray = $response->getBody();
if (isset($contentArray["id"]) && isset($contentArray["isOpen"])) { return $contentArray; }
.... }}
Only what we need
{ "id": "456t-889-4444", "isOpen": false, "createAt": "26/05/2015", "publisherId": "11111-5555-3333-5555", "name": "a channel name", "spot": "a channel spot", "messagges_count": 450
... ...} The original response
ARE WE MISSINGARE WE MISSINGSOMETHINGSOMETHING
??
<?php
interface ServiceIntegrationInterface{ /** * @param $message * * @throws ServiceNotAvailableException */ public function onServiceNotAvailable($message);
/** * @param $message * * @throws ServiceFailureException */ public function onServiceFailure($message);}
namespace MessageContext\Domain\Service\Gateway;
interface ChannelGatewayInterface extends ServiceIntegrationInterface{ /** * @param ChannelId $channelId * * @throws MicroServiceIntegrationException * @return Channel */ public function getChannel(ChannelId $channelId);}
class ChannelGateway implements ChannelGatewayInterface { ... /** * @param $message * @throws ServiceNotAvailableException */ public function onServiceNotAvailable($message) { throw new ServiceNotAvailableException($message); }
/** * @param $message * @throws ServiceFailureException */ public function onServiceFailure($message) { throw new ServiceFailureException($message); }
ARE WE STILLARE WE STILLMISSINGMISSING
SOMETHINGSOMETHING
??
IF AN INTEGRATIONIF AN INTEGRATIONIS NOT AVAILABLE,IS NOT AVAILABLE,WHY CONTINUINGWHY CONTINUING
TO SEND REQUESTSTO SEND REQUESTSTO IT?TO IT?
CIRCUIT BREAKERCIRCUIT BREAKER
"A CIRCUIT BREAKER IS"A CIRCUIT BREAKER ISUSED TO DETECT FAILURESUSED TO DETECT FAILURESAND ENCAPSULATES LOGICAND ENCAPSULATES LOGICOF PREVENTING A FAILUREOF PREVENTING A FAILURE
TO REOCCURTO REOCCURCONSTANTLY"CONSTANTLY"
HTTPS://GITHUB.COM/EJSMONT-ARTUR/PHP-HTTPS://GITHUB.COM/EJSMONT-ARTUR/PHP-CIRCUIT-BREAKERCIRCUIT-BREAKER
namespace MessageContext\InfrastructureBundle\CircuitBreaker;
class CircuitBreaker implements PostContextCircuitBreakerInterface{ private $circuitBreaker; ...
public function isAvailable($serviceName) { return $this->circuitBreaker->isAvailable($serviceName); }
public function reportSuccess($serviceName) { $this->circuitBreaker->reportSuccess($serviceName); }
public function reportFailure($serviceName) { $this->circuitBreaker->reportFailure($serviceName); }}
public function getChannel(ChannelId $channelId){ if ($this->circuitBreaker->isAvailable($this->serviceName)) { try { $channel = $this->channelAdapter->toChannel($channelId); $this->circuitBreaker->reportSuccess($this->serviceName);
return $channel; } catch (UnableToProcessResponseFromService $e) { $this->handleNotExpectedResponse($e->getResponse()); } }
$this->onServiceNotAvailable("Service not available");}
private function handleNotExpectedResponse(Response $response){ $this->circuitBreaker->reportFailure($this->serviceName);
...}
CONCLUSIONCONCLUSION(AGAIN)(AGAIN)
SYMFONY CAN BE USED INSYMFONY CAN BE USED INSOME MICROSERVICESOME MICROSERVICE
ARCHITECTURES,ARCHITECTURES,ESPECIALLY IF WE STARTESPECIALLY IF WE STARTWITH A MONOLITH FIRSTWITH A MONOLITH FIRST
APPROACHAPPROACH
HOWEVER WE MAY NEED TOHOWEVER WE MAY NEED TOCONSIDER SOMECONSIDER SOME
EXPEDIENTS EXPEDIENTS (env variables, avoid complex tools)
(IT DEPENDS ON THE TEAM!!)(IT DEPENDS ON THE TEAM!!)
MINIMAL INTEGRATIONSMINIMAL INTEGRATIONS&
HANDLING FAILURESHANDLING FAILURES
ARE A GOOD STARTING POINTARE A GOOD STARTING POINTTO FACE THE OVERCOMPLEXITYTO FACE THE OVERCOMPLEXITY
INTRODUCED BYINTRODUCED BYTHIS ARCHITECTURETHIS ARCHITECTURE
WHAT WE HAVEWHAT WE HAVEMISSEDMISSED
OTHER INTEGRATION WAYS:OTHER INTEGRATION WAYS:
SERVICE CHOREOGRAPHY SERVICE CHOREOGRAPHY
OTHER TESTING WAYS:OTHER TESTING WAYS:
CONSUMER DRIVEN CONTRACTCONSUMER DRIVEN CONTRACTTESTSTESTS
QUESTIONS?QUESTIONS?https://www.slideshare.net/danieledangeli10/symfony-in-
microservice-architecture