PHP: Пример использования ORM Doctrine.
В данном посте рассмотрим простенький пример использования ORM, а именно, Doctrine. Если кто не в курсе, то ORM (Object-Relational Mapping, объектно-реляционная проекция) — такая штука, которая обеспечивают классам прозрачный доступ к базе данных. Правда не всяким классам, а тем, которые представляют описание нашей модели данных. В общем мне бы пару лет назад узнать о такой штуке, может быть я и не забросил изучение php, и вообще, много чего полезного сделал :D. Если кто-то знаком с паттернами проектирования, то можно сказать, что Doctrine соответствует шаблону Active Record. К своему стыду, сам я не знаком с ними, поэтому ничего конкретней сказать не могу пока что :-).
Кстати, мы уже затрагивали мельком тему ORM, но только для python. Можете посмотреть тут про SQLAlchemy.
Далее будет рассмотрен пример написания модуля на php с использованием ORM Doctrine для отправки личных сообщений между пользователей.
Для начала определимся с нашей таблицей. Я решил использовать всего одну табличку, и для каждого сообщения так же хранить только одну запись в БД. И так, поля таблицы выглядят у меня примерно следующим образом:
- id — id сообщения;
- uid_from — id отправившего пользователя;
- uid_to — id получателя;
- title — заголовок сообщения;
- text — текст сообщения;
- flag_unread — флаг нового сообщения;
- flag_del_from — признак удалённого сообщения из исходящих;
- flag_del_to — признак удалённого сообщения из входящих;
- date — дата отправки в формате unix timestamp;
На этом этап проектирования у меня закончился :). Теперь перейдём к технической стороне вопроса.
Качаем тот самый Doctrine (ссылка в конце поста), находим в архиве директорию, где лежит файл Doctrine.php и копируем всё содержимое этого каталога в отдельную папку в нашем проекте (я скопировал в doctrine). Следуя документации, создаём файл bootstrap.php в корне проекта с таким содержанием:
<?php
require_once(dirname(__FILE__) . '/doctrine/Doctrine.php');
spl_autoload_register(array('Doctrine', 'autoload'));
$manager = Doctrine_Manager::getInstance();
$manager->setAttribute(Doctrine::ATTR_MODEL_LOADING, Doctrine::MODEL_LOADING_CONSERVATIVE);
$manager->setAttribute(Doctrine::ATTR_AUTOLOAD_TABLE_CLASSES, true);
$manager->setAttribute(Doctrine::ATTR_EXPORT, Doctrine::EXPORT_ALL);
$manager->setAttribute(Doctrine::ATTR_VALIDATE, Doctrine::VALIDATE_ALL);
$manager->setAttribute(Doctrine::ATTR_QUOTE_IDENTIFIER, true);
$user = 'tst';
$password = 'tst';
$host = 'localhost';
$db = 'tst';
$dsn = "mysql://$user:$password@$host/$db";
$conn = Doctrine_Manager::connection($dsn);
Doctrine::loadModels('models');
?>
Тут настраивается подключение к БД и некоторые параметры («ленивый» способ загрузки моделей, валидация данных и т.п.). Последняя строчка указывает Doctrine где искать наши модели.
Теперь в корне проекта создаём папку models и в ней файл PrivateMessage.php. Учтите, что имя файла и класса, который описан в нём, должны совпадать. Вот как у меня выглядит этот файл:
<?php
class PrivateMessage extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('uid_from', 'integer', 8, array(
'unsigned' => true,
'notnull' => true
)
);
$this->hasColumn('uid_to', 'integer', 8, array(
'unsigned' => true,
'notnull' => true
)
);
$this->hasColumn('title', 'string', 128);
$this->hasColumn('text', 'string', 255);
$this->hasColumn('flag_unread', 'boolean', array(
'default' => false,
'notnull' => true
)
);
$this->hasColumn('flag_del_from', 'boolean', array(
'notnull' => true,
'default' => false
)
);
$this->hasColumn('flag_del_to', 'boolean', array(
'notnull' => true,
'default' => false
)
);
$this->hasColumn('date', 'integer', 8, array(
'notnull' => true,
'default' => time()
)
);
}
}
?>
Тут мы не описали поле id, т.к. Doctrine создаст его автоматически, если не указано других первичных ключей (primary key). Так же в таблице не указаны индексов, что не есть хорошо. Неплохо было бы сделать их для полей uid_from и uid_to хотя бы. Но не сделаем, ибо лень, как-нибудь в другой раз ;-).
Следующим шагом создаём в корне проекта файл test.php, в котором будут описаны основные функции для работы с личными сообщениями и некоторый код для демонстрации функционала. Вот содержимое этого файла:
<?php
require_once('bootstrap.php');
$conn->export->exportClasses(array('PrivateMessage'));
#FIXME: Set current user ID from session.
$curr_uid = 1;
function get_messages($inbox, $uid) {
/*
* If $inbox == true then returns inbox of user $uid,
* else returns outbox.
*/
if($inbox) {
$q = 'pm.uid_to = ? AND pm.flag_del_to = ?';
} else {
$q = 'pm.uid_from = ? AND pm.flag_del_from = ?';
}
return Doctrine_Query::create()
->from('PrivateMessage pm')
->where($q, array($uid, 0))
->fetchArray();
}
function get_message($id, $uid) {
$q = Doctrine_Query::create()
->from('PrivateMessage pm')
->where('pm.id = ? AND (pm.uid_from = ? OR pm.uid_to = ?)', array($id, $uid, $uid));
$q = $q->fetchArray();
if(count($q)) {
if($uid == $q[0]['uid_to'] && $q[0]['flag_unread']) {
Doctrine_Query::create()
->update('PrivateMessage pm')
->set('pm.flag_unread', '?', false)
->where('pm.id = ?', $q[0]['id'])
->execute();
}
return $q[0];
} else {
return false;
}
}
function delete_messages($ids, $uid) {
/*
* $ids - array of ids of messages, that will be deleted.
* $uid - current user id.
* P.S. Broken English - is the most popular language in the world ;-).
*/
foreach($ids as $id) {
Doctrine_Query::create()
->update('PrivateMessage pm')
->set('pm.flag_del_to', '?', true)
->where('pm.id = ? AND pm.uid_to = ?', array($id, $uid))
->execute();
Doctrine_Query::create()
->update('PrivateMessage pm')
->set('pm.flag_del_from', '?', true)
->where('pm.id = ? AND pm.uid_from = ?', array($id, $uid))
->execute();
}
Doctrine_Query::create()
->delete('PrivateMessage pm')
->where('pm.flag_del_from = 1 AND pm.flag_del_to = 1')
->execute();
}
function send_message($to, $title, $text, $uid) {
$pm = new PrivateMessage();
$pm->uid_from = $uid;
$pm->uid_to = $to;
$pm->title = $title;
$pm->text = $text;
$pm->flag_unread = true;
// Если хранить в исходящих ненадо,
// то можно установить следующий флаг в true.
$pm->flag_del_from = false;
$pm->flag_del_to = false;
$pm->save();
}
?>
<?php
echo 'Current user:' . $curr_uid;
// Send message section.
if((int)@$_REQUEST['to'] && !@empty($_REQUEST['title']) && !@empty($_REQUEST['text'])) {
send_message((int)$_REQUEST['to'], $_REQUEST['title'], $_REQUEST['text'], $curr_uid);
}
// End of send message section.
// Delete messages section.
if(@count($_REQUEST['del_messages'])) {
echo '
';
delete_messages($_REQUEST['del_messages'], $curr_uid);
echo '
';
}
// End of delete messages section.
// Inbox section.
?>
Inbox:


