[Из песочницы] Magento шаг за шагом

Magento — система управления интернет-магазинами. По данным Alexa, Magento — самая популярная система управления интернет-магазинами в мире на февраль 2013 г. В настоящее время, из всех e-commerce решений, я отдаю предпочтение именно Magento:

EAV-модель базы данных, позволяет легко манипулировать аттрибутами товаров, категорий, пользователей Большое количество платных и бесплатных модулей на Connect (пусть и не всегда написанных хорошо) Открытый исходный код Легкоразворачиваемый и обновляемый out-of-the-box интернет-магазин Отличная и мощная админка В сравнении с остальными, пожалуй, самый мощный базовый функционал для ecommerce В интернете есть полно статей на эту тему, но на хабре этот раздел все еще наполняется, поэтому попробую поделиться своими знаниями в этой области.Любая разработка модулей в Magento должна вестись в local (или community, если вы планируете выкладывать свой модуль в connect)Итак, базовый модуль состоит из:

app/etc/modules/[Namespace]_[Module].xml — bootstrap-файл модуля. Содержит в себе местоположение модуля (core|community|local) и зависимости (depends) app/code/local/[Namespace]/[Module] — основная директория модуляetc директория с конфигурационными файлами модуля (config.xml|system.xml|adminhtml.xml|api.xml|api2.xml …) controllers — директория контроллеров модуля data — директория установочных скриптов модуля, только данные sql — директория установочных скриптов модуля, только структуры таблиц Block — директория классов блоков Helper — директория вспомогательных классов Model — директория классов моделей Подгрузка модулей довольно проста: Magento сперва грузит app/etc/local.xml с настройками БД, сессий, кэша и т.д., затем подгружает bootstraps модулей из app/etc/modules/*.xml, сортирует их с учетом зависимостей в , подгружает app/code/*/*/*/etc/config.xml, сливает все XML в один глобальный XML, и снова грузит app/etc/local.xml в глобальный XML, чтобы избежать утери данных из local.xml.

По сути, смысл всей разработки в Magento заключается в том, чтобы вмешиваться в исходный код модулей ядра (или любого другого модуля) по минимуму, для этого в Magento существует несколько подходов:1) и, пожалуй, самый основной — события. Если нужно добавить/изменить какой-то существующий функционал, в большинстве случаев достаточно перехвата события.2) Rewrite класса3) Локальный override

СобытияСобытия в Magento это один из самых правильных подходов при изменении существующего функционала. При разработке собственного функционала, не забывайте использовать события, тем более, что это не так уж и сложно, например Mage: dispatchEvent ('namespace_module_something', array ('model' => $this, 'something' => $that)) Далее, при перехвате события: // app/code/local/Namespace/Module/Observer.php public function someName (Varien_Event_Observer $observer) { $model = $observer→getModel (); // getData ('model') $something = $observer→getSomething (); // getData ('something') } Само определение перехвата описывается в config.xml в ноде events: namespace_module/observer someName Так же, перехват события глобально — не всегда нужен, поэтому events можно прописать, помимо глобального, еще и в frontend / adminhtml / crontabRewrite класса Модели, хелперы и блоки практически везде в коде, вызываются через фабричные методы: Mage: getModel () / Mage: getSingleton () Mage: helper () Mage: getBlockSingleton / Mage_Core_Model_Layout: createBlock Все эти методы используют соответственно getModelClassName / getHelperClassName / getBlockClassName в Mage_Core_Model_ConfigПервым параметром во всех методах идет алиас модели/хелпера/блока: например, catalog/productМожно так же использовать и имя класса напрямую, но это будет неверно, т.к. убьет напрочь систему rewrite-ов — по сути то же самое, что делать new Namespace_Module_Model_Something () — вы никак не сможете через конфиг перопределить этот класс, поэтому старайтесь в своих модулях не использовать прямых имен классов, например: fgetcsv ($handle, 0, Namespace_Module_Model_Something: DELIMITER, Namespace_Module_Model_Something: ENCLOSURE, Namespace_Module_Model_Something: ESCAPE) Фактически заставит всех пользователей вашего модуля использовать только DELIMITER/ENCLOSURE/ESCAPE указанные в нем. Такая проблема, например, несколько версий назад была в Mage_ImportExport модуле, и подобные ей до сих пор встречаются иногда в коде.Итак, рассмотрим конфигурационный файл: Namespace_Module_Model Namespace_Module_Helper Namespace_Module_Block В нем указывано, что модели, хелперы и блоки будут доступны под алиасом namespace_moduleТаким образом, если нужно достучаться до Namespace_Module_Model_Modelname, достаточно использовать Mage: getModel ('namespace_module/modelname') (или Mage: getSingleton, если нужен синглтон). С блоками и хелперами та же ситуация, с одним лишь дополнением: Mage: helper ('namespace_module') вызовет основной хелпер модуля: Namespace_Module_Helper_DataТак же этот хелпер будут использовать блоки и контроллеры при вызове функции перевода ($this→__(«Somestring»)), поэтому хелпер должен быть унаследован от Mage_Core_Helper_Abstract.Для реврайта достаточно указать следующее: … Namespace_Module_Model_Product … Для каждой модели нужно указывать свой реврайт. В XML выше мы переопределяем модель товара на Namespace_Module_Model_Product, таким образом Mage: getModel ('catalog/product') абсолютно везде будет возвращать Namespace_Module_Model_ProductЛокальный override Нужен только при отсутствии возможности исправления ошибки в файле, например при ошибке в Abstract-классе, который никак нельзя переопределить (особенно если этот абстрактный класс используется десятком других классов).По умолчанию, include_path = app/code/local; app/code/community; app/code/core; lib, поэтому при ошибке в community или core классе, его можно скопировать в то же место, что и файл, только в local: например local/Mage/Catalog/Model/Abstract.php, и файл будет загружен из local вместо core.Не самый лучший способ, конечно, т.к. при обновлении Magento файл надо будет обновлять (если проблема не будет устранена), но имеет право на жизнь, особенно когда дело доходит до оптимизации.Применение на практике Задание: добавить поле «is_exported» для заказов и отобразить его в списке заказов в админке.В первую очередь, создадим bootstrap: app/etc/modules/Easy_Export.xml

true local В зависимостях обязательно нужно установить Mage_Sales, так как мы собираемся изменить структуру таблицы, создаваемой модулем Mage_Sales. Если этого не установить, все пройдет без проблем на уже существующей Magento, однако при разворачивании с нуля, инсталляционные скрипты могут «упасть» ввиду отсутствия таблицы sales_flat_order, если Ваш скрипт запустится первым.app/code/local/Easy/Export/etc/config.xml 0.0.1 Easy_Export Mage_Sales_Model_Resource_Setup Easy_Export_Helper Easy_Export_Block в ноде «resources» по сути указан, что у модуля есть инсталляционный скрипт и что он будет класса Mage_Sales_Model_Resource_Setupсоздадим app/code/local/Easy/Export/sql/easy_export_setup/mysql4-install-0.0.1.php /* @var $this Mage_Sales_Model_Resource_Setup */ $this→addAttribute ('order', 'is_exported', array ('type' => 'int', 'grid' => true)); Здесь добавляем int-аттрибут в таблицу заказов, grid => true так же указывает что нужно обновить таблицу грида этим аттрибутом. Важно не использовать заглавных букв в имени полей — геттеры и сеттеры не смогут с ними работать правильно — (get)setSomeValue эквивалентно только (get)setData ('some_value'), но никак не (get)setData ('SomeValue')Изначально, заказы были реализованы в паттерне EAV, однако, ввиду особенности EAV — записывать один объект с несколькими дочерними объектами, суммарно с пол-тысячей аттрибутов в десяток таблиц только чтобы записать один заказ — невыгодно с точки зрения производительности, поэтому заказы и все, что с ними связано — инвойсы, доставки, квоты, адреса — это плоские таблицы, которые используют дублирующую grid-таблицу.Хорошая практика использовать DDL-методы, т.к. испольовать $this→run ($sql) безопасно можно только для новосоздаваемых таблиц, использование ALTER TABLE в методе run не очищает кэш таблиц Zend и можно надолго «залипнуть» в непонимании причин несохранения поля :)Теперь попробуем сделать небольшую модификацию админки в списке заказов.Часто встречаемая ошибка в community-модулях — JOIN-ы в списке заказов — вместо того, чтоб просто записывать данные в grid-таблицу, люди присоединяют данные к коллекции 'sales/order_grid_collection', переопределив блок Mage_Adminhtml_Block_Sales_Order_Grid, что не совсем верно с точки зрения производительности: если данные уже итак записываются в grid-таблице, почему бы просто не добавить туда еще одно или несколько полей? Итак, переопределим блок Mage_Adminhtml_Block_Sales_Order_Grid. Его алиас — adminhtml/sales_order_grid:

