Прикручиваем ActiveRecord к сайту

Введение

В процессе создания более ли мене сложного сайта приходится задумываться об организации доступа к БД(базе данных). Если сайт создается на базе существующего фреймворка или CMS, то там как правило имеются встроенные механизмы ORM (с англ. — Объектно-реляционное отображение, подробнее в вики). В данной статье я расскажу как можно прикрутить популярную и простую ORM систему ActiveRecord к собственному фреймворку.

Как работает ActiveRecord?

Компонент представляет из себя набор основных классов, необходимых для работы(Model,Config, ConnectionManager и др.), набор адаптеров для подключения к конкретной СУБД и точки входа, файла инициализации ActiveRecord.php который содержит функцию автозагрузки классов наших моделей проекта. Все классы определенны в пространстве имен ActiveRecord, наш проект скорее всего будет находится в другом пространстве или в глобальном, поэтому, чтобы при наследовании классов каждый раз не писать конструкции вроде extends \ActiveRecord\Model или использовать директиву use ActiveRecord, имеет смысл создать собственную обертку над ActiveRecord. Это также позволит расширить возможности нашей ORM не затрагивая компонент AR.

Итак, чтобы воспользоваться всеми методами AR, нам необходимо подключить файл инициализации ActiveRecord.php к проекту, создать для каждой таблицы в БД класс-модель и унаследовать его от \ActiveRecord\Model(например class Book extends \ActiveRecord\Model {} ), инициализировать подключение к БД с помощью конструкции:

$connections = array(
	'development' => 'mysql://invalid',
	'production' => 'mysql://test:test@127.0.0.1/test'
);

ActiveRecord\Config::initialize(function($cfg) use ($connections)
{
    $cfg->set_model_directory('.');
    $cfg->set_connections($connections);
}); 


После этого мы можем обращаться к нашим моделям и вызывать необходимые методы, например Book::first() — вернет первую строку из таблицы определенной в модели Book.

Создание обертки AR

В проекте возможно потребуется обращение к БД из разных файлов, да и конфигурация обычно храниться в отдельном файле, стандартных возможностей AR не всегда хватает и сама форма записи через пространство имен \ActiveRecord не очень красиво. Эта тема тянет на несколько статьей, поэтому здесь я постараюсь изложить суть вопроса.

В простом случае нам потребуется создать всего 2 класса, один мы наследуем от \ActiveRecord\Model и другой будет основным, в котором мы будем проводить инициализацию и конфигурацию AR. Создадим 2 файла-класса:

//Orm.php

class Orm
{
/**
         *  array $models_ Массив всех моделей проекта, если какая либо модель не будет определенна в этом массиве, ее нельзя будет подключить
         * массив имеет следующие елементы [Имя модели]=>array('path'=>Путь к директории в которой храниться модель , 'namespace'=> Пространство имен в котором определен класс модели)
         */
        public $models_ = array();


        /**
         * Проверка минимиальной версии PHP, подключение необходимых классов, регистрация автозагрузчика для моделей, инициализация конфигруации
         *
         * @param null $name
         */
        function __construct($name = null)
        {
            if (!defined('PHP_VERSION_ID') || PHP_VERSION_ID < 50300)
                die('PHP ActiveRecord requires PHP 5.3 or higher');

            define('PHP_ACTIVERECORD_VERSION_ID', '1.0');

            include_once 'lib/Singleton.php';
            include_once 'lib/Config.php';
            include_once 'lib/Utils.php';
            include_once 'lib/DateTime.php';
            include_once 'lib/Model.php';
            include_once 'lib/Table.php';
            include_once 'lib/ConnectionManager.php';
            include_once 'lib/Connection.php';
            include_once 'lib/SQLBuilder.php';
            include_once 'lib/Reflections.php';
            include_once 'lib/Inflector.php';
            include_once 'lib/CallBack.php';
            include_once 'lib/Exceptions.php';

            spl_autoload_register(__NAMESPACE__ . '\ActiveRecord::activerecord_autoload');
            Config::initialize(function ($cfg) {
                $cfg->set_connections(array(
                    'development' => Configuration::$dbtype . "://"
                    . Configuration::$db_user . ":" 
                    . Configuration::$db_password . "@" 
                    . Configuration::$db_host . "/"
                    . Configuration::$db_name
                ));
             /* Следует явно задать формат времени, он будет использоваться в других классах AR, по умолчанию действует "Y-m-d H:i:s T" что добавляет
                 часовой пояс к строке, такой формат времени не очень нравиться полями типа datetime в MySQL
             */
                $cfg->set_date_format("Y-m-d H:i:s");
            });

        }

