RSS Мои друзья Контакты

Magento модели от А до Я: модели ядра

Поскольку Magento построена, как MVC приложение, то было бы логичным найти в базовом функционале подобие ORM или ActiverRecord. Для четкого разделения функицонала модели разделены на 2 типа: отвечающие за бизнес логику и за предоставление данных. Последние обычно работают с базой данных, но это может быть и что угодно другое (csv, plain text, etc.). Бизнес и ресурс модели связаны между собой посредством конфигурации модуля. Для работы с базой и определения бизнес логики существуют 3 абстрактных класса: Mage_Core_Model_Abstract, Mage_Core_Model_Mysql4_Abstract, Mage_Core_Model_Mysql4_Collection_Abstract. Начиная с версии 1.6 в именах моделей Mysql4 заменено на Resource в связи с реализацией поддержки разных СУБД в Magento.

Бизнес модели

Обычно все бизнес модели наследуются от Mage_Core_Model_Abstract. Поскольку в последнем реализован базовый функционал работы с ресурс моделью. Сам же класс наследует весь функционал Varien_Object. Это очень удобно, потому что он реализовывает магические методы доступа к данным. Любая таблица имеет набор полей и строк. Одна бизнес модель - одна строка в базе. Например для блоков сайта существует таблица cms_block. Она имеет поле title. Чтобы получить его значение для определенного блока достаточно написать

echo $cmsBlock->getTitle();

Аналогично со всеми другими полями. Нужно придерживаться правила: в базе, в именах сложных полей, слова разделять при помощи нижнего подчеркивания, каждое слово с маленькой буквы, например is_active. Тогда в коде через бизнес моделей к этому полю можно обратится при помощи выражения

echo $cmsBlock->getIsActive();

Класс имеет несколько интересных свойств: $_eventPrefix, $_eventObject и $_isObjectNew. Первый позволяет задать префикс для событий в классе наследнике (по умолчанию значение равно core_abstract), второй задает имя параметра в объекте события для получения модели (по умолчанию object), а третий определяет является ли объект новым созданным или загруженым из ресурс модели (обычно это база данных).

Класс выбрасывает следующие события:

  • model_load_before, $_eventPrefix + ' _load_before' - вызываются в момент загрузки модели, вызов метода load
  • model_load_after, $_eventPrefix + '+load_after' - вызываются после загрузки модели
  • model_save_commit_after, $_eventPrefix + '_save_commit_after' - вызывается внутри ресурс модели, перед комитом транзакции
  • model_save_before, $_eventPrefix + '_save_before' - вызывается перед сохранением данных, вызов метода save
  • model_save_after, $_eventPrefix + '_save_after' - вызывается после сохранения модели
  • model_delete_before, $_eventPrefix + '_delete_before' - вызывается перед удалением данных, вызов метода delete
  • model_delete_after, $_eventPrefix + 'delete_after' - вызывается после удаление данных
  • $_eventPrefix + '_clear' - вызывается во время очистки объекта от циклических ссылок, вызов метода clearInstance (появился с версии 1.5.1.0)

Во всех случаях когда подряд вызывается 2 события, во втором в качестве параметров передается массив, который можно изменять при помощи защищенной ф-ции _getEventData в каждом классе наследнике отдельно.

Бизнес модель имеет несколько основных открытых методов, о которых я уже упомянул:

  • load - загружает данные в модель. Принимает 2 параметра: значение по которому нужно искать (primary или unique поле) и второй, необязательный, имя поля по которому нужно искать, по умолчанию используется поле primary key
  • save - сохраняет данные
  • delete - удаляет данные, в случае с СУБД, строку из базы
  • isObjectNew - проверяет является ли объект загруженым из базы или создан. Не рекомендую использовать его для проверки! По скольку он возвращает установлено ли значение идентификатора для модели (строки базы, primary key). Для проверки лучше использовать метод getOrigData() - возвращает массив  реальных не измененных данных, которые находятся в базе для этой модели. Если массив пуст значит объект "новый", если нет - значит нет
  • hasDataChanges - проверяет изменились ли данные
  • cleanModelCache - чистит кэш для этой модели по тегам
  • clearInstance - удаляет циклические ссылки для модели (в основном используется для моделей продуктов). Когда Вы уверенны, что объект продукта больше не нужен вызывайте этот метод. Это предотвратит возможные утечки памяти (метод доступный с версии 1.5.1.0)
  • getResource - возвращает ресурс модель

