Рейтинг@Mail.ru

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:

<?php
$m = get_messages(true, $curr_uid);
foreach($m as $message) {
// Mark as bold unread messages.
if($message['flag_unread']) {
$b_open = '';
$b_close = '
';
} else {
$b_open = '';
$b_close = '';
}
// End mark.
echo "

";
}
?>

Del Message ID Date From Title
$b_open$message[id]$b_close $b_open" . date('d.m.y H:i', $message['date']) . "$b_close $b_open$message[uid_from]$b_close $b_open$message[title]$b_close

&lt?php
// End of inbox section.

// Outbox section.
?>


Outbox:

<?php
$m = get_messages(false, $curr_uid);
foreach($m as $message) {
// Mark as bold unread messages.
if($message['flag_unread']) {
$b_open = '';
$b_close = '
';
} else {
$b_open = '';
$b_close = '';
}
// End mark.
echo "

";
}
?>

Del Message ID Date To Title
$b_open$message[id]$b_close $b_open" . date('d.m.y H:i', $message['date']) . "$b_close $b_open$message[uid_to]$b_close $b_open$message[title]$b_close

?>
// End of outbox section.

// View message section.
if((int)@$_REQUEST['message_id']) {
$m = get_message($_REQUEST['message_id'], $curr_uid);
if($m !== false) {
echo "


View message #$m[id]:";
echo '
';
echo "
From: $m[uid_from]
To: $m[uid_to]
Title: $m[title]
Message: $m[text]

";
}
}
// End of view message section.

// Send message section.
echo "


New message:

From: $curr_uid
To:
Title:
Message:

";
// End of send message section.
?>

$conn->export->exportClasses(array(‘PrivateMessage’)) — это строка создаст таблицу private_messaages, если таковой ещё нет. Так что можно её перенести в установочный скрипт, но поскольку у нас нет такого, то мы её оставим.
$curr_uid — эта переменная по идее должна браться из сессии. Как она должна называться в сессии я не знал, поэтому присвоил ей значение в коде 1.
get_messages($inbox, $uid) — возвращает все входящие сообщения пользователя $uid, если $inbox равна true, иначе — исходящие. Неплохо было бы добавить лимит на выборку записей (например, с 1й по 20ю, с 21й по 40ю), но поскольку этот модуль нигде применять не планирую, то и доделывать его нет желания. Если кому то надо — пишите в комменты ;-).
get_message($id, $uid) — возвращает сообщение $id, $uid передаётся во все функции для проверки принадлежности сообщений текущему пользователю. Ещё раз повторю, идентификатор ползьователя должен браться из сессии.
delete_messages($ids, $uid) — $ids — массив id’шников сообщений, которые надо удалить. Реально удаляются записи из БД только те, у которых flag_del_from и flag_del_to равны 0.
send_message($to, $title, $text, $uid) — $to — id получателя, $uid — отправителя, остально думаю понятно.
Дальше по коду идёт часть с html-выводом и обработкой переданных парамтеров, так что ничего интересного. Код по моему везде достаточно просто, так что подробно объяснять не стал. Если вопросы остались — в комменты.

P.S. Это моя первая попытка сделать что-либо с использованием Doctrine :).
Ссылки к статье:
http://ru.wikipedia.org/wiki/ORM — ORM;
http://www.doctrine-project.org/ — официальный сайт проекта Doctrine.

web, Программирование , ,

Пожалуйста, оцените полезность и качество данной статьи. Одна звезда - плохо, 5 - хорошо.
1 звезда2 звезды3 звезды4 звезды5 звёзд (8 голосов, средний: 4,63 из 5)
Loading ... Loading ...

  1. Maxim
    22 Март 2010 в 14:15 | #1

    Спасибо за статью. Пригодилась. Вы бы выложили исхдники sql и набор тестовых данных для таблицы.

  2. 22 Март 2010 в 20:46 | #2

    Ох, что-то страшное тут случилось с форматированием исходников :(, надо поправить.

    @Maxim
    Надо будет глянуть осталось ли что-нибудь у меня от проекта, в котором doctrine использовался. Но помнится мне ручками я только базу создавал, для создания таблиц и их структуры что-то прописывал в bootstrap.php, возможно это что-то есть в этом примере ;), на неделе постараюсь ответить подробнее, если сам не разберётесь к тому моменту :).

  3. 28 Март 2010 в 21:15 | #3

    @Maxim
    Ну в общем то как и ожидалось, за год исходники успели потеряться, осталось только тут, да и не знаю насколько актуально, doctrine уже альфа второй версии появилась, так что советую посмотреть в официальных доках :). Таблицы doctrine умеет сам создавать (если не ошибаюсь, эта строчка $manager->setAttribute(Doctrine::ATTR_EXPORT, Doctrine::EXPORT_ALL)), данные можно в процессе тестирования примера нагенерировать :).

  4. scanner85
    6 Апрель 2010 в 06:08 | #4

    Насчет набора тестовых данных Максим немного наверное спутал — тесты для тестов, девел для девел, а продакшн для продакнш. Вторая ветка доктрины все еще сыровата. Существующая доктрина легко творит простые чудеса: схемы со связями в виде YML файла, построение моделей, SQL файлов, и прочие вкусности. Важно то,что доктрина существенно облегчает нашу жизнь, и делает ее комфортнее :)

  5. 13 Апрель 2010 в 21:23 | #5

    Вторая ветка доктрины все еще сыровата. — на то она и альфа :)

    Спасибо за ответ, думаю вопрошающему он поможет).

  6. paranoid
    17 Сентябрь 2010 в 13:44 | #6

    а можно файлы обновить?
    а то их удалили

  7. 23 Сентябрь 2010 в 21:12 | #7

    @paranoid
    К сожалению не осталось архива локально, но там должно быть всё то же самое, что и в посте, только сам doctrine надо с оф сайта скачать и положить рядом.

  8. paranoid
    27 Сентябрь 2010 в 13:47 | #8

    )) подсветка синтаксиса ОООЧЕНЬ корявая — после неё нужно в каждой строчке по пиццот символов исправлять

  9. 30 Сентябрь 2010 в 23:02 | #9

    @paranoid
    Пардоньте, скорей проблема в автозамене вордпресса, но именно в этом посте правил чтобы было всё нормально.

    Хмм или Вы не о том, потому как выглядит всё более-менее в порядке… Попробуйте на кнопочку «view source» нажать, которая появляется сверху справа при наведении на блок кода :).

  1. Пока что нет уведомлений.
*