        /**
         Установка текущей директории с классами моделей, создание и возвращение объекта модели, если модель не найдена возвращается FALSE
         */
        public function getModel($model)
        {
            $config = Config::instance();

            if (array_key_exists($model, $this->models_)) {

                $config->set_model_directory($this->models_[$model]['path']);

if( $this->models_[$model]['namespace'] )
                $class = "\\" . $this->models_[$model]['namespace'] . "\\" . $model;
else
$class =  $model;
                return new $class;
            } else {
                return false;
            }
        }

        /**
         Автозагрузчик классов моделей, файл загружается из папки установленной в getModel()       
         $class_name Имя загружаемой модели, передается автоматически при использовании оператора NEW
        в методе getModel()
         */
        public static function activerecord_autoload($class_name)
        {
            $root = Config::instance()->get_model_directory();

            $class_name = explode('\\', $class_name);
            $class_name = end($class_name);

            $file = $root . $class_name . ".php";

            if (file_exists($file))
                require $file;
        }
}

//Model.php

class Model extends \ActiveRecord\Model
{
        /* Имя таблицы в БД, данную переменную стоит переопределить в потомках, 
           если имя таблицы не совпадает с именем класса */
	static $table_name = 'simple_name';

	// Имя столбца с первичным ключем
	static $primary_key = 'id';

	// Имя соединения используемого при подключении
	static $connection = 'production';

	// Явное указание имени БД, при генерации SQL будет использоваться конструкция -  db.table_name
	static $db = 'test';

      /*
       * Можно определить собственные методы и свойства необходимые для работы всех моделей
       */
}


От класса Model мы будем наследовать все модели существующих таблиц. Также предположим, что вся конфигурация приложения хранится в отдельном файле Configuration.php:

class Configuration{
/*.....*/

         /**
         *  $db_host Имя хоста на котором расположена БД
         */
        static $db_host = 'localhost';
        /**
         * $db_user Имя пользователя БД
         */
        static $db_user = 'root';
        /**
         *  $db_password Пароль пользователя БД
         */
        static $db_password = 'root';
        /**
         *  $db_name Имя базы данных
         */
        static $db_name = 'db_name';
        /**
         * $dbtype Тип подключения к БД
         */
        static $dbtype = 'mysql';

/*.....*/

}


В конструкторе класса Orm(этот код взят из ActiveRecord.php) подключаем необходимые классы и регестрируем автозагрузчик, в самом конце инициализируем подключение к БД.

Особое внимание стоит уделить формату времени, если его оставить по дефолту, то во время операций записей данных в БД поля типа datetime будут генерировать ошибку, т.к. AR генерирует строки в формате 2000-02-03 16:23:27 MSK, т.е. указывает индекс часового пояса. Изменить конфиг не достаточно, не знаю почему, но разработчики AR используют в других классах формат даты и времени не из конфига, а явно указывают его в требуемых методах, поэтому придется внести еще измения в следующие файлы:
/lib/Column.php метод cast
return new DateTime($value->format('Y-m-d H:i:s T')) 

на
return new DateTime($value->format(Config::instance()->get_date_format()))

Аналогично в файлах /lib/Connection.php методы datetime_to_string() string_to_datetime(), и /lib/Model.php метод assign_attribute().

