Slide 1

Slide 1 text

PRESENTED BY JEFF CAROUTH @jcarouth Dependency Injection, Dependency Inversion, and You

Slide 2

Slide 2 text

Past experience developing software shows us coupling is bad.

Slide 3

Slide 3 text

Past experience developing software shows us coupling is bad. But it also suggests coupling is unavoidable.

Slide 4

Slide 4 text

Coupling and dependencies

Slide 5

Slide 5 text

Dependencies are the other objects, resources, or functions any given object uses to accomplish its responsibility.

Slide 6

Slide 6 text

Common Dependencies

Slide 7

Slide 7 text

Common Dependencies • Infrastructure – e.g., Database, third party module

Slide 8

Slide 8 text

Common Dependencies • Infrastructure – e.g., Database, third party module • Utility objects – e.g., Logger

Slide 9

Slide 9 text

Common Dependencies • Infrastructure – e.g., Database, third party module • Utility objects – e.g., Logger • Environment – e.g., FileSystem, system clock

Slide 10

Slide 10 text

Common Dependencies • Infrastructure – e.g., Database, third party module • Utility objects – e.g., Logger • Environment – e.g., FileSystem, system clock • Static Method calls – e.g., DataAccess::saveUser($u)

Slide 11

Slide 11 text

class OrderProcessor { public function __construct() { $this->orderRepository = new MysqlOrderRepository(); $this->logger = Logger::getInstance(); } public function completeOrder($order) { $order->complete = true; $this->logger->log("Order {$order->id} marked as complete"); $this->orderRepository->save($order); Mailer::sendOrderCompleteEmail($order); } }

Slide 12

Slide 12 text

Dependency Injection

Slide 13

Slide 13 text

As the name suggests, this literally means to inject dependencies into the objects that need them.

Slide 14

Slide 14 text

DI Techniques • Constructor Injection • Setter or Property Injection • “Parameter Injection”

Slide 15

Slide 15 text

DI Techniques • Constructor Injection • Setter or Property Injection • “Parameter Injection”

Slide 16

Slide 16 text

Constructor Injection Dependencies are injected upon instantiation through the constructor. class Alpha { } class DependsOnAlpha { public function __construct(Alpha $a) { /*...*/ } }

Slide 17

Slide 17 text

Constructor Injection Makes dependencies explicit. You cannot modify the dependencies after instantiation. Discourages violation of the Single Responsibility Principle. You might end up with many constructor params. You could have dependencies stored in the object’s state which are not needed for all methods.

Slide 18

Slide 18 text

class Database { public function where($column, $value) { $query = "..."; return $this->connection->fetchAll($query); } } class UserService { public function __construct(Database $db) { $this->db = $db; } public function findByEmail($email) { $data = $this->db->where('email', $email); return new User($data); } }

Slide 19

Slide 19 text

Setter Injection Dependencies are injected through a setter (or using a public property if you like to be wild.) class Beta { } class DependsOnBeta { public function setBeta(Beta $b) { /*...*/ } }

Slide 20

Slide 20 text

Setter Injection Works well with optional dependencies for a class. Works well with optional dependencies for a class. Flexible across the object’s lifecycle. Can leads to invalid state because setters must be called in certain order.

Slide 21

Slide 21 text

class PaymentGateway { public function process(Payment $payment) { /*…*/ } } class OrderService { private $paymentGateway; public function setPaymentGateway(PaymentGateway $gateway) { $this->paymentGateway = $gateway; } public function charge(Order $order) { foreach ($order->getPayments() as $payment) { $this->paymentGateway->process($payment); } } }

Slide 22

Slide 22 text

Parameter “Injection” Dependencies are passed as parameters to specific methods which need them. class Gamma {} class Delta { public function dependsOnGamma(Gamma $c) { /*...*/ } }

Slide 23

Slide 23 text

Parameter “Injection” Does not clutter your object with refs to collaborators which are not needed. Almost everything else about it.

Slide 24

Slide 24 text

