Pull to refresh

Поэтапное создание расширения для Magento на примере debug-консоли

Reading time17 min
Views10K
Здравствуйте.
Заметно, что Хабр не избалован статьями о Magento, несмотря на то что платформа достаточно популярная и при этом — не простая. В статье будет показан путь создания реального расширения, доступного для скачивания. Это не hello world, скорее желание предоставить сообществу бесплатную альтернативу Commercebug (если вы работали с Magento — вы наверняка о нем слышали).

Итак, что должно представлять из себя готовое расширение? Это должен быть участок на странице с данными о текущих используемых блоках, шаблонах, handles и прочей полезной информацией. Мне показалось, что удобнее всего это было бы сделать в виде сворачиваемой «консоли» — вещи, которую мы привыкли видеть во многих играх вроде Half-Life. Естественно, никакой командной строки там не будет потому что она там просто не нужна.

Первое, с чего нужно начать — это создание файловой структуры. Поскольку я описываю реальное расширение, то прошу позволения использовать в статье реальные имена пространства имен и классов.
Определимся с нашим пространством имен (namespace) — это самая первая часть имени любого класса, она отождествляется с разработчиком расширения. Например, все внутренние классы Magento, разработанные внутри компании имеют namespace «Mage». У нас это будет «Linker». Создаем директорию с этим именем в app/code/community.
Напомню, что у платформы есть три стандартных пула кода (code pool) — core, community и local. В первом содержится только код, разработанный внутри компании, во втором — то что разрабатывает сообщество (это могут быть как платные, так и бесплатные расширения), а в третьем — изменения для данного конкретного сайта, которыми вы не собираетесь делиться.

Далее, создаем внутри директорию с именем модуля (Insider), а в ней — стандартную структуру каталогов:

Insider
|-- Block
|-- controllers
|-- etc
|-- — config.xml
|-- Helper
|-- — Data.php
|-- Model


Data.php и config.xml — это обязательные файлы расширения, без них код просто не запустится.

Добавляем в config.xml следующие вещи:

<?xml version="1.0"?> <!-- привет, я - XML! -->
<config> <!-- по стандарту root node может быть только один -->
  <modules>
    <Linker_Insider> <!-- представимся -->
      <version>0.1.0.0</version>
    </Linker_Insider>
  </modules>  
</config>


Здесь мы просто сообщили системе, что я — такой-то модуль и у меня такая-то версия. Пока что ничего существенного. Вот содержимое Data.php:

Copy Source | Copy HTML
  1. <?php
  2. class Linker_Insider_Helper_Data extends Mage_Core_Helper_Data {}


Это заглушка. Мы просто не собираемся использовать helper, но этот файл необходим для системы.
Немного о соглашении об именовании классов: в основном оно соответствует с таковым от Zend, то есть глядя на имя класса вы точно можете сказать, где он находится. Фактически, имя соответствует расположению в файловой системе если заменить "_" на "/".
Теперь давайте пойдем и включим наш модуль в общем списке. Для этого открываем etc/modules/Mage_All.xml и добавляем между описанием последнего модуля и </config> следющие строки:

<Linker_Insider>
  <active>true</active>
  <codePool>community</codePool>
</Linker_Insider>


Таким образом мы сообщили, что хотим включить (<active>true</active>) модуль под названием Linker_Insider, который находится в пуле кода сообщества (app/code/community).
Отлично, включили! Но мы пока не можем это наблюдать на сайте, потому что он еще ничего не делает. Давайте-ка его заставим.
Для этого необходимо создать файл шаблона. Шаблоны для frontend хранятся в app/design/frontend/base/default/template. Там создаем директорию с названием нашего модуля (insider), а в ней — файлик disclose.phtml. В нем пишем, к примеру «hello» (нам пока что просто нужно увидеть, работает ли модуль). Теперь нужно указать системе, в каком месте его вставлять — это уже касается разметки. Шаблоны разметки представляют собой XML-файлы и располагаются в app/design/frontend/base/default/layout. Создадим там файл с именем insider.xml с таким содержанием:

