Pro Yearly is on sale from $80 to $50! »

Dependency Injection, Dependency Inversion, and You

0f930e13633535c1c4041e95b8881308?s=47 Jeff Carouth
September 13, 2014

Dependency Injection, Dependency Inversion, and You

Poor application design can be an incredibly frustrating experience. Often we can attribute frustration in software projects to tight coupling and high interdependence between modules within the design. As developers, the Dependency Inversion Principle helps us design maintainable software by showing us the direction dependencies should flow. In this session we will look at the ideas of dependency injection and dependency inversion and how they can make your applications easier to work with for yourself and your colleagues. At the end of this session you will have a grasp on DI and even a taste of using modern PHP dependency injection containers correctly in your applications.

0f930e13633535c1c4041e95b8881308?s=128

Jeff Carouth

September 13, 2014
Tweet

Transcript

  1. PRESENTED BY JEFF CAROUTH @jcarouth Dependency Injection, Dependency Inversion, and

    You
  2. Past experience developing software shows us coupling is bad.

  3. Past experience developing software shows us coupling is bad. But

    it also suggests coupling is unavoidable.
  4. Coupling and dependencies

  5. Dependencies are the other objects, resources, or functions any given

    object uses to accomplish its responsibility.
  6. Common Dependencies

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

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

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

    • Utility objects – e.g., Logger • Environment – e.g., FileSystem, system clock
  10. 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)
  11. 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); } }
  12. Dependency Injection

  13. As the name suggests, this literally means to inject dependencies

    into the objects that need them.
  14. DI Techniques • Constructor Injection • Setter or Property Injection

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

    • “Parameter Injection”
  16. Constructor Injection Dependencies are injected upon instantiation through the constructor.

    class Alpha { } class DependsOnAlpha { public function __construct(Alpha $a) { /*...*/ } }
  17. 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.
  18. 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); } }
  19. 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) { /*...*/ } }
  20. 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.
  21. 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); } } }
  22. Parameter “Injection” Dependencies are passed as parameters to specific methods

    which need them. class Gamma {} class Delta { public function dependsOnGamma(Gamma $c) { /*...*/ } }
  23. Parameter “Injection” Does not clutter your object with refs to

    collaborators which are not needed. Almost everything else about it.
  24. class EmailService { public function send() { /*…*/ } }

    class Order { public function complete(EmailService $mailer) { //... $mailer->send(new OrderCompleteMessage($this)); } }
  25. Dependency Inversion Principle

  26. High level modules should not depend on low level modules.

    Both should depend on abstractions.
  27. OrderProcessor PDOOrderRepository

  28. OrderProcessor OrderRepository <<interface>> PDOOrderRepository

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

    abstractions.
  30. OrderProcessor OrderRepository <<interface>> MySQLOrderRepository InMemoryOrderRepository

  31. Dependency Injection Containers

  32. A dependency injection container is an object used to manage

    instantiation of other objects.
  33. The power of a DiC comes in it’s ability to

    resolve specific dependencies and their dependencies at runtime.
  34. You do not need a DiC to practice dependency injection.

  35. 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
  36. Full DI FizzBuzz

  37. 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}"; } }
  38. 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}"; } }
  39. class Fizz { public function __toString() { return "Fizz"; }

    } class Buzz { public function __toString() { return "Buzz"; } }
  40. $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) { } }
  41. 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
  42. class Fizz { } class Buzz { } class FizzBuzz

    { }
  43. 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); } }
  44. 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(); };
  45. $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(); };
  46. 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
  47. 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; } }
  48. 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']) );
  49. class Database { private $host; public function __construct(array $config) {

    } } class OrderMapper { private $db; public function __construct(Database $db) { } } $di = new Container();
  50. 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']; };
  51. 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); };
  52. 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);
  53. class OrderMapper#12 (1) { private $db => class Database#13 (1)

    { private $host => string(9) "localhost" } } [jcarouth@fizzlabs] $ php ordermapperditwittee.php
  54. 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); };
  55. 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);
  56. class OrderMapper#3 (1) { private $db => class Database#14 (1)

    { private $host => string(9) "localhost" } } [jcarouth@fizzlabs] $ php ordermapperditwittee.php
  57. Aura.di A Dependency Injection Container in a little more than

    a Tweet https://github.com/auraphp/Aura.Di
  58. 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); } }
  59. namespace AwesomeCart; interface OrderRepository { public function save(Order $order); }

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

    class InMemoryOrderRepository implements OrderRepository {} class MySqlOrderRepository implements OrderRepository {} Refactor: Extract Interface
  61. 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); } }
  62. 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); } }
  63. namespace AwesomeCart; use Aura\Di\Container; use Aura\Di\Factory; $di = new Container(new

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

    Factory()); $di->params['AwesomeCart\OrderProcessor']['orderRepository'] = $di->lazyNew('AwesomeCart\InMemoryOrderRepository');
  65. 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') );
  66. 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');
  67. 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.”
  68. <?php // web/index.php require_once __DIR__ . ‘/../vendor/autoload.php'; use MyApp\UserRepository; $app

    = new \Slim\Slim(); $app->get('/', function() { $userRepository = new UserRepository(new PDO(/*…*/)); $users = $userRepository->listAll(); header('Content-Type: application/json'); echo json_encode($users); }); $app->run();
  69. <?php // web/index.php require_once __DIR__ . ‘/../vendor/autoload.php'; use MyApp\UserRepository; $app

    = new \Slim\Slim(); $app->get('/', function() { $userRepository = new UserRepository(new PDO(/*…*/)); $users = $userRepository->listAll(); header('Content-Type: application/json'); echo json_encode($users); }); $app->run();
  70. <?php // config/dev.php use Aura\Di\Container; use Aura\Di\Factory; $container = new

    Container(new Factory()); $container->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;
  71. <?php // config/dev.php use Aura\Di\Container; use Aura\Di\Factory; $container = new

    Container(new Factory()); $container->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;
  72. <?php // config/dev.php use Aura\Di\Container; use Aura\Di\Factory; $container = new

    Container(new Factory()); $container->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;
  73. <?php // web/index.php require_once __DIR__ . ‘/../vendor/autoload.php'; use MyApp\UserRepository; $di

    = require_once __DIR__ . '/../config/dev.php'; $app = new \Slim\Slim(); $app->get('/', function() use($di) { $userRepository = new UserRepository($di->get('database')); $users = $userRepository->listAll(); header('Content-Type: application/json'); echo json_encode($users); }); $app->run();
  74. <?php // web/index.php require_once __DIR__ . ‘/../vendor/autoload.php'; use MyApp\UserRepository; $di

    = require_once __DIR__ . '/../config/dev.php'; $app = new \Slim\Slim(); $app->get('/', function() use($di) { $userRepository = new UserRepository($di->get('database')); $users = $userRepository->listAll(); header('Content-Type: application/json'); echo json_encode($users); }); $app->run();
  75. <?php // web/index.php require_once __DIR__ . ‘/../vendor/autoload.php'; use MyApp\UserRepository; $di

    = require_once __DIR__ . '/../config/dev.php'; $app = new \Slim\Slim(); $app->get('/', function() use($di) { $userRepository = new UserRepository($di->get('database')); $users = $userRepository->listAll(); header('Content-Type: application/json'); echo json_encode($users); }); $app->run();
  76. 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
  77. 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);
  78. Recap

  79. Recap • Dependencies in code are unavoidable, but that doesn’t

    mean they need to be unmanageable.
  80. 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.
  81. 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.
  82. BONUS!

  83. 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.
  84. class GuestRepository { public function __construct() { } public function

    delete(Guest $guest) { /*…*/ } }
  85. 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; } }
  86. 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; } }
  87. $guestRepository = new GuestRepository(); $guest = new Guest(); $guest->id =

    8945221; $guestRepository->delete($guest);
  88. 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); } }
  89. 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; } }
  90. class GuestRepository { private $db; public function __construct(\PDO $db) {

    $this->db = $db; } public function delete(Guest $guest) {} }
  91. 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);
  92. 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; } }
  93. 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... } }
  94. $guestRepository = new GuestRepository(new \PDO('...'), new PurchaseRepository()); $guest = new

    Guest(); $guest->id = 8945221; $guestRepository->delete($guest);
  95. 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; } }
  96. 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; } }
  97. $db = new \PDO('...'); $guestRepository = new GuestRepository($db, new PurchaseRepository($db));

    $guest = new Guest(); $guest->id = 8945221; $guestRepository->delete($guest);
  98. 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); } }
  99. Thank You @jcarouth joind.in/13751