Mysql
 sql >> Database >  >> RDS >> Mysql

Alternativa singleton per PHP PDO

L'uso del pattern singleton (o antipattern) è considerato una cattiva pratica perché rende molto difficile il test del codice e le dipendenze molto contorte fino a quando il progetto non diventa difficile da gestire a un certo punto. Puoi avere solo un'istanza fissa del tuo oggetto per processo php. Quando si scrivono unit test automatizzati per il codice, è necessario essere in grado di sostituire l'oggetto utilizzato dal codice che si desidera testare con un test double che si comporti in modo prevedibile. Quando il codice che vuoi testare utilizza un singleton, non puoi sostituirlo con un test double.

Il modo migliore (per quanto ne so) per organizzare l'interazione tra oggetti (come il tuo oggetto database e altri oggetti che utilizzano il database) sarebbe invertire la direzione delle dipendenze. Ciò significa che il tuo codice non sta richiedendo l'oggetto di cui ha bisogno da una fonte esterna (nella maggior parte dei casi globale come il metodo statico "get_instance" dal tuo codice) ma ottiene invece il suo oggetto di dipendenza (quello di cui ha bisogno) servito dall'esterno prima che ne abbia bisogno. Normalmente dovresti usare un Depency-Injection Manager/Container come questo uno dal progetto symfony per comporre i tuoi oggetti.

Gli oggetti che utilizzano l'oggetto database lo otterrebbero iniettato durante la costruzione. Può essere iniettato con un metodo setter o nel costruttore. Nella maggior parte dei casi (non in tutti) è meglio inserire la dipendenza (il tuo oggetto db) nel costruttore perché in questo modo l'oggetto che usa il oggetto db non sarà mai in uno stato non valido.

Esempio:

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']);

Nota come le diverse classi non si conoscono. Non ci sono dipendenze dirette tra di loro. Questo viene fatto non richiedendo la classe effettiva nel costruttore, ma richiedendo invece l'interfaccia che fornisce i metodi di cui ha bisogno.

In questo modo puoi sempre scrivere sostituzioni per le tue classi e semplicemente sostituirle nel contenitore delle dipendenze. Non è necessario controllare l'intera base di codice perché la sostituzione deve solo implementare la stessa interfaccia utilizzata da tutte le altre classi. Sai che tutto continuerà a funzionare perché ogni componente che utilizza la vecchia classe conosce solo l'interfaccia e chiama solo i metodi conosciuti dall'interfaccia.

P.S.:Scusa i miei continui riferimenti al progetto symfony, è proprio quello a cui sono più abituato. Probabilmente anche altri progetti come Drupal, Propel o Zend hanno concetti come questo.