L'utilisation du modèle singleton (ou antimodèle) est considérée comme une mauvaise pratique car elle rend le test de votre code très difficile et les dépendances très alambiquées jusqu'à ce que le projet devienne difficile à gérer à un moment donné. Vous ne pouvez avoir qu'une seule instance fixe de votre objet par processus php. Lors de l'écriture de tests unitaires automatisés pour votre code, vous devez pouvoir remplacer l'objet que le code que vous souhaitez tester utilise par un test-double qui se comporte de manière prévisible. Lorsque le code que vous souhaitez tester utilise un singleton, vous ne pouvez pas le remplacer par un test double.
La meilleure façon (à ma connaissance) d'organiser l'interaction entre les objets (comme votre Database-Object et d'autres objets utilisant la base de données) serait d'inverser le sens des dépendances. Cela signifie que votre code ne demande pas l'objet dont il a besoin à partir d'une source externe (dans la plupart des cas, un global comme la méthode statique 'get_instance' de votre code) mais obtient à la place son objet de dépendance (celui dont il a besoin) servi de l'extérieur avant qu'il en ait besoin. Normalement, vous utiliseriez un gestionnaire/conteneur d'injection de dépendance comme ceci un du projet symfony pour composer vos objets.
Les objets qui utilisent l'objet de base de données le feraient injecter lors de la construction. Il peut être injecté soit par une méthode setter, soit dans le constructeur. Dans la plupart des cas (pas tous), il est préférable d'injecter la dépendance (votre objet de base de données) dans le constructeur, car de cette façon, l'objet qui utilise l'objet de base de données ne sera jamais dans un état invalide.
Exemple :
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']);
Remarquez comment les différentes classes ne se connaissent pas. Il n'y a pas de dépendances directes entre eux. Cela se fait en n'exigeant pas la classe réelle dans le constructeur, mais plutôt l'interface qui fournit les méthodes dont elle a besoin.
De cette façon, vous pouvez toujours écrire des remplacements pour vos classes et simplement les remplacer dans le conteneur d'injection de dépendance. Vous n'avez pas à vérifier l'intégralité de la base de code car le remplacement doit simplement implémenter la même interface que celle utilisée par toutes les autres classes. Vous savez que tout continuera à fonctionner car chaque composant utilisant l'ancienne classe ne connaît que l'interface et n'appelle que les méthodes connues par l'interface.
P.S. :veuillez excuser mes références constantes au projet symfony, c'est juste ce à quoi je suis le plus habitué. D'autres projets comme Drupal, Propel ou Zend ont probablement aussi des concepts comme celui-ci.