Данная статья посвящена внедрению ORM Doctrine в ядро CMS Wordpress. Все вопросы и утверждения по типу: «А зачем», «А почему», «Да это только наложит дополнительный оверхэд и ничего с этого не выиграешь», возможно, будут проигнорированы:)

Я считаю, что Doctrine имеет место быть в ядре Wordpress, как минимум, для удобства разработки крупного сайта в большой команде из 10+ человек, для совместимости Wordpress с PostgreSQL и обратной совместимости с MySQL (как пример) и использования обновления схемы БД, миграции и прочего полезного «сахара». Должна же быть возможность использовать при необходимости другую СУБД вместо MySQL. Спросите зачем PostgreSQL в вордпрессе? Вы просто еще мало видели:)

В настоящее время, чтобы сертифицироваться в Российском стеке, необходимо использование именно PostgreSQL PRO лицензии. К примеру, в Битриксе есть возможность развернуться на постгре из коробки. Почему разработчики ядра wordpress не предусмотрели такую возможность — мне не совсем понятно. Ну да ладно.

Итак, начнем.

Будем использовать composer в качестве пакетного менеджера.

Для начала установим Doctrine:

composer require doctrine/orm

Также для работы доктрины нам понадобится symfony/cache

composer require symfony/cache

И еще миграции:)

composer require doctrine/migrations

