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

Magento модели от А до Я: события и rewrite классов

Стараясь сделать свою систему максимально гибкой и расширяемой разработчики Magento предусмотрели возможность перезаписи любых классов, при чем аж двумя разными способами. Первый базируется на 3-х скоупах и особенностях автолоадера (назовем его copy-paste), а второй на конфигурации модуля (назовем его extends).

Copy-Paste

Из предыдущей статьи стало известно о скоупах Magento. Благодаря им переопределять классы можно посредством copy-paste файлов. И потом поменять логику в классе на свое усмотрение. Рассмотрим, что происходить при вызове в коде следующей строки

$product = Mage::getModel('catalog/product');

Как уже известно Magento основываясь на конфигурации динамически создаст имя класса, в данном случае - это Mage_Catalog_Model_Product. Потом создается объект этого класса и вызывается автолоадер. Последний в свой очередь преобразовывает имя класса в строку Mage/Catalog/Model/Product.php. Потом сервер автоматически ищет данный файл среди 3-х скоупов, благодаря такой фиче в PHP как include_path. Если открыть файл app/Mage.php, то можно убедится в том, что Magento изменяет этот параметр.

При чем проверка идет в следующем порядке: localcommunitycore. Т.е. если нужно внести, какие-то мелкие изменения в класс продукта, то нужно просто в директории local создать соответствующую структуру каталогов и скопировать файл класса туда. В конкретном случае создаем в app/code/local каталог Mage, внутри него Catalog, внутри него Model и копируем в последнюю директорию файл app/code/core/Mage/Catalog/Model/Product.php. Теперь можно добавить кастом функционал со спокойной душой.

Extends

Этот способ предоставляет возможность заменить стандартный класс честно, без copy-paste. В статье о базовой конфигурации модуля я специально пропустил директиву rewrite. Ее можно прописывать внутри models, blocks и helpers. Например, для переопределения стандартного класса продукта

............................................................
<models>
    <catalog>
        <rewrite>
            <product>FI_Catalog_Model_Product</product>
        </rewrite>
    </catalog>
</models>
..........................................................

Если нужен также и стандартный функционал, тогда класс FI_Catalog_Model_Product должен быть потомком Mage_Catalog_Model_Product.

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

Например, чтобы перезаписать класс Mage_Adminhtml_Sales_Order_Create, в конфигурации (в секции models) пропишем следующие строки

<adminhtml>
    <rewrite>
        <sales_order_create>FI_Sales_Model_Adminhtml_Sales_Order_Create</sales_order_create>
    </rewrite>
</adminhtml>

Тогда при выполнении строки

$createModel = Mage::getModel('adminhtml/sales_order_create');

Magento сначала проверит есть ли rewrite для этой модели если есть, то использует имя заданного класса, если нету составит динамически. Все это относится к хелперам и блокам.

Этот метод лучше предыдущего по нескольким причинам:

  • отсутствие copy-paste
  • меньше проблем с обновлением ядра
  • rewrite является частью модуля, как и сам класс

Почему rewrite-ы зло?

Оба эти метода имеют очень большой минус! Это конфликт сторонних расширений. Например, 2 модуля имеют rewrite на один и тот же класс. Тогда будет утеряна часть функционала, что не очень хорошо. Конечно это можно исправить изменив код в одном из модулей, но тогда будут проблемы с обновлением этого расширения, что тоже плохо.

Как любят говорить в универе, а потом и на работе - ЗАБУДЬТЕ ВСЕ ТО, ЧТО ВЫ ЗНАЛИ! Так и здесь такой способ существует, но лучше его не использовать. Можно только в редких случаях, когда Вы уверены, что в будущем это не принесет гору хлопот.

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

События в Magento

В Magento реализован Observer pattern, при помощи которого в нужных местах можно генерировать произвольное событие. А потом посредством конфигурации создавать наблюдателя за любым событием. Этот патерн позволяет делать независимые модули, которые внедряют свой функционал посредством событий. Это очень удобно, например, для реализации сохранения комментарии при сохранении заказа.

Как приатачить наблюдателя на событие написано в статье о конфигурации модуля.

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

Выбросить событие очень просто

$arrayOfEventParams = array(
    'key'   => 2,
    'value' => 'test'
);
Mage::dispatchEvent('my_event_name', $arrayOfEventParams);

