Введение
В процессе создания более ли мене сложного сайта приходится задумываться об организации доступа к БД(базе данных). Если сайт создается на базе существующего фреймворка или CMS, то там как правило имеются встроенные механизмы ORM (с англ. — Объектно-реляционное отображение, подробнее в вики). В данной статье я расскажу как можно прикрутить популярную и простую ORM систему ActiveRecord к собственному фреймворку.
Как работает ActiveRecord?
Компонент представляет из себя набор основных классов, необходимых для работы(Model,Config, ConnectionManager и др.), набор адаптеров для подключения к конкретной СУБД и точки входа, файла инициализации ActiveRecord.php который содержит функцию автозагрузки классов наших моделей проекта. Все классы определенны в пространстве имен ActiveRecord, наш проект скорее всего будет находится в другом пространстве или в глобальном, поэтому, чтобы при наследовании классов каждый раз не писать конструкции вроде extends ActiveRecordModel или использовать директиву use ActiveRecord, имеет смысл создать собственную обертку над ActiveRecord. Это также позволит расширить возможности нашей ORM не затрагивая компонент AR.
Итак, чтобы воспользоваться всеми методами AR, нам необходимо подключить файл инициализации ActiveRecord.php к проекту, создать для каждой таблицы в БД класс-модель и унаследовать его от ActiveRecordModel(например class Book extends ActiveRecordModel {} ), инициализировать подключение к БД с помощью конструкции:
$connections = array(
'development' => 'mysql://invalid',
'production' => 'mysql://test:test@127.0.0.1/test'
);
ActiveRecordConfig::initialize(function($cfg) use ($connections)
{
$cfg->set_model_directory('.');
$cfg->set_connections($connections);
});
После этого мы можем обращаться к нашим моделям и вызывать необходимые методы, например Book::first() — вернет первую строку из таблицы определенной в модели Book.
Создание обертки AR
В проекте возможно потребуется обращение к БД из разных файлов, да и конфигурация обычно храниться в отдельном файле, стандартных возможностей AR не всегда хватает и сама форма записи через пространство имен ActiveRecord не очень красиво. Эта тема тянет на несколько статьей, поэтому здесь я постараюсь изложить суть вопроса.
В простом случае нам потребуется создать всего 2 класса, один мы наследуем от ActiveRecordModel и другой будет основным, в котором мы будем проводить инициализацию и конфигурацию 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 ActiveRecordModel
{
/* Имя таблицы в БД, данную переменную стоит переопределить в потомках,
если имя таблицы не совпадает с именем класса */
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.
Автор: JoomCheese