Restless Microservices with Symfony

Da2d2829b89cde136392973a35b68959?s=47 nealio82
September 13, 2019

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.

Da2d2829b89cde136392973a35b68959?s=128

nealio82

September 13, 2019
Tweet

Transcript

  1. 3.
  2. 4.
  3. 5.
  4. 9.
  5. 10.
  6. 11.
  7. 21.
  8. 34.

    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.
  9. 37.

    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
  10. 41.

    Wolff, E (2016): “Microservices”, p57 A failure in the deployment

    of a single Microservice [doesn’t] impact the system as a whole.
  11. 46.

    What problems do they solve? What new problems do they

    introduce? Should I use microservices?
  12. 47.

    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.
  13. 51.

    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.
  14. 54.

    What problems do they solve? What new problems do they

    introduce? Should I use microservices?
  15. 55.

    What problems do they solve? What new problems do they

    introduce? Should I stick to a monolith? Should I use microservices?
  16. 56.
  17. 57.
  18. 58.
  19. 59.
  20. 60.
  21. 61.
  22. 63.

    Search / buy music Upload songs Billing Profile { "id":

    "9541df33-2077-499e-a6f3-579eec03cd0c", "eventType": "invoice", "value": "100" } JSON over HTTP
  23. 64.
  24. 73.
  25. 74.
  26. 75.
  27. 78.
  28. 79.
  29. 80.
  30. 81.
  31. 82.
  32. 83.
  33. 84.
  34. 85.
  35. 87.

    marketplace upload billing profile bin public src config bin public

    src config bin public src config bin public src config templates
  36. 88.

    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
  37. 89.

    marketplace upload billing profile bin public src config bin public

    src config bin public src config bin public src config templates
  38. 93.
  39. 96.
  40. 97.
  41. 98.
  42. 100.
  43. 101.

    { "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
  44. 102.

    { "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
  45. 103.

    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/" } } }
  46. 104.

    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 { // ... }
  47. 105.

    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 { // ... }
  48. 106.

    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 { // ... }
  49. 107.

    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 { // ... }
  50. 108.

    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 { // ... }
  51. 109.

    <?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
  52. 110.

    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; }
  53. 111.

    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 { // ... } // ... }
  54. 112.

    <?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
  55. 113.
  56. 114.
  57. 118.

    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 { // ... } }
  58. 119.

    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 { // ... } }
  59. 120.
  60. 121.

    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 { // ... } }
  61. 125.
  62. 126.

    { "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
  63. 127.

    { "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
  64. 128.

    { "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
  65. 129.

    { "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
  66. 130.

    { "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
  67. 131.

    { "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
  68. 132.

    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
  69. 133.

    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
  70. 134.

    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
  71. 135.

    <?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
  72. 136.

    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
  73. 137.

    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
  74. 138.

    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
  75. 139.

    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 { // ... } }
  76. 140.

    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 { // ... } }
  77. 141.

    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); } }
  78. 142.

    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); } }
  79. 143.

    <?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
  80. 144.

    <?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
  81. 145.

    <?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
  82. 146.

    <?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
  83. 147.

    <?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
  84. 148.

    <?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
  85. 149.

    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>
  86. 150.

    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>
  87. 151.
  88. 154.

    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)); } }
  89. 155.
  90. 156.
  91. 163.
  92. 164.
  93. 166.
  94. 167.
  95. 168.
  96. 169.
  97. 170.
  98. 171.
  99. 172.
  100. 173.
  101. 178.

    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
  102. 179.

    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)
  103. 180.
  104. 181.