Модуль Active Record (mod_orm)

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(имя_переменной).

Подробнее см. раздел Переопределение геттеров и сеттеров ниже.

Методы

find()

Осуществляет поиск по id

d()->User->find(34);

Непосредственно запрос выполнится перед получением данных (ленивый запрос).

Отменяет действие выполненных ранее функций find(), find_by_*(), where()

Если в конструктор передано одно число, оно также перейдёт в метож find:

$user = new User(3); //Аналогично $user = new User(3); $user->find(3);

Таким образом, можно использовать следующую упрощённую конструкцию:

d()->User(34);

search()

Осуществляет обычный поиск по содержимому

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

find_by()

Ищет по определённому полю и автоматически экранирует запрос для предотвращения SQL- инъекций. Принимает два параметра: первый - поле, второй - значение.

Например:

d()->User->find_by('login', 'ainu')     // ищет по полю login
d()->User->find_by('username', 'ainu') // ищет по полю username

Отменяет действие выполненных ранее функций find(), find_by_*(), where()

find_by_<field>

Ищет по определённому полю и автоматически экранирует запрос для предотвращения SQL- инъекций. Например:

d()->User->find_by_login('ainu')    // ищет по полю login
d()->User->find_by_username('ainu') // ищет по полю username

Отменяет действие выполненных ранее функций find(), find_by_*(), where()

get ($name)

getvariablevalue ($name)

get и get_variable_value являются синонимами.

Возвращает значение поля. В большинстве случаев d()->user->get('title') равнозначно d()->user->title.

Однако в случае использования этого метода будут проигнорированы функции-геттеры. Подробнее см. раздел Переопределение геттеров и сеттеров ниже. Используется для переопределения самого свойства при его получении (например, даты в удобном виде или превью для аватары).

where()

where задаёт условия для будущего запроса и автоматически экранирует запрос для предотвращения SQL-инъекций. Отменяет действие выполненных ранее функций find(), find_by_*(), where()

Например:

d('User')->where('login = ? and password=?', $username, md5($password));
d('User')->where('login LIKE ?', '%строка%');

all

Выполняет SQL-запрос (если не выполнен) и возвращает массив всех данных (в виде массива объектов Active Record).

$goods = d()->Good->find_by_color('red')->all;
print $goods[0]->title;

При этом каждой строчке в базе данных соответствует один объект. Каждый из этих объектов независим, например, он может независимо выполнять метод save после изменения его свойств.

tree

Выполняет 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; 

slice()

Нарезает результат для вывода построчно. Например, если в результате ожидается 10 элементов, и выполнено clients->slice(3), то следующий код

<foreach clients>
    ---
    <foreach this>
        {.id}
    </foreach>
</foreach>

Выведет следующее:

---
1
2
3
---
4
5
6
---
7
8
9
---
10

Удобно использовать для табличного и построчного вывода.

one

Выполняет SQL-запрос (если не выполнен) и возвращает один объект (первый) в виде объекта. Почти всегда можно опускать (при запросе свойства запрос выполнится автоматически).

$maika = d()->Good->find(12)->one;

is_empty

Немедленно выполняет SQL - запрос (до попытки получения первого свойства), и возвращает true, если получено 0 элементов.

expand и expand_to

Функции копируют данные из 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, если подходящих строк не было найдено.

show

Проверяет свойство template текущего объекта и запускает функцию имя которой записано в этом свойстве. Возвращает результат работы функции.

См. раздел Вывод объекта ниже.

set ($name, $value)

setvariablevalue ($name, $value)

set и set_variable_value являются синонимами.

Устанавливает значение свойства. Следующие записи аналогичны:

d()->user->login = 'dr.pepper';
d()->set_variable_value('login', 'dr.pepper');
d()->set('login', 'dr.pepper');

Однако в случае использования этого метода будут проигнорированы сеттеры, даже если метод set_login существует. Подробнее см. раздел Переопределение геттеров и сеттеров ниже. Используется для переопределения самого свойства при его сохранении.

new, save и delete

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);
    }
}       

Цепочки (scopes)

Данные можно объединять в цепочки, например:

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 //Так нельзя

comments powered by Disqus