Matthieu Napoli
Leading open source web analytics platform
Coupling between 2 modules, i.e. classes
class ReportGenerator {
public function generateReport() {
$db = Database::getInstance();
$data = $db->selectAll('some_table');
Logger::info('Generating the report');
$pdf = new PdfGenerator();
return $pdf->generate($data);
}
}
Not Composer dependencies
Dependency Injection: design pattern
Container: tool
New:
$generator = new PdfReportGenerator();
$generator->generate();
Static method:
PdfReportGenerator::generate();
Singleton:
$generator = PdfReportGenerator::getInstance();
$generator->generate();
Application:
$controller = new ReportController();
$controller->viewAction();
ReportController
:
public function viewAction() {
$generator = new PdfReportGenerator();
echo $generator->generate();
}
PdfReportGenerator
:
public function generate() {
$db = new Database();
$data = $db->selectAll('sales');
return /* ... */;
}
public function doSomething() {
$cache = new Redis();
// ...
}
Coupling
I want to change the caching backend => I have to change it everywhere…
class PdfReportGenerator
{
public function generate() {
$data = Db::selectAll('sales');
return /* ... */;
}
}
Tests
hard to test code using a webservice, database, cache…
public function copyData($fromDatabase, $toDatabase) {
$data = Db::selectAll('my_data');
Db::insert($data);
// D'oh!
}
Extensibility
I want X to use a different Y than the one by default…
Before:
class Foo {
public function run() {
$bar = new Bar(); // or $bar = Bar::getInstance()
$bar->doSomething();
}
}
After:
class Foo {
private $bar;
public function __construct(Bar $bar) {
$this->bar = $bar;
}
public function run() {
$this->bar->doSomething();
}
}
Application:
$db = new Database();
$generator = new PdfReportGenerator($db);
$controller = new ReportController($generator);
$controller->viewAction();
ReportController
:
public function viewAction() {
echo $this->generator->generate();
}
PdfReportGenerator
:
public function generate() {
$data = $this->db->selectAll('sales');
return /* ... */;
}
The dependency inversion principle:
Code against abstractions
class StoreService
{
public function __construct(GoogleMaps $geolocator) { … }
// ...
}
class GoogleMaps
{
public function getCoordinates($address) { … }
}
class StoreService {
public function __construct(GeolocationService $geolocationService) { … }
}
interface GeolocationService {
public function getCoordinates($address);
}
class GoogleMaps implements GeolocationService { … }
class OpenStreetMap implements GeolocationService { … }
$db = new Database();
$generator = new PdfReportGenerator($db);
$controller = new ReportController($generator);
Application:
$controller = $container->get('ReportController');
$controller->viewAction();
ReportController
:
public function viewAction() {
echo $this->generator->generate();
}
PdfReportGenerator
:
public function generate() {
$data = $this->db->selectAll('sales');
return /* ... */;
}
$controller = $container->get('ReportController');
=
$db = new Database();
$generator = new PdfReportGenerator($db);
$controller = new ReportController($generator);
The container builds object graphs
but how?
$db = new Database();
$generator = new PdfReportGenerator($db);
$controller = new ReportController($generator);
$container = new Container();
$mailer = new Mailer();
$reportSender = new ReportSender($mailer);
$reportController = new ReportController($reportSender);
$container->set('Mailer', $mailer);
$container->set('ReportSender', $reportSender);
$container->set('ReportController', $reportController);
$container['Mailer'] = function () {
return new Mailer();
};
$container['ReportSender'] = function ($container) {
return new ReportSender($container['Mailer']);
};
$reportSender = $container['ReportSender'];
Lazy graphs
services:
mailer:
class: Acme\Hello\Mailer
report_sender:
class: Acme\Hello\ReportSender
arguments: ["@mailer"]
<services>
<service id="mailer" class="Acme\Hello\Mailer" />
<service id="report_sender" class="Acme\Hello\ReportSender">
<argument>@mailer</argument>
</service>
</services>
$container->set('Mailer', $container->lazyNew('Mailer'));
$container->set('ReportSender', $container->lazyNew('ReportSender'));
$container->params['ReportSender']['mailer'] = $container->lazyGet('Mailer');
The dependency injection container for humans
class ReportSender
{
public function __construct(Mailer $mailer)
{
}
}
~ 80% of the use cases -> no configuration!
class ReportController {
/**
* @Inject
* @var ReportSender
*/
private $reportSender;
public function sendReportNow() {
$this->reportSender->sendReport($_GET['id']);
// ...
}
}
Not meant to be used everywhere, but very useful in controllers
<?php
return [
'Psr\Log\LoggerInterface' => DI\object('Monolog\Logger')
->constructor('webservice', Monolog\Logger::LEVEL_DEBUG),
'My\Service' => DI\factory(function () {
$object = new Stuff();
$object->init('foo', 'bar');
return $object;
}),
];
Don't rewrite one…
/**
* @deprecated
*/
class StaticContainer
{
private static $container;
public static function getContainer()
{
if (self::$container === null) {
self::$container = self::createContainer();
}
return self::$container;
}
// ...
Remove it ASAP
class Database
{
/**
* @deprecated
*/
public static function getInstance()
{
return StaticContainer::getInstance()->get('Database');
}
// ...
Move the old getInstance()
to the container config
Turn the static class into a static proxy:
class OldDbClass
{
/**
* @deprecated
*/
public static function selectAll($table)
{
$db = StaticContainer::getInstance()->get('NewDbClass');
return $db->selectAll($table);
}
// ...
Remove deprecated classes/methods
Use an IDE
Before:
$logFile = PIWIK_PATH . '/tmp/piwik.log';
$customId = Config::get('customId');
if ($customId !== null) {
$tmpPath = Common::rewriteTmpPathWithCustomId($id, $tmpPath);
}
After
# default config
tmpPath: /tmp
logFile: $tmpPath/piwik.log
# plugin config
tmpPath: /tmp/123
$mailer = $container->get('Mailer');
Service Locator anti-pattern
Is that really worth it?
It depends