<?xml version="1.0"?>
<layout>
  <default> <!-- хочу его видеть везде -->
    <block type="core/template" name="insider" template="insider/disclose.phtml"/>
  </default>
</layout>


Атрибут 'type' говорит какой блок будет управлять рендерингом шаблона 'template'. В данном случае это будет блок с именем класса Mage_Core_Block_Template. type=«modulename/blockname» расшифровывается как Namespace_modulename_Block_blockname. Предполагается, что namespace уже зарегистрирован в системе и она знает, где искать блоки, начинающиеся на modulename. Mage, естественно, зарегистрирован, а когда нам понадобится использовать собственный блок — нужно будет дописать кое что в наш конфигурационный файл, но об этом дальше.
Атрибут 'name' нужен для того, чтобы другие блоки могли «зацепиться» за него с помощью <reference> — это будет понятнее тоже чуть дальше.

insider.xml сам по себе не подхватиться, нам нужно явно указать, что наш модуль собирается вносить изменения в разметку — для этого создадим еще один узел в config.xml:

<frontend> <!-- изменения, касающиеся публичной части сайта -->
  <layout> <!-- все что внутри этого узла - касается разметки -->
    <updates> <!-- да, да - мы хотим внести изменения -->
      <insider> <!-- имя по вкусу -->
        <file>insider.xml</file> <!-- файл, содержащий собственно изменения -->
      </insider>
    </updates>
  </layout>
</frontend>


Теперь бегите обратно на сайт и жмите F5 — наш «hello» должен появиться где-то внизу на любой странице фронтенда. Не появился? Уверены, что не используете кеш? Напомню, что отключить его можно в админке: System -> Cache Management.
Не знаю как вам, а мне, например, не нравится, что наша запись болтается в самом низу. Давайте-ка забросим ее повыше? Для того чтобы это сделать нужно немного рассказать о механизме создания разметки — перед отображением страницы Magento парсит все .xml-файлы из папки layout (естественно, только те, которые непосредственно влияют на отображение текущей страницы) и собирает из них один, но большой. С помощью атрибутов 'before' и 'after' мы можем указать парсеру, соответственно до или после какого блока вставить наш. Если поместить блок внутрь узла <reference>, то можно «прицепиться» к определенному блоку — вставить себя внутрь. Практически каждый блок имеет атрибут 'name' — так он определяет себя в общей разметке и к нему можно обратиться с просьбой «подвинуться» или «пустить переночевать».
Для того чтобы знать, какой блок в каком месте как себя определяет нужно изучать файлы разметки других модулей. К примеру, если мы заглянем в page.xml, то увидим там блок с именем 'after_body_start' c лейблом «Page Top». Кажется, это то что нужно! Пожалуй, к нему-то мы и прицепимся. Для этого поместим описание блока в файле insider.xml внутрь узла <reference>:

<reference name="after_body_start"> <!-- дяденька after_body_start, мы хотим к вам! -->
  <block type="core/template" name="insider" template="insider/disclose.phtml"/>
</reference>


Ну, так-то лучше. Но что-то наш «hello» перестал меня вдохновлять. Давайте добавим чтоли какой-нибудь завалящий <div> в наш шаблон, может класс какой-нибудь навесим, а к нему стиль в .css опишем, а потом еще немного динамики через .js добавим. Код оставляю на ваш вкус, описываю лишь как сделать так, чтобы он работал. Создадим файл в папке skin/frontend/base/default/css/insider/insider.css и запишем туда какой-нибудь код, который должен влиять на стиль отображения disclose.phtml. Для того чтобы добавить этот файл на страницу, нужно отредактировать файл разметки (insider.xml) и добавить следующее внутрь узла <default>:

<reference name="head"> <!-- это соответствует HTML узлу <head> на странице -->
  <action method="addCss"><stylesheet>css/insider/insider.css</stylesheet></action>
</reference>


