sql >> Databasteknik >  >> RDS >> Mysql

Singleton alternativ för PHP PDO

Att använda singleton-mönstret (eller antimönster) anses vara dålig praxis eftersom det gör att testa din kod mycket svårt och beroenden mycket invecklade tills projektet blir svårt att hantera någon gång. Du kan bara ha en fast instans av ditt objekt per php-process. När du skriver automatiserade enhetstester för din kod måste du kunna ersätta objektet som koden du vill testa använder med en test-dubbel som beter sig på ett förutsägbart sätt. När koden du vill testa använder en singelton, kan du inte ersätta den med en testdubbel.

Det bästa sättet (såvitt jag vet) att organisera interaktionen mellan objekt (som ditt databasobjekt och andra objekt som använder databasen) skulle vara att vända om riktningen för beroenden. Det betyder att din kod inte begär objektet den behöver från en extern källa (i de flesta fall en global som den statiska 'get_instance'-metoden från din kod) utan istället får sitt depency-objekt (det den behöver) betjänat utifrån innan den behöver det. Normalt skulle du använda en Depency-Injection Manager/Container som den här en från symfonyprojektet att komponera dina objekt.

Objekt som använder databasobjektet skulle få det injicerat vid konstruktion. Det kan injiceras antingen med en sättermetod eller i konstruktorn. I de flesta fall (inte alla) är det bättre att injicera beroendet (ditt db-objekt) i konstruktorn eftersom det objektet som använder db-objektet aldrig kommer att vara i ett ogiltigt tillstånd.

Exempel:

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

Lägg märke till hur de olika klasserna inte känner till varandra. Det finns inga direkta beroenden mellan dem. Detta görs genom att inte kräva den faktiska klassen i konstruktorn, utan istället kräva gränssnittet som tillhandahåller de metoder den behöver.

På så sätt kan du alltid skriva ersättningar för dina klasser och bara byta ut dem i behållaren för beroendeinjektion. Du behöver inte kontrollera hela kodbasen eftersom ersättningen bara måste implementera samma gränssnitt som används av alla andra klasser. Du vet att allt kommer att fortsätta att fungera eftersom varje komponent som använder den gamla klassen bara känner till gränssnittet och anropar endast metoder som gränssnittet känner till.

P.S.:Ursäkta mina ständiga referenser till symfoniprojektet, det är precis vad jag är mest van vid. Andra projekt som Drupal, Propel eller Zend har förmodligen också koncept som detta.




  1. PHP och Postgres:fångar fel?

  2. Ändra hur isql kör SQL

  3. Tomcat 6/7 JNDI med flera datakällor

  4. Använder Oracle ref cursor i Java utan Oracle-beroende