Например, для моделей CMS блоков

$block = Mage::getModel('cms/block')
    ->setStoreId(Mage::app()->getStore()->getId())
    ->load(12);

echo $block->getId(); # 12

$block->setTitle('Test CMS Block')
    ->save();

$block = Mage::getModel('cms/block')->setId(33);

var_dump($block->isObjectNew()); # false, ERROR!

$isObjectNew = empty($block->getOrigData());
var_dump($isObjectNew); # true

Ресурс модели

Ресурс модели предоставляют интерфейс для получения данных. Для выборки данных используется Zend_Db_Select. Для инициализации модели в классе есть защищенный метод _init, который принимает 2 параметра: table-path (например catalog/product) и имя PRIMARY поля (уникального идентификатора строки в таблице).

В качестве примера можно посмотреть, как это сделано в модуле Mage_Cms для блоков

class Mage_Cms_Model_Mysql4_Block extends Mage_Core_Model_Mysql4_Abstract
{
    protected function _construct()
    {
        $this->_init('cms/block', 'block_id');
    }
............................................................
}

Получить реальное имя таблицы (так как она называется в базе данных) можно при помощи метода getTable(), который принимает единственный параметр table-path.

В Magento за чтение и запись отвечают разные объекты соединений. Получить доступ к ним внутри класса можно при помощи методов _getReadAdapter и _getWriteAdapter соответственно. Объект SELECT-а можно получить только при помощи read соединения, а вставка и обновление данных происходит при участии write соединения. Например, класс имеет защищенный метод _getLoadSelect, который возвращает объект SELECT-а, так давайте посмотрим что там внутри

$select = $this->_getReadAdapter()->select()
   ->from($this->getMainTable())
   ->where($this->getMainTable().'.'.$field.'=?', $value);

Эта строка вызывается для загрузки данных в методе load, который принимает 3 параметра: объект бизнес модели, значение поля, по-которому ищем и третий необязательный имя поля по-которому ищем. Если последний параметр не передать, то в качестве поля по-которому будет происходить поиск будет использован PRIMARY KEY.

Также класс имеет метод save с единственным параметром - объект бизнес модели. Внутри него происходят разные проверки, например, на уникальность некоторых полей, что нужно делать update или insert, определяются поля таблицы при помощи запроса DESCRIBE TABLE (выполнение запроса кэшируется). Метод delete также принимает только один параметр - бизнес модель.

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

  • _afterLoad
  • _beforeSave
  • _afterSave
  • _beforeDelete
  • _afterDelete

Данный класс также имеет еще одно полезное свойство - $_serializableFields. Это массив. В нем можно указать, какие поля в базе хранятся в сериализированом виде и при загрузке модели они автоматически будут десереализированы. Структура массива следующая

/**
 * Structure: array(
 *     <field_name> => array(
 *         <default_value_for_serialization>,
 *         <default_for_unserialization>,
 *         <whether_to_unset_empty_when serializing> // optional parameter
 *     ),
 * )
 *
 */

При сохранении данных будет произведено обратное действие над такими полями. Это позволяет не думать о формате хранения и сосредоточится на бизнес логике.

Также при помощи метода addUniqueField можно добавить имена уникальных полей и при сохранении Magento проверит их уникальность. Но это не очень удобный способ.

Псевдо-ресурс модели

К этому типу было решено (мною) отнести классы коллекций. Так как они находятся среди ресурс моделей (по причине того, что они делают запрос в базу на выборку множества строк), но по сути являются контейнером для бизнес моделей, которые в свою очередь зависят от ресурс моделей. Какой-то замкнутый круг. Я думаю, что это кодоляп. Разработчики не захотели усложнять коллекции, по-этому в таких классах можно увидеть смешанную бизнес и ресурс логику.

Класс коллекции Mage_Core_Model_Mysql4_Collection_Abstract является потомком Varien_Data_Collection_Db, который рассматривался в предыдущей статье. Как и ресурс модель он инициализируется при помощи метода _init, который принимает один обязательный параметр Magento-path бизнес модели и второй необязательный Magento-path ресурс модели, если не задан, то второй равен первому.

