While Singletons have become a Pattern-Non-Grata over the years, you still find it surprisingly often in PHP applications and frameworks. This talk will explain what the Singleton pattern is, how it works in PHP and why you should avoid it in your application.
9. class Singleton
…
public static function getInstance()
{
if (!isset(self::$instance)) {
self::$instance = new self;
}
return self::$instance;
}
}
10. abstract class Singleton
…
public static function getInstance()
{
return isset(static::$instance)
? static::$instance
: static::$instance = new static;
}
final private function __construct()
{
static::init();
}
protected function init() {}
}
class A extends Singleton {
protected static $instance;
}
11. trait Singleton
…
protected static $instance;
final public static function getInstance()
{
return isset(static::$instance)
? static::$instance
: static::$instance = new static;
}
final private function __construct() {
static::init();
}
protected function init() {}
}
class A {
use Singleton;
}
12. „Nobody should need a mechanism to make
it as easy as pie to clutter the code base
with singletons“
- Sebastian Bergmann
14. „In my stilted view of the universe anything that
impedes testing is something to be avoided. There
are those who don't agree with this view, but I'll
pit my defect rates against theirs any time.“
- Robert „Uncle Bob“ C. Martin
15. class TableDataGateway
…
public function __construct()
{
$this->db = Db::getInstance();
}
public insert(array $data)
{
$this->db->sql('INSERT …', $data);
}
}
16. class TableDataGatewayTest extends PHPUnit…
/**
* @dataProvider provideInsertData
*/
public function testInsertCallsDbWithData($data)
{
$gateway = new TableDataGateway;
$gateway->insert($data);
$this->assertSame(
$data,
$gateway->findById($data['id'])
);
}
}
20. class TableDataGatewayTest extends PHPUnit…
/**
* @dataProvider provideInsertData
*/
public function testInsertCallsDbWithData($data)
{
Registry::set('pdo', array('…'));
Registry::set('log', '/dev/null');
$gateway = new TableDataGateway;
$gateway->insert($data);
$this->assertSame(
$data,
$gateway->findById($data['id'])
);
}
}
21. Your tests are not isolated.
You still need a real database.
No easy way to mock the Db Singleton.
22. „The real problem with Singletons is that they
give you such a good excuse not to think carefully
about the appropriate visibility of an object.“
- Kent Beck
23. Hard to change code
manifests itself in a cascade of
subsequent changes in dependent
code
33. SRP Violation
Creation Logic + Business Logic
= 2 reasons to change
Mitigated by using Abstract Singletons
But responsibilities are still strongly
coupled through inheritance
34. OCP Violation
In PHP < 5.2 Singletons are closed for extending
Boilerplate code has to be removed when no
longer needed
35. DIP Violation
Global access breaks encapsulation
Hides dependencies
Adds concrete dependencies into Clients
Signature lies
39. Recap:
“Ensure a class has only one
instance, and provide a global
access point to it.”
40. „So much of the Singleton pattern is about
coaxing language protection mechanisms into
protecting this one aspect: singularity. I guess it
is important, but it seems to have grown out of
proportion.“
- Ward Cunningham
41. You do not need to ensure singularity
when you are going to instantiate the
object only once anyway.
42. You do not need to provide a Global
Access Point when you can inject the
object.
43. class TableDataGateway
…
public function __construct()
{
$this->db = Db::getInstance();
}
}
class Db extends Singleton
…
protected function init()
{
$this->logger = Logger::getInstance();
$this->pdo = new PDO(Registry::get('pdo'));
}
}
44. class TableDataGateway
…
public function __construct(Db $db)
{
$this->db = $db;
}
}
class Db
…
public function __construct(PDO $pdo, Log $logger)
{
$this->logger = $logger;
$this->pdo = $pdo;
}
}
45. But then I have to push dependencies all
the way through my object graph?!
46. Recap:
Creational Pattern
„control when and how objects are
created“
48. class UserControllerBuilder
{
public function build($config)
{
$log = new Log($config['log']);
$pdo = new PDO($config['…']);
$db = new Db($pdo, $log);
$tdg = new TableDataGateway($db);
return new UserController($tdg);
}
}
49. class UserControllerBuilder
{
public function build($config)
{
$log = new Log($config['log']);
$pdo = new PDO($config['…']);
??? $db = new Db($pdo, $log);
$tdg = new TableDataGateway($db);
return new UserController($tdg);
}
}
50. class UserControllerBuilder
{
public function build($config)
{
$db = new LogDecorator(
new PDO($config['…']);
new Log($config['log']);
);
$tdg = new TableDataGateway($db);
return new UserController($tdg);
}
}
51. // index.php
include '/path/to/autoload.php';
$config = new Config('/path/to/config.ini');
$router = new Router(
array(
'/user/{id}' => function() use ($config)
{
$builder = new UserControllerBuilder;
return $builder->build($config);
}
)
);
$router->route(
new Request($_GET, $_POST, $_SERVER),
new Response
);