Easy_Export_Block_Adminhtml_Sales_Order_Grid … Обратите внимание, я добавил Adminhtml в начале имени блока — так удобнее отличать блоки с админки и с фронта — ведь Sales_Order_Grid находится в модуле Adminhtml, а не Sales, и если Вам потребуется переопределить еще и sales/order_history — это приведет к появлению в директории Block/Order рядом с Grid.php еще и History.php, что приведет к неразберихе.Создадим файл app/code/local/Easy/Export/Block/Sales/Order/Grid.php:

$this→helper ('eav')→__('No'), 1 => $this→helper ('eav')→__('Yes'), );

$this→addColumnAfter ( 'is_exported', array ( 'header' => $this→__('Exported'), 'index' => 'is_exported', 'type' => 'options', 'width' => '70 px', 'options' => $options ), 'status' ); $this→sortColumnsByOrder (); } } Все просто, добавили колонку Exported в грид, после статуса. Так как type = options, нужно указать опции в виде массива id => value.Использовать напрямую eav/entity_attribute_source_boolean нельзя, так как там No = 0, а не NULL, как в нашем случае.Для тестирования подобного функционала я использую простенький консольный скрипт:

load (194)→setIsExported (1)→save (); Так как аттрибут is_exported существует в обеих таблицах, при сохранении обьекта заказа, а точнее в методе Mage_Sales_Model_Abstract::_afterCommitCallback выполняется updateGridRecords из ресурс-модели заказа, которая и копирует данные полей из модели в grid-таблицу.Теперь же достаточно реализовать любой интересующий нас экспорт через событие (не забудьте добавить описание модели):