Имя основной таблицы можно получить при помощи метода getMainTable() (аналогичный существует в классе ресурс модели). Объект запроса можно получить вызвав метод getSelect(). В классе Zend_Db_Select реализован магический метод __toString, который превращает объект в строку SQL запроса.

Для выборки значения в котором присутствуют SQL ф-ции реализован интересный метод addExpressionFieldToSelect. Но использовать его я не рекомендую. Лучше написать отдельный метод для коллекции чем мешать логику. Приведу php-doc из источников

/**
 * Add attribute expression (SUM, COUNT, etc)
 *
 * Example: ('sub_total', 'SUM({{attribute}})', 'revenue')
 * Example: ('sub_total', 'SUM({{revenue}})', 'revenue')
 *
 * For some functions like SUM use groupByAttribute.
 *
 * @param string $alias
 * @param string $expression
 * @param array $fields
 * @return Mage_Eav_Model_Entity_Collection_Abstract <- кодоляп =)
 */

Есть метод removeFieldFromSelect при помощи которого можно удалить поле из запроса.

Получить ресурс модель можно вызвав метод getResource. Получить все идентификаторы можно при помощи метода getAllIds. Также существует метод save, который в цикле вызывает сохранение для всех моделей находящихся в коллекции.

Класс выбрасывает 4 события:

  • core_collection_abstract_load_before, $_eventPrefix + '_load_before'
  • core_collection_abstract_load_after, $_eventPrefix + '_load_after'

Magento path

Несколько раз я уже упомянул о таком понятии как Magento path. На самом деле это что-то на подобии xpath к настройкам модуля в зависимости от метода, которому он передается. Помним что в настройках модуля есть такие директивы как: blocks и models. И обычно в параметре class прописано что-то вроде %module_name%_Block и %module_name%_Model соответственно. Таким образом мы указываем Magento префиксы для классов моделей и блоков.

Magento path состоит из 2 частей: названия модуля и пути к объекту. Рассмотрим пример

$product = Mage::getModel('catalog/product'); # create model
$block   = Mage::app()->getLayout()->createBlock('core/template'); # create block

И в настройках для моделей модуля Mage_Catalog

<models>
    <catalog>
        <class>Mage_Catalog_Model</class>
        <resourceModel>catalog_resource_eav_mysql4</resourceModel>
    </catalog>
............................................................
</models>

Magento смотрит секцию моделей (models) и ищет там директиву catalog. Если нашла берет то, что внутри параметра class, как префикс имени класса. Все первые буквы слов после слэша, которые разделены символом нижнего подчеркивания, приводятся к верхнему регистру и конкатенируются с префиксом, т.е. в данном примере получается, что имя класса равно Mage_Catalog_Model_Product. Аналогично все происходит и с блоками.

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

Скоупы Magento

В Magento существует 3 скоупа (3 разные директории) для кода: app/code/community, app/code/core, app/code/local. Первый предназначен для сторонних расширений скаченных и установленных при помощи magento connect менеджера. Второй - это ядро Magento, классы в этом скоупе не рекомендуется менять. И третий для своих локальных наработок и модификаций. Наличие этих скоупов дает возможность переопределять (при необходимости) классы ядра или расширения.

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

P.S.: в следующей статье рассмотрим возможность перезаписи классов и проблемы связанные с этим

Добавить комментарий

Комментариев: 4

  • Дмитрий
    Ответить 25 февраля 2012 г., 19:46
    Спасибо за статью.
  • Виталий
    Ответить 15 июня 2013 г., 16:18
    Классная статья. А почему Вы больше ничего не пишете из статей?
    • Сергей (Администратор)
      Ответить 17 июня 2013 г., 0:20
      С Magento я работаю сейчас только на уровне хобби (исправляю баги в 0 Step Checkout). Сейчас больше пишу на Ruby & JavaScript. На статьи особо времени не хватает, а если честно, то идей хороших для статей у меня нет пока.
  • Дмитрий
    Ответить 5 июля 2016 г., 9:50
    Спасибо. Очень помогли.