Прикручиваем 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:
В конструкторе класса 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 поместить массив всех моделей используемых в проекте, формат массива можно узнать в комментарии в коде. Вот возможный пример реализации всего сказанного:
loadOrm(); > function loadOrm()< include 'Orm.php' self::$ORM = new Orm(); self::_models = array('Book'=>array('path'=>'models', 'namespace'=>__NAMESPACE__)); > > new Controller; ?> //в другом файле мы выводим например всех авторов имеющихся книг в БД getModel('Book'); $books = $model->all(); foreach($books as $book) echo $book->author;
Конечно, данный способ требует еще доработки, например можно сделать статические методы у Orm класса, тогда при запуске проекта нам нужно будет инициализировать его, а дальше везде использовать конструкцию вроде Orm::getModel(‘Имя модели’);
AR довольно мощный и гибкий инструмент, в нем поддерживаются кроме стандартных операций CRUD, также и связи между таблицами(включая сложные связи через — through), имеется SQLBuilder для построения SQL запросов, валидация, конвертация и др.
Официальная документация на английском и в ней освещены элементарные вопросы, есть также форум, на котором можно найти большинство ответов по работе с AR, но я так и не смог нагуглить более мене нормального источника с информацией о внедрении AR в собственный фреймворк или простой движек сайта.
По ходу своей работы мне пришелось в плотную сталкнуться с данной библиотекой, и если эта тема интересна, то я продолжу данный цикл статьей по ActiveRecord.
README
Active record is an approach to access data in a database. A database table or view is wrapped into a class, thus an object instance is tied to a single row in the table. After creation of an object, a new row is added to the table upon save. Any object loaded gets its information from the database; when an object is updated, the corresponding row in the table is also updated. The wrapper class implements accessor methods or properties for each column in the table or view.
More details can be found here.
This implementation is inspired and thus borrows heavily from Ruby on Rails’ ActiveRecord. We have tried to maintain their conventions while deviating mainly because of convenience or necessity. Of course, there are some differences which will be obvious to the user if they are familiar with rails.
Minimum Requirements
Supported Databases
Features
- Finder methods
- Dynamic finder methods
- Writer methods
- Relationships
- Validations
- Callbacks
- Serializations (json/xml)
- Transactions
- Support for multiple adapters
- Miscellaneous options such as: aliased/protected/accessible attributes
Installation
Setup is very easy and straight-forward. There are essentially only three configuration points you must concern yourself with:
- Setting the model autoload directory.
- Configuring your database connections.
- Setting the database connection to use for your environment.
ActiveRecord\Config::initialize(function($cfg) < $cfg->set_model_directory('/path/to/your/model_directory'); $cfg->set_connections( array( 'development' => 'mysql://username:password@localhost/development_database_name', 'test' => 'mysql://username:password@localhost/test_database_name', 'production' => 'mysql://username:password@localhost/production_database_name' ) ); >);
Alternatively (w/o the 5.3 closure):
$cfg = ActiveRecord\Config::instance(); $cfg->set_model_directory('/path/to/your/model_directory'); $cfg->set_connections( array( 'development' => 'mysql://username:password@localhost/development_database_name', 'test' => 'mysql://username:password@localhost/test_database_name', 'production' => 'mysql://username:password@localhost/production_database_name' ) );
PHP ActiveRecord will default to use your development database. For testing or production, you simply set the default connection according to your current environment (‘test’ or ‘production’):
ActiveRecord\Config::initialize(function($cfg) < $cfg->set_default_connection(your_environment); >);
Once you have configured these three settings you are done. ActiveRecord takes care of the rest for you. It does not require that you map your table schema to yaml/xml files. It will query the database for this information and cache it so that it does not make multiple calls to the database for a single schema.
Basic CRUD
Retrieve
These are your basic methods to find and retrieve records from your database. See the Finders section for more details.
$post = Post::find(1); echo $post->title; # 'My first blog post!!' echo $post->author_id; # 5 # also the same since it is the first record in the db $post = Post::first(); # finding using dynamic finders $post = Post::find_by_name('The Decider'); $post = Post::find_by_name_and_id('The Bridge Builder',100); $post = Post::find_by_name_or_id('The Bridge Builder',100); # finding using a conditions array $posts = Post::find('all',array('conditions' => array('name=? or id > ?','The Bridge Builder',100)));
Create
Here we create a new post by instantiating a new object and then invoking the save() method.
$post = new Post(); $post->title = 'My first blog post!!'; $post->author_id = 5; $post->save(); # INSERT INTO `posts` (title,author_id) VALUES('My first blog post!!', 5)
Update
To update you would just need to find a record first and then change one of its attributes. It keeps an array of attributes that are «dirty» (that have been modified) and so our sql will only update the fields modified.
$post = Post::find(1); echo $post->title; # 'My first blog post!!' $post->title = 'Some real title'; $post->save(); # UPDATE `posts` SET title='Some real title' WHERE >$post->title = 'New real title'; $post->author_id = 1; $post->save(); # UPDATE `posts` SET title='New real title', author_id=1 WHERE >Deleting a record will not destroy the object. This means that it will call sql to delete the record in your database but you can still use the object if you need to.
$post = Post::find(1); $post->delete(); # DELETE FROM `posts` WHERE >echo $post->title; # 'New real title'
Contributing
Please refer to CONTRIBUTING.md for information on how to contribute to PHP ActiveRecord.