Внедрение зависимостей PHP, когда несколько типов баз данных используются в одном классе - PullRequest
0 голосов
/ 16 мая 2018

Я работаю над очень неорганизованным (без тестов, везде логика) приложением PHP.Я начал читать книгу Модернизация унаследованных приложений в PHP и сейчас нахожусь в положении, когда мне нужно заменить global переменные.

В моем случае у нас сильно смешаны MySQL и MongoDBприложение, использующее глобальные переменные: $database для MySQL и $mongo для MongoDB.Мне интересно, каким будет лучший способ внедрить обе эти базы данных.Большинство примеров (в книге и в других местах) имеют что-то вроде этого:

class Database {
    public function __construct() {}
}

class MySQL extends Database {
    private $mysql;

    public function __construct() {
        $default_options = [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES   => false,
        ];
        // get config from file
        $this->mysql = new PDO(...);
    }

    public function get_instance() { return $this->mysql; }

}

class MongoDB extends Database {

    private $mongodb;  

    public function __construct() {
        // get config from file
        $mongodb = new MongoClient(...);
    }

    public function get_instance() { return $this->mongodb; }
}

class User {

    private $database = null;
    private $table = "users";

    public function __construct(Database $database) {
        $this->database = $database;
    }

}

// In another file:
$user = new User(new \Database\MySQL);

Это круто, но у меня проблема в том, что User хранит свои атрибуты, такие как name, email, id и т. Д. В MySQL, и он хранит что-то еще, например user_type в Mongo (очевидно, это больше, чем это, но я просто привожу простой пример), чем работает внедрение зависимостей?

Нужно ли передавать в конструктор два Database класса?

Есть ли у меня функция set_database, с которой мне приходится переключаться, когда я хочу сделать вызов?

class User {

    private $database = null;
    private $table = "users";

    public function __construct(Database $database) {
        $this->database = $database;
    }

    public function get_user() {
        $first_set = $this->database->run('select * from users',[]);
        $this->database->set_database(new \Database\MongoDB);
        $second_set = $this->database->run('users',[]);
    }
}

Я пытаюсь понять, как это будет работать и продолжит работать, если другие разработчики добавят базы данных, которые должны были вызываться в классе User (например, Redis).

Ответы [ 2 ]

0 голосов
/ 16 мая 2018

Вы можете абстрагировать его на несколько слоев, но первый шаг будет выглядеть примерно так:

class PdoConfig {

    /**
     * @var string
     */
    private $dsn;

    /**
     * @var string|null
     */
    private $username;

    /**
     * @var string|null
     */
    private $password;

    /**
     * @var array
     */
    private $options;

    public function __construct(string $dsn, string $username = null, string $password = null, array $options = []) {
        $this->dsn = $dsn;
        $this->username = $username;
        $this->password = $password;
        $this->options = $options;
    }

    public static function constructFromConfigurationFile(string $file_path): self {
        //read data from config
        return new self($dsn, $username, $password, $options);
    }

    public function getDsn(): string {
        return $this->dsn;
    }

    public function getUsername(): ?string {
        return $this->username;
    }

    public function getPassword(): ?string {
        return $this->password;
    }

    public function getOptions(): array {
        return $this->options;
    }
}

class MongoDbConfig {

    // pattern as in PDOOptions
}

class Database {

    /**
     * @var PDO
     */
    private $pdo;

    /**
     * @var MongoClient
     */
    private $mongo_db;

    public function __construct(PdoConfig $pdo_config, MongoDbConfig $mongo_db_config) {
        $this->pdo = new PDO(
            $pdo_config->getDsn(),
            $pdo_config->getUsername(),
            $pdo_config->getPassword(),
            $pdo_config->getOptions()
        );

        $this->mongo_db = new MongoClient(...);
    }

    public function getPdo(): PDO {
        return $this->pdo;
    }

    public function getMongoDb(): MongoClient {
        return $this->mongo_db;
    }

}

class User {

    /**
     * @var Database
     */
    private $database;

    private $table = 'users';

    public function __construct(Database $database) {
        $this->database = $database;
    }

}

$database = new Database(
    PdoConfig::constructFromConfigurationFile('/path/to/pdo_config'),
    MongoDbConfig::constructFromConfigurationFile('/path/to/mongo_config')
);

