Het gebruik van het singleton-patroon (of antipatroon) wordt als een slechte gewoonte beschouwd omdat het het testen van uw code erg moeilijk maakt en de afhankelijkheden erg ingewikkeld totdat het project op een gegeven moment moeilijk te beheren wordt. Je kunt maar één vaste instantie van je object per php-proces hebben. Bij het schrijven van geautomatiseerde unit-tests voor uw code moet u het object dat de code die u wilt testen gebruikt, kunnen vervangen door een test-double die zich op een voorspelbare manier gedraagt. Als de code die je wilt testen een singleton gebruikt, dan kun je die niet vervangen door een test double.
De beste manier (voor zover ik weet) om de interactie tussen objecten (zoals je Database-Object en andere objecten die de database gebruiken) te organiseren, zou zijn om de richting van de afhankelijkheden om te keren. Dat betekent dat uw code het object dat het nodig heeft niet van een externe bron vraagt (in de meeste gevallen een globale zoals de statische 'get_instance'-methode van uw code), maar in plaats daarvan het dependency-object (degene die het nodig heeft) van buitenaf krijgt voordat het het nodig heeft. Normaal gesproken zou u een Depency-Injection Manager/Container gebruiken zoals dit een uit het symfony-project om uw objecten samen te stellen.
Objecten die het database-object gebruiken, zouden het bij constructie geïnjecteerd krijgen. Het kan worden geïnjecteerd door een setter-methode of in de constructor. In de meeste gevallen (niet alle) is het beter om de afhankelijkheid (uw db-object) in de constructor te injecteren, omdat op die manier het object dat het db-object gebruikt, nooit in een ongeldige staat zal zijn.
Voorbeeld:
interface DatabaseInterface
{
function query($statement, array $parameters = array());
}
interface UserLoaderInterface
{
public function loadUser($userId);
}
class DB extends PDO implements DatabaseInterface
{
function __construct(
$dsn = 'mysql:host=localhost;dbname=kida',
$username = 'root',
$password = 'root',
) {
try {
parent::__construct($dsn, $username, $password, array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8'");
parent::setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch(PDOException $e) {
echo $e->getMessage();
}
}
function query($statement, array $parameters = array())
{
# ...
}
}
class SomeFileBasedDB implements DatabaseInterface
{
function __construct($filepath)
{
# ...
}
function query($statement, array $parameters = array())
{
# ...
}
}
class UserLoader implements UserLoaderInterface
{
protected $db;
public function __construct(DatabaseInterface $db)
{
$this->db = $db;
}
public function loadUser($userId)
{
$row = $this->db->query("SELECT name, email FROM users WHERE id=?", [$userId]);
$user = new User();
$user->setName($row[0]);
$user->setEmail($row[1]);
return $user;
}
}
# the following would be replaced by whatever DI software you use,
# but a simple array can show the concept.
# load this from a config file
$parameters = array();
$parameters['dsn'] = "mysql:host=my_db_server.com;dbname=kida_production";
$parameters['db_user'] = "mydbuser";
$parameters['db_pass'] = "mydbpassword";
$parameters['file_db_path'] = "/some/path/to/file.db";
# this will be set up in a seperate file to define how the objects are composed
# (in symfony, these are called 'services' and this would be defined in a 'services.xml' file)
$container = array();
$container['db'] = new DB($parameters['dsn'], $parameters['db_user'], $parameters['db_pass']);
$container['fileDb'] = new SomeFileBasedDB($parameters['file_db_path']);
# the same class (UserLoader) can now load it's users from different sources without having to know about it.
$container['userLoader'] = new UserLoader($container['db']);
# or: $container['userLoader'] = new UserLoader($container['fileDb']);
# you can easily change the behaviour of your objects by wrapping them into proxy objects.
# (In symfony this is called 'decorator-pattern')
$container['userLoader'] = new SomeUserLoaderProxy($container['userLoader'], $container['db']);
# here you can choose which user-loader is used by the user-controller
$container['userController'] = new UserController($container['fileUserLoader'], $container['viewRenderer']);
Merk op hoe de verschillende klassen niet van elkaar weten. Er zijn geen directe afhankelijkheden tussen hen. Dit wordt gedaan door niet de daadwerkelijke klasse in de constructor te vereisen, maar in plaats daarvan de interface te vereisen die de methoden biedt die het nodig heeft.
Op die manier kun je altijd vervangingen voor je klassen schrijven en ze gewoon vervangen in de dependency-injection-container. U hoeft niet de hele codebase te controleren, omdat de vervanging gewoon dezelfde interface moet implementeren die door alle andere klassen wordt gebruikt. Je weet dat alles zal blijven werken omdat elk onderdeel dat de oude klasse gebruikt alleen de interface kent en alleen methoden aanroept die bekend zijn bij de interface.
P.S.:Excuseer mijn constante verwijzingen naar het symfony-project, het is precies wat ik het meest gewend ben. Andere projecten zoals Drupal, Propel of Zend hebben waarschijnlijk ook dergelijke concepten.