В метод класса наблюдателя передается объект Varien_Event_Observer, который содержит в себе объект события, получить который можно вызвав метод getEvent. Внутри последнего находятся параметры переданные вторым аргументом методу dispatchEvent. Так как класс события является потомком Varien_Object, то к данным можно обратится посредством геттеров

$key   = $observer->getEvent->getKey();   # = 2
$value = $observer->getEvent->getValue(); # = test

Приведу пример метода из модуля Mage_Catalog. Метод удаляет таблицы при удалении стора (store)

class Mage_Catalog_Model_Observer
{
............................................................
    public function storeDelete(Varien_Event_Observer $observer)
    {
        if (Mage::helper('catalog/category_flat')->isEnabled(true)) {
            $store = $observer->getEvent()->getStore();
            Mage::getResourceModel('catalog/category_flat')->deleteStores($store->getId());
        }
        return $this;
    }
............................................................
}

События в блоках

В моделях можно встретить множество событий. К сожалению (или так сразу задумывалось разработчиками) событий в блоках достаточно мало. События в блоках в общем-то сказать и не нужны. Ответ на вопрос почему очень прост. Структура страницы определяется при помощи layout update файлов, о чем было рассказано в одной из предыдущих глав. Существует целых 2 способа, как можно изменить функционал блока не используя события:

  • используя директиву remove удалить блок и добавить свой, который будет потомком старого (можем переопределить любой метод, изменить поведение)
  • если можно обойтись публичными методами, то можно просто добавить свой новый блок (как соседа, к тому который нужно изменить) и потом реализовать все изменения в методе _prepareLayout

Например, в административном интерфейсе, на странице списка продуктов, нужно добавить кнопку экспорта продуктов. Решая такую задачу первое, что пришло мне в голову использовать событие core_block_abstract_prepare_layout_after. Это событие вызывается на каждом блоке, который определен на странице, т.е. нужно еще и добавить проверку по имени блока, чтобы знать, что кнопка будет добавлена в правильное место.

class FI_ImportExport_Model_Observer
{
    public function addExportButton(Varien_Event_Observer $observer)
    {
        $block = $observer->getEvent()->getBlock();
        if ($block->getNameInLayout() != 'products_list') {
            return $this;
        }

        $block->addButton('export', array(
            'label'   => Mage::helper('fi_importexport')->__('Export'),
            'onclick' => "setLocation('{$block->getUrl('*/sync/export')}'); Element.show('loading-mask');"
        ));
    }
}

Это была одна из самых плохих моих идей.

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

<layout>
    <adminhtml_catalog_product_index>
        <reference name="content">
            <block type="fi_importexport/button" name="export.button" after="products_list" />
        </reference>
    </adminhtml_catalog_product_index>
</layout>

Блок-кнопка ничего не выводит, так как является потомком абстрактного класса ядра. Сама реализация последнего достаточно проста

class FI_ImportExport_Block_Button extends Mage_Core_Block_Abstract
{
    protected $_addButtonTo = 'products_list';

    protected function _prepareLayout()
    {
        if ($list = $this->getLayout()->getBlock($this->_addButtonTo)) {
            $list->addButton('export', array(
                'label'   => Mage::helper('fi_importexport')->__('Export Products'),
                'onclick' => "setLocation('" . $this->getUrl('adminhtml/sync/export') . "')"
            ));
        }
    }}

Данный способ намного меньше нагружает систему, по-этому эффективнее.

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

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

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

  • Димс
    Ответить 27 февраля 2012 г., 8:56
    Хотелось бы еще статью. Модуль + cron.
    Спасибо за статью.
  • Валера
    Ответить 1 марта 2012 г., 17:59
    Спасибо огромной!Ждем прожолжения!
  • Дима
    Ответить 4 марта 2012 г., 12:03
    Еще интересна тема вставки/импорта продуктов из кода.
    • Сергей (Администратор)
      Ответить 19 марта 2012 г., 15:39
      Я же писал статью - http://freaksidea.com/php_and_somethings/show-21-magento-import-eksport-cli-terminal-versiia

      или там что-то не понятно? Если так, то спрашивайте конкретней
  • Виктор
    Ответить 12 марта 2012 г., 14:15
    Оказывается что многое что я делал раньше можно делать проще) Спасибо за статью
  • Валера!
    Ответить 27 марта 2012 г., 11:36
    Спасибо!