Теперь приведу пример как можно всем этим пользоваться. Сначала нам нужно создать переменную в которой мы будем хранить объект нашего класса Orm, эта переменная должна быть доступна в любом нужном нам месте любого скрипта, поэтому ее лучше объявлять как статическую главного Контроллера или глобальную. После создания объекта необходимо в массив _models поместить массив всех моделей используемых в проекте, формат массива можно узнать в комментарии в коде. Вот возможный пример реализации всего сказанного:

<?php

class Controller{
public static $ORM;

function __construct(){
$this->loadOrm();
}

function loadOrm(){
include 'Orm.php'

self::$ORM = new Orm();
self::_models = array('Book'=>array('path'=>'models', 'namespace'=>__NAMESPACE__));

}
}
new Controller;
?>

//в другом файле мы выводим например всех авторов имеющихся книг в БД
<?php

$model = Controller::$ORM ->getModel('Book');
$books = $model->all();

foreach($books as $book)
echo $book->author;


Конечно, данный способ требует еще доработки, например можно сделать статические методы у Orm класса, тогда при запуске проекта нам нужно будет инициализировать его, а дальше везде использовать конструкцию вроде Orm::getModel('Имя модели');
AR довольно мощный и гибкий инструмент, в нем поддерживаются кроме стандартных операций CRUD, также и связи между таблицами(включая сложные связи через — through), имеется SQLBuilder для построения SQL запросов, валидация, конвертация и др.

Официальная документация на английском и в ней освещены элементарные вопросы, есть также форум, на котором можно найти большинство ответов по работе с AR, но я так и не смог нагуглить более мене нормального источника с информацией о внедрении AR в собственный фреймворк или простой движек сайта.

По ходу своей работы мне пришелось в плотную сталкнуться с данной библиотекой, и если эта тема интересна, то я продолжу данный цикл статьей по ActiveRecord.
Поделиться публикацией