// In another file:
$user = new User($database);

Но это только первый шаг.Вторым может быть какой-то один класс (например, фабрика), который создает экземпляры User объектов, передавая ему объект Database.Но, безусловно, вы можете создавать все больше и больше слоев абстракции (парсеры конфигурации, программы чтения файлов, средства отображения данных и т. Д.).Инжектор зависимостей может пригодиться тогда - лично мне нравится Auryn .

0 голосов
/ 16 мая 2018

Предупреждение : следующий пост содержит использование антипаттерна Singleton в качестве временного инструмента для рефакторинга крупномасштабного кластерного фука

Я думаю, что вы, возможно, пропустили важную часть рефакторинга (или, по крайней мере, сосредоточились на неправильной части). Вы должны стремиться как конкретный конечный результат в этом процессе. Конкретная архитектурная структура, которую вы хотите увидеть (и с которой согласны все ваши коллеги). Прямо сейчас вы, кажется, просто бесцельно пытаетесь "сделать это менее дерьмовым" .

В начале ...

Ваша ближайшая веха - "избавиться от глобальных переменных" . Итак, сосредоточимся на этом. Попытка одновременно переключиться на полностью внедренную структуру зависимости приведет к путанице и увеличит вероятность неудачи.

Вместо этого вам следует начать с переключения в реестр (вы можете использовать синглтоны в качестве временного шага, но потом их сложнее очистить).

Итак, код, который выглядел так:

class Foobar {
    function something() {
        global $database; 
        $database->query('SELECT ... blah');
    }
}

Будет переписано как:

class Foobar {
    function something() {
        $database = Registry::getDatabase(); 
        $database->query('SELECT ... blah');
    }
}

Это решило бы одну конкретную проблему: избавление от глобальных переменных.

Второй шаг

Когда это будет сделано, вы можете приступить к переключению на инъекции зависимостей. Но не путем добавления шрифтов. Это потребовало бы огромных переписываний с большой «ударной поверхностью» (что-то легко сломать).

Вместо этого вы начинаете вводить контейнер DI, но только для частей, которые ранее использовали синглтоны или реестр. Вы создаете синглтон, который содержит экземпляр контейнера DI (вы в основном используете его как локатор службы antipattern), и код переписывается как:

class Foobar {
    function something() {
        $database = Locator::get('database'); 
        $database->query('SELECT ... blah');
    }
}

Вот как можно реорганизовать процедурный код (который также включает классы только для статики) в этом направлении.

Третий шаг

Затем вам нужно начать выполнять ООП. Вам нужно, чтобы эти (ранее глобальные) переменные были перемещены в общий доступ с экземпляром класса:

class Foobar {
    private $database;

    public function __construct() {
        $this->database = Locator::get('database'); 
    }

    public function something() {
        $this->database->query('SELECT ... blah');
    }
}

Четвертый шаг

И только теперь вы можете перейти к использованию фактических зависимостей в конструкторах.

class Foobar {
    private $database;

    public function __construct(Database $database) {
        $this->database = $database; 
    }

    public function something() {
        $this->database->query('SELECT ... blah');
    }
}

Вам также необходимо заменить каждый случай, где у вас есть: $thing = new Foobar(); на
$thing = Locator::get('foobar');, а затем повторите шаг 3 для класса, в котором использовался Foobar, до достижения начальной загрузки.

Конец цикла

Когда вы завершите процесс миграции, вы перестанете использовать одноэлементную оболочку Locator в файле начальной загрузки и переключитесь на использование фактического экземпляра контейнера DI.

Вы могли также заметить, что каждый из шагов может быть выполнен, в то время как новые функции также добавляются в базу кода, потому что ни в коем случае не требуется касаться больших участков кода в несвязанном месте. Миграция каждого класса может быть выполнена отдельно, не нарушая функциональность приложения в целом.

Но

(а это большое но).

Из фрагмента кода, который вы показали, у вас будет другая проблема, которую вам нужно решить. Весьма вероятно, что ваш класс User на самом деле реализует активную запись antipattern. Итак, когда вы избавитесь от глобальных переменных, одним из следующих возможных шагов будет отделение логики постоянства от бизнес-логики и переключение на шаблон data mapper . Для дальнейшего прочтения я спамлю три моих старых сообщения: 1 , 2 и 3 .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...