Giving up Control

Inversion of Control for Beginners

Matthew Weier O'Phinney (@mwop)

SunshinePHP, February 2015

Analogy:

A Cuppa

Attempt 1:

Make it up as you go

A cuppa


class Cuppa
{
  public function prepare();
}
          

Make some tea appear


class Cuppa
{
  public function prepare()
  {
    $packet = new TeaBag();
  }
}
          

Make some water appear


class Cuppa
{
  public function prepare()
  {
    $packet = new TeaBag();
    $liquid = new HotWater();
    $packet->dissolve($liquid);
  }
}
          

How do we know the water is hot?


class HotWater
{
  private $water;
  private $kettle;

  public function __construct()
  {
    $this->water  = new Water();
    $this->kettle = new Kettle();
  }
}
          

Observations

Efficient, from a code perspective

Hidden, undocumented dependencies

What if we want to change things?

Attempt 2:

Grab as you go

A cuppa


class Cuppa
{
  private $drinkPacket;
  private $hotLiquid;

  public function prepare();
}
          

Procure some tea


class Cuppa
{
  public function prepare()
  {
    $packet = Registry::get('DrinkPacket');
  }
}
          

Prepare some water


class Cuppa
{
  public function prepare()
  {
    $this->packet = Registry::get('DrinkPacket');
    $this->liquid = Registry::get('HotLiquid');
    $this->packet->dissolve($this->liquid);
    return $this;
  }
}
          

Where did the hot water come from?


class HotWater implements HotLiquidInterface
{
  private $water;
  private $kettle;

  public function __construct()
  {
    $this->water  = Registry::get('Water');
    $this->kettle = Registry::get('Kettle');
  }
}
          

Observations

Still efficient, from a code perspective

Still has hidden, undocumented dependencies

Who controls the Registry?

Attempt 3:

Grab bag

A cuppa


class Cuppa
{
  private $drinkPacket;
  private $hotLiquid;

  public function __construct(Bag $bag)
  {
    $this->drinkPacket = $bag->get('DrinkPacket');
    $this->hotLiquid = $bag->get('HotLiquid');
  }

  public function prepare() { /* ... */ }
}
          

Prepare the water


class HotWater implements HotLiquidInterface
{
  private $water;
  private $kettle;

  public function __construct(Bag $bag)
  {
    $this->water  = $bag->get('Water');
    $this->kettle = $bag->get('Kettle');
  }
}
          

Digression

How does the HotWater get the container?


$bag->put('HotWater', function () use ($bag) {
  return new HotWater($bag);
});
      

Observations

We removed global state!

And yet still have hidden, undocumented dependencies

Is the bag really a dependency?

Attempt 4:

Pour it in

A cuppa


class Cuppa
{
  private $drinkPacket;
  private $hotLiquid;

  public function __construct(
    DrinkPacketInterface $drinkPacket,
    HotLiquidInterface $hotLiquid,
  ) {
    $this->drinkPacket = $drinkPacket;
    $this->hotLiquid = $hotLiquid;
  }

  public function prepare() { /* ... */ }
}
          

Prepare the water


class HotWater implements HotLiquidInterface
{
  private $water;
  private $kettle;

  public function __construct(
    Water $water,
    HeatingInterface $heater
  ) {
    $this->water = $water;
    $this->kettle = $heater;
  }
}
          

Digression

How is it all prepared?


$hotWater = new HotWater(
  new Water(),
  new Kettle()
);
$cuppa = new Cuppa(new TeaBag(), $hotWater);
      

Observations

No "middle man" dependencies

Dependencies are documented, and enforced

We can provide substitutions

But we have a new problem...

This looks like work.

Dependency Injection

Dependency Injection is the practice
of coding classes and functions such that they
receive their dependencies as arguments.

Do not fetch or build dependencies;
have others give them.

How?

Constructor Injection


public function __construct(
  DatabaseInterface $db,
  HttpClientInterface $http,
  CacheInterface $cache,
  LogInterface $logger
) {
  /* ... */
}
      

Using Constructor Injection


$ballOfMud = new BallOfMud(
  $database,
  $httpClient,
  $cache,
  $logger
);
      

Setter Injection


public function setDatabase(DatabaseInterface $db) { }
public function setHttpClient(HttpClientInterface $http) { }
public function setCache(CacheInterface $cache) { }
public function setLogger(LogInterface $logger) { }
      

Using Setter Injection