Что это еще за action? Узел <action> позволяет вызвать метод блока и передать ему параметры прямо из файла разметки. Это очень мощная штука. Если вы вернетесь в page.xml вы увидите, что именем «head» подписался блок 'page/html_head', а это, в свою очередь, класс под названием Mage_Core_Page_Block_Html_Head. Если вы взглянете на его реализацию, то без труда найдете метод addCss(). Теперь понимаете, какая это дикая штуковина — <action>?
Давайте займемся JavaScript. Создадим файл js/insider/insider.js и приправим кодом по вкусу. Добавлять нужно почти так же, как и CSS — в том же узле <reference>, который ссылается на «head» создайте такую запись:

  <action method="addJs"><script>insider/insider.js</script></action>


Я думаю, здесь нечего объяснять — по аналогии все должно быть понятно.

Все это, конечно, хорошо, однако наше расширение до сих пор не делает ни черта полезного. Пора исправить эту ситуацию! Для того чтобы влиять на поведение шаблона и передавать в него информацию нам понадобится свой собственный блок. Создадим файл Linker/Insider/Block/Disclose.php. Заполним его следующим содержимым:

Copy Source | Copy HTML<br/><?php<br/>class Linker_Insider_Block_Disclose extends Mage_Core_Block_Template<br/>{<br/>    // Magento рекомендует изменять поведение именно этого метода вместо __construct() (разница в одном "_")<br/>    function _construct()<br/>    {<br/>        // Получаем объект запроса (по аналогии с Zend Framework)<br/>        $request = $this->getRequest();<br/>        // Получаем информацию о том, кто управляет текущей страницей<br/>        $this->module = $request->getModuleName();<br/>        $this->controller = $request->getControllerName();<br/>        $this->action = $request->getActionName();<br/>    }<br/>}<br/> <br/>


Для того чтобы он смог передать эту информацию в шаблон, нам нужно изменить тип блока в файле insider.xml:

<block type="insider/disclose" name="insider" template="insider/disclose.phtml"/>


Но здесь есть один нюанс: просто так это не заработает. Помните, у нас же свой namespace, и мы его еще нигде никак не описали? Нужно это сделать, иначе система не сможет найти класс блока. Вносим изменения в config.xml:

<global> <!-- изменения, затрагивающие поведение системы в целом -->
  <blocks> <!-- изменения в поведении блоков -->
    <insider> <!-- что делать, когда мы встречается Mage::getModel('insider/четочето') -->
      <class>Linker_Insider_Block</class> <!-- искать Linker_Insider_Block_четочето -->
    </insider>
  </blocks>
</global>


Шаблон, фактически, рендерится методом самого блока, а значит имеет доступ ко всем полям и методам этого самого блока, даже к защищенным и приватным. Отобразить содержимое переменных можно таким образом (писать в disclose.phtml):

Copy Source | Copy HTML<br/> <br/>Module: <?php echo $this->module; ?><br/>Controller: <?php echo $this->controller; ?><br/>Action : <?php echo $this->action; ?><br/> <br/>


Теперь представим, что нам вдруг захотелось красиво отображать и заполнять данными наш блок. В основном у нас тут будет name=value схема, так что проще всего это сделать через форму. Для формы полагается отдельный файл, это будет Linker/Insider/Block/Disclose/Form/Controller.php. Пускай вас не смущает название — это будет всего лишь форма с информацией о текущем контроллере/модуле/действии, вот содержимое файла:

Copy Source | Copy HTML<br/> <br/><?php<br/>class Linker_Insider_Block_Disclose_Form_Controller extends Varien_Data_Form<br/>{<br/>    public function __construct()<br/>    {<br/>        // в параметрах мы определили HTML id формы<br/>        parent::__construct(array('id' => 'insider-form-controller'));<br/> <br/>        // HTML id поля, тип поля (смотри классы Varien_Data_Form_Element_*), <br/>        // и его конфигурация (ZF-документация во многом подойдет)<br/>        $this->addField('module', 'text', array('label' => 'Module'));<br/>        $this->addField('controller', 'text', array('label' => 'Controller'));<br/>        $this->addField('action', 'text', array('label' => 'Action'));<br/>    }<br/>}<br/> <br/>


Готово, теперь давайте заполним ее данными. По концепции MVC данные должны исходить от модели, так что придется нам переместить содержимое нашего блока в модель. Создадим файл Linker/Insider/Model/Insider.php:

Copy Source | Copy HTML<br/> <br/><?php<br/>class Linker_Insider_Model_Insider extends Mage_Core_Model_Abstract<br/>{<br/>    public function _construct()<br/>    {<br/>        // В отличае от блока, у родителей модели нет метода getRequest(), поэтому получим его таким способом:<br/>        $request = Mage::app()->getRequest();<br/>        $module = $request->getModuleName();<br/>        $controller = $request->getControllerName();<br/>        $action = $request->getActionName();<br/> <br/>        // setData() записывает информацию в protected массив $_data<br/>        // получается, что ключи массива соответствуют id полей,<br/>        // так мы показываем, какие данные к каким полям прицепить<br/>        $this->setData('controller', array(<br/>            'module' => $module,<br/>            'controller' => $controller,<br/>            'action' => $action,<br/>        ));<br/>    }<br/>} <br/>


Теперь мы можем заполнить форму данными из модели:

Copy Source | Copy HTML<br/>...<br/>$this->addField('action', 'text', array('label' => 'Action'));<br/>$model = Mage::getSingleton('insider/insider');<br/>$this->addValues($model->getData('controller'));<br/> <br/>


Но это тоже не будет работать просто так. Система хоть и знает, где искать блоки начинающиеся на 'insider', но не знает, где искать такие модели. Объясним ей в файле config.xml, узел <global>:

<!-- тут все по аналогии с блоками -->
<models>
  <insider>
    <class>Linker_Insider_Model</class>
  </insider>
</models>



Теперь мы сможем обращаться к модели, но нужно теперь вывести нашу форму на глаза. Для этого изменим содержимое Disclose.php таким образом:

Copy Source | Copy HTML<br/> <br/><?php<br/>class Linker_Insider_Block_Disclose extends Mage_Core_Block_Template<br/>{<br/>    public $forms = array();<br/> <br/>    protected function _construct()<br/>    {<br/>        $this->forms = array(<br/>            'controller' => new Linker_Insider_Block_Disclose_Form_Controller(),<br/>        );<br/>    }<br/>}<br/> <br/>


А в шаблоне удалим те три строчки которые мы добавили ранее и заменим на что-то более простое:

Copy Source | Copy HTML<br/> <br/><?php echo $this->forms['controller']->toHtml(); ?> <br/>


Окей, все готово. Для того чтобы придать форме «консольный» вид можно написать свой рендерер. Это не слишком сложное задание и все должно быть понятно из исходного кода, поэтому я пропущу этот шаг здесь.
Теперь займемся чем-то более серьезным — было бы полезно получить список всех блоков, которые участвуют в отображении текущей страницы. Сделать это из модели можно таким вот образом:

Copy Source | Copy HTML<br/>// Отсюда можно вытащить абсолютно все, что касается текущей страницы<br/>$layout = Mage::app()->getLayout();<br/>foreach ($layout->getAllBlocks() as $name => $block) {<br/>    $class = get_class($block);<br/>    // Некоторые блоки могут наследовать, к примеру, Mage_Core_Block_Abstract, у которого нет такого метода<br/>    if (method_exists($block, 'getTemplate')) {<br/>        /* @var $block Mage_Core_Block_Template */<br/>        $template = $block->getTemplate();<br/>    } else {<br/>        $template = false;<br/>    }<br/> <br/>    $blocks[] = array(<br/>        // e.g. Mage_Catalog_Product_List<br/>        'class' => $class,<br/>        // e.g. catalog/product_list.phtml<br/>        'template' => $template,<br/>        // e.g. "head"<br/>        'name' => $name,<br/>        // e.g. /home/user/magento/app/code/core/Mage/Catalog/Product/List.php<br/>        'blockFile' => mageFindClassFile($class),<br/>        // e.g. /home/user/magento/app/design/frontend/base/default/template/catalog/product_list.phtml<br/>        'templateFile' => $template ? $block->getTemplateFile() : '',<br/>    );<br/>}<br/> <br/>


Создать форму, заполнить ее этими данными и отрендерить в шаблоне доверяю вам. Ну то есть это все доступно в исходном коде. Для чего тогда здесь этот кусок? Для того, чтобы вы увидели несостоятельность данного подхода. В списке блоков мы получим и себя (ок, не велика печаль), но также мы получим его не весь (стоп-стоп-стоп, это почему?). Хотя бы потому что наш блок рендерится одним из первых, почти сразу после тега <body>. Следовательно, модель сможет вытащить информацию только о блоках, которые там уже есть, а это всего ничего. Как преодолеть это препятствие? Первое, что приходит в голову — это посмотреть какой блок рендерится последним, и встать за ним с помощью 'after'. Но если подумать, то это не подойдет хотя бы потому что может появится другой блок — который будет рендериться еще позже, или банально прицепится к нам же с помощью 'after' — и мы его не увидим. Нет, здесь нужен уже другой уровень. В процессе обработки запроса Magento генерирует достаточно большое количество событий, за которые тоже можно «зацепиться» (почти как за блоки). Как насчет события, о котором сообщается буквально до отправки страницы браузеру? Звучит отлично! Все блоки отрендерены, они уже выполнили все что умели и у нас на руках, фактически, готовый HTML. Осталось только вытащить (теперь уже точно полную) информацию о всех участвовавших блоках и добавить в конечный output свой HTML. Что для этого нужно? Во-первых, запретить рендерится автоматически (в списке блоков мы каким-то по счету, но стоим там — и система обязательно попытается нас как следует отрендерить на каком-то этапе). Самый простой способ — вставить заглушку вместо метода, который вызывается для генерации и отдачи HTML-код. Обновим наш Disclose.php новым методом:

Copy Source | Copy HTML<br/> <br/>public function renderView()<br/>{<br/>    return '';<br/>}<br/> <br/>


А теперь создадим свой, который сможем вызывать по требованию — именно когда нам понадобится:

Copy Source | Copy HTML<br/> <br/>public function renderSelf()<br/>{<br/>    return parent::renderView();<br/>}<br/> <br/>


Хорошо, теперь сделаем так, чтобы он вызывался в строго определенный момент — по событию, о котором мы говорили. Событие имеет имя, которое звучит как 'controller_front_send_response_before'. Для того чтобы «зацепиться» за него, нам нужно создать свой observer. Создадим файл Linker/Insider/Model/Observer.php с таким содержимым:

Copy Source | Copy HTML<br/> <br/>class Linker_Insider_Model_Observer<br/>{<br/>    public function renderSelf(Varien_Event_Observer $observer)<br/>    {<br/>        // смотрим, есть ли в разметке наш блок - вдруг мы решили не включать его на какой-то странице?<br/>        // или мы сейчас находимся в админке, где им и не пахнет (но событие-то все равно fire'ится)<br/>        if ($block = Mage::app()->getLayout()->getBlock('insider')) {<br/>            // получаем HTML-код нашего блока<br/>            $insiderHtml = $block->renderSelf();<br/>            // в параметре любезно содержится front controller, который содержит в себе HTML всей страницы<br/>            $front = $observer->getData('front');<br/>            // добавляемся в конец<br/>            $front->getResponse()->append('insider', $insiderHtml);<br/>        }<br/>    }<br/>}<br/> <br/>


Теперь мы должны описать, на что наш наблюдатель должен реагировать. Это делается в config.xml, узел <global>:

<events> <!-- поговорим о событиях -->
  <controller_front_send_response_before> <!-- конкретно - вот об этом -->
    <observers> <!-- у нас для тебя есть наблюдатель! -->
      <insider_renderself> <!-- имя по вкусу -->
        <class>insider/observer</class> <!-- в каком классе... -->
        <method>renderSelf</method> <!-- какой метод вызывать -->
      </insider_renderself>
    </observers>
  </controller_front_send_response_before>
</events>


Вот, собственно, и все о чем я хотел рассказать. Надеюсь, кому-то это поможет понять некоторые внутренние схемы работы Magento.
Для того чтобы избежать упреков в самопиаре, я не привожу никаких ссылок. Те, кто захочет оценить полезность расширения могут найти его на Magento Connect.
Tags:
Hubs:
Total votes 27: ↑24 and ↓3+21
Comments22

Articles