mod_orm добавляет поддержку объекта Active Record для упрощения SQL-запросов. Не смотря на то, что это подключаемый и отчуждаемый модуль, он глубоко вшит в систему, забирает себе функцию __autoload()
и предоставляет единственный способ работы с базой данных (кроме непосредственной работы при помощи родных функций mysql_*
). Поэтому он расположен в директории cms и загружается раньше пользовательских модулей.
Особенностью использования модуля является следующее: если функция (метод) не имеет параметров, то её можно вызывать в качестве переменной, просто опустив скобки (в большинстве случаев также доступен классический подход), например:
d()->User->all;
d()->User->one;
d()->User->expand_to_client;
//Аналогично
d()->User->all();
d()->User->one();
d()->User->expand_to_client();
Для указания того, какой таблице соответствует объект, ничего не нужно. Объявлять класс для каждого отдельного типа объекта тоже не обязательно, при запросе переменной, начинающейся с заглавной буквы, вернётся новый экземпляр соответствующего класса, готовый к использованию.
Для поиска по таблице users используется запись d()->User;
Например:
d()->User->find(2)->title;
Для поиска по любой другой таблице используется имя объекта, начинаемое с заглавной буквы, и представляющее собой название таблицы в единственном числе по правилам английской грамматики.
Например, d()->Category
ищет по таблице categories, а d()->News
ищет по таблице news
Альтернативная запись: d('User')
(Примечание: такая запись подходит для запроса любого
свойства основного объекта (переменной), например, d('title')
или d('article')
).
Для дальнейшего использования можно передавать вызовы по цепочке или используя промежуточный объект:
print d()->User->find(2)->title;
равнозначно
$user= d()->User;
$user=$user->find(2);
print $user->title;
либо равнозначно
$user2= d()->User;
$user2->find(2);
print $user2->login;
Также разрешён альтернативный подход
$user2= new User();
$user2->find(2);
print $user2->login;
Также разрешается объявлять класс самому, наследуя его от класса ar. При этом имя класса должно представлять из себя имя таблицы в единственном числе и начинаться с большой буквы.
В этом случае имеется возможность использовать свои методы класса и переопределять существующие.
При запросе переменной в первую очередь опрашивается метод класса с аналогичным именем.
Например:
class User extends ar {
function title()
{
return '<b>'.$this->get('title').'</b>';
}
}
print d()->User->find(1)->title;
Для того, чтобы не вызывать рекурсии подобной этой:
class User extends ar {
function title()
{
return '<b>'.$this->title.'</b>'; //Рекурсия функции title
}
}
используется метод get(имя_переменной)
или её синоним get_variable_value(имя_переменной)
.
Подробнее см. раздел Переопределение геттеров и сеттеров ниже.
Осуществляет поиск по id
d()->User->find(34);
Непосредственно запрос выполнится перед получением данных (ленивый запрос).
Отменяет действие выполненных ранее функций find()
, find_by_*()
, where()
Если в конструктор передано одно число, оно также перейдёт в метож find:
$user = new User(3); //Аналогично $user = new User(3); $user->find(3);
Таким образом, можно использовать следующую упрощённую конструкцию:
d()->User(34);
Осуществляет обычный поиск по содержимому
d()->User->search('name', 'email', 'text', 'id', $_GET['s']);
Последний параметр - искомая строка. Первые параметры - поля.
Если параметр только один, то поиск ведётся по полям text и title
По сути, более простой для использования вариант следующей конструкции:
d()->User->->where('`title` LIKE ? or `text` LIKE ?','%'.$_GET['s'].'%','%'.$_GET['s'].'%');
Можно использовать дополнительные параметры:
d()->User->search('name', 'email', 'text', 'id', $_GET['s'])->only('admin')->order_by('title');
Данная конструкция вызовет следующий запрос.
SELECT * FROM `users` WHERE ( `name` LIKE '%поиск%' OR `email` LIKE '%поиск%' OR `text` LIKE '%поиск%' OR `id` LIKE '%поиск%' ) AND ( `is_admin` = 1 ) ORDER BY title
Ищет по определённому полю и автоматически экранирует запрос для предотвращения SQL- инъекций. Принимает два параметра: первый - поле, второй - значение.
Например:
d()->User->find_by('login', 'ainu') // ищет по полю login
d()->User->find_by('username', 'ainu') // ищет по полю username
Отменяет действие выполненных ранее функций find()
, find_by_*()
, where()
Ищет по определённому полю и автоматически экранирует запрос для предотвращения SQL- инъекций. Например:
d()->User->find_by_login('ainu') // ищет по полю login
d()->User->find_by_username('ainu') // ищет по полю username
Отменяет действие выполненных ранее функций find()
, find_by_*()
, where()
get
и get_variable_value
являются синонимами.
Возвращает значение поля. В большинстве случаев d()->user->get('title')
равнозначно d()->user->title
.
Однако в случае использования этого метода будут проигнорированы функции-геттеры. Подробнее см. раздел Переопределение геттеров и сеттеров ниже. Используется для переопределения самого свойства при его получении (например, даты в удобном виде или превью для аватары).
where задаёт условия для будущего запроса и автоматически экранирует запрос для
предотвращения SQL-инъекций. Отменяет действие выполненных ранее функций find()
,
find_by_*()
, where()
Например:
d('User')->where('login = ? and password=?', $username, md5($password));
d('User')->where('login LIKE ?', '%строка%');
Выполняет SQL-запрос (если не выполнен) и возвращает массив всех данных (в виде массива объектов Active Record).
$goods = d()->Good->find_by_color('red')->all;
print $goods[0]->title;
При этом каждой строчке в базе данных соответствует один объект. Каждый из этих объектов независим, например, он может независимо выполнять метод save
после изменения его свойств.
Выполняет SQL-запрос (если не выполнен) и возвращает массив всех данных (в виде массива объектов Active Record). При этом, даже если была запрошена вся таблица, в массиве будут только данные из корня.
Допустим, текущая таблица comments. Каждый комментарий имеет поле page_id
(или, допустим, url страницы) и comment_id
, указывающий на родительский комментарий.
Тогда, получив все комментарии к одной странице, где page_id=номер страницы
, и запросив tree, мы получим только корневые элементы - комментарии. Запросив свойство tree каждого из этих комментариев, мы получим дочерние комментарии. При этом делается один запрос, и данные сортируются и прозрачно помещаются в многомерный массив.
Используется для простой выдачи многомерных деревьев (комментарии, главные меню и так далее).
По сути, функция tree всего лишь сортирует полученные данные в массиве, ориентируясь на поле element_id, где текущая таблица - elements, и возвращает результат. Таким образом одним запросом получаются все данные дерева, программно сортируются, и выводятся в виде заранее подготовленного массива с данными. Глубина вложенности может быть любой.
//Дерево комментариев
d()->comments = d()->Comment->find_by_page_id(12)->tree;
//Заголовок текстовой страницы четвёртого уровня вложенности
print d()->Text->tree[0]->tree[0]->tree[0]->tree[0]->title;
Нарезает результат для вывода построчно. Например, если в результате ожидается 10 элементов, и выполнено clients->slice(3)
, то следующий код
<foreach clients>
---
<foreach this>
{.id}
</foreach>
</foreach>
Выведет следующее:
---
1
2
3
---
4
5
6
---
7
8
9
---
10
Удобно использовать для табличного и построчного вывода.
Выполняет SQL-запрос (если не выполнен) и возвращает один объект (первый) в виде объекта. Почти всегда можно опускать (при запросе свойства запрос выполнится автоматически).
$maika = d()->Good->find(12)->one;
Немедленно выполняет SQL - запрос (до попытки получения первого свойства), и возвращает true, если получено 0 элементов.
Функции копируют данные из Active Record объекта в глобальную область объектов для использования в шаблонах
d('Text')->find_by_url(url(1))->expand_to_page;
После этого в шаблоне можно указывать {page.title}
d()->Client->find_by_name('Керхер')->expand_to_client;
После этого в шаблоне можно указывать {client.title}
d()->Client->find(374)->expand;
После этого в шаблоне можно указывать {title}
Примечание: от функции expand_to можно отказаться классическим методом:
d()->page = d('Text')->find_by_url(url(1));
После этого в шаблоне можно указывать {page.title}
Возвращает false
, если подходящих строк не было найдено.
Проверяет свойство template
текущего объекта и запускает функцию имя которой записано в этом свойстве. Возвращает результат работы функции.
См. раздел Вывод объекта ниже.
set
и set_variable_value
являются синонимами.
Устанавливает значение свойства. Следующие записи аналогичны:
d()->user->login = 'dr.pepper';
d()->set_variable_value('login', 'dr.pepper');
d()->set('login', 'dr.pepper');
Однако в случае использования этого метода будут проигнорированы сеттеры, даже если метод set_login
существует. Подробнее см. раздел Переопределение геттеров и сеттеров ниже. Используется для переопределения самого свойства при его сохранении.
save
и delete
завершают предыдущие операции, используются для установки флага
дальнейшего создания строки в базе данных, сохранения данных и удаления строки. Эти методы нужны для изменения состояния базы данных (CrUD);
delete
- удаляет последний найденный через find элемент, если элементы искались через
where, то удаляется первый. Фактически, при $var->delete;
удаляется элемент с id, равным $var->id
.
Если такой строки нет, ничего не произойдёт.
save
- сохраняет изменения. Функция new не совершает никаких действий с базой данных,
только указывает на необходимость создания новой строки в базе данных. Если до save было
сделано $var->new, то при сохранении создастся новая строка, иначе изменится последняя
найденная через find или where строка. Если при попытке сохранения для изменений после
поиска строк было несколько, то изменится первая. Фактически, при $var->save; меняется
элемент с id
= $var->id. Если такой строки нет, ничего не произойдёт.
Для задания поля необходимо присвоить значение соответствующей переменной. Например,
$var->title='Заголовок';
Внимание: регистр букв важен. Итак, основные действия:
Создание:
d()->client = d()->Client->new;
d()->client->title='ainu';
d()->client->text='Суперклиент';
d()->client->save();
Альтернативный вариант:
d()->Client->create(array(
'title'=>'ainu',
'text'=>'Суперклиент'
));
Редактирование:
d()->client = d()->Client->find(12);
d()->client->title = "Новый заголовок";
d()->client->save();
Удаление:
d()->client = d()->Client->find(12);
d()->client->delete();
Обратите внимание на то, что при использовании create
нельзя передавать весь массив параметров.
Вот пример правильного сохранения:
function create()
{
if(d()->validate('clients#update')){
d()->Client->create(array(
'title'=>d()->params['title'],
'text'=>d()->params['text']
));
header('Location: /clients/');
}
print d()->clients_edit_tpl();
}
А вот пример неправильного:
function create()
{
if(d()->validate('clients#update')){
d()->Client->create(d()->params)); //НЕПРАВИЛЬНО
header('Location: /clients/');
}
print d()->clients_edit_tpl();
}
Последний пример работает, и он более простой. Но есть одно важное предупреждение.
В последнем случае пользователь может изменить любые данные (такие, как is_admin
).
Если Вы считаете, что нужно где-либо задавать разрешённые клиентам записи, задавайте их напрямую,
как в правильных примерах. Количество написанных строк будет тем же (напишете Вы их в каком-либо конфиге,
модели, или же при сохранении).
Если (например, при редактировании или создании) не было присвоено значение полю, то в самом запросе с этим полем действий не произойдёт, т.е. изменения происходят только с теми полями, которые заданы явно.
На данном этапе абстракций нет, поэтому для указания, например, автора статьи, необходимо указывать числовое id, вместо классического присваивания объекта как значение:
$article = d()->Article->new;
$article->author_id = 21;
$article->save;
При указании ->new
можно действовать так:
d()->client = d()->Client;
d()->client->new;
d()->client->title='Имя';
d()->client->save;
или так:
d()->client = d()->Client;
d()->client->new->title='Имя';
d()->client->save;
В случае с методом delete
можно вообще обойтись без промежуточного объекта:
d()->Client->find(7)->delete;
Данные функции являются низкоуровневыми, программисту надо следить, что он удаляет. Например, попытка удаления несуществующего объекта, может вернуть ошибку изза отсуствия поля $var->id
.
Как правило, при запросе конструкции d()->Объект->свойство
вернётся значение соотвествующей ячейки по имени столбца.
Однако эту конструкцию можно использовать и в других целях (если такого столбца нет).
Если запрашивается свойство, заведомо не являющееся столбцом таблицы и именем таблицы, то проводится попытка найти соотвествующую строку по полю url (возможно, в дальнейшем - name):
//SELECT * from texts where url='about'
d()->Text->about->title;
d()->Option->email_options->send_to;
d()->User->admin->password;
При попытке вывести объект как строку, проверяется свойство template и выполняется шаблон, находящийся в этом свойстве.
При этом в d()->this записывается текущий объект. Аналогичное действе выполяет функция/свойство show
.
Допустим, в поле template
пользователя находится значение vip_user
(но может находиться и другое значение, например, common_user
)
Файл vip_user.html:
<h1>Элитный пользователь {this.login}</h1>
<img src="/crown.gif" alt="Корона">
<img src="{this.avatar}" alt="Аватар">
Контроллер:
d()->current_user = d()->User->find_by_login($login);
print d()->d()->current_user->show();
Более короткая запись того же самого действия:
print d()->user->find_by_login($login);
По сути, механизм работы крайне прост (ниже приведён сам код модуля):
public function __toString()
{
return $this->show();
}
public function show()
{
if($this->template!='') {
d()->this = $this;
return d()->call($this->template);
}
return '';
}
Для использования переопределения геттеров и сеттеров необходимо объявить класс модели, если он её не объявлен.
Например, если мы используем конструкцию print d()->User->avatar_preview
, то следует объявить класс User:
class User extends ar
{
function avatar_preview()
{
return preview($this->avatar);
}
}
Для запроса переменной в обход функции используется метод get(имя_переменной)
или её синоним get_variable_value(имя_переменной)
.
Например:
class User extends ar
{
function avatar()
{
return preview($this->get('avatar'));
}
}
Это необходимо для избавления от возможной рекурсии, как в следующем примере:
class User extends ar
{
function avatar()
{
// Тут будет бесконечно вызываться метод avatar ()
return preview($this->avatar));
}
}
Для использования сеттеров метод должен начинаться с _set
:
class User extends ar
{
function set_avatar_preview($value)
{
$this->avatar = unpreview($value);
}
}
Если необходимо переопределить то свойство, которое мы задаём, можно использовать метод set_variable_value(имя_переменной, значение)
(или его синоним set(имя_переменной, значение)
):
class User extends ar
{
function set_date($value)
{
$this->set_variable_value('date',user_date_to_caninical_date($value));
}
}
В последнем примере при попытке указать d()->user->date='13.03.2011';
перед сохранением данные будут обработаны.
Также можно вообще не использовать присваивания в обработчике, тогда при сохранении объекта данное свойство будет проигнорировано.
При переопределении свойством самого себя без этого метода может образоваться рекурсия:
class User extends ar
{
function set_date($value)
{
//Вот тут вызовется set_date(), и так будет продолжаться до падения скрипта
$this->date = user_date_to_caninical_date($value);
}
}
Данные можно объединять в цепочки, например:
function clients_sorted()
{
d()->clients=d()->Client->banned;
print d()->clients_index_tpl();
d()->clients=d()->Client->admins;
print d()->clients_index_tpl();
d()->clients=d()->Client->admins->banned;
print d()->clients_index_tpl();
}
class Client extends ar{
function banned()
{
$this->where('is_banned = 1');
return $this;
}
function admins()
{
$this->where('is_admin = 1');
return $this;
}
}
Функции banned
и admins
ничем не отличаются от переопределённых или пользовательских свойств, их не нужно объявлять
отдельно как scopes, всё будет работать само собой.
Все три вида связей (one-to-many, many-to-one, many-to-many) объявляются автоматически.
TODO:
$user->posts
$post->user
Связь many-to-many легко реализуется через объект-посредник. Если таблицу, связывающую таблицы groups и categories назвать permissions, то можно использовать следующую конструкцию:
<foreach d()->Group(2)->categories_throw_permissions>
{this.title}<br>
</foreach>
Другой вариант использования (меньше магии):
<foreach d()->Group(2)->permissions->all_of_categories>
{this.title}<br>
</foreach>
Или (нет магии):
<foreach d()->Group(2)->permissions->all_of('categories')>
{this.title}<br>
</foreach>
Если не хватает фантазии для именования таблиц, можно просто назвать её categoriestogroups.
Три варианта в этом случае:
<foreach d()->Group(2)->categories_throw_categories_to_groups>
Другой вариант использования (меньше магии):
<foreach d()->Group(2)->categories_to_groups->all_of_categories>
Или (нет магии):
<foreach d()->Group(2)->categories_to_groups->all_of('categories')>
Простое правильное именование таблицы позволяет сильно упростить код.
Простая реализация many-to-many:
class User
{
function groups()
{
return $this->groups_throw_groups_to_users;
}
}
Реализация (например, список групп пользователя):
<foreach d()->Auth->user->groups>
{.title},
</foreach>
Пока в данном случае возвращается массив (не ленивый запрос) в связи с несколькими таблицами. Таким образом, нельзя сделать
d()->Auth->user->groups->all_titles //Так нельзя