(7 голосов, средний: 4,57 из 5)
Спасибо за статью. Пригодилась. Вы бы выложили исхдники sql и набор тестовых данных для таблицы.
Ох, что-то страшное тут случилось с форматированием исходников :(, надо поправить.
@Maxim
Надо будет глянуть осталось ли что-нибудь у меня от проекта, в котором doctrine использовался. Но помнится мне ручками я только базу создавал, для создания таблиц и их структуры что-то прописывал в bootstrap.php, возможно это что-то есть в этом примере ;), на неделе постараюсь ответить подробнее, если сам не разберётесь к тому моменту :).
@Maxim
Ну в общем то как и ожидалось, за год исходники успели потеряться, осталось только тут, да и не знаю насколько актуально, doctrine уже альфа второй версии появилась, так что советую посмотреть в официальных доках :). Таблицы doctrine умеет сам создавать (если не ошибаюсь, эта строчка $manager->setAttribute(Doctrine::ATTR_EXPORT, Doctrine::EXPORT_ALL)), данные можно в процессе тестирования примера нагенерировать :).
Насчет набора тестовых данных Максим немного наверное спутал — тесты для тестов, девел для девел, а продакшн для продакнш. Вторая ветка доктрины все еще сыровата. Существующая доктрина легко творит простые чудеса: схемы со связями в виде YML файла, построение моделей, SQL файлов, и прочие вкусности. Важно то,что доктрина существенно облегчает нашу жизнь, и делает ее комфортнее :)
Вторая ветка доктрины все еще сыровата. — на то она и альфа :)
Спасибо за ответ, думаю вопрошающему он поможет).
а можно файлы обновить?
а то их удалили
@paranoid
К сожалению не осталось архива локально, но там должно быть всё то же самое, что и в посте, только сам doctrine надо с оф сайта скачать и положить рядом.
)) подсветка синтаксиса ОООЧЕНЬ корявая — после неё нужно в каждой строчке по пиццот символов исправлять
@paranoid
Пардоньте, скорей проблема в автозамене вордпресса, но именно в этом посте правил чтобы было всё нормально.
Хмм или Вы не о том, потому как выглядит всё более-менее в порядке… Попробуйте на кнопочку «view source» нажать, которая появляется сверху справа при наведении на блок кода :).