Далее добавим файл db.php в ваш проект (дира wp-content по дефолту, если не используется другая структура проекта по типу как в https://roots.io/bedrock/), чтобы включить необходимые файлы и классы в проект.

Код файла db.php:

<?php
if (!defined('DOCTRINE4WP_ROOT')) {
    define('DOCTRINE4WP_DEBUG', false);
    define('DOCTRINE4WP_LOG_ERRORS', true);
    define('DOCTRINE4WP_INSECURE', false);

    if (file_exists(dirname(__FILE__) . '/doctrine4wp'))
        define('DOCTRINE4WP_ROOT', dirname(__FILE__) . '/doctrine4wp');
    else if (file_exists($_SERVER['DOCUMENT_ROOT'] . '/app/plugins/doctrine4wp'))
        define('DOCTRINE4WP_ROOT', $_SERVER['DOCUMENT_ROOT'] . '/app/plugins/doctrine4wp');
    else if (file_exists($_SERVER['DOCUMENT_ROOT'] . '/app/doctrine4wp'))
        define('DOCTRINE4WP_ROOT', $_SERVER['DOCUMENT_ROOT'] . '/app/pg4wp');
    else
        die('DOCTRINE4WP file directory not found');
    require_once(DOCTRINE4WP_ROOT . '/core.php');
} // Protection against multiple loading

Затем, добавим директорию doctrine4wp к проекту. В директории будет всего 2 файла: core.php и class-wpdb.php (перезаписанный стандартный вп-шный класс для работы с БД). Также в директории будет динамически создана поддиректория logs для хранения логов ошибок запросов к бд и т д.

Код файла core.php:

<?php
/**
 * @package PostgreSQL_For_Wordpress
 * @version $Id$
 * @author  GOSWEB
 */

/**
 * This file does all the initialisation tasks
 */

// Logs are put in the pg4wp directory
define('DOCTRINE4WP_LOG', DOCTRINE4WP_ROOT . '/logs/');
// Check if the logs directory is needed and exists or create it if possible
if ((DOCTRINE4WP_DEBUG || DOCTRINE4WP_LOG_ERRORS) &&
    !file_exists(DOCTRINE4WP_LOG) &&
    is_writable(dirname(DOCTRINE4WP_LOG)))
    mkdir(DOCTRINE4WP_LOG);

// Load the driver defined in 'db.php' + composer dependencies and new wpdb2 class
$params = [
    'db' => DB_NAME,
    'password' => DB_PASSWORD,
    'user' => DB_USER,
    'host' => DB_HOST,
    'port' => $GLOBALS['_ENV']['DB_PORT'] ?? '3306'
];

require_once(DOCTRINE4WP_ROOT . '/class-wpdb.php');
try {
    Src\WpDoctrine\WpOrm::get_instance()->init($params);

    if (!isset($wpdb)) {
        $wpdb = wpdb2::get_instance(Src\WpDoctrine\WpOrm::get_instance()->getConnection(), $params);
    }

} catch (\Doctrine\DBAL\Exception $e) {
    error_log($e->getMessage());
}

Перезаписываем стандартный вп-шный класс wpdb и меняем его на wpdb2.

Файлик class-wpdb.php (здесь не показываю ввиду его объема)

Далее подключаем основной класс для инициализации Doctrine. Для этого в корне проекта создаем директорию src со следующей структурой:

Структура директории src
Структура директории src

Файл DbSingleton.php (используется в классе wpdb2 шагом ранее):

<?php

namespace Src\Patterns;

use Doctrine\DBAL\Connection;

abstract class DbSingleton
{
    private static array $instances = array();

    public static function get_instance(Connection $conn, array $params)
    {
        $class = get_called_class();
        if (array_key_exists($class, self::$instances) === false) {
            self::$instances[$class] = new $class($conn, $params);
        }
        return self::$instances[$class];
    }
}

Код файла WpOrm.php

<?php

namespace Src\WpDoctrine;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Exception;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Exception\MissingMappingDriverImplementation;
use Doctrine\ORM\ORMSetup;

class WpOrm
{
    private static array $instances = array();
    private Connection $connection;
    private ?EntityManager $entityManager = null;

    /**
     * @return WpOrm
     */
    public static function get_instance(): WpOrm
    {
        $class = get_called_class();
        if (array_key_exists($class, self::$instances) === false) {
            self::$instances[$class] = new $class();
        }
        return self::$instances[$class];
    }

    /**
     * @param array $params
     * @return void
     * @throws Exception
     */
    public function init(array $params): void
    {
        $paths = [dirname(__FILE__, 2) . '/Entity'];

        $dbParams = [
            'driver' => $_ENV['DB_DRIVER'] ?? 'pdo_mysql',
            'host' => $params['host'],
            'user' => $params['user'],
            'password' => $params['password'],
            'dbname' => $params['db'],
            'port' => $params['port'],
        ];

        $config = ORMSetup::createAttributeMetadataConfiguration($paths, true);
        $this->connection = DriverManager::getConnection($dbParams, $config);

        try {
            $this->entityManager = new EntityManager($this->connection, $config);
        } catch (MissingMappingDriverImplementation $e) {
            error_log($e->getMessage());
        }
    }

    public function getManager(): ?EntityManager
    {
        return $this->entityManager;
    }

    public function getConnection(): Connection
    {
        return $this->connection;
    }
}

Модельки подключаются из директории src/Entity. Принцип работы такой же как и в Symfony https://symfony.com/doc/current/doctrine.html.

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

Создадим директорию config и в ней добавим 2 файла:

cli-config.php

for-cli.php

Теперь нам также будут доступны команды для создания и обновления схемы БД, и миграции (миграции создаются в директории src/Migrations):

php bin/console doctrine:schema:create
php bin/console doctrine:schema:update --force
php bin/console doctrine:migrations:migrate

Собственно все :)

Полный листинг

Комментарии (4)


  1. MihaOo
    28.10.2024 18:19

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

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

    Есть ли у вас решение для подобных проблем?


    1. powernic
      28.10.2024 18:19

      Для wordpress есть wpackagist, но к сожалению это никак не решает проблему неймспейсов. Можно только надеяться на чудо, что нет пересечения неймспейсов с плагинами, которые ты используешь. На практике если использовать актуальные версии популярных плагинов, то проблем особо не возникает. Уже около 10 проектов мной было интегрирован Symfony фреймворк с Wordpress и пока работает все стабильно.


  1. DenisDangerous
    28.10.2024 18:19

    доктрина для любителей делать n+1 запросов


    1. powernic
      28.10.2024 18:19

      Это N+1 легко решается, например через QueryBuilder