Mysql
 sql >> Base de Dados >  >> RDS >> Mysql

Alternativa singleton para PHP PDO


Usar o padrão singleton (ou antipattern) é considerado uma má prática porque torna o teste do seu código muito difícil e as dependências muito complicadas até que o projeto se torne difícil de gerenciar em algum ponto. Você só pode ter uma instância fixa do seu objeto por processo php. Ao escrever testes de unidade automatizados para o seu código, você precisa ser capaz de substituir o objeto que o código que deseja testar usa por um test-double que se comporta de maneira previsível. Quando o código que você deseja testar usa um singleton, você não pode substituí-lo por um duplo de teste.

A melhor maneira (que eu saiba) de organizar a interação entre objetos (como seu objeto de banco de dados e outros objetos usando o banco de dados) seria inverter a direção das dependências. Isso significa que seu código não está solicitando o objeto de que precisa de uma fonte externa (na maioria dos casos, uma global, como o método estático 'get_instance' do seu código), mas obtém seu objeto de dependência (o que precisa) servido de fora antes de precisar. Normalmente você usaria um gerenciador/contêiner de injeção de dependência como este um do projeto symfony para compor seus objetos.

Objetos que usam o objeto de banco de dados seriam injetados na construção. Ele pode ser injetado por um método setter ou no construtor. Na maioria dos casos (não em todos) é melhor injetar a dependência (seu db-object) no construtor porque dessa forma o objeto que usa o db-object nunca estará em um estado inválido.

Exemplo:


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 como as diferentes classes não se conhecem. Não há dependências diretas entre eles. Isso é feito por não exigir a classe real no construtor, mas exigir a interface que fornece os métodos necessários.

Dessa forma, você sempre pode escrever substituições para suas classes e apenas substituí-las no contêiner de injeção de dependência. Você não precisa verificar toda a base de código porque a substituição só precisa implementar a mesma interface que é usada por todas as outras classes. Você sabe que tudo continuará funcionando porque cada componente usando a classe antiga conhece apenas a interface e chama apenas métodos conhecidos pela interface.

P.S.:Por favor, desculpem minhas constantes referências ao projeto symfony, é exatamente o que estou mais acostumado. Outros projetos como Drupal, Propel ou Zend provavelmente também têm conceitos como este.