Easy_Export_Model easy_export/observer exportOrder Как узнать название события? Все очень просто — откройте app/Mage.php, найдите метод dispatchEvent и добавьте логирование: public static function dispatchEvent ($name, array $data = array ()) { Mage: log ($name, LOG_DEBUG, 'events', true); Затем выполните требуемое действие и смотрите список запущенных событий. Правильнее всего будет выбрать afterCommit, т.к. это будет именно то событие, которое будет запущено при успешном сохранении объекта заказа. Если делать это afterSave, то если какой то из модулей бросит исключение, может оказаться, что ваш модуль мог уже экспортировать данные, что неверно, т.к. вся транзакция будет отменена и заказ вернется в исходное состояние (которое может оказаться несуществующим).Создадим сам класс Observer app/code/local/Easy/Export/Model/Observer.php:

getOrder (); /* @var $order Mage_Sales_Model_Order */ if (!$order→getIsExported () && $order→getState () == Mage_Sales_Model_Order: STATE_PROCESSING) { try { Mage: getModel ('easy_export/order')→export ($order); $order→setIsExported (1)→addStatusHistoryComment ('Exported order'); } catch (Exception $ex) { $order→addStatusHistoryComment ('Failed exporting order: ' . $ex→getMessage ())→save (); } } } } Ничего сложного, простая проверка не экспортирован ли заказ уже и можно ли его вообще экспортировать — только заказы в состоянии PROCESSING подходят под экспорт — это оплаченные заказы.Ну и, наконец easy_export/order app/code/local/Easy/Export/Model/Order.php

load (188)→setDummyValue (1)→save (); Установка несуществуюего значения нужно банально для того, чтобы пошло сохранение заказа — сохранение модели в Magento не происходит, если _origData = _data, поэтому события beforeSave/afterSave/afterCommit не будут запущены. После запуска этого скрипта, в истории комментариев заказа появится новый комментарий:22/07/2014 6:43:43 AM|Processing Customer Not Notified Failed exporting order: Not implemented

Финальный config.xml 0.0.1 Easy_Export Mage_Sales_Model_Resource_Setup Easy_Export_Model Easy_Export_Helper Easy_Export_Block Easy_Export_Block_Adminhtml_Sales_Order_Grid easy_export/observer exportOrder Вот таким вот нехитрым образом в 12 килобайт, мы только что добавили скелет для экспорта в Magento при создании заказа. Дальше остается всего лишь реализовать нужный Вам алгоритм экспорта в модели Easy_Export_Model_Order.

© Habrahabr.ru