Share
Restless Microservices with Symfony
When we think about microservices we usually imagine a bunch of self-contained and job-focused applications, each specifying and managing their own boundaries and dependencies, and talking to each other over HTTP / REST APIs.
Used in the right context, the microservice architecture can deliver number of great benefits to a business. But those benefits come with tradeoffs.
Unfortunately a lot of people find out too far down the road just how difficult REST-based microservices can be to implement and maintain.
In this presentation we dug into the pros and cons of using microservices, considered if it was worth sticking with a monolith for projects, and then saw how MyBuilder.com have tackled the problem in the real world using Symfony, keeping services isolated and testable while addressing some of the challenges that many people face when trying to adopt such a pattern.
nealio82
Transcript
182 slides
-
bin public src templates config billing marketplace profile upload Billing
Search / buy music Profile Upload songs -
bin public src templates config marketplace profile upload Billing Search
/ buy music Profile Upload songs billing -
Noback, M (2017): “Microservices for everyone”, p3 Splitting one big
software solution into multiple smaller ones will make it easier to cut away bad parts, to integrate new parts, and to run experiments, at infrastructure and code level. This entails a higher speed of change than what we normally experience when working on a monolithic application. -
With a large, monolithic service, we have to scale everything
together. […] if that behaviour is locked up in a giant monolithic application, we have to handle scaling everything as a piece. With smaller services, we can just scale those services that need scaling, allowing us to run other parts of the system on smaller, less powerful hardware. Newman, S (2015): “Building Microservices”, p5 -
Wolff, E (2016): “Microservices”, p57 A failure in the deployment
of a single Microservice [doesn’t] impact the system as a whole. -
What problems do they solve? What new problems do they
introduce? Should I use microservices? -
Wolff, E (2016): “Microservices”, p72 With a Deployment Monolith it
is relatively straightforward to monitor the system. When problems arise, the administrator can log into the system [and] analyse errors. Microservice based systems contain so many systems that this approach is no longer feasible. Consequently, there has to be a monitoring system that brings monitoring information from all the services together. -
Newman, S (2015): “Building Microservices”, p5 […] we need to
understand the new sources of failure that distributed systems have to deal with. Networks can and will fail, as will machines. -
What problems do they solve? What new problems do they
introduce? Should I use microservices? -
What problems do they solve? What new problems do they
introduce? Should I stick to a monolith? Should I use microservices? -
Search / buy music Upload songs Billing Profile { "id":
"9541df33-2077-499e-a6f3-579eec03cd0c", "eventType": "invoice", "value": "100" } JSON over HTTP -
marketplace upload billing profile bin public src config bin public
src config bin public src config bin public src config templates -
marketplace upload billing profile bin public src config bin public
src config bin public src config bin public src config templates { "id": "9541df33-2077-499e-a6f3-579eec03cd0c", "eventType": "invoice", "value": "100" } JSON over HTTP -
marketplace upload billing profile bin public src config bin public
src config bin public src config bin public src config templates -
{ "name": "acme/billing-package", "description": "Handles billing for things in our
application", "version": "0.0.1", "license": "proprietary", "require": { "php": ">=7.3" }, "require-dev": { "phpunit/phpunit": "^8" }, "config": { "bin-dir": "bin" }, "autoload": { "psr-4": { "Acme\\Package\\Billing\\": "src/" } }, "autoload-dev": { "psr-4": { "Tests\\": "tests/" } } } composer.json -
{ "name": "acme/billing-package", "description": "Handles billing for things in our
application", "version": "0.0.1", "license": "proprietary", "require": { "php": ">=7.3" }, "require-dev": { "phpunit/phpunit": "^8" }, "config": { "bin-dir": "bin" }, "autoload": { "psr-4": { "Acme\\Package\\Billing\\": "src/" } }, "autoload-dev": { "psr-4": { "Tests\\": "tests/" } } } composer.json -
composer.json { "name": "acme/billing-package", "description": "Handles billing for things in
our application", "version": "0.0.1", "license": "proprietary", "require": { "php": ">=7.3" }, "require-dev": { "phpunit/phpunit": "^8" }, "config": { "bin-dir": "bin" }, "autoload": { "psr-4": { "Acme\\Package\\Billing\\": "src/" } }, "autoload-dev": { "psr-4": { "Tests\\": "tests/" } } } -
src composer.json tests CardPayment.php CustomerRepository.php PaymentProcessor.php <?php namespace Acme\Package\Billing; class
CardPayment { private $paymentProcessor; private $repository; public function __construct( PaymentProcessor $paymentProcessor, CustomerRepository $repository ) { $this->paymentProcessor = $paymentProcessor; $this->repository = $repository; } public function chargeCard(int $customerId, int $amountInPounds): void { $customer = $this->repository->find($customerId); if (null === $customer) { throw new \Exception('Customer not found'); } $this->paymentProcessor->debitAccount( $customer, $amountInPounds ); } public function listPayments(int $customerId): array { // ... } -
src composer.json tests CardPayment.php CustomerRepository.php PaymentProcessor.php <?php namespace Acme\Package\Billing; class
CardPayment { private $paymentProcessor; private $repository; public function __construct( PaymentProcessor $paymentProcessor, CustomerRepository $repository ) { $this->paymentProcessor = $paymentProcessor; $this->repository = $repository; } public function chargeCard(int $customerId, int $amountInPounds): void { $customer = $this->repository->find($customerId); if (null === $customer) { throw new \Exception('Customer not found'); } $this->paymentProcessor->debitAccount( $customer, $amountInPounds ); } public function listPayments(int $customerId): array { // ... } -
src composer.json tests CardPayment.php CustomerRepository.php PaymentProcessor.php <?php namespace Acme\Package\Billing; class
CardPayment { private $paymentProcessor; private $repository; public function __construct( PaymentProcessor $paymentProcessor, CustomerRepository $repository ) { $this->paymentProcessor = $paymentProcessor; $this->repository = $repository; } public function chargeCard(int $customerId, int $amountInPounds): void { $customer = $this->repository->find($customerId); if (null === $customer) { throw new \Exception('Customer not found'); } $this->paymentProcessor->debitAccount( $customer, $amountInPounds ); } public function listPayments(int $customerId): array { // ... } -
src composer.json tests CardPayment.php CustomerRepository.php PaymentProcessor.php <?php namespace Acme\Package\Billing; class
CardPayment { private $paymentProcessor; private $repository; public function __construct( PaymentProcessor $paymentProcessor, CustomerRepository $repository ) { $this->paymentProcessor = $paymentProcessor; $this->repository = $repository; } public function chargeCard(int $customerId, int $amountInPounds): void { $customer = $this->repository->find($customerId); if (null === $customer) { throw new \Exception('Customer not found'); } $this->paymentProcessor->debitAccount( $customer, $amountInPounds ); } public function listPayments(int $customerId): array { // ... } -
src composer.json tests CardPayment.php CustomerRepository.php PaymentProcessor.php <?php namespace Acme\Package\Billing; class
CardPayment { private $paymentProcessor; private $repository; public function __construct( PaymentProcessor $paymentProcessor, CustomerRepository $repository ) { $this->paymentProcessor = $paymentProcessor; $this->repository = $repository; } public function chargeCard(int $customerId, int $amountInPounds): void { $customer = $this->repository->find($customerId); if (null === $customer) { throw new \Exception('Customer not found'); } $this->paymentProcessor->debitAccount( $customer, $amountInPounds ); } public function listPayments(int $customerId): array { // ... } -
<?php namespace Acme\Package\Billing; interface PaymentProcessor { public function debitAccount(Customer $customer,
int $amount): void; public function fetchTransactions(Customer $customer): array; } src composer.json tests CardPayment.php CustomerRepository.php PaymentProcessor.php -
src composer.json tests CardPayment.php CustomerRepository.php PaymentProcessor.php <?php namespace Acme\Package\Billing; interface
PaymentProcessor { public function debitAccount(Customer $customer, int $amount): void; public function fetchTransactions(Customer $customer): array; } -
src composer.json tests CardPayment.php CustomerRepository.php PaymentProcessor.php CardPaymentTest.php <?php namespace Tests;
// ... class CardPaymentTest extends TestCase { protected function setUp() { $this->processor = $this->createMock(PaymentProcessor::class); $this->repository = $this->createMock(CustomerRepository::class); $this->cardPayment = new CardPayment($this->processor,$this->repository); } public function test_a_card_is_charged_the_correct_amount(): void { $customer = new Customer(); // ... $this->cardPayment->chargeCard($customer->getId(), 100); } public function test_an_exception_is_thrown_when_customer_is_not_found(): void { // ... } // ... } -
<?php namespace Tests; // ... class CardPaymentTest extends TestCase {
protected function setUp() { $this->processor = $this->createMock(PaymentProcessor::class); $this->repository = $this->createMock(CustomerRepository::class); $this->cardPayment = new CardPayment($this->processor,$this->repository); } public function test_a_card_is_charged_the_correct_amount(): void { $customer = new Customer(); // ... $this->cardPayment->chargeCard($customer->getId(), 100); } public function test_an_exception_is_thrown_when_customer_is_not_found(): void { // ... } // ... } src composer.json tests CardPayment.php CustomerRepository.php PaymentProcessor.php CardPaymentTest.php -
bin public src config vendor acme billing-package profile-package upload-package adapter
PaymentProcessorAdapter.php <?php namespace App\Adapter; use Acme\Package\Billing\PaymentProcessor; class PaymentProcessorAdapter implements PaymentProcessor { private $stripeApiKey; public function __construct(string $stripeApiKey) { $this->stripeApiKey = $stripeApiKey; } public function debitAccount(Customer $customer, int $amount): void { \Stripe\Stripe::setApiKey($this->stripeApiKey); \Stripe\Charge::create([ "amount" => $amount, "currency" => "gbp", "source" => $customer->getAuthToken() ]); } public function fetchTransactions(Customer $customer): array { // ... } } -
bin public src config adapter PaymentProcessorAdapter.php vendor acme billing-package profile-package
upload-package <?php namespace App\Adapter; use Acme\Package\Billing\PaymentProcessor; class PaymentProcessorAdapter implements PaymentProcessor { private $stripeApiKey; public function __construct(string $stripeApiKey) { $this->stripeApiKey = $stripeApiKey; } public function debitAccount(Customer $customer, int $amount): void { \Stripe\Stripe::setApiKey($this->stripeApiKey); \Stripe\Charge::create([ "amount" => $amount, "currency" => "gbp", "source" => $customer->getAuthToken() ]); } public function fetchTransactions(Customer $customer): array { // ... } } -
bin public src config adapter PaymentProcessorAdapter.php vendor acme billing-package profile-package
upload-package <?php namespace App\Adapter; use Acme\Package\Billing\PaymentProcessor; class PaymentProcessorAdapter implements PaymentProcessor { private $stripeApiKey; public function __construct(string $stripeApiKey) { $this->stripeApiKey = $stripeApiKey; } public function debitAccount(Customer $customer, int $amount): void { \Stripe\Stripe::setApiKey($this->stripeApiKey); \Stripe\Charge::create([ "amount" => $amount, "currency" => "gbp", "source" => $customer->getAuthToken() ]); } public function fetchTransactions(Customer $customer): array { // ... } } -
{ "name": "acme/billing-bridge", "description": "Handles the infrastructure for the billing
package", "version": "0.0.1", "license": "proprietary", "require": { "php": ">=7.3", "ext-PDO": "*", "ext-json": "*", "acme/billing-package": "*@dev", "symfony/config": ">=2.8,<3.5", "symfony/http-kernel": ">=2.8,<3.5", "symfony/dependency-injection": "<3.5" }, "require-dev": { "phpunit/phpunit": "^8" }, "autoload": { "psr-4": { "Acme\\Bridge\\Billing\\": "./src" } }, "autoload-dev": { "files": [ "tests/AppKernel.php" ] } } composer.json -
{ "name": "acme/billing-bridge", "description": "Handles the infrastructure for the billing
package", "version": "0.0.1", "license": "proprietary", "require": { "php": ">=7.3", "ext-PDO": "*", "ext-json": "*", "acme/billing-package": "*@dev", "symfony/config": ">=2.8,<3.5", "symfony/http-kernel": ">=2.8,<3.5", "symfony/dependency-injection": "<3.5" }, "require-dev": { "phpunit/phpunit": "^8" }, "autoload": { "psr-4": { "Acme\\Bridge\\Billing\\": "./src" } }, "autoload-dev": { "files": [ "tests/AppKernel.php" ] } } composer.json -
{ "name": "acme/billing-bridge", "description": "Handles the infrastructure for the billing
package", "version": "0.0.1", "license": "proprietary", "require": { "php": ">=7.3", "ext-PDO": "*", "ext-json": "*", "acme/billing-package": "*@dev", "symfony/config": ">=2.8,<3.5", "symfony/http-kernel": ">=2.8,<3.5", "symfony/dependency-injection": "<3.5" }, "require-dev": { "phpunit/phpunit": "^8" }, "autoload": { "psr-4": { "Acme\\Bridge\\Billing\\": "./src" } }, "autoload-dev": { "files": [ "tests/AppKernel.php" ] } } composer.json -
{ "name": "acme/billing-bridge", "description": "Handles the infrastructure for the billing
package", "version": "0.0.1", "license": "proprietary", "require": { "php": ">=7.3", "ext-PDO": "*", "ext-json": "*", "acme/billing-package": "*@dev", "symfony/config": ">=2.8,<3.5", "symfony/http-kernel": ">=2.8,<3.5", "symfony/dependency-injection": "<3.5" }, "require-dev": { "phpunit/phpunit": "^8" }, "autoload": { "psr-4": { "Acme\\Bridge\\Billing\\": "./src" } }, "autoload-dev": { "files": [ "tests/AppKernel.php" ] } } composer.json -
{ "name": "acme/billing-bridge", "description": "Handles the infrastructure for the billing
package", "version": "0.0.1", "license": "proprietary", "require": { "php": ">=7.3", "ext-PDO": "*", "ext-json": "*", "acme/billing-package": "*@dev", "symfony/config": ">=2.8,<3.5", "symfony/http-kernel": ">=2.8,<3.5", "symfony/dependency-injection": "<3.5" }, "require-dev": { "phpunit/phpunit": "^8" }, "autoload": { "psr-4": { "Acme\\Bridge\\Billing\\": "./src" } }, "autoload-dev": { "files": [ "tests/AppKernel.php" ] } } composer.json -
{ "name": "acme/billing-bridge", "description": "Handles the infrastructure for the billing
package", "version": "0.0.1", "license": "proprietary", "require": { "php": ">=7.3", "ext-PDO": "*", "ext-json": "*", "acme/billing-package": "*@dev", "symfony/config": ">=2.8,<3.5", "symfony/http-kernel": ">=2.8,<3.5", "symfony/dependency-injection": "<3.5" }, "require-dev": { "phpunit/phpunit": "^8" }, "autoload": { "psr-4": { "Acme\\Bridge\\Billing\\": "./src" } }, "autoload-dev": { "files": [ "tests/AppKernel.php" ] } } composer.json -
src DependencyInjection BillingBridgeExtension.php PaymentProcessor Resources Config services.xml composer.json tests AppKernel.php
<?php namespace Acme\Bridge\Billing\DependencyInjection; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; class Configuration implements ConfigurationInterface { public function getConfigTreeBuilder() { $treeBuilder = new TreeBuilder(); $rootNode = $treeBuilder->root('billing_bridge'); $rootNode ->children() ->arrayNode('services') ->isRequired() ->children() ->scalarNode('pdo_connection') ->isRequired() ->cannotBeEmpty() ->end() ->end() ->end() ->arrayNode('parameters') ->isRequired() ->children() ->scalarNode('payment_provider_key') ->isRequired() ->cannotBeEmpty() ->end() ->end() ->end() ->end(); Configuration.php StripePaymentProcessor.php -
src DependencyInjection BillingBridgeExtension.php PaymentProcessor Resources Config services.xml composer.json tests AppKernel.php
Configuration.php <?php namespace Acme\Bridge\Billing\DependencyInjection; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; class Configuration implements ConfigurationInterface { public function getConfigTreeBuilder() { $treeBuilder = new TreeBuilder(); $rootNode = $treeBuilder->root('billing_bridge'); $rootNode ->children() ->arrayNode('services') ->isRequired() ->children() ->scalarNode('pdo_connection') ->isRequired() ->cannotBeEmpty() ->end() ->end() ->end() ->arrayNode('parameters') ->isRequired() ->children() ->scalarNode('payment_provider_key') ->isRequired() ->cannotBeEmpty() ->end() ->end() ->end() ->end(); StripePaymentProcessor.php -
src DependencyInjection BillingBridgeExtension.php PaymentProcessor Resources Config services.xml composer.json tests AppKernel.php
Configuration.php <?php namespace Acme\Bridge\Billing\DependencyInjection; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; class Configuration implements ConfigurationInterface { public function getConfigTreeBuilder() { $treeBuilder = new TreeBuilder(); $rootNode = $treeBuilder->root('billing_bridge'); $rootNode ->children() ->arrayNode('services') ->isRequired() ->children() ->scalarNode('pdo_connection') ->isRequired() ->cannotBeEmpty() ->end() ->end() ->end() ->arrayNode('parameters') ->isRequired() ->children() ->scalarNode('payment_provider_key') ->isRequired() ->cannotBeEmpty() ->end() ->end() ->end() ->end(); StripePaymentProcessor.php -
<?php namespace Acme\Bridge\Billing\DependencyInjection; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; class Configuration implements
ConfigurationInterface { public function getConfigTreeBuilder() { $treeBuilder = new TreeBuilder(); $rootNode = $treeBuilder->root('billing_bridge'); $rootNode ->children() ->arrayNode('services') ->isRequired() ->children() ->scalarNode('pdo_connection') ->isRequired() ->cannotBeEmpty() ->end() ->end() ->end() ->arrayNode('parameters') ->isRequired() ->children() ->scalarNode('payment_provider_key') ->isRequired() ->cannotBeEmpty() ->end() ->end() ->end() ->end(); src DependencyInjection BillingBridgeExtension.php PaymentProcessor Resources Config services.xml composer.json tests AppKernel.php Configuration.php StripePaymentProcessor.php -
src DependencyInjection BillingBridgeExtension.php PaymentProcessor Resources Config services.xml composer.json tests AppKernel.php
Configuration.php <?php namespace Acme\Bridge\Billing\DependencyInjection; use InvalidArgumentException; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader; use Symfony\Component\HttpKernel\DependencyInjection\Extension; class BillingBridgeExtension extends Extension { public function load(array $configs, ContainerBuilder $container): void { $config = $this->processConfiguration(new Configuration(), $configs); $loader = new Loader\XmlFileLoader( $container, new FileLocator(__DIR__ . '/../Resources/config') ); $container->setAlias( 'billing_bridge.pdo_connection', $config['services']['pdo_connection'] ); $container->setAlias( 'billing_bridge.payment_provider_key', $config['parameters']['payment_provider_key'] ); $loader->load('services.xml'); } } StripePaymentProcessor.php -
src DependencyInjection BillingBridgeExtension.php PaymentProcessor Resources Config services.xml composer.json tests AppKernel.php
Configuration.php <?php namespace Acme\Bridge\Billing\DependencyInjection; use InvalidArgumentException; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader; use Symfony\Component\HttpKernel\DependencyInjection\Extension; class BillingBridgeExtension extends Extension { public function load(array $configs, ContainerBuilder $container): void { $config = $this->processConfiguration(new Configuration(), $configs); $loader = new Loader\XmlFileLoader( $container, new FileLocator(__DIR__ . '/../Resources/config') ); $container->setAlias( 'billing_bridge.pdo_connection', $config['services']['pdo_connection'] ); $container->setAlias( 'billing_bridge.payment_provider_key', $config['parameters']['payment_provider_key'] ); $loader->load('services.xml'); } } StripePaymentProcessor.php -
src DependencyInjection BillingBridgeExtension.php PaymentProcessor Resources Config services.xml composer.json tests AppKernel.php
Configuration.php <?php namespace Acme\Bridge\Billing\DependencyInjection; use InvalidArgumentException; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader; use Symfony\Component\HttpKernel\DependencyInjection\Extension; class BillingBridgeExtension extends Extension { public function load(array $configs, ContainerBuilder $container): void { $config = $this->processConfiguration(new Configuration(), $configs); $loader = new Loader\XmlFileLoader( $container, new FileLocator(__DIR__ . '/../Resources/config') ); $container->setAlias( 'billing_bridge.pdo_connection', $config['services']['pdo_connection'] ); $container->setAlias( 'billing_bridge.payment_provider_key', $config['parameters']['payment_provider_key'] ); $loader->load('services.xml'); } } StripePaymentProcessor.php -
src DependencyInjection BillingBridgeExtension.php PaymentProcessor StripePaymentProcessor.php Resources Config services.xml composer.json tests
AppKernel.php Configuration.php <?php namespace Acme\Bridge\Billing\Adapter; use Acme\Package\Billing\PaymentProcessor; class StripePaymentProcessor implements PaymentProcessor { private $stripeApiKey; public function __construct(string $stripeApiKey) { $this->stripeApiKey = $stripeApiKey; } public function debitAccount(Customer $customer, int $amount): void { \Stripe\Stripe::setApiKey($this->stripeApiKey); \Stripe\Charge::create([ "amount" => $amount, "currency" => "gbp", "source" => $customer->getAuthToken() ]); } public function fetchTransactions(Customer $customer): array { // ... } } -
src DependencyInjection BillingBridgeExtension.php PaymentProcessor StripePaymentProcessor.php Resources Config services.xml composer.json tests
AppKernel.php Configuration.php <?php namespace Acme\Bridge\Billing\Adapter; use Acme\Package\Billing\PaymentProcessor; class StripePaymentProcessor implements PaymentProcessor { private $stripeApiKey; public function __construct(string $stripeApiKey) { $this->stripeApiKey = $stripeApiKey; } public function debitAccount(Customer $customer, int $amount): void { \Stripe\Stripe::setApiKey($this->stripeApiKey); \Stripe\Charge::create([ "amount" => $amount, "currency" => "gbp", "source" => $customer->getAuthToken() ]); } public function fetchTransactions(Customer $customer): array { // ... } } -
src DependencyInjection BillingBridgeExtension.php PaymentProcessor StripePaymentProcessor.php Resources Config services.xml composer.json tests
AppKernel.php Configuration.php <?php namespace Acme\Bridge\Billing\Adapter; use Acme\Package\Billing\PaymentProcessor; class StripePaymentProcessor implements PaymentProcessor { private $stripeApiKey; public function __construct(string $stripeApiKey) { $this->stripeApiKey = $stripeApiKey; } public function debitAccount(Customer $customer, int $amount): void { // ... } public function fetchTransactions(Customer $customer): array { \Stripe\Stripe::setApiKey($this->stripeApiKey); $list = \Stripe\Charge::all([ "customer" => $customer->getAuthToken() ]); return array_map(function ($payment) { return new PaymentDto($payment['name'], $payment['amount']); }, $list); } } -
src DependencyInjection BillingBridgeExtension.php PaymentProcessor StripePaymentProcessor.php Resources Config services.xml composer.json tests
AppKernel.php Configuration.php <?php namespace Acme\Bridge\Billing\Adapter; use Acme\Package\Billing\PaymentProcessor; class StripePaymentProcessor implements PaymentProcessor { private $stripeApiKey; public function __construct(string $stripeApiKey) { $this->stripeApiKey = $stripeApiKey; } public function debitAccount(Customer $customer, int $amount): void { // ... } public function fetchTransactions(Customer $customer): array { \Stripe\Stripe::setApiKey($this->stripeApiKey); $list = \Stripe\Charge::all([ "customer" => $customer->getAuthToken() ]); return array_map(function ($payment) { return new PaymentDto($payment['name'], $payment['amount']); }, $list); } } -
<?php namespace Acme\Bridge\Billing\Repository; use Acme\Package\Billing\CustomerRepository; class PdoCustomerRepository implements CustomerRepository {
private $connection; public function __construct(\PDO $connection) { $this->connection = $connection; } public function find(int $customerId): Customer { $query = $this->connection ->prepare('SELECT name, token FROM customer_accounts WHERE id = :id'); $query->bindValue(':id', $customerId); $result = $query->execute(); $customer = $result->fetchAll(\PDO::FETCH_ASSOC); return new Customer($customer[0]['name'], $customer[0]['token']); } } src DependencyInjection BillingBridgeExtension.php PaymentProcessor StripePaymentProcessor.php Resources Config services.xml tests AppKernel.php Configuration.php Repository PdoCustomerRepository.php -
<?php namespace Acme\Bridge\Billing\Repository; use Acme\Package\Billing\CustomerRepository; class PdoCustomerRepository implements CustomerRepository {
private $connection; public function __construct(\PDO $connection) { $this->connection = $connection; } public function find(int $customerId): Customer { $query = $this->connection ->prepare('SELECT name, token FROM customer_accounts WHERE id = :id'); $query->bindValue(':id', $customerId); $result = $query->execute(); $customer = $result->fetchAll(\PDO::FETCH_ASSOC); return new Customer($customer[0]['name'], $customer[0]['token']); } } src DependencyInjection BillingBridgeExtension.php PaymentProcessor Resources Config services.xml tests AppKernel.php Configuration.php Repository PdoCustomerRepository.php StripePaymentProcessor.php -
<?php namespace Acme\Bridge\Billing\Repository; use Acme\Package\Billing\CustomerRepository; class PdoCustomerRepository implements CustomerRepository {
private $connection; public function __construct(\PDO $connection) { $this->connection = $connection; } public function find(int $customerId): Customer { $query = $this->connection ->prepare('SELECT name, token FROM customer_accounts WHERE id = :id'); $query->bindValue(':id', $customerId); $result = $query->execute(); $customer = $result->fetchAll(\PDO::FETCH_ASSOC); return new Customer($customer[0]['name'], $customer[0]['token']); } } src DependencyInjection BillingBridgeExtension.php PaymentProcessor Resources Config services.xml tests AppKernel.php Configuration.php Repository PdoCustomerRepository.php StripePaymentProcessor.php -
<?xml version="1.0" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony services-1.0.xsd"> <services>
<service id="acme.billing.card_payment" class="Acme\Package\Billing\CardPayment"> <argument type="service" id="billing_bridge.payment_provider" /> <argument type="service" id="billing_bridge.customer_repository" /> </service> <service id="billing_bridge.customer_repository" class="Acme\Bridge\Billing\Repository\PdoCustomerRepository" public="false"> <argument type="service" id="billing_bridge.pdo_connection" /> </service> <service id="billing_bridge.payment_provider" class="Acme\Bridge\Billing\PaymentProcessor\StripePaymentProcessor" public="false"> <argument>%billing_bridge.payment_provider_key%</argument> </service> </services> </container> src DependencyInjection PaymentProcessor Configuration.php BillingBridgeExtension.php Resources Config services.xml tests AppKernel.php Repository PdoCustomerRepository.php StripePaymentProcessor.php -
<?xml version="1.0" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony services-1.0.xsd"> <services>
<service id="acme.billing.card_payment" class="Acme\Package\Billing\CardPayment"> <argument type="service" id="billing_bridge.payment_provider" /> <argument type="service" id="billing_bridge.customer_repository" /> </service> <service id="billing_bridge.customer_repository" class="Acme\Bridge\Billing\Repository\PdoCustomerRepository" public="false"> <argument type="service" id="billing_bridge.pdo_connection" /> </service> <service id="billing_bridge.payment_provider" class="Acme\Bridge\Billing\PaymentProcessor\StripePaymentProcessor" public="false"> <argument>%billing_bridge.payment_provider_key%</argument> </service> </services> </container> src DependencyInjection PaymentProcessor Configuration.php BillingBridgeExtension.php Resources Config services.xml tests AppKernel.php Repository PdoCustomerRepository.php StripePaymentProcessor.php -
<?xml version="1.0" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony services-1.0.xsd"> <services>
<service id="acme.billing.card_payment" class="Acme\Package\Billing\CardPayment"> <argument type="service" id="billing_bridge.payment_provider" /> <argument type="service" id="billing_bridge.customer_repository" /> </service> <service id="billing_bridge.customer_repository" class="Acme\Bridge\Billing\Repository\PdoCustomerRepository" public="false"> <argument type="service" id="billing_bridge.pdo_connection" /> </service> <service id="billing_bridge.payment_provider" class="Acme\Bridge\Billing\PaymentProcessor\StripePaymentProcessor" public="false"> <argument>%billing_bridge.payment_provider_key%</argument> </service> </services> </container> src DependencyInjection PaymentProcessor Configuration.php BillingBridgeExtension.php Resources Config services.xml tests AppKernel.php Repository PdoCustomerRepository.php StripePaymentProcessor.php -
src DependencyInjection PaymentProcessor Configuration.php BillingBridgeExtension.php Resources Config services.xml tests AppKernel.php
Repository PdoCustomerRepository.php StripePaymentProcessor.php <?xml version="1.0" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony services-1.0.xsd"> <services> <service id="acme.billing.card_payment" class="Acme\Package\Billing\CardPayment"> <argument type="service" id="billing_bridge.payment_provider" /> <argument type="service" id="billing_bridge.customer_repository" /> </service> <service id="billing_bridge.customer_repository" class="Acme\Bridge\Billing\Repository\PdoCustomerRepository" public="false"> <argument type="service" id="billing_bridge.pdo_connection" /> </service> <service id="billing_bridge.payment_provider" class="Acme\Bridge\Billing\PaymentProcessor\StripePaymentProcessor" public="false"> <argument>%billing_bridge.payment_provider_key%</argument> </service> </services> </container> -
src DependencyInjection PaymentProcessor Configuration.php BillingBridgeExtension.php Resources Config services.xml tests AppKernel.php
Repository PdoCustomerRepository.php StripePaymentProcessor.php <?xml version="1.0" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony services-1.0.xsd"> <services> <service id="acme.billing.card_payment" class="Acme\Package\Billing\CardPayment"> <argument type="service" id="billing_bridge.payment_provider" /> <argument type="service" id="billing_bridge.customer_repository" /> </service> <service id="billing_bridge.customer_repository" class="Acme\Bridge\Billing\Repository\PdoCustomerRepository" public="false"> <argument type="service" id="billing_bridge.pdo_connection" /> </service> <service id="billing_bridge.payment_provider" class="Acme\Bridge\Billing\PaymentProcessor\StripePaymentProcessor" public="false"> <argument>%billing_bridge.payment_provider_key%</argument> </service> </services> </container> -
app config config.yml src Controller MarketplaceController.php <?php namespace App\Controller; use
Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Response; class MarketplaceController extends Controller { public function chargeCardAction($customerId, $amount): Response { // ... $this->container ->get('acme.billing.card_payment') ->debitAccount($customerId, $amount); } public function listChargesAction($customerId): Response { // ... $charges = $this->container ->get('acme.billing.card_payment') ->listPayments($customerId); return new Response(\json_encode($charges)); } } -
Layer Role Has domain knowledge? Services Dependencies Testing level Package
Business logic Yes Provides services to bridge None / Language level Unit Bridge Wiring & Concretions Minimal Provides / forwards services to application layer Package / Symfony DI Integration Application / FW Talks to the outside world No Uses services provided by bridge layer Bridge End-to-end -
Pros Cons Less boilerplate for creating separate services More boilerplate
for integrating domain code Layers kept isolated & easily testable Scaling not 100% microservice-esque Clear separation of concerns Requires effort in understanding Easily port domain code into applications Not 100% separately deployable Easily switch between monolith or microservice Not truly asynchronous Communication via JSON between services Breaking one thing breaks all things Full log trace from request to response Low latency (method calls vs network calls)