class EmailService { public function send() { /*…*/ } } class Order { public function complete(EmailService $mailer) { //... $mailer->send(new OrderCompleteMessage($this)); } }

Slide 25

Slide 25 text

Dependency Inversion Principle

Slide 26

Slide 26 text

High level modules should not depend on low level modules. Both should depend on abstractions.

Slide 27

Slide 27 text

OrderProcessor PDOOrderRepository

Slide 28

Slide 28 text

OrderProcessor OrderRepository <> PDOOrderRepository

Slide 29

Slide 29 text

Abstractions should not depend on details. Details should depend on abstractions.

Slide 30

Slide 30 text

OrderProcessor OrderRepository <> MySQLOrderRepository InMemoryOrderRepository

Slide 31

Slide 31 text

Dependency Injection Containers

Slide 32

Slide 32 text

A dependency injection container is an object used to manage instantiation of other objects.

Slide 33

Slide 33 text

The power of a DiC comes in it’s ability to resolve specific dependencies and their dependencies at runtime.

Slide 34

Slide 34 text

You do not need a DiC to practice dependency injection.

Slide 35

Slide 35 text

Twittee class Container { protected $s=array(); function __set($k, $c) { $this->s[$k]=$c; } function __get($k) { return $this->s[$k]($this); } } A Dependency Injection Container in a Tweet http://twittee.org

Slide 36

Slide 36 text

Full DI FizzBuzz

Slide 37

Slide 37 text

class FizzBuzz { public function __construct(Fizz $fizz, Buzz $buzz) { $this->fizz = $fizz; $this->buzz = $buzz; } public function of($number) { if ($number % 3 == 0 && $number % 5 == 0) { return "{$this->fizz}{$this->buzz}"; } else if ($number % 3 == 0) { return "{$this->fizz}"; } else if ($number % 5 == 0) { return "{$this->buzz}"; } return "{$number}"; } }

Slide 38

Slide 38 text

class FizzBuzz { public function __construct(Fizz $fizz, Buzz $buzz) { $this->fizz = $fizz; $this->buzz = $buzz; } public function of($number) { if ($number % 3 == 0 && $number % 5 == 0) { return "{$this->fizz}{$this->buzz}"; } else if ($number % 3 == 0) { return "{$this->fizz}"; } else if ($number % 5 == 0) { return "{$this->buzz}"; } return "{$number}"; } }

Slide 39

Slide 39 text

class Fizz { public function __toString() { return "Fizz"; } } class Buzz { public function __toString() { return "Buzz"; } }

Slide 40

Slide 40 text

$fizzbuzz = new FizzBuzz(new Fizz(), new Buzz()); for ($n = 1; $n <= 20; $n++) { print "FizzBuzz of {$n} is {$fizzbuzz->of($n)}" . PHP_EOL; } class Fizz { } class Buzz { } class FizzBuzz { public function __construct(Fizz $fizz, Buzz $buzz) { } public function of($number) { } }

Slide 41

Slide 41 text

FizzBuzz of 1 is 1 FizzBuzz of 2 is 2 FizzBuzz of 3 is Fizz FizzBuzz of 4 is 4 FizzBuzz of 5 is Buzz FizzBuzz of 6 is Fizz FizzBuzz of 7 is 7 FizzBuzz of 8 is 8 FizzBuzz of 9 is Fizz FizzBuzz of 10 is Buzz FizzBuzz of 11 is 11 FizzBuzz of 12 is Fizz FizzBuzz of 13 is 13 FizzBuzz of 14 is 14 FizzBuzz of 15 is FizzBuzz FizzBuzz of 16 is 16 FizzBuzz of 17 is 17 FizzBuzz of 18 is Fizz FizzBuzz of 19 is 19 FizzBuzz of 20 is Buzz [jcarouth@fizzlabs] $ php fizzbuzz.php

Slide 42

Slide 42 text

class Fizz { } class Buzz { } class FizzBuzz { }

Slide 43

Slide 43 text

class Fizz { } class Buzz { } class FizzBuzz { } class Container { protected $s=array(); function __set($k, $c) { $this->s[$k]=$c; } function __get($k) { return $this->s[$k]($this); } }

Slide 44

Slide 44 text

class Fizz { } class Buzz { } class FizzBuzz { } class Container { protected $s=array(); function __set($k, $c) { $this->s[$k]=$c; } function __get($k) { return $this->s[$k]($this); } } $container = new Container(); $container->fizz = function() { return new Fizz(); }; $container->buzz = function() { return new Buzz(); };

Slide 45

Slide 45 text

$fizzbuzz = new FizzBuzz($container->fizz, $container->buzz); for ($n = 1; $n <= 20; $n++) { print "FizzBuzz of {$n} is {$fizzbuzz->of($n)}" . PHP_EOL; } class Fizz { } class Buzz { } class FizzBuzz { } class Container { protected $s=array(); function __set($k, $c) { $this->s[$k]=$c; } function __get($k) { return $this->s[$k]($this); } } $container = new Container(); $container->fizz = function() { return new Fizz(); }; $container->buzz = function() { return new Buzz(); };

Slide 46

Slide 46 text

FizzBuzz of 1 is 1 FizzBuzz of 2 is 2 FizzBuzz of 3 is Fizz FizzBuzz of 4 is 4 FizzBuzz of 5 is Buzz FizzBuzz of 6 is Fizz FizzBuzz of 7 is 7 FizzBuzz of 8 is 8 FizzBuzz of 9 is Fizz FizzBuzz of 10 is Buzz FizzBuzz of 11 is 11 FizzBuzz of 12 is Fizz FizzBuzz of 13 is 13 FizzBuzz of 14 is 14 FizzBuzz of 15 is FizzBuzz FizzBuzz of 16 is 16 FizzBuzz of 17 is 17 FizzBuzz of 18 is Fizz FizzBuzz of 19 is 19 FizzBuzz of 20 is Buzz [jcarouth@fizzlabs] $ php fizzbuzz.php

Slide 47

Slide 47 text

class Database { private $host; public function __construct(array $config) { $this->host = $config['host']; } } class OrderMapper { private $db; public function __construct(Database $db) { $this->db = $db; } }

Slide 48

Slide 48 text

class Database { private $host; public function __construct(array $config) { } } class OrderMapper { private $db; public function __construct(Database $db) { } } $orderMapper = new OrderMapper( new Database(['host' => ‘localhost']) );

Slide 49

Slide 49 text

class Database { private $host; public function __construct(array $config) { } } class OrderMapper { private $db; public function __construct(Database $db) { } } $di = new Container();

Slide 50

Slide 50 text

class Database { private $host; public function __construct(array $config) { } } class OrderMapper { private $db; public function __construct(Database $db) { } } $di = new Container(); $di->dbConfig = function() { return ['host' => 'localhost']; };

Slide 51

Slide 51 text

class Database { private $host; public function __construct(array $config) { } } class OrderMapper { private $db; public function __construct(Database $db) { } } $di = new Container(); $di->dbConfig = function() { return ['host' => 'localhost']; }; $di->database = function($c) { return new Database($c->dbConfig); };

Slide 52

Slide 52 text

class Database { private $host; public function __construct(array $config) { } } class OrderMapper { private $db; public function __construct(Database $db) { } } $di = new Container(); $di->dbConfig = function() { return ['host' => 'localhost']; }; $di->database = function($c) { return new Database($c->dbConfig); }; $orderMapper = new OrderMapper($di->database); var_dump($orderMapper);

Slide 53

Slide 53 text

class OrderMapper#12 (1) { private $db => class Database#13 (1) { private $host => string(9) "localhost" } } [jcarouth@fizzlabs] $ php ordermapperditwittee.php

Slide 54

Slide 54 text

class Database { private $host; public function __construct(array $config) { } } class OrderMapper { private $db; public function __construct(Database $db) { } } $di = new Container(); $di->dbConfig = function() { return ['host' => 'localhost']; }; $di->database = function($c) { return new Database($c->dbConfig); };

Slide 55

Slide 55 text

class Database { private $host; public function __construct(array $config) { } } class OrderMapper { private $db; public function __construct(Database $db) { } } $di = new Container(); $di->dbConfig = function() { return ['host' => 'localhost']; }; $di->database = function($c) { return new Database($c->dbConfig); }; $di->orderMapper = function($c) { return new OrderMapper($c->database); }; var_dump($di->orderMapper);

Slide 56

Slide 56 text

class OrderMapper#3 (1) { private $db => class Database#14 (1) { private $host => string(9) "localhost" } } [jcarouth@fizzlabs] $ php ordermapperditwittee.php

Slide 57

Slide 57 text

Aura.di A Dependency Injection Container in a little more than a Tweet https://github.com/auraphp/Aura.Di

Slide 58

Slide 58 text

class OrderProcessor { public function __construct() { $this->orderRepository = new MySqlOrderRepository(); $this->logger = Logger::getInstance(); } public function completeOrder($order) { $order->complete = true; $this->logger->log("Order {$order->id} marked as complete"); $this->orderRepository->save($order); Mailer::sendOrderCompleteEmail($order); } }

Slide 59

Slide 59 text

namespace AwesomeCart; interface OrderRepository { public function save(Order $order); } class InMemoryOrderRepository implements OrderRepository {} Refactor: Extract Interface

Slide 60

Slide 60 text

namespace AwesomeCart; interface OrderRepository { public function save(Order $order); } class InMemoryOrderRepository implements OrderRepository {} class MySqlOrderRepository implements OrderRepository {} Refactor: Extract Interface

Slide 61

Slide 61 text

namespace AwesomeCart; class OrderProcessor { public function __construct(OrderRepository $orderRepository) { $this->orderRepository = $orderRepository; $this->logger = Logger::getInstance(); } public function completeOrder($order) { $order->complete = true; $this->logger->log("Order {$order->id} marked as complete"); $this->orderRepository->save($order); Mailer::sendOrderCompleteEmail($order); } }

Slide 62

Slide 62 text

namespace AwesomeCart; class OrderProcessor { public function __construct(OrderRepository $orderRepository) { $this->orderRepository = $orderRepository; $this->logger = Logger::getInstance(); } public function completeOrder($order) { $order->complete = true; $this->logger->log("Order {$order->id} marked as complete"); $this->orderRepository->save($order); Mailer::sendOrderCompleteEmail($order); } }

Slide 63

Slide 63 text

namespace AwesomeCart; use Aura\Di\Container; use Aura\Di\Factory; $di = new Container(new Factory());

Slide 64

Slide 64 text

namespace AwesomeCart; use Aura\Di\Container; use Aura\Di\Factory; $di = new Container(new Factory()); $di->params['AwesomeCart\OrderProcessor']['orderRepository'] = $di->lazyNew('AwesomeCart\InMemoryOrderRepository');

Slide 65

Slide 65 text

namespace AwesomeCart; use Aura\Di\Container; use Aura\Di\Factory; $di = new Container(new Factory()); $di->params['AwesomeCart\OrderProcessor']['orderRepository'] = $di->lazyNew('AwesomeCard\InMemoryOrderRepository'); $di->set( 'order_processor', $di->lazyNew(‘AwesomeCart\OrderProcessor') );

Slide 66

Slide 66 text

namespace AwesomeCart; use Aura\Di\Container; use Aura\Di\Factory; $di = new Container(new Factory()); $di->params['AwesomeCart\OrderProcessor']['orderRepository'] = $di->lazyNew('AwesomeCard\InMemoryOrderRepository'); $di->set( 'order_processor', $di->lazyNew(‘AwesomeCart\OrderProcessor') ); $orderProcessor = $di->get('order_processor');

Slide 67

Slide 67 text

Using a Container In a typical application you will use the container from within your “controllers” and use them to inject dependencies into your “models.”

Slide 68

Slide 68 text

get('/', function() { $userRepository = new UserRepository(new PDO(/*…*/)); $users = $userRepository->listAll(); header('Content-Type: application/json'); echo json_encode($users); }); $app->run();

Slide 69

Slide 69 text

get('/', function() { $userRepository = new UserRepository(new PDO(/*…*/)); $users = $userRepository->listAll(); header('Content-Type: application/json'); echo json_encode($users); }); $app->run();

Slide 70

Slide 70 text

params['PDO'] = [ 'dsn' => 'mysql:dbname=my_app_dev;host=127.0.0.1', 'username' => 'my_app_user', 'passwd' => 'super_secret_passphrase', ]; $container->set('database', $container->lazyNew('PDO')); return $container;

Slide 71

Slide 71 text

params['PDO'] = [ 'dsn' => 'mysql:dbname=my_app_dev;host=127.0.0.1', 'username' => 'my_app_user', 'passwd' => 'super_secret_passphrase', ]; $container->set('database', $container->lazyNew('PDO')); return $container;

Slide 72

Slide 72 text

params['PDO'] = [ 'dsn' => 'mysql:dbname=my_app_dev;host=127.0.0.1', 'username' => 'my_app_user', 'passwd' => 'super_secret_passphrase', ]; $container->set('database', $container->lazyNew('PDO')); return $container;

Slide 73

Slide 73 text

get('/', function() use($di) { $userRepository = new UserRepository($di->get('database')); $users = $userRepository->listAll(); header('Content-Type: application/json'); echo json_encode($users); }); $app->run();

Slide 74

Slide 74 text

get('/', function() use($di) { $userRepository = new UserRepository($di->get('database')); $users = $userRepository->listAll(); header('Content-Type: application/json'); echo json_encode($users); }); $app->run();

Slide 75

Slide 75 text

get('/', function() use($di) { $userRepository = new UserRepository($di->get('database')); $users = $userRepository->listAll(); header('Content-Type: application/json'); echo json_encode($users); }); $app->run();

Slide 76

Slide 76 text

Beware Service Location “…please note that this package is intended for use as a dependency injection system, not as a service locator system. If you use it as a service locator, that's bad, and you should feel bad.” – Aura.DI Readme

Slide 77

Slide 77 text

class Foo { } class Bar { protected $foo; public function __construct(Container $c) { $this->foo = $c->foo; } } $c = new Container(); $c->foo = function() { return new Foo(); }; $bar = new Bar($c);

Slide 78

Slide 78 text

Recap

Slide 79

Slide 79 text

Recap • Dependencies in code are unavoidable, but that doesn’t mean they need to be unmanageable.

Slide 80

Slide 80 text

Recap • Dependencies in code are unavoidable, but that doesn’t mean they need to be unmanageable. • Inverting dependencies is a way to create more flexible software.

Slide 81

Slide 81 text

Recap • Dependencies in code are unavoidable, but that doesn’t mean they need to be unmanageable. • Inverting dependencies is a way to create more flexible software. • DI containers are a helpful tool for maintaining single responsibility within objects.

Slide 82

Slide 82 text

BONUS!

Slide 83

Slide 83 text

A testing Example This example shows why having dependencies hidden becomes problematic when using the code. To illustrate that point we will test a simple piece of functionality.

Slide 84

Slide 84 text

class GuestRepository { public function __construct() { } public function delete(Guest $guest) { /*…*/ } }

Slide 85

Slide 85 text

class GuestRepository { public function __construct() { $this->db = new \PDO('...'); } public function delete(Guest $guest) { $purchaseRepository = new PurchaseRepository(); $purchasesAssignedToGuest = $purchaseRepository->findByGuest($guest); if (count($purchasesAssignedToGuest) > 0) { throw new \Exception('Cannot delete Guest since it is assigned to purchases'); } $deleteQuery = "DELETE FROM guest WHERE id = :id"; $statement = $this->db->prepare($deleteQuery); $statement->execute(array('id' => $guest->id)); return $statement->rowCount() < 1; } }

Slide 86

Slide 86 text

class PurchaseRepository { public function __construct() { $this->db = new \PDO('...'); } public function findByGuest($guest) { $query = 'SELECT * FROM purchases WHERE assigned_to = :guest_id'; $statement = $this->db->prepare($query); $statement->execute(); $purchases = $statement->fetchAll(); $purchaseCollection = array(); foreach ($purchases $result) { $purchase = new Purchase(); $purchase = $this->mapDatabaseResultToPurchase($purchase, $result); $purchaseCollection[] = $purchase; } return $purchaseCollection; } }

Slide 87

Slide 87 text

$guestRepository = new GuestRepository(); $guest = new Guest(); $guest->id = 8945221; $guestRepository->delete($guest);

Slide 88

Slide 88 text

class GuestRepositoryTest extends \PHPUnit_Framework_TestCase { public function testCannotDeleteGuestWhenAssignedToPurchases() { $sql = "INSERT INTO guests VALUES(1, 'Test User')"; $this->db->prepare($sql)->execute(); $sql = "INSERT INTO purchases VALUES(1, 1), (2, 1)"; $this->db->prepare($sql)->execute(); $guestToDelete = new Guest(); $guestToDelete->id = 1; $repository = new GuestRepository(); $this->setExpectedException('\Exception'); $repository->delete($guestToDelete); } }

Slide 89

Slide 89 text

class GuestRepository { public function __construct() { $this->db = new \PDO('...'); } public function delete(Guest $guest) { $purchaseRepository = new PurchaseRepository(); $purchasesAssignedToGuest = $purhaseRepository->findByGuest($guest); if (count($purchasesAssignedToGuest) > 0) { throw new \Exception('Cannot delete Guest since it is assigned to purchases'); } $deleteQuery = "DELETE FROM guest WHERE id = :id"; $statement = $this->db->prepare($deleteQuery); $statement->execute(array('id' => $guest->id)); return $statement->rowCount() < 1; } }

Slide 90

Slide 90 text

class GuestRepository { private $db; public function __construct(\PDO $db) { $this->db = $db; } public function delete(Guest $guest) {} }

Slide 91

Slide 91 text

class GuestRepository { private $db; public function __construct(\PDO $db) { $this->db = $db; } public function delete(Guest $guest) {} } $guestRepository = new GuestRepository(new \PDO('...')); $guest = new Guest(); $guest->id = 8945221; $guestRepository->delete($guest);

Slide 92

Slide 92 text

class GuestRepository { public function __construct(\PDO $db) { $this->db = $db; } public function delete(Guest $guest) { $purchaseRepository = new PurchaseRepository(); $purchasesAssignedToGuest = $purchaseRepository->findByGuest($guest); if (count($purchasesAssignedToGuest) > 0) { throw new \Exception('Cannot delete Guest since it is assigned to purchases'); } $deleteQuery = "DELETE FROM guest WHERE id = :id"; $statement = $this->db->prepare($deleteQuery); $statement->execute(array('id' => $guest->id)); return $statement->rowCount() < 1; } }

Slide 93

Slide 93 text

class GuestRepository { private $db; private $purchaseRepository; public function __construct(\PDO $db, PurchaseRepository $purchaseRepository) { $this->db = $db; $this->purchaseRepository = $purchaseRepository; } public function delete(Guest $guest) { $purchasesAssignedToGuest = $this->purchaseRepository->findByGuest($guest); if (count($purchasesAssignedToGuest) > 0) { throw new \Exception('Cannot delete Guest since it is assigned to purchases'); } //...snip... } }

Slide 94

Slide 94 text

$guestRepository = new GuestRepository(new \PDO('...'), new PurchaseRepository()); $guest = new Guest(); $guest->id = 8945221; $guestRepository->delete($guest);

Slide 95

Slide 95 text

class PurchaseRepository { public function __construct() { $this->db = new \PDO('...'); } public function findByGuest($guest) { $query = 'SELECT * FROM purchases WHERE assigned_to = :guest_id'; $statement = $this->db->prepare($query); $statement->execute(); $purchases = $statement->fetchAll(); $purchaseCollection = array(); foreach ($purchases $result) { $purchase = new Purchase(); $purchase = $this->mapDatabaseResultToPurchase($purchase, $result); $purchaseCollection[] = $purchase; } return $purchaseCollection; } }

Slide 96

Slide 96 text

class PurchaseRepository { public function __construct(\PDO $db) { $this->db = $db; } public function findByGuest($guest) { $query = 'SELECT * FROM purchases WHERE assigned_to = :guest_id'; $statement = $this->db->prepare($query); $statement->execute(); $purchases = $statement->fetchAll(); $purchaseCollection = array(); foreach ($purchases $result) { $purchase = new Purchase(); $purchase = $this->mapDatabaseResultToPurchase($purchase, $result); $purchaseCollection[] = $purchase; } return $purchaseCollection; } }

Slide 97

Slide 97 text

$db = new \PDO('...'); $guestRepository = new GuestRepository($db, new PurchaseRepository($db)); $guest = new Guest(); $guest->id = 8945221; $guestRepository->delete($guest);

Slide 98

Slide 98 text

class GuestRepositoryTest extends \PHPUnit_Framework_TestCase { public function testCannotDeleteGuestWhenAssignedToPurchases() { $sql = "INSERT INTO guests VALUES(1, 'Test User')"; $this->db->prepare($sql)->execute(); $purchaseRepository = $this->getMockBuilder('PurchaseRepository') ->disableOriginalConstructor() ->getMock(); $purchaseRepository->expects($this->once()) ->method('') ->will(This->returnValue(array($this->getMock('Purchase')))); $guestToDelete = new Guest(); $guestToDelete->id = 1; $repository = new GuestRepository($this->db, $purchaseRepository); $this->setExpectedException('\Exception'); $repository->delete($guestToDelete); } }

Slide 99

Slide 99 text

Thank You @jcarouth joind.in/13751