El uso del patrón singleton (o antipatrón) se considera una mala práctica porque hace que probar su código sea muy difícil y las dependencias muy complicadas hasta que el proyecto se vuelve difícil de administrar en algún momento. Solo puede tener una instancia fija de su objeto por proceso php. Al escribir pruebas unitarias automatizadas para su código, debe poder reemplazar el objeto que usa el código que desea probar con un doble de prueba que se comporte de manera predecible. Cuando el código que desea probar usa un singleton, no puede reemplazarlo con un doble de prueba.
La mejor manera (que yo sepa) de organizar la interacción entre objetos (como su objeto de base de datos y otros objetos que usan la base de datos) sería invertir la dirección de las dependencias. Eso significa que su código no solicita el objeto que necesita de una fuente externa (en la mayoría de los casos, una global como el método estático 'get_instance' de su código), sino que obtiene su objeto de dependencia (el que necesita) servido desde afuera antes de que lo necesite. Normalmente, usaría un administrador/contenedor de inyección de dependencia como este uno del proyecto symfony para componer tus objetos.
Los objetos que usan el objeto de la base de datos se inyectarían durante la construcción. Se puede inyectar mediante un método setter o en el constructor. En la mayoría de los casos (no en todos) es mejor inyectar la dependencia (su objeto db) en el constructor porque de esa manera el objeto que usa el objeto db nunca estará en un estado inválido.
Ejemplo:
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']);
Observe cómo las diferentes clases no se conocen entre sí. No hay dependencias directas entre ellos. Esto se hace sin requerir la clase real en el constructor, sino que requiere la interfaz que proporciona los métodos que necesita.
De esa manera, siempre puede escribir reemplazos para sus clases y simplemente reemplazarlos en el contenedor de inyección de dependencia. No tiene que verificar todo el código base porque el reemplazo solo tiene que implementar la misma interfaz que usan todas las demás clases. Sabes que todo seguirá funcionando porque cada componente que usa la clase anterior solo conoce la interfaz y llama solo a los métodos conocidos por la interfaz.
P.D.:Disculpe mis constantes referencias al proyecto Symfony, es justo a lo que estoy más acostumbrado. Otros proyectos como Drupal, Propel o Zend probablemente también tengan conceptos como este.