$ballOfMud = (new BallOfMud())
  ->setDatabase($database)
  ->setHttpClient($httpClient)
  ->setCache($cache)
  ->setLogger($logger);
      

Interface Injection


interface EventManagerAwareInterface {
  public function setEventManager(
      EventManagerInterface $events
  );
}

class Foo implements EventManagerAwareInterface {
  public function setEventManager(
      EventManagerInterface $events
  ) {
      $this->events = $events;
  }
}
      

Implementing Interface Injection


if ($instance instanceof EventManagerAwareInterface) {
  $instance->setEventManager($events);
}
      

Total Control


$db      = new Database;
$http    = new HttpClient;
$cache   = new Cache;
$logger  = new Logger;
$awesome = new Awesome($db, $http, $cache, $Logger);
      

Losing Control

Our scenario

A normal web workflow:

  • Setup Request object
  • Setup Routing object
  • Pass Request to Routing
  • Based on Routing results, dispatch something
  • Return a Response

Old Workflow

Outside in:

  • We instantiate the controller,
  • which fetches the model,
  • which fetches the database.

Old Workflow, Example 1


class Controller
{
  public function action()
  {
    $model = new Model();
    $model->execute();
  }
}

class Model
{
  public function execute()
  {
    $db = new Db();
    $db->query();
  }
}
      

Old Workflow, Example 2


class Controller
{
  public function action()
  {
    $model = Registry::get('Model');
    $model->execute();
  }
}

class Model
{
  public function execute()
  {
    $db = Registry::get('Db');
    $db->query();
  }
}
      

Old Workflow, Example 3


class Controller implements ContainerAwareInterface {
  private $container;
  public function setContainer($container) { /* ... */ }
  public function action() {
    $model = $this->container->get('Model');
    $model->execute();
  }
}

class Model implements ContainerAwareInterface {
  private $container;
  public function setContainer($container) { /* ... */ }
  public function execute() {
    $db = $this->container->get('Db');
    $db->query();
  }
}
      

Desired Workflow

  • We inject our controller
  • with our model
  • which we inject with our database
  • all by asking for the controller.

Controller


class Controller
{
  private $model;

  public function __construct(
    ModelInterface $model
  ) {
    $this->model = $model;
  }

  public function action()
  {
    $this->model->execute();
  }
}
          

Model


class Model
{
  private $db;

  public function __construct(
    DatabaseInterface $db
  ) {
    $this->db = $db;
  }

  public function execute()
  {
    $this->db->query();
  }
}
          

We have to delegate instantiation.

Inversion of Control

Inversion of Control is the practice of
delegating instantiation
of your objects to another process.

Create a container


$container->set('Controller', function ($container) {
  return new Controller($container->get('ModelInterface'));
});
$container->set('ModelInterface', function ($container) {
  return new Model($container->get('DatabaseInterface'));
});
$container->set('DatabaseInterface', function ($container) {
  return new Database;
});

$controller = $container->get('Controller');
      

IoC means we delegate instantiation,
and thus dependency resolution,
to the container.

Types of IoC

Dependency Injection Containers

Automated, using one or more of...

  • declared typehints
  • annotations
  • explicit map of types/implementations

Using a DI container


$controller = $container->get('Controller');
/* And it magically identifies your dependencies
   and injects them for you. And any of their
   dependencies. */
      

DI Container Examples

Factory-based Containers

Maps types to factories that can create the implementation

Using a factory container


$container->set('Controller', function ($container) {
  return new Controller($container->get('ModelInterface'));
});

// which needs:
$container->set('ModelInterface', function ($container) {
  return new Model($container->get('DatabaseInterface'));
});

// which needs:
$container->set('DatabaseInterface', function ($container) {
  return new Database();
});

// Which means this now works:
$controller = $container->get('Controller');
      

Factory Container Examples

  • Aura.Di
  • Pimple
  • Symfony\Component\DependencyInjection
  • Zend\ServiceManager

Benefits of IoC

Testing

  • Explicit dependencies => substitions => mocking!
  • No need for a container or registry!

Alternative implementations

  • Typehint against interface => swap implementations
  • Use interface as container key => swap implementations

Automation

  • Interface injection

Control

  • Define exactly how an object should be created.
  • In exactly one place.
You're not giving up control
you're taking control.

Resources

You probably already use IoC

if you use...

  • Aura.Di
  • Pimple
  • Symfony (DependencyInjection component)
  • Zend Framework (Zend\Di, Zend\ServiceManager)

Thank You!

Matthew Weier O'Phinney (@mwop)

https://joind.in/13442