Комментарии 18

    +1
    Дабы не изобретать ещё больше велосипедов, ActiveRecord следует подключать через composer
      –6
      Двигаемся в сторону Ruby On Rails. Следующим шагом будет использование haml и scss.
        –5
        Ну может кто-нибудь прокомментирует из заминусовавших в чем я не прав? Речь идет об автоматизации работы с субд немного другого уровня. Я не говорю, что это не надо делать. Надо, конечно надо, и огромное спасибо автору за освещение этого вопроса и безусловно ему надо продолжить цикл статей, освещающих тему ar. Просто интересно наблюдать как пхп-шные программисты постепенно начинают использовать то, что уже давно успешно работает на рельсах.
          +4
          AR на PHP уже 1000 лет. При чём тут RoR?
            –1
            при том, что у меня не получилось найти нормальный фреймфорк на пхп, использующий ar. Наверное, уместнее сравнивать не фреймворк и язык, а фреймворк и фреймворк.
              0
              а точнее — сравнивать использование библиотеки по паттерну ar в контексте языка и использованиее библиотеки внутри фреймворка
                +2
                Надеюсь открою вам глаза: www.yiiframework.com
                  0
                  PHP уже обогнал Ruby и вовсю юзается DM :)
            +7
            Вся статья — один большой антипаттерн
              0
              AR как следует из ссылки на packagist — это паттерн и, как в случае того пакета, библиотека. Очевидно, что в первую очередь это паттерн. Если я ничего не путаю первая более или менее путная реализация этого паттерна (как и все остальное путное и полезное) — это PropelOrm, который по дефолту был в симфони с самого начала до версии 1.2 включительно. Потом симфонийцы решили, что DataMapper полезнее будет и перешли к доктрине. когда были первые удачные (в том смысле слова, что они достаточно хорошо работают, чтобы их использовать на конвеере в продакшене) реализации были у RoR — я не знаю. Не писал ничего на нем кроме тьюториала ) Важно другое, Паттерн рабочий, а вот реализация в данном посте, как мне кажется не очень удачная.

              1. Взять хотя бы нарушение хорошего (в том же смысле, что и удачного) принципа «delegation instead of inheritance».

              От класса Model мы будем наследовать все модели существующих таблиц


              2. «new Controller;» в конца файла с исходниками этого класса — тут сишники плачут кровавыми слезами

              // ... } new Controller; ?>

              3. автолоад «размазанный» по классу Orm. Это минус потому что (опять очевидно) автолоад — не является специфической задачей для любой прикладной библиотеки

              4. в том месте, где у уважаемого автора идет

              Orm::getModel

              это паттерн фабричного метода. нарушение одного из принципов SOLID — конкретно того, что спрятан за буквой S

              5. Самый критичный кусок фукционала, ИМХО, define, die и инклюды в конструкторе Orm. Почему бы вам такую тяжелую инициализацию этой библиотеки не вынести в отдельные метода/файлы (автолоад, define, проверка версионности) а в конструкторе базового класса библиотеки оставить только то, что непосредственно к этой сущности относится?

              Это не плохо, раз уж работает, но по этим самым причинам разработка как и развитие вашего решения будет медленнее, если бы этих минусов не было
                0
                4. в том месте, где у уважаемого автора идет

                Orm::getModel

                это паттерн фабричного метода. нарушение одного из принципов SOLID — конкретно того, что спрятан за буквой S

                Можете расписать почему, я например, не понимаю :(
                  +2
                  Все очень просто. Класс Orm — это у вас и ядро, и обработчик конфигураций, и создатель моделей, и автолоад — 4 ответственности. А тот самый принцип («Single responsibility principle») предлагает вам делать это в разных — то есть в 4-х — классах.

                  и еще про автолоад немного, Если ваши класс именуются по PSR-0, то автолоад обернутый в метод какого-нибудь класса вам в принципе вообще не нужен. можно делать через выделенную функцию.
                    0
                    Автор не я. А про другие ответственности, то что он ещё и Autoload и обработчик конфигурации я грешен, не прочитал в статье :)
                    Просто удивился из названия, чем это плохо.
                +1
                Рад слышать много конструктивных замечаний! Конечной целью сего является создание отдельного модуля(компонента) ORM на базе AR, который можно просто распаковать в папку и буквально за пару строк кода проинициализировать, в итоге получить объект посредством которого используя все методы AR будем управлять всеми нашими данными в БД. Без использования дополнительных тулсов(вроде composer) и не зависимо от наименования классов(PSR-0 к примеру, во фреймворке модели могут располагать и именовать по разному, в моем случае я ввел ограничение на наименование внутри папки моделей — имя файла должно совпадать с именем класса модели внутри). Не зависимо от паттерна проекта, компонент должен максимально просто встраиваться и работать.
                  0
                  У меня тоже была подобная проблема.
                  После недолгих раздумий решил взять и query builder, и ar от известного framework-а laravel. Главное что все это чудо имеется на packagist.org под именем illuminate/database.
                  Все что надо это подключить библиотеку через composer и далее по инструкции инициируем: github.com/illuminate/database

                  Все быстро и красиво.
                    +1
                    Если кому интересно, могу написать подробнее, хотя там все просто.
                    0
                    Вообще не понял, почему используется
                    $model = Controller::$ORM ->getModel('Book'); $books = $model->all();
                    когда во всех примерах документации PHPActiveRecord предлагается делать так:
                    $books = Book::all();
                      0
                      Конструкция Book::all() не сможет работать по нескольким причинам: в автозагрузчике классов моделей AR необходимо указывать путь к каталогу содежращему эту модель. В примере простейший случай рассматривается — все в одной папке, в реальности у нас модели могут находиться в нескольких папках и автозагрузчик просто не будет знать где их искать, также нужно учитывать пространство имен в которых определены наши модели, AR работает в своем пространстве ActiveRecord, а модели могут быть определены в глобальном, тогда надо будет писать так \Book::all(), предварительно задав необходимый каталог через
                      $config = Config::instance();
                      $config->set_model_directory('Путь_к_папке_с_классом_модели');
                      

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое