Buy

Container: Force Single Objects, Celebrate

Home stretch! Our goal is to make Container responsible for creating every service object: like PDO, but also ShipLoader and BattleManager.

Guaranteeing only One PDO Object

Here's our issue: if we called $container->getPDO() twice on the same request, we'd still end up with multiple PDO objects, and so, multiple database connections. Ok, if we're careful, we can avoid this. We can do better: let's guarantee that only one PDO object is ever created.

We did this before in ShipLoader. Create a private $pdo property at the top of Container. In getPDO(), add an if statement to see if the property is null. If it is, create the new PDO() object and set it on the property. Return $this->pdo at the bottom:

32 lines lib/Container.php
... lines 1 - 2
class Container
{
... lines 5 - 6
private $pdo;
... lines 8 - 16
public function getPDO()
{
if ($this->pdo === null) {
$this->pdo = new PDO(
$this->configuration['db_dsn'],
$this->configuration['db_user'],
$this->configuration['db_pass']
);
... lines 25 - 26
}
... line 28
return $this->pdo;
}
... lines 31 - 32

Again, the first time we call this: the pdo property is null, so we create the object and set the property. The second, third and fourth time we call this, the object is already there, so we just return it.

Oh, and while I'm here, I'll paste back one line I lost on accident earlier:

32 lines lib/Container.php
... lines 1 - 18
if ($this->pdo === null) {
$this->pdo = new PDO(
$this->configuration['db_dsn'],
$this->configuration['db_user'],
$this->configuration['db_pass']
);
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
... lines 28 - 32

This just sets up PDO to throw nice exceptions if something goes wrong so I can see them.

Move ShipLoader to the Container

Keep going! We don't want to instantiate a ShipLoader object manually in battle.php and index.php. Let's just do it inside Container.

Follow the same pattern: create a private property called $shipLoader, and a public function getShipLoader():

46 lines lib/Container.php
... lines 1 - 2
class Container
{
... lines 5 - 8
private $shipLoader;
... lines 10 - 36
public function getShipLoader()
{
... lines 39 - 43
}
}

In here, add the same if statement: if ($this->shipLoader === null), then $this->shipLoader = new ShipLoader(). Remember, it has a required argument for the PDO object. That's easy, just say $this->getPDO(). At the bottom return $this->shipLoader and add the PHPDoc above it:

46 lines lib/Container.php
... lines 1 - 2
class Container
{
... lines 5 - 8
private $shipLoader;
... lines 10 - 33
/**
* @return ShipLoader
*/
public function getShipLoader()
{
if ($this->shipLoader === null) {
$this->shipLoader = new ShipLoader($this->getPDO());
}
return $this->shipLoader;
}
}

Use it! In index.php, say $shipLoader = $container->getShipLoader(). And I have a bonus for you! We don't need the $pdo variable anymore - we only did that to pass it to ShipLoader. Simplify!

121 lines index.php
... lines 1 - 3
$container = new Container($configuration);
$shipLoader = $container->getShipLoader();
... lines 7 - 121

Copy the new $shipLoader line and repeat this in battle.php:

108 lines battle.php
... lines 1 - 3
$container = new Container($configuration);
$shipLoader = $container->getShipLoader();
... lines 7 - 108

Ok, make sure this is all working. Refresh! Somebody make a sad trombone noise:

Call to a member function getShips() on a non-object index.php line 6.

Ok, trusty debugging cap back on. On line 6, we're calling getShips() on the $shipLoader, which is apparently null. So $container->getShipLoader() must not be returning the object for some reason. How rude.

Oh, and the problem is me! I added an extra ! in my if statement so that it never got inside. Lame. Make sure your's looks like mine does now:

46 lines lib/Container.php
... lines 1 - 2
class Container
{
... lines 5 - 8
private $shipLoader;
... lines 10 - 33
/**
* @return ShipLoader
*/
public function getShipLoader()
{
if ($this->shipLoader === null) {
$this->shipLoader = new ShipLoader($this->getPDO());
}
return $this->shipLoader;
}
}

Ok, now it works.

Move BattleManager to the Container

Only one more service to go! In battle.php, we create the BattleManager. Let's move it! Add the private $battleManager property and then the public function getBattleManager(). Copy the ship loader code to save time... and so I don't mess up again. Update it for battleManager: $this->battleManager = new BattleManager(). And return $this->battleManager:

60 lines lib/Container.php
... lines 1 - 2
class Container
{
... lines 5 - 10
private $battleManager;
... lines 12 - 47
/**
* @return BattleManager
*/
public function getBattleManager()
{
if ($this->battleManager === null) {
$this->battleManager = new BattleManager();
}
return $this->battleManager;
}
}

Go use it in battle.php: $battleManager = $container->getBattleManager():

109 lines battle.php
... lines 1 - 26
$battleManager = $container->getBattleManager();
... lines 28 - 109

Ok, let's try the whole thing! Start a battle... and Engage. Ok, the bad guys won, but our app still works. And the code behind it is so much more awesome.

Leave a comment!

  • 2016-08-09 Victor Bocharsky

    Hey Aistis,

    1. Yes, we need to be able to call all our services from the container but should worry about to instantiate them only once. We don't want to have many objects of some loader class. First of all, to have multiple objects of one class is more expensive for PHP and has performance impact. And we also can have some configuration for service, why we need to worry about to apply this configuration to the all instantiated services? So a service container is solving these things: it creates only one instance per service no matter how many time I call this service from container (if I call it 0 times - it even shouldn't be created! Good for performance!).

    2. And yes, your example right! Most probably you will have a lot of different services/helpers in your project, it's a good practise to use one feature per service. And you need to teach your service container to handle, i.e. instantiate them. But there're many approaches to declare your services in service container. Here we reviewed one of them. But there're a lot of different third-party service containers, which allows to declare services in different ways. Symfony has it's own Service Container which could read service definition from different sources: YAML/XML files, PHP code, etc. which then generates the result single service container file for you.

    Cheers!

  • 2016-08-09 Aistis Cekanauskis

    Ok i have couple of questions:
    1. We moved all of the services to one php class ( Container ) why ?
    How i think : PDO is simple, so it will be called once, i get it. But what about other services? so we could call them once too ? Or just to make everything look cleaner ?
    2. When i write a program should i create lot of files/classes with different functionality, but include all of them to one class/Container and call all of them just from that file ?
    For example : i have a class 'class makingFood'. So i should create getMakingFood in Container class and then call it $makingFood = $container->getMakingFood() where i need?

    Sorry for bad English :D