1. Модель данных, таблицы и связи
Многие задавались вопросом, как правильно задать метод
$articles->findAllByCategoryId($categoryId);
или
$category->findAllArticles();
Причем оба варианта подразумевают связывание объектов — $category «знает» об $articles или наоборот.
А можно ли обойтись без таких методов, которые слишком много знают? Кто-то предпочитает doctrine, я же пошел своим путем – написал несколько классов, наследовав их от Zend_Db_Table_Abstract и Zend_Db_Table_Row_Abstract
Цели были
1) получить тонкий и легко читаемый контроллер,
(Удивительно, но модель при таком подходе также осталась не очень толстой)
2) сделать очеловеченные объекты модели,
3) избавиться от реализации кучи методов getAllSomethingByAnythingAlsoWhere(...)
4) а также добиться того, чтобы модели не знали друг о друге ничего(*).
Постарался обойтись без употребления в контроллере и модели таких слов как select, adapter, fetch, Zend_Db_Expr, quote,… ну это личное.
Проще все рассмотреть на примере, который хоть и является «сферическим конем в вакууме», но довольно нагляден.
Контроллер:
<?php
class User_IndexController extends EXT_Controller_Action
{
/* Action 1 */
public function indexAction()
{
$table = new User_Model_DbTable_Users();
$users = $table->with('Address', 'User_Model_DbTable_Address', 'address_id')
->with('Location', 'User_Model_DbTable_Locations', 'Location.id = Address.location_id')
->addRelationMany('Places', 'User_Model_DbTable_Address', 'user_id')
->addRelationMany('Friends', 'User_Model_DbTable_Friends', 'user_id', true)
->findAll();
$this->view->users = $users;
}
/* Action 2 */
public function listAction()
{
$table = new User_Model_DbTable_Users();
$users = $table->withRelated()
->addRelationMany('Places', 'User_Model_DbTable_Address', 'user_id')
->addRelationMany('Friends', 'User_Model_DbTable_Friends', 'user_id', true)
->addFilter(array('OR',
array('name', 'LIKE', 'Alex%'),
array('AND',
array('Location.title', '=', 'Swietokrzyskie'),
array('Address.street', 'LIKE', 'Sovi%') )))
->setOrder('created_at DESC')
->setLimit(10)
->findAll();
$this->view->users = $users;
}
}
* This source code was highlighted with Source Code Highlighter.
А вот объекты БД (модель):
/* Таблица пользователей */
class User_Model_DbTable_Users extends EXT_Db_Table
{
/* имя таблицы */
protected $_name = 'user__users';
/* класс объекта - строки таблицы */
protected $_rowClass = 'User_Model_User';
/* связи. если они и присутствуют, то только в этом $_relations */
protected $_relations = array('Address' => array('User_Model_DbTable_Address', 'address_id'),
'Location' => array('User_Model_DbTable_Locations', 'Location.id = Address.location_id'));
/* будет ли id строки являться индексом резельтата выборки (массива) */
protected $_isIndexedRowset = true;
const AUTH_NAMESPACE = 'auth';
/* фильтры по умолчанию. Хорошо сочетаются с добавленными */
public function loadDefaultFilters()
{
if (!$this->isLoadDefaultFilters()) {
return $this;
}
$this->addFilter('is_deleted', '<>', 1);
return $this;
}
}
/* Один пользователь */
class User_Model_User extends EXT_Db_Table_Row
{
}
/* Адреса */
class User_Model_DbTable_Address extends EXT_Db_Table
{
protected $_name = 'user__address';
protected $_rowClass = 'User_Model_Address';
protected $_relations = array('Location' => array('User_Model_DbTable_Locations', 'location_id'),
'User' => array('User_Model_DbTable_Users', 'user_id'),);
}
/* Адрес */
class User_Model_Address extends EXT_Db_Table_Row
{
}
/* Друзья, таблица, связывающая user'ов' */
class User_Model_DbTable_Friends extends EXT_Db_Table
{
protected $_name = 'user__friends';
protected $_rowClass = 'User_Model_Friend';
protected $_relations = array('User' => array('User_Model_DbTable_Users', 'user_id'),
'Friend' => array('User_Model_DbTable_Users', 'friend_id'));
}
* This source code was highlighted with Source Code Highlighter.
В примере два практически одинаковых action-a. Выборка пользователей.
В основной SQL-запрос попадают данные, которые привязаны к таблице пользователей «один-к-одному» с помощью ->with() и будут доступны через объекты пользователей без дополнительных запросов к БД примерно так:
$user->getLocation()->title // синоним $user->Location__title
$user->getAddress()->street // синоним $user->Address__street
Естественно нет никаких функций getAddress() и getLocation() — они реализованы динамически через __call().
Здесь я вернусь к началу топика.
У меня в шаблоне view используются
$user->getAllFriends() // получение всех друзей пользователя
$user->getAllPlaces() // получение всех адресов пользователя
Эти несуществующие (также __call()-вызов) методы обращаются к связям, заданным в контроллере:
1) $table->with(...) // задает связь «к-одному» — эти данные будут включены в основной запрос findAll(), findRow(), findAllBy(), findRowBy().
Можно задать только необходимые присоединенные поля в запрос, уменьшив его.
Еще в with можно передавать не имя класса, а например $friendsTable — таблицу с установленными фильтрами и своими связями.
Вообще описать все нюансы может выйти дольше, чем написать сами классы. :)
2) $table->addRelationMany(...) // задает связь «ко многим» — данные в запрос не включаются, но будут подгружены из БД при первом непосредственном обращении ( $user->getAllFriends() )
Связующее поле можно задать именем, имя+префикс, полное обычное условие равенства полей.
Т.е. сами модели не связаны, но при этом связи можно прозрачно использовать.
* Вы наверняка заметили protected $_relations =…
Всё-таки бывают очевидные, постоянно использующиеся связи, которые для удобства можно (если очень хочется) прописать в единственном(!) месте в заголовке класса, не запутывая объекты избыточными связями.
Во втором моем action-е метод $table->withRelated() говорит, что эти связи надо включить в основной запрос.
Если этого не сделать, то при вызове $user->getAddress() для каждого пользователя будет дополнительно вызываться SQL.
(прим. 'Location' — таблица дерева «Страна-Область-Город»)
2. Фильтры — условия выборки
Я считаю, что у Zend_Db нет хорошего построителя where() – условий для SQL запросов.
Т.е. условие (field_a=1 OR field_b=2) AND (field_c=1 OR field_d=2) уже вызывает некоторые сложности в построении.
Во втором action-e моего контроллера я привел пример построения сложных условий на базе своего самодельного класса фильтров.
Этот фильтр нормально «переваривает» знаки равенства / неравенства в IS NULL / IS NOT NULL, IN (1,2,3) / NOT IN (1,2,3) и т.д.
Вот какая монстр-конструкция была построена этим фильтром в связке с findAll() в итоге:
SELECT `user__users`.*,
`Address`.`id` AS `Address__id`,
`Address`.`street` AS `Address__street`,
`Address`.`house` AS `Address__house`,
`Address`.`floor` AS `Address__floor`,
`Address`.`flat` AS `Address__flat`,
`Address`.`location_id` AS `Address__location_id`,
`Address`.`user_id` AS `Address__user_id`,
`Location`.`id` AS `Location__id`,
`Location`.`parent_id` AS `Location__parent_id`,
`Location`.`root_path` AS `Location__root_path`,
`Location`.`title` AS `Location__title`,
`Location`.`level` AS `Location__level`,
`Location`.`code` AS `Location__code`
FROM `user__users`
LEFT JOIN `user__address` AS `Address`
ON user__users.address_id=Address.id
LEFT JOIN `user__locations` AS `Location`
ON Location.id = Address.location_id
WHERE (((`user__users`.`is_deleted` != 1)
AND ((`user__users`.`name` LIKE 'Alex%')
OR ((`Location`.`title` = 'Swietokrzyskie') AND (`Address`.`street` LIKE 'Sovi%')))))
ORDER BY `created_at` DESC
LIMIT 10
* This source code was highlighted with Source Code Highlighter.
Есть нюанс с лишней парой скобок, но на правильность запроса это не влияет.
Вообще чаще используются более простые условия выборки, чем в примере:
->addFilter('created_at', '>=', $dateStart)
->addFilter('is_new', '=', 1)
Фильтры, заданные в loadDefaultFilters() применяются автоматически, если не сброшены принудительно этим clearFilters(false) или этим setIsLoadDefaultFilters(false). Так-же задаются сортировка и лимит по умолчанию.
Свои надстройки над Zend я поместил в отдельную библиотечку. Требует рефакторинга и прочего шлифования, но тем не менее уже работоспособно и довольно удобно (imho).
скачать — там только библиотека, модуль и таблицы из примера.
Можно «поковырять» или прикрутить к своему Zend-приложению, запустить и снова поковырять.
P.S. Если будет интересно, могу выложить еще описание своей ACL на базе Zend_Acl — там есть интересные, возможно СПОРНЫЕ (как и вышеописанные) решения. А также класс простого дерева Tree без nested sets — были причины сделать именно без. К тому же nested sets для Zend уже успешно реализован и без меня.
P.P.S.
Забыл показать шаблон вида. И удивляюсь, почему так много уделяют внимания контроллеру.
Вот собственно:
<div class="container2">
<div class="container">
<?php foreach ($this->users as $user):?>
<div>
<?= $user->name ?> (<?= $user->email ?>),
<?= $user->Location__title ?> <?= $user->getAddress()->street ?>, <?= $user->Address__house ?>
<ul>
<?php foreach ($user->getAllPlaces() as $place):?>
<li><?= $place->street ?> <?= $place->house ?> <?= $place->getLocation()->title ?></li>
<?php endforeach;?>
</ul>
<ul>
<?php foreach ($user->getAllFriends() as $friend):?>
<li>friend: <?= $friend->Friend__name ?> </li>
<?php endforeach;?>
</ul>
</div>
<?php endforeach;?>
</div>
</div>
* This source code was highlighted with Source Code Highlighter.