Mysql
 sql >> Datenbank >  >> RDS >> Mysql

Singleton-Alternative für PHP PDO

Die Verwendung des Singleton-Musters (oder Antimusters) wird als schlechte Praxis angesehen, da es das Testen Ihres Codes sehr schwierig und die Abhängigkeiten sehr kompliziert macht, bis das Projekt irgendwann schwer zu verwalten ist. Sie können nur eine feste Instanz Ihres Objekts pro PHP-Prozess haben. Wenn Sie automatisierte Unit-Tests für Ihren Code schreiben, müssen Sie in der Lage sein, das Objekt, das der zu testende Code verwendet, durch ein Test-Double zu ersetzen, das sich auf vorhersagbare Weise verhält. Wenn der Code, den Sie testen möchten, einen Singleton verwendet, können Sie diesen nicht durch einen Test-Double ersetzen.

Der beste Weg (meines Wissens), die Interaktion zwischen Objekten (wie Ihrem Datenbank-Objekt und anderen Objekten, die die Datenbank verwenden) zu organisieren, wäre, die Richtung der Abhängigkeiten umzukehren. Das bedeutet, dass Ihr Code das benötigte Objekt nicht von einer externen Quelle anfordert (in den meisten Fällen eine globale wie die statische „get_instance“-Methode aus Ihrem Code), sondern stattdessen sein Abhängigkeitsobjekt (das benötigte) von außen bedient bekommt bevor es gebraucht wird. Normalerweise würden Sie einen Depency-Injection Manager/Container wie diesen verwenden eine aus dem Symfony-Projekt um Ihre Objekte zusammenzustellen.

Objekte, die das Datenbankobjekt verwenden, würden es bei der Konstruktion injizieren. Es kann entweder durch eine Setter-Methode oder im Konstruktor injiziert werden. In den meisten Fällen (nicht allen) ist es besser, die Abhängigkeit (Ihr Db-Objekt) in den Konstruktor einzufügen, weil auf diese Weise das Objekt, das das Db-Objekt verwendet, niemals in einem ungültigen Zustand sein wird.

Beispiel:

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

Beachten Sie, dass die verschiedenen Klassen nichts voneinander wissen. Es bestehen keine direkten Abhängigkeiten zwischen ihnen. Dies wird erreicht, indem nicht die eigentliche Klasse im Konstruktor benötigt wird, sondern stattdessen die Schnittstelle, die die benötigten Methoden bereitstellt.

Auf diese Weise können Sie immer Ersatz für Ihre Klassen schreiben und sie einfach im Dependency-Injection-Container ersetzen. Sie müssen nicht die gesamte Codebasis überprüfen, da der Ersatz lediglich dieselbe Schnittstelle implementieren muss, die von allen anderen Klassen verwendet wird. Sie wissen, dass alles weiter funktionieren wird, da jede Komponente, die die alte Klasse verwendet, nur das Interface kennt und nur Methoden aufruft, die dem Interface bekannt sind.

P.S.:Bitte entschuldigen Sie meine ständigen Verweise auf das Symfony-Projekt, es ist einfach das, woran ich am meisten gewöhnt bin. Andere Projekte wie Drupal, Propel oder Zend haben wahrscheinlich auch solche Konzepte.