Мапперы используются для записи данных из объектов в базу данных (persistence layer) и, наоборот, из persistence layer в объекты. Основное назначение мапперов - избавить объекты от знания информации о том, как и где они хранятся.
Мапперы реализуют простейший интерфейс маппера:
Начиная с версии alpha3 мапперы также реализуют интерфейсы finder-ов. Чаще всего это метод findById($id), однако в некоторых случаях подобных методов может быть достаточно много.
В ядро системы входят мапперы, которые работают с базой данных в качестве persistence layer-а, однако ничего не мешает сделать маппер, который будет, например, хранить объекты в файле, сессии или куках.
Для получения мапперов клиенты могут использовать метод lmbBaseToolkit :: createDataMapper($name). Например, так:
$toolkit = Limb :: toolkit(); $mapper = $toolkit->createDataMapper('ArticleMapper');
Для поиска мапперов по умолчанию используется DataMapperFileResolver, который ищет мапперы в папке LPKG_CORE_DIR/core/orm/data_mappers/ или /data_mappers/ в пакетах.
Данная диаграмма показывает несколько классов мапперов, которые входят в ядро Limb. Стоит также отметить, что, роль интерфейса у нас выполняет класс lmbAbstractDataMapper, который на диаграмме не показан.
| Маппер | Описание |
|---|---|
| TreeNodeMapper | Работает с объектом класса lmbTreeNode (элемент дерева). Использует объект с интерфейсом lmbTree для записи данных в дерево и обратно. |
| lmbOneTableObjectMapper | Записывает информацию об объектах, чьи атрибуты хранятся в одной записи одной таблицы. Получает название класса lmbDbTable, который записывает и считывает данные из этой таблицы. |
| VersionedObjectMapper | Может использоваться для управления процессом создания версий объектов. Он реализует паттерн Decorator, в качестве делегируемого маппера можно ему передать любой маппер, в том числе и сложный (композитный). VersionedObjectMapper при вызове метода update будет вызывать у делегируемого маппера метод insert. Таким образом создаются версии (копии) объекта, и в будущем можно будет получить все версии одного объекта. В реальном приложении данный маппер ни разу не использовался, так что ничего более конкретного мы сказать не можем. |
Остальные мапперы обычно пишутся для конкретных приложений, так как маппер обычно очень сильно привязан к определенной структуре базы данных, а мы хотим сделать Limb по-настоящему абстрактной библиотекой.
Приведем пример маппера доменного объекта класса Recipe. Recipe имеет атрибут $recipe_items - коллекцию ингредиентов, а также атрибут $dish (посуда, на котором все это хозяйство подается)- объект класса Dish. Пусть посуда имеет отношение 1..N с рецептами, то есть одна посуда может быть использована в нескольких рецептах. Путь между Recipe и Ingredient есть отношение N..M и оно моделируется через объект класса RecipeItem (например, с аттрибутами $amount, $time, $order). Класс Recipe показан на странице документации по доменным объектам.
class RecipeMapper extends lmbOneTableObjectMapper { function _defineClassName() { return 'Recipe'; } function _doMapRecordToObject($ds, $object, $prefix) { $object = parent :: _doMapRecordToObject($ds, $object, $prefix); if(!$this->_isMapped($object->getDish()) && ($dish_id = $object->get('dish_id'))) { $dish = $this->_generateProxyFor('Dish', $dish_id); $object->setDish($dish); } if(!$this->_isMapped($object->getRecipeItems())) { $dao = new RecipeItemsDAO($object->getId()); $mapper = $this->_createMapperFor('RecipeItem'); $proxy = new lmbPersistentObjectProxyCollection($dao, $mapper, new RecipeItem()); $object->setRecipeItems($proxy); } return $object; } function save($object) { if($dish = $object->getDish()) $object->set('dish_id', $dish->getId()); $this->_markAsDeletedRemovedCollectionItems($object->getRecipeItems()); parent :: save($object); } function delete($object) { foreach($object->getRecipeItems() as $recipe_item) $this->uow->delete($recipe); parent :: delete($object); } }
Прокомментируем код.
class RecipeMapper extends lmbOneTableObjectMapper { [...] }
Маппер RecipeMapper наследуется от lmbOneTableObjectMapper. lmbOneTableObjectMapper является маппером, которых позволяет хранить данные по доменному объекту в одной таблице базы данных. lmbOneTableObjectMapper для работы с таблицей базы данных использует lmbTableGateway или один из его потомков. В нашем случае маппер будет работать с таблицей recipe через класс RecipeDbTable. Большинство доменных объектов являются одно-табличными, поэтому мы чаще всего будем наследовать мапперы доменных объектов от lmbOneTableObjectMapper.
function _defineClassName() { return 'Recipe'; }
Маппер должен знать, доменные объекты какого класса он загружает. Каждый маппер доменного объекта должен перекрывать данный метод.
function _doMapRecordToObject($ds, $object, $prefix) { $object = parent :: _doMapRecordToObject($ds, $object, $prefix); [...]
RecipeMapper наследуется от lmbOneTableObjectMapper, который в свою очередь наследуется от lmbAbstractDataMapper. lmbAbstractDataMapper имеет базовую реализацию метода mapRecordToObject, в которой производится проверка наличия кеша, регистрация загружаемого объекта в lmbUnitOfWork и некоторые другие операции. Дочерние классы lmbAbstractDataMapper должны в свою очередь перекрывать метод _doMapRecordToObject, что мы и сделали. И еще, parent :: _doMapRecordToObject в данном случая будет означать помещение в $object данные из $ds, которые соответсвуют полям таблицы recipe.
if(!$this->_isMapped($object->getDish()) && ($dish_id = $object->get('dish_id'))) { $dish = $this->_generateProxyFor('Dish', $dish_id); $object->setDish($dish); }
После загрузки в доменный объект Recipe данных из таблицы recipe необходимо загрузить остальные атрибуты. Однако в целях снижения нагрузки на базу данных, маппер не загружает реальный объект класса Dish, а создает proxy-объект класса lmbPersistentObjectProxy. $this→_generateProxyFor('Dish', $dish_id); - это метод класса lmbAbstractDataMapper, который позволяет сократить размер кода для создания такого прокси. Обратите внимание на проверку $this→_isMapped($object→getDish()) - таким образом мы проверяем, не находится ли уже в загружаемом объекте необходимый атрибут. Такое может случиться, когда объекты загружаются целыми группами по результатам одного запроса.
if(!$this->_isMapped($object->getRecipeItems())) { $dao = new RecipeItemsDAO($object->getId()); $mapper = $this->_createMapperFor('RecipeItem'); $proxy = new lmbPersistentObjectProxyCollection($dao, $mapper, new RecipeItem()); $object->setRecipeItems($proxy); }
То же самое для атрибута $recipe_items. Для создания $proxy-объекта для атрибута $recipe_items создается объект класса lmbPersistentObjectProxyCollection и в его конструктор передается $dao, который позволит загрузить данные по реальным объектам, $mapper, которых из этих данных сможет сделать реальные объекты и пустой объект класса RecipeItem, которыми будет наполнена коллекция $recipe_items.
function save($object) { if($dish = $object->getDish()) $object->set('dish_id', $dish->getId()); $this->_markAsDeletedRemovedCollectionItems($object->getRecipeItems()); parent :: save($object); }
При сохранении рецепта маппер проверяет, изменилась ли коллекция $recipe_items и помечает в lmbUnitOfWork удаленные объекты класса RecipeItem. Для того, чтобы у нас хранилась информация о удаленных элементах коллекции $recipe_items, эта коллекция должна быть объектом класса lmbPersistentObjectCollection. Обычно об этом не нужно беспокоиться, так как lmbPersistentObjectProxyCollection создает именно такую коллекцию, когда $proxy заменяется реальным объектом.
function delete($object) { foreach($object->getRecipeItems() as $recipe_item) $this->uow->delete($recipe); parent :: delete($object); }
Так как Recipe и Ingredient связаны отношением много-ко-многим при удалении рецепта нам необходимо удалять все RecipeItems связанные с удаляемым рецептом. Для этого мы помечаем все элементы коллекции на удаление в lmbUnitOfWork.
Теперь можно рассмотреть небольшой пример маппера, использующего COOKIE для хранения информации. Данный маппер может быть использован, например, для создания корзины товаров электронного магазина.
class CartMapper extends lmbAbstractDataMapper { function _defineClassName() { return 'Cart'; } function _doMapRecordToObject($ds, $object, $prefix) { if(!$object->getItems()->isChanged()) { $ids = $this->_getIds(); $dao = new CartItemsDAO($ids); $mapper = $this->_createMapperFor('ShopItem'); $proxy = new lmbPersistentObjectProxyCollection($dao, $mapper, new ShopItem()); $object->setItems($proxy); } return $object; } function save($object) { $ids = array(); foreach($object->getItems() as $shop_item) $ids[] = $shop_item->getId(); $response = Limb :: toolkit()->getResponse(); if(sizeof($ids)) $response->setCookie('CartItems', implode(',', $ids)); else $response->setCookie('CartItems', ''); } function _getIds() { $request = Limb :: toolkit()->getRequest(); $cookie = $request->getCookie('CartItems'); if($cookie == null || $cookie == 'deleted') return array(); $ids = explode(',', $cookie); foreach($ids as $key => $id) $ids[$key] = (int)$id; return $ids; } function findCurrent() { $cart = $this->toolkit->createObject($this->_defineClassName()); $ds = new Dataspace(); $ds->set('id', $cart->getId()); return $this->mapRecordToObject($ds, $cart); } }
Что здесь интересного: