Разработка модулей для Magento 1.x — большой гайд

от автора


Привет, Хабр!
Несмотря на давно уже выпущеную Magento 2, Magento первой версии еще живее всех живых и пока еще не собирается нас покидать. Команда Magento будет поддерживать первую версию продукта 3 года с даты выпуска версии 2, т.е. примерно до ноября 2018. Рынок пестрит широчайшим выбором тем, модулей и сервисов заточеных под Magento 1.x версии. И большое количество сайтов, которые сейчас на Magento 1.x, не торопятся обновляться. Работы много — выхлопа мало. А значит, разработка под Magento первых версий еще актуальна и так будет несколько лет.

Но не о перспективах развития e-commerce решений пойдет речь в этой статье. Тут я решил собрать своеобразный гайд по созданию модулей для Magento 1.x (далее просто Magento). Но не простой гайд, в котором надо всего лишь следовать инструкциям, а с небольшими пояснениями «почему пишем так, а не иначе». Я старался найти золотую середину между краткостью и достаточностью. И в первую очередь, гайд несет пользу новичкам в деле разработки модулей для Magento. Но и более опытным пользователям данный материал может принести пользу.

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

Подготовка

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

Сервер на Ubuntu 16.04 LTS

Скачиваем дистрибутив Ubuntu 16.04, конфигурируем «виртуалку». И устанавливаем Ubuntu на наш виртуальный компьютер. Процесс установки в целом простой и не требует документации, но весь процесс установки и настройки можно пройти в видео ниже.

Видео: Установка UBUNTU 16.04 — Nginx + php7-fpm + mysql + samba

Установим и настроим необходимый софт.

sudo su apt-get install && apt-get upgrade 

Ставим файловый менеджер, редактор и диспетчер задач

apt-get install mc nano htop 

Настроим статически IP адрес (в принципе это можно и не делать, а статический адрес назначить на стороне роутера)

nano /etc/network/interfaces 

Пример настройки:

iface eth0 inet static address 192.168.0.100 netmask 255.255.255.0 gateway 192.168.0.1 dns-nameservers 192.168.0.1 8.8.8.8 auto eth0 

где eth0 — сетевой интерфейс. Его можно посмотреть написав ifconfig
Вебсервер Nginx

apt-get install nginx 

PHP 7.0 FPM

apt-get install php-fpm php-xdebug php-soap php-gd php-mbstring php-mcrypt php-curl php-xml 

MySQL 5.7 и phpMyAdmin

apt-get install mysql-server-5.7 phpmyadmin 

Сменим владельца и права на папку, где будут файлы магазина.

chown -R dev:dev /var/www chmod -R 777 /var/www 

dev:dev — имя и группа пользователя. Я использовал это имя при установке Ubuntu.
Теперь необходимо настроить установленое ПО.

Nginx

Я сделал 3 конфига для Nginx: динамический домен, конфиг для Magento 2 (пригодится), конфиг для phpMyAdmin
Прицнип действия так называемого конфига с динамическими доменами прост.

  • Мы настраиваем у себя соответствие домен — IP. Как мы это делаем, не важно, я прописываю в hosts файле. Например, magento.dev 192.168.0.100
  • Когда Nginx получает запрос, он делает server_root путь вида /var/www/(доменное имя). Пример: пишем в браузере magento.dev — server_root /var/www/magento.dev
  • Ну а наш магазин необходимо разместить в папке /var/www/magento.dev
dynamic.conf

    server {     listen 80;     server_name $http_host;     root /var/www/$http_host;      location / {     index index.html index.php;     try_files $uri $uri/ @handler;     expires 30d;     }      location  /. {     return 404;     }      location @handler {     rewrite / /index.php;     }      location ~ .php/ {     rewrite ^(.*.php)/ $1 last;     }      location ~ .php$ {     if (!-e $request_filename) { rewrite / /index.php last; }      expires        off;     fastcgi_pass unix:/run/php/php7.0-fpm.sock;     fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;     fastcgi_param  SCRIPT_NAME  $document_root$fastcgi_script_name;     fastcgi_param  MAGE_RUN_TYPE store;     include        fastcgi_params;     }     }      

m2.conf

    # Magento Vars      #     # Example configuration:     upstream fastcgi_backend {     server   unix:unix:/run/php/php7.0-fpm.sock;     }     server {     set $MAGE_ROOT /var/www/m2.dev;     set $MAGE_MODE default; # or production or developer     listen 80;     server_name m2.dev;     root /var/www/m2.dev/pub;      index index.php;     autoindex off;     charset off;      add_header 'X-Content-Type-Options' 'nosniff';     add_header 'X-XSS-Protection' '1; mode=block';      location /setup {     root $MAGE_ROOT;     location ~ ^/setup/index.php {     fastcgi_pass   fastcgi_backend;     fastcgi_index  index.php;     fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;     include        fastcgi_params;     }      location ~ ^/setup/(?!pub/). {     deny all;     }      location ~ ^/setup/pub/ {     add_header X-Frame-Options "SAMEORIGIN";     }     }      location /update {     root $MAGE_ROOT;      location ~ ^/update/index.php {     fastcgi_split_path_info ^(/update/index.php)(/.+)$;     fastcgi_pass   fastcgi_backend;     fastcgi_index  index.php;     fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;     fastcgi_param  PATH_INFO        $fastcgi_path_info;     include        fastcgi_params;     }      # deny everything but index.php     location ~ ^/update/(?!pub/). {     deny all;     }      location ~ ^/update/pub/ {     add_header X-Frame-Options "SAMEORIGIN";     }     }      location / {     try_files $uri $uri/ /index.php?$args;     }      location /pub {     location ~ ^/pub/media/(downloadable|customer|import|theme_customization/.*\.xml) {     deny all;     }     alias $MAGE_ROOT/pub;     add_header X-Frame-Options "SAMEORIGIN";     }      location /static/ {     if ($MAGE_MODE = "production") {     expires max;     }     location ~* \.(ico|jpg|jpeg|png|gif|svg|js|css|swf|eot|ttf|otf|woff|woff2)$ {     add_header Cache-Control "public";     add_header X-Frame-Options "SAMEORIGIN";     expires +1y;      if (!-f $request_filename) {     rewrite ^/static/(version\d*/)?(.*)$ /static.php?resource=$2 last;     }     }     location ~* \.(zip|gz|gzip|bz2|csv|xml)$ {     add_header Cache-Control "no-store";     add_header X-Frame-Options "SAMEORIGIN";     expires    off;      if (!-f $request_filename) {     rewrite ^/static/(version\d*/)?(.*)$ /static.php?resource=$2 last;     }     }     if (!-f $request_filename) {     rewrite ^/static/(version\d*/)?(.*)$ /static.php?resource=$2 last;     }     add_header X-Frame-Options "SAMEORIGIN";     }      location /media/ {     try_files $uri $uri/ /get.php?$args;      location ~ ^/media/theme_customization/.*\.xml {     deny all;     }      location ~* \.(ico|jpg|jpeg|png|gif|svg|js|css|swf|eot|ttf|otf|woff|woff2)$ {     add_header Cache-Control "public";     add_header X-Frame-Options "SAMEORIGIN";     expires +1y;     try_files $uri $uri/ /get.php?$args;     }     location ~* \.(zip|gz|gzip|bz2|csv|xml)$ {     add_header Cache-Control "no-store";     add_header X-Frame-Options "SAMEORIGIN";     expires    off;     try_files $uri $uri/ /get.php?$args;     }     add_header X-Frame-Options "SAMEORIGIN";     }      location /media/customer/ {     deny all;     }      location /media/downloadable/ {     deny all;     }      location /media/import/ {     deny all;     }      location ~ cron\.php {     deny all;     }      location ~ (index|get|static|report|404|503)\.php$ {     try_files $uri =404;     fastcgi_pass   fastcgi_backend;      fastcgi_param  PHP_FLAG  "session.auto_start=off \n suhosin.session.cryptua=off";     fastcgi_param  PHP_VALUE "memory_limit=256M \n max_execution_time=600";     fastcgi_read_timeout 600s;     fastcgi_connect_timeout 600s;     fastcgi_param  MAGE_MODE $MAGE_MODE;      fastcgi_index  index.php;     fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;     include        fastcgi_params;     }     }      

phpmyadmin.conf

    server {     listen 80;     server_name pma myadmin;     root /usr/share/phpmyadmin/;     index index.php;      location /setup/index.php {     deny all;     }     location ~ .php$ {     if (!-e $request_filename) { rewrite / /index.php last; }      expires        off;     fastcgi_pass unix:/run/php/php7.0-fpm.sock;     fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;     fastcgi_param  SCRIPT_NAME  $document_root$fastcgi_script_name;     fastcgi_param  MAGE_RUN_TYPE store;     include        fastcgi_params;     }     include        fastcgi_params;     }     

Кладем конфиги в папку /etc/nginx/sites-availiable/ и делаем симлинки на них в папке /etc/nginx/sites-enabled/
Или просто складываем их в папку /etc/nginx/sites-enabled/

PHP 7.0 FPM

Редактируем /etc/php/7.0/fpm/php.ini
Нас волнуют только некоторые параметры, которые в принципе можно настроить на свой вкус.
max_execution_time = 300
max_input_time = 160
memory_limit = 512M
display_errors = On
log_errors = On
html_errors = On
date.timezone = (тут свою таймзону указать)

Samba server
Мне нравится работать через самбу, подмонтировать себе сетевой диск и спокойно копировать файлы. Но вам она может и не понадобиться. На вкус и цвет, как говорится…
Мой конфиг таков:

smb.conf

     [global]     workgroup = WORKGROUP      server string = %h server (Samba, Ubuntu)      dns proxy = no      log file = /var/log/samba/log.%m      max log size = 1000      syslog = 0      panic action = /usr/share/samba/panic-action %d      server role = standalone server     passdb backend = tdbsam      obey pam restrictions = yes      unix password sync = yes      passwd program = /usr/bin/passwd %u     passwd chat = *Enter\snew\s*\spassword:* %n\n *Retype\snew\s*\spassword:* %n\n *password\supdated\ssuccessfully* .      pam password change = yes      map to guest = bad user     null passwords = Yes     guest account = www-data      [www]     path = /var/www/     comment = WWW folder     guest ok = yes     browseable = yes     read only = no     locking = no     force user = www-data     force group = www-data     

Установка тестового магазина

Процесс установки прост и не требует каких-то особых умений. Но для внесения ясности, оставлю видео-инструкцию спрятанную под спойлером.

Видео: Устанавливаем тестовый магазин Magento

Cоздание модуля

Структура и конфигурация

Видео: Структура и конфигурация модуля Magento

Созданная в уроке структура модуля IGN_Siteblocks-1.zip

Учиться создавать модули будем на примере модуля для вывода блоков на страницах магазина (его frontend части). И первым делом мы придумываем название модуля. Название должно быть коротким и нести смысл.
А еще нам нужно выбрать неймспейс (обычно название компании разработчика или его ФИО). И финальное наименование принимает вид Namespace_Modulename. В нашем случае я назвал IGN_Siteblocks.
Создадим регистрационный XML файл:

app/etc/modules/IGN_Siteblocks.xml

    <?xml version="1.0" ?>     <config>         <modules>             <IGN_Siteblocks>                 <active>true</active> <!-- модуль включен -->                 <codePool>local</codePool>             </IGN_Siteblocks>         </modules>     </config>     

Поговорим о codePool. Всего их 3: local, community, coreenterprise в Enterprise версии Magento)
И сразу решим, что в core мы ничего изменять не будем, там базовые файлы системы и если их надо изменить, то есть другие способы, помимо их непосредственного редактирования.
Мы можем спокойно использовать local и community (На самом деле, лучше сразу взять community, но в этом примере будет local)
Зайдем в админку магазина, в раздел System > Configuration > Advanced > Disable Modules Output и увидим наш IGN_Siteblocks.
Создадим папки для нашего модуля:
app/code/local/IGN/Siteblocks/

  1. Block — классы блоков, отвечают за рендеринг страниц
  2. controllers — контроллеры принимают запросы
  3. etc — тут всякие конфигурационные файлы
  4. Helper — дополнительные классы помощники
  5. Model — модели
  6. sql — инсталяционные скрипты

Модули в Magento реализуют паттерн MVC
У нас есть модели, вид (блоки, темплейты и макеты) и контроллеры.
В папке etc создадим config.xml

app/code/local/IGN/Siteblocks/etc/config.xml

    <?xml version="1.0" ?>     <config>         <modules>             <IGN_Siteblocks>                 <version>1.0.0</version>             </IGN_Siteblocks>         </modules>         <global>             <!-- Тут будут модели, блоки, хелперы, реврайты, глобальные обсерверы -->         </global>         <frontend>             <!-- Все касательно frontend части магазина: роуты, макеты, переводы, обсерверы -->         </frontend>         <admin>             <!-- Все касательно admin части магазина: роуты, макеты, переводы-->         </admin>         <adminhtml>             <!-- Все касательно admin части магазина:  макеты, переводы, обсерверы -->         </adminhtml>         <defalut>             <!-- Все касательно admin части магазина:  макеты, переводы, обсерверы -->         </defalut>     </config>     

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

Отладка кода XDEBUG + PHPSTORM

Видео: Отладка кода XDEBUG + PHPSTORM

Тут я бы все-таки рекомендовал посмотреть на видеоинструкцию.
Сначала настроим сервер:
apt-get install php-xdebug
Отредактируем настройки в php.ini или xdebug.ini

/etc/php/7.0/conf.d/20-xdebug.ini

zend_extension = xdebug.so
xdebug.idekey = "PHPSTORM"
xdebug.remote_autostart = 1
xdebug.remote_connect_back = 1
xdebug.remote_enable = 1
xdebug.remote_port = 9000

Сохраняем и не забываем перезагрузить сервис service php7.0-fpm restart
В PHPSTORM создаем новый Remote Debug конфиг.

Добавляем сервер с соответсвтующим адресом и портом.
В поле IDE key вводим слово PHPSTORM.

Модели, коллекции. Работа с базой данных.

Видео: Модели, коллекции. Работа с базой данных Magento

Созданная в уроке структура модуля IGN_Siteblocks-2.zip

Модели представляют собой классы для работы с данными и только данными. Никаких тонкостей со способом сохранения этих данных в базе. Никакого кода связанного с рендерингом этих данных. В Magento это: Customer, Product, Order и тд.
Что бы наш модуль мог использовать модели, необходимо отконфигурировать config.xml
Напомню, что модели, блоки и хелперы добавляются в global секцию.
config.xml принимает следующий вид:

app/code/local/IGN/Siteblocks/etc/config.xml

    <?xml version="1.0" ?>     <config>         <modules>             <IGN_Siteblocks>                 <version>1.0.0</version>             </IGN_Siteblocks>         </modules>         <global>             <models>                 <siteblocks> <!-- Как правило тут namespace_modulename или просто modulename -->                     <class>IGN_Siteblocks_Model</class>                     <resourceModel>siteblocks_resource</resourceModel>                 </siteblocks>                 <siteblocks_resource>                     <class>IGN_Siteblocks_Resource</class>                     <entities>                         <block> <!-- наименование модели -->                             <table>ign_siteblock</table> <!-- название таблицы к которой будет "привязана" модель -->                         </block>                     </entities>                 </siteblocks_resource>             </models>             <resources>                 <siteblocks_setup> <!-- именно в папку с таким названием нужно складывать install и upgrade скрипты -->                     <setup>                         <module>IGN_Siteblocks</module>                     </setup>                 </siteblocks_setup>             </resources>         </global>         <frontend>          </frontend>         <admin>          </admin>         <defalut>          </defalut>     </config>     

Важно определиться с названием префикса (не знаю какой термин тут подойдет лучше). Я выбрал siteblocks. Это произвольное название и как правило формируется из неймспейса и имя модуля или только имени модуля. Ну или для запутывания разработчиков, можно выбрать совершенно произвольную строку, заранее прикупив оберег от проклятий.
Выбирайте четко и желательно без использования заглавных символов. Одна опечатка, и будете долго копаться, искать проблему. Название модели и привязка к таблице. Имя модели соответствует названию файла модели. Название таблицы в базе произвольное.
В моем случае что бы обратиться к модели, нужно написать так:

Mage::getModel('siteblocks/block'); 

Теперь можно добавлять модели. Создадим модель Block.
Для каждой, привязанной к таблице, модели нужно создавать 3 файла: модель, ресурсная модель, модель коллекции.
Модель абстрагируется от работы с базой, ресурсные модели находятся уровнем ниже. Там мы реализуем логику фильтрации, сортировки, обработки данных до их сохранения и после загрузки из базы.
Код модели Block.php:

app/code/local/IGN/Siteblocks/Model/Block.php

    <?php class IGN_Siteblocks_Model_Block extends Mage_Core_Model_Abstract {      public function _construct()     {         parent::_construct();         $this->_init('siteblocks/block'); //Все в соотвествии с указанными в config.xml параметрами     }     }     

Модели наследуется от Mage_Core_Model_Abstract.
Ресурсные модели сохраняем в папке Model/Resource.

app/code/local/IGN/Siteblocks/Model/Resource/Block.php

    <?php class IGN_Siteblocks_Model_Resource_Block extends  Mage_Core_Model_Mysql4_Abstract {      public function _construct()     {         $this->_init('siteblocks/block','block_id'); //block_id это наш PRIMARY KEY в таблице, по умолчанию entity_id     }      }     

app/code/local/IGN/Siteblocks/Model/Resource/Block/Collection.php

    <?php class IGN_Siteblocks_Model_Resource_Block_Collection extends Mage_Core_Model_Mysql4_Collection_Abstract {      public function _construct()     {         parent::_construct();         $this->_init('siteblocks/block');     }     }     

Наши классы пусты, но они уже реализуют необходимый минимум функционала по наследованию.
Добавлять в них код будем по необходимости. А если мы хотим добавить еще моделей, то добавляем еще одну привязку модели к таблице и новых 3 файла. Можно сколько угодно добавлять моделей, не привязанных к таблице (просто для реализации какого-то функционала), то просто добавляем новый файл, наследоваться от
Mage_Core_Model_Abstract не обязательно.
Не забываем создать инсталяционный скрипт, который будет создавать таблицу для нашей модели.

app/code/local/IGN/Siteblocks/sql/siteblocks_setup/install-1.0.0.sql

    <?php /** @var Mage_Core_Model_Resource_Setup $installer */ $installer = $this; $installer->startSetup();      $installer->run("     CREATE TABLE IF NOT EXISTS `{$this->getTable('siteblocks/block')}` (     `block_id` int(11) NOT NULL AUTO_INCREMENT,     `title` varchar(500) NOT NULL,     `content` text NOT NULL,     `block_status` tinyint(4) NOT NULL,     `created_at` datetime NOT NULL,     PRIMARY KEY (`block_id`)     ) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;     ");      //Альтернативный способ создания таблицы     $table = $installer->getConnection()     ->newTable($this->getTable('siteblocks/block'))     ->addColumn('block_id',Varien_Db_Ddl_Table::TYPE_INTEGER,null,array(     'identity' => true,     'unsigned' => true,     'nullable' => false,     'primary'  => true     ))     ->addColumn('title',Varien_Db_Ddl_Table::TYPE_VARCHAR,null,array(     'nullable' => false     ))     ->addColumn('content',Varien_Db_Ddl_Table::TYPE_TEXT,null,array(     'nullable' => false     ))     ->addColumn('block_status',Varien_Db_Ddl_Table::TYPE_TINYINT,null,array(     'nullable' => false     ))     ->addColumn('created_at',Varien_Db_Ddl_Table::TYPE_DATETIME,null,array(     'nullable' => false     ));      $installer->endSetup();     

ВАЖНЫЙ МОМЕНТ!

Если вы уже пробовали зайти в админку, при установленом модуле, когда еще не было инсталяционного скрипта. Скорее всего ваш инсталл скрипт больше никогда не запустится. В этом случае необходимо найти и удалить запись siteblocks_setup из таблицы core_resource в базе магазина.
При апгрейде версии модуля. Мы указываем новую версию в config.xml, например: 1.0.1. И создаем апгрейд скрипт: upgrade-1.0.0-1.0.1.php
И в таком же духе при последующих апгрейдах.
Говоря о моделях и коллекциях, нельзя не упомянуть о самых базовых методах этих классов.

Немного примеров использования моделей

    //Загрузить объект из таблицы block_id = 1     $block = Mage::getModel('siteblocks/block')->load(1);      //Удалить блок     $block->delete();      //Cохранить     $block->save();      //удалить блок не делая его загрузки из базы     Mage::getModel('siteblocks/block')->setId(1)->delete();      //Загрузить коллекцию блоков из таблицы     $blocks = Mage::getModel('siteblocks/block')->getCollection();      //Коллекция блоков где block_id = 1, 2 и 3     $blocks->addFieldToFilter('block_id',array('in'=>array(1,2,3))) ;      echo $blocks->getSelect(); //выведет сформировавшийся SQL запрос      //Альтернативный способ загрузки коллекции     $blocks = Mage::getResourceModel('siteblocks/block_collection');     

Контроллеры и роутинг

Видео: Контроллеры и роутинг в Magento.

Созданная в уроке структура модуля IGN_Siteblocks-3.zip

Контроллеры, согласно паттерну MVC, отвечают за обработку запросов. Принимают на себя так называемый входной сигнал в виде HTTP запроса. Перешел по ссылке — отработал соответствующий контроллер.
Перед созданием контроллеров сконфигурируем роутинг в config.xml. Роутинг для frontend и admin части настраивается отдельно. А значит добавляем routers в секцию frontend и admin.
config.xml принимает вид:

app/code/local/IGN/Siteblocks/etc/config.xml

    <?xml version="1.0" ?>     <config>         <modules>             <IGN_Siteblocks>                 <version>1.0.0</version>             </IGN_Siteblocks>         </modules>         <global>             <models>                 <siteblocks>                     <class>IGN_Siteblocks_Model</class>                     <resourceModel>siteblocks_resource</resourceModel>                 </siteblocks>                 <siteblocks_resource>                     <class>IGN_Siteblocks_Resource</class>                     <entities>                         <block>                             <table>ign_siteblock</table>                         </block>                     </entities>                 </siteblocks_resource>             </models>             <resources>                 <siteblocks_setup>                     <setup>                         <module>IGN_Siteblocks</module>                     </setup>                 </siteblocks_setup>             </resources>         </global>         <frontend>             <routers>                 <siteblocks>                     <use>standard</use>                     <args>                         <module>IGN_Siteblocks</module>                         <frontName>siteblocks</frontName><!-- любое название, не конфликтуйте с существующими роутерами -->                     </args>                 </siteblocks>             </routers>         </frontend>         <admin>             <routers>                 <adminhtml>                     <args>                         <modules>                             <siteblocks before="Mage_Adminhtml">IGN_Siteblocks_Adminhtml</siteblocks>                         </modules>                     </args>                 </adminhtml>             </routers>         </admin>         <default>          </default>     </config>     

Теперь можно создавать свои контроллеры в папке controllers нашего модуля.
Класс контроллера для frontend части должен наследоваться от класса Mage_Core_Controller_Front_Action
Создадим тестовый контроллер TestController.php

app/code/local/IGN/Siteblocks/controllers/TestController.php

<?php class IGN_Siteblocks_TestController extends Mage_Core_Controller_Front_Action {      public function mytestAction()     {         die('test');     } } 

Если сейчас перейти по URL вида example.com/siteblocks/test/mytest
Вы увидете белый экран с надписью «test». Если этого не произошло, значит на каком-то этапе произошла ошибка. Перепроверяйте код и читайте логи.
URL состоит из router (siteblocks) / controller (TestController) / action (mytestAction)

GET параметры можно передавать 2мя способами:

Контроллеры для админки создаются в папке controllers/Adminhtml.
Класс контроллера для frontend части должен наследоваться от класса Mage_Adminhtml_Controller_Action
Создадим тестовый контроллер TestController.php

app/code/local/IGN/Siteblocks/controllers/Adminhtml/TestController.php

<?php class IGN_Siteblocks_Adminhtml_TestController extends Mage_Adminhtml_Controller_Action {      public function mytestAction()     {         die('admin');     } } 

На него можно зайти по URL: example.com/admin/test/mytest — где admin это ваш путь в админку.
И тут есть ньюанс: такой урл уже может быть занят другим модулем. Выхода тут 2: меняем название контроллера на заведомо неконфликтное (например IgntestController.php) или складываем контроллеры в подпапку.

app/code/local/IGN/Siteblocks/controllers/Adminhtml/Siteblocks/TestController.php

<?php class IGN_Siteblocks_Adminhtml_Siteblocks_TestController extends Mage_Adminhtml_Controller_Action {      public function mytestAction()     {         die('admin');     } } 

Теперь наш URL принимает вид: example.com/admin/siteblocks_test/mytest

Хелперы

Видео: Хелперы в Magento

Созданная в уроке структура модуля IGN_Siteblocks-4.zip

Классы хелперов в Magento используются как дополнительные классы. В них стоит реализовывать стороннюю логику, которая не вписывается в функционал моделей, блоков или контроллеров. Но модуль нуждается как минимум в одном классе хелпера Data.php.
Этот хелпер используется по-умолчанию для перевода текста (лейблов, пунктов меню и тд) и другой логики.
В хелпере рекомендуется декларировать методы чтения настроек из конфига.
Хелперы должны наследоваться от класса Mage_Core_Helper_Abstract

app/code/loca/IGN/Siteblocks/Helper/Data.php

<?php class IGN_Siteblocks_Helper_Data extends Mage_Core_Helper_Abstract {  }

Для переводов текста в хелпере существует метод __(), а его применение выглядит так

echo Mage::helper('siteblocks')->__('Some text') 

Файлы переводов мы декларируем в config.xml.

app/code/local/IGN/Siteblocks/etc/config.xml

<?xml version="1.0" ?> <config>     <modules>         <IGN_Siteblocks>             <version>1.0.0</version>         </IGN_Siteblocks>     </modules>     <global>         <models>             <siteblocks>                 <class>IGN_Siteblocks_Model</class>                 <resourceModel>siteblocks_resource</resourceModel>             </siteblocks>             <siteblocks_resource>                 <class>IGN_Siteblocks_Resource</class>                 <entities>                     <block>                         <table>ign_siteblock</table>                     </block>                 </entities>             </siteblocks_resource>         </models>         <resources>             <siteblocks_setup>                 <setup>                     <module>IGN_Siteblocks</module>                 </setup>             </siteblocks_setup>         </resources>         <helpers>             <siteblocks>                 <class>IGN_Siteblocks_Helper</class>             </siteblocks>         </helpers>     </global>     <frontend>         <routers>             <siteblocks>                 <use>standard</use>                 <args>                     <module>IGN_Siteblocks</module>                     <frontName>siteblocks</frontName>                 </args>             </siteblocks>         </routers>         <translate>             <modules>                 <IGN_Siteblocks>                     <files>                         <default>IGN_Siteblocks.csv</default>                     </files>                 </IGN_Siteblocks>             </modules>         </translate>     </frontend>     <admin>         <routers>             <adminhtml>                 <args>                     <modules>                         <siteblocks before="Mage_Adminhtml">IGN_Siteblocks_Adminhtml</siteblocks>                     </modules>                 </args>             </adminhtml>         </routers>     </admin>     <defalut>      </defalut> </config> 

А файл IGN_Siteblocks.csv создаем в папке app/locale/en_US/
Содержимое вида:
«Some text»,«Some text»
Стараемся выводить текст с использованием своего хелпера и в таком случае, упрощается локализация модуля на разные языки.
Достаточно скопировать файл переводов в соответствующую локаль и перевести второй столбец и нет необходимости копаться в коде.

Конфигурация модуля в админке

Видео: Конфигурация модуля в админке Magento

Созданная в уроке структура модуля IGN_Siteblocks-5.zip

Для придания модулю гибкости, мы создадим страницу с настройками модуля.
Делается это сугубо через xml файлы.
Нам необходимо создать 2 файла:
system.xml — где будут добавлены поля
adminhtml.xml — где будут указаны разделы и права доступа
А стандартные значения настроек мы можем указать в секции default в файле config.xml

app/code/local/IGN/Siteblocks/etc/config.xml

<?xml version="1.0" ?> <config>     <modules>         <IGN_Siteblocks>             <version>1.0.0</version>         </IGN_Siteblocks>     </modules>     <global>         <models>             <siteblocks>                 <class>IGN_Siteblocks_Model</class>                 <resourceModel>siteblocks_resource</resourceModel>             </siteblocks>             <siteblocks_resource>                 <class>IGN_Siteblocks_Resource</class>                 <entities>                     <block>                         <table>ign_siteblock</table>                     </block>                 </entities>             </siteblocks_resource>         </models>         <resources>             <siteblocks_setup>                 <setup>                     <module>IGN_Siteblocks</module>                 </setup>             </siteblocks_setup>         </resources>         <helpers>             <siteblocks>                 <class>IGN_Siteblocks_Helper</class>             </siteblocks>         </helpers>     </global>     <frontend>         <routers>             <siteblocks>                 <use>standard</use>                 <args>                     <module>IGN_Siteblocks</module>                     <frontName>siteblocks</frontName>                 </args>             </siteblocks>         </routers>         <translate>             <modules>                 <IGN_Siteblocks>                     <files>                         <default>IGN_Siteblocks.csv</default>                     </files>                 </IGN_Siteblocks>             </modules>         </translate>     </frontend>     <admin>         <routers>             <adminhtml>                 <args>                     <modules>                         <siteblocks before="Mage_Adminhtml">IGN_Siteblocks_Adminhtml</siteblocks>                     </modules>                 </args>             </adminhtml>         </routers>     </admin>     <defalut> <siteblocks>                 <settings>                     <enabled>1</enabled>                     <block_count>10</block_count>                 </settings>             </siteblocks>     </defalut> </config> 

app/code/local/IGN/Siteblocks/etc/adminhtml.xml

<?xml version="1.0"?> <config>     <acl>         <resources>             <admin>                 <children>                     <system>                         <children>                             <config>                                 <children>                                     <siteblocks translate="title" module="siteblocks">                                         <title>Siteblocks</title>                                     </siteblocks>                                 </children>                             </config>                         </children>                     </system>                 </children>             </admin>         </resources>     </acl> </config> 

app/code/local/IGN/Siteblocks/etc/system.xml

<?xml version="1.0"?> <config>     <tabs>          <ign translate="label" module="siteblocks"> <!-- Добавим свою вкладку в меню слева-->             <label>IGN</label>             <sort_order>2</sort_order>         </ign>     </tabs>     <sections>         <siteblocks module="siteblocks" translate="label">             <label>Siteblocks</label>             <tab>ign</tab> <!-- В какой вкладке вывести наши настройки -->             <frontend>text</frontend>             <sort_order>1</sort_order>             <show_in_default>1</show_in_default>             <show_in_website>1</show_in_website>             <show_in_store>1</show_in_store>             <groups>                 <settings module="siteblocks" translate="label">                     <label>Settings</label>                     <expanded>1</expanded>                     <sort_order>1</sort_order>                     <show_in_default>1</show_in_default>                     <show_in_Website>1</show_in_Website>                     <show_in_store>1</show_in_store>                     <fields>                         <enabled translate="label comment" module="siteblocks">                             <label>Enabled</label>                             <frontend_type>select</frontend_type> <!-- существующие типы можем посмотреть в папке lib/Varien/Data/Form/Element -->                             <source_model>siteblocks/source_status</source_model> <!-- используется для вывода опций -->                             <sort_order>1</sort_order>                             <show_in_default>1</show_in_default>                             <show_in_Website>1</show_in_Website>                             <show_in_store>1</show_in_store>                             <comment>Is module enabled</comment>                         </enabled>                         <blocks_count>                             <label>Blocks on page</label>                             <frontend_type>text</frontend_type>                             <sort_order>2</sort_order>                             <show_in_default>1</show_in_default>                             <show_in_Website>1</show_in_Website>                             <show_in_store>1</show_in_store>                             <depends><enabled>1</enabled></depends> <!-- Так можно указать зависимость от значения другого поля -->                         </blocks_count>                         <raw_text>                             <label>Raw text</label>                             <frontend_type>textarea</frontend_type>                             <sort_order>3</sort_order>                             <show_in_default>1</show_in_default>                             <show_in_Website>1</show_in_Website>                             <show_in_store>1</show_in_store>                             <depends><enabled>1</enabled></depends>                         </raw_text>                     </fields>                 </settings>             </groups>         </siteblocks>     </sections> </config> 

В наших настройках выводится дропдаун с опциями, и используется собственная модель для этих опций:

app/code/local/IGN/Siteblocks/Model/Source/Status.php

<?php class IGN_Siteblocks_Model_Source_Status {     const ENABLED = '1';     const DISABLED = '0';      /**      * Options getter      *      * @return array      */     public function toOptionArray()     {         return array(             array('value' => self::ENABLED, 'label'=>Mage::helper('siteblocks')->__('Enabled')),             array('value' => self::DISABLED, 'label'=>Mage::helper('siteblocks')->__('Disabled')),         );     }      /**      * Get options in "key-value" format      *      * @return array      */     public function toArray()     {         return array(             self::DISABLED => Mage::helper('siteblocks')->__('Disabled'),             self::ENABLED => Mage::helper('siteblocks')->__('Enabled'),         );     }  

Frontend блоки. Макеты. Темплейты

Видео: Frontend блоки. Макеты. Темплейты Magento

Созданная в уроке структура модуля IGN_Siteblocks-6.zip

Займемся выводом информации на frontend части магазина.
И, как не сложно догадаться из заголовка, у нас будут задействованы 3 типа файлов: блоки, макеты и темплейты.
Блоки это классы, отвечающие за подготовку и вывод информации. Блоки используют для вывода темплейты, но не всегда. Если используется темплейт, то он просто инклюдится в методе fetchView

Поэтому из темплейта к блоку обращаемся через $this

app/code/local/IGN/Siteblocks/Block/List.php

<?php class IGN_Siteblocks_Block_List extends Mage_Core_Block_Template {      public function getBlocks()     {         //return Mage::getResourceModel('siteblocks/block_collection');         return Mage::getModel('siteblocks/block')->getCollection()             ->addFieldToFilter('block_status',array('eq'=>IGN_Siteblocks_Model_Source_Status::ENABLED));     } } 

Блок наследуется от класса Mage_Core_Block_Template
Но это зависит от того, что наш блок будет выводить. Так, например, при выводе списка товаров, желательно наследоваться от блока Mage_Catalog_Block_Product_List
Макеты используются для построения структуры страницы, какие элементы выводить на странице и в каком порядке.
Создадим файл макетов:

app/design/frontend/base/default/layout/siteblocks.xml

<?xml version="1.0"?> <layout version="1.0.0">     <siteblocks_index_index> <!-- это соответствует URL example.com/siteblocks/index/index -->         <reference name="head">             <action method="setTitle">                 <title>My Siteblocks</title>             </action>         </reference>         <reference name="content">             <block name="siteblocks.list" as="siteblocks" type="siteblocks/list" template="siteblocks/list.phtml"/>         </reference>     </siteblocks_index_index>      <catalog_category_default> <!-- это уже существующий handle и мы можем добавить свой блок для вывода на этой странице -->         <reference name="left">             <block name="siteblocks.list" as="siteblocks" type="siteblocks/list" template="siteblocks/list.phtml"/>         </reference>         <reference name="right">             <block name="siteblocks.list" as="siteblocks" type="siteblocks/list" template="siteblocks/list.phtml"/>         </reference>     </catalog_category_default>      <catalog_product_view> <!-- Добавим вывод нашего блока на странице товара -->         <reference name="product.info.extrahint"> <!-- этот блок уже задекларирован в другом макете catalog.xml и мы добавляем свой блок для вывода внутри этого -->             <block name="siteblocks.list" before="-" as="siteblocks" type="siteblocks/list" template="siteblocks/list.phtml"/>         </reference>     </catalog_product_view> </layout> 

В макете мы можем добавлять js, css файлы в head, мы можем добавить или удалить блок в на какой-то интересующей нас странице. Тема макетов довольно обширна и сверху я привел минимально простой макет, который добавит наш блок в нескольких местах сайта.

Альтернативным вариантом (без макето) вы можете в контроллере вывести HTML код:

$html = Mage::app()->getLayout()->createBlock('siteblocks/list')->setTemplate('siteblocks/list.phtml')->toHtml() $this->getResponse()->setBody($html); 

И будет выведен HTML код только этого блока. Такое часто нужно, например при использовании AJAX запросов.

В макете у нас упоминается файл siteblocks/list.phtml. Его можно и не указывать, если в темплейте указать его по-умолчанию.

class IGN_Siteblocks_Block_List extends Mage_Core_Block_Template { protected $_template = 'siteblocks/list.phtml'; } 

Создадим темплейт:

app/design/base/default/template/siteblocks/list.phtml

<?php foreach($this->getBlocks() as $block):?>     <div class="siteblock">         <div class="block-title"><?php echo $block->getTitle()?></div>         <div class="block-content"><?php echo $block->getContent()?></div>     </div> <?php endforeach;?> 

Как видно в коде, мы вызываем метод блока getBlocks, возвращающий коллекцию записей, которые мы и выводим. Переименуем TestController или создадим новый. IndexController

app/code/local/IGN/Siteblocks/controllers/IndexController.php

<?php class IGN_Siteblocks_IndexController extends Mage_Core_Controller_Front_Action {      public function indexAction()     {         $this->loadLayout(); #загружаем макеты         $this->renderLayout(); #выводим html     } } 

URL по которому мы увидим вывод имеет вид: example.com/siteblocks/index/index или example.com/siteblocks, т.к. index/index можно опустить.
А handle в макете будет использоваться такой: siteblocks_index_index
Что бы посмотреть на вывод записей, необходимо добавить их на прямую в базу или перейти к следующему шагу разработки формы редактирования.

Admin интерфейс. Грид. Форма редактирования.

Видео: Admin интерфейс. Грид. Форма редактирования в Magento

Созданная в уроке структура модуля IGN_Siteblocks-7.zip

Процесс создания Admin интерфейса состоит из нескольких этапов:

  • Добавляем пункты в меню
  • Создаем блоки
  • Создаем контроллеры

Добавляем пункты в меню:

app/code/local/IGN/Siteblocks/etc/adminhtml.xml

<?xml version="1.0"?> <config>     <acl>         <resources>             <admin>                 <children>                     <system>                         <children>                             <config>                                 <children>                                     <siteblocks translate="title" module="siteblocks">                                         <title>Siteblocks</title>                                     </siteblocks>                                 </children>                             </config>                         </children>                     </system>                     <cms>                         <children>                             <siteblocks translate="title" module="siteblocks">                                 <title>Siteblocks</title>                             </siteblocks>                         </children>                     </cms>                 </children>             </admin>         </resources>     </acl>     <menu>         <cms> <!-- Раздел в котором мы добавим свой пункт -->             <children>                 <siteblocks translate="title" module="siteblocks">                     <title>Siteblocks</title>                     <action>adminhtml/siteblocks</action> <!-- На какой контроллер ведет этот пункт меню, index в этом случае я опустил -->                     <sort_order>20</sort_order>                 </siteblocks>             </children>         </cms>     </menu> </config> 

Правильный код раздела (в примере cms) мы можем подсмотреть в adminhtml.xml файлах стандартных модулей Magento. Там же и посмотреть как создать свой раздел.
Не забываем продублировать информацию в блоке acl.

Создадим контроллер и 1 экшен для начала.

app/code/local/IGN/Siteblocks/controllers/Adminhtml/SiteblocksController.php

<?php class IGN_Siteblocks_Adminhtml_SiteblocksController extends Mage_Adminhtml_Controller_Action {      public function indexAction()     {         $this->loadLayout();         $this->_addContent($this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks'));         $this->renderLayout();     } } 

Мы могли бы создать макет для админки, но нужные блоки можно добавлять прямо в контроллере. Тут мы в контент добавили нашу страницу. Index экшен будет нам выводить страницу с Grid записей.
Теперь, можно перейти к созданию блоков.

app/code/local/IGN/Siteblocks/Block/Adminhtml/Siteblocks.php

<?php  class IGN_Siteblocks_Block_Adminhtml_Siteblocks extends Mage_Adminhtml_Block_Widget_Grid_Container {      public function __construct()     {         $this->_controller = 'adminhtml_siteblocks';         $this->_blockGroup = 'siteblocks';         $this->_headerText = Mage::helper('siteblocks')->__('Siteblocks');         $this->_addButtonLabel = Mage::helper('siteblocks')->__('Add New Block');         parent::__construct();     } } 

Почему мы прописали такие значения свойств, сейчас увидим в методе класса Mage_Adminhtml_Block_Widget_Grid_Container

Таким образом, формируется block type блока grid.

app/code/local/IGN/Siteblocks/Block/Adminhtml/Siteblocks/Grid.php

<?php  class IGN_Siteblocks_Block_Adminhtml_Siteblocks_Grid extends Mage_Adminhtml_Block_Widget_Grid {      public function __construct()     {         parent::__construct();         $this->setId('cmsBlockGrid');         $this->setDefaultSort('block_identifier');         $this->setDefaultDir('ASC');     }      protected function _prepareCollection()     {         $collection = Mage::getModel('siteblocks/block')->getCollection();         /* @var $collection Mage_Cms_Model_Mysql4_Block_Collection */         $this->setCollection($collection);         return parent::_prepareCollection();     }      protected function _prepareColumns()     {          $this->addColumn('title', array(             'header'    => Mage::helper('siteblocks')->__('Title'),             'align'     => 'left',             'index'     => 'title',         ));          $this->addColumn('block_status', array(             'header'    => Mage::helper('cms')->__('Status'),             'align'     => 'left',             'type'      => 'options',             'options'   => Mage::getModel('siteblocks/source_status')->toArray(),             'index'     => 'block_status'         ));           $this->addColumn('created_at', array(             'header'    => Mage::helper('siteblocks')->__('Created At'),             'index'     => 'created_at',             'type'      => 'date',          ));           return parent::_prepareColumns();     }      protected function _prepareMassaction()     {         $this->setMassactionIdField('block_id');         $this->getMassactionBlock()->setIdFieldName('block_id');         $this->getMassactionBlock()             ->addItem('delete',                 array(                     'label' => Mage::helper('siteblocks')->__('Delete'),                     'url' => $this->getUrl('*/*/massDelete'),                     'confirm' => Mage::helper('siteblocks')->__('Are you sure?')                 )             )             ->addItem('status',                 array(                     'label' => Mage::helper('siteblocks')->__('Update status'),                     'url' => $this->getUrl('*/*/massStatus'),                     'additional' =>                         array('block_status'=>                         array(                             'name' => 'block_status',                             'type' => 'select',                             'class' => 'required-entry',                             'label' => Mage::helper('siteblocks')->__('Status'),                             'values' => Mage::getModel('siteblocks/source_status')->toOptionArray()                         )                     )                 )             );          return $this;     }      /**      * Row click url      *      * @return string      */     public function getRowUrl($row)     {         return $this->getUrl('*/*/edit', array('block_id' => $row->getId()));     }  } 

Блок Grid в нашем случае принимает такой вид. В принципе, по названию методов и свойств, можно понять как происходит добавление колонок, формируется URL на страницу редактирования и подготовку коллекции записей для вывода в таблице.
Важно отметить, что доступные по-умолчанию типы колонок и принципы их построения можно посмотреть в папке app/code/core/Mage/Adminhtml/Block/Widget/Grid/Column/Renderer/

Страница редактирования так же будет состоять из 2х блоков: блок контейнер и блок формы.

app/code/local/IGN/Siteblocks/Block/Adminhtml/Siteblocks/Edit.php

<?php  class IGN_Siteblocks_Block_Adminhtml_Siteblocks_Edit extends Mage_Adminhtml_Block_Widget_Form_Container {     public function __construct()     {         $this->_objectId = 'block_id';         $this->_controller = 'adminhtml_siteblocks';         $this->_blockGroup = 'siteblocks';          parent::__construct();          $this->_updateButton('save', 'label', Mage::helper('siteblocks')->__('Save Block'));         $this->_updateButton('delete', 'label', Mage::helper('siteblocks')->__('Delete Block'));          $this->_addButton('saveandcontinue', array(             'label'     => Mage::helper('adminhtml')->__('Save and Continue Edit'),             'onclick'   => 'saveAndContinueEdit()',             'class'     => 'save',         ), -100);          $this->_formScripts[] = "               function saveAndContinueEdit(){                 editForm.submit($('edit_form').action+'back/edit/');             }         ";     }      /**      * Get edit form container header text      *      * @return string      */     public function getHeaderText()     {         if (Mage::registry('siteblocks_block')->getId()) {             return Mage::helper('siteblocks')->__("Edit Block '%s'", $this->escapeHtml(Mage::registry('siteblocks_block')->getTitle()));         }         else {             return Mage::helper('siteblocks')->__('New Block');         }     } } 

И тут значения свойств подстраиваюся под метод родительского класса, что бы получился block type siteblocks/adminhtml_siteblocks_edit_form

Класс формы:

app/code/local/IGN/Siteblocks/Block/Adminhtml/Siteblocks/Edit/Form.php

<?php  class IGN_Siteblocks_Block_Adminhtml_Siteblocks_Edit_Form extends Mage_Adminhtml_Block_Widget_Form {      /**      * Init form      */     public function __construct()     {         parent::__construct();         $this->setId('block_form');         $this->setTitle(Mage::helper('siteblocks')->__('Block Information'));     }      protected function _prepareForm()     {         $model = Mage::registry('siteblocks_block');         $form = new Varien_Data_Form(             array(                 'id' => 'edit_form',                 'action' => $this->getUrl('*/*/save',array('block_id'=>$this->getRequest()->getParam('block_id'))),                 'method' => 'post'             )         );          $form->setHtmlIdPrefix('block_');          $fieldset = $form->addFieldset('base_fieldset', array('legend'=>Mage::helper('siteblocks')->__('General Information'), 'class' => 'fieldset-wide'));          if ($model->getBlockId()) {             $fieldset->addField('block_id', 'hidden', array(                 'name' => 'block_id',             ));         }          $fieldset->addField('title', 'text', array(             'name'      => 'title',             'label'     => Mage::helper('siteblocks')->__('Block Title'),             'title'     => Mage::helper('siteblocks')->__('Block Title'),             'required'  => true,         ));           $fieldset->addField('block_status', 'select', array(             'label'     => Mage::helper('siteblocks')->__('Status'),             'title'     => Mage::helper('siteblocks')->__('Status'),             'name'      => 'block_status',             'required'  => true,             'options'   => Mage::getModel('siteblocks/source_status')->toArray(),         ));           $fieldset->addField('content', 'textarea', array(             'name'      => 'content',             'label'     => Mage::helper('siteblocks')->__('Content'),             'title'     => Mage::helper('siteblocks')->__('Content'),             'style'     => 'height:36em',             'required'  => true,          ));          $form->setValues($model->getData());         $form->setUseContainer(true);         $this->setForm($form);          return parent::_prepareForm();     } } 

Поля добавляются простым образом и с понятным набором опций, а типы стандартных полей можно посмотреть в папке lib/Varien/Data/Form/Element/
Теперь разберемся почему тут у нас находится экземпляр модели сайтблока $model = Mage::registry(‘siteblocks_block’); и добавим оставшиеся экшены в контроллер.
Нам нужны экшены, редактирования, сохранения, удаления записей. Так же у нас будут добавлены экшены массового удаления и изменения статуса, когда пользователь в таблице может отметить несколко строчек и нажать кнопку удаления этих отмеченых записей.

Контроллер принимает следующий вид:

app/code/local/IGN/Siteblocks/controllers/Adminhtml/SiteblocksController.php

<?php class IGN_Siteblocks_Adminhtml_SiteblocksController extends Mage_Adminhtml_Controller_Action {      public function indexAction()     {         $this->loadLayout();         $this->_addContent($this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks'));         $this->renderLayout();     }      public function newAction()     {         $this->_forward('edit');     }      public function editAction()     {         $id = $this->getRequest()->getParam('block_id');         Mage::register('siteblocks_block',Mage::getModel('siteblocks/block')->load($id));         $blockObject = (array)Mage::getSingleton('adminhtml/session')->getBlockObject(true);         if(count($blockObject)) {             Mage::registry('siteblocks_block')->setData($blockObject);         }         $this->loadLayout();         $this->_addContent($this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks_edit'));         $this->renderLayout();     }      public function saveAction()     {         try {             $id = $this->getRequest()->getParam('block_id');             $block = Mage::getModel('siteblocks/block')->load($id);             /*$block                 ->setTitle($this->getRequest()->getParam('title'))                 ->setContent($this->getRequest()->getParam('content'))                 ->setBlockStatus($this->getRequest()->getParam('block_status'))                 ->save();*/             $block                 ->setData($this->getRequest()->getParams())                 ->setCreatedAt(Mage::app()->getLocale()->date())                 ->save();              if(!$block->getId()) {                 Mage::getSingleton('adminhtml/session')->addError('Cannot save the block');             }         } catch(Exception $e) {             Mage::logException($e);             Mage::getSingleton('adminhtml/session')->addError($e->getMessage());             Mage::getSingleton('adminhtml/session')->setBlockObject($block->getData());             return  $this->_redirect('*/*/edit',array('block_id'=>$this->getRequest()->getParam('block_id')));         }          Mage::getSingleton('adminhtml/session')->addSuccess('Block was saved successfully!');          $this->_redirect('*/*/'.$this->getRequest()->getParam('back','index'),array('block_id'=>$block->getId()));     }       public function deleteAction()     {         $block = Mage::getModel('siteblocks/block')             ->setId($this->getRequest()->getParam('block_id'))             ->delete();         if($block->getId()) {             Mage::getSingleton('adminhtml/session')->addSuccess('Block was deleted successfully!');         }         $this->_redirect('*/*/');      }       public function massStatusAction()     {         $statuses = $this->getRequest()->getParams();         try {             $blocks= Mage::getModel('siteblocks/block')                 ->getCollection()                 ->addFieldToFilter('block_id',array('in'=>$statuses['massaction']));             foreach($blocks as $block) {                 $block->setBlockStatus($statuses['block_status'])->save();             }         } catch(Exception $e) {             Mage::logException($e);             Mage::getSingleton('adminhtml/session')->addError($e->getMessage());             return $this->_redirect('*/*/');         }         Mage::getSingleton('adminhtml/session')->addSuccess('Blocks were updated!');          return $this->_redirect('*/*/');      }      public function massDeleteAction()     {         $blocks = $this->getRequest()->getParams();         try {             $blocks= Mage::getModel('siteblocks/block')                 ->getCollection()                 ->addFieldToFilter('block_id',array('in'=>$blocks['massaction']));             foreach($blocks as $block) {                 $block->delete();             }         } catch(Exception $e) {             Mage::logException($e);             Mage::getSingleton('adminhtml/session')->addError($e->getMessage());             return $this->_redirect('*/*/');         }         Mage::getSingleton('adminhtml/session')->addSuccess('Blocks were deleted!');          return $this->_redirect('*/*/');      } } 

Теперь, в нашем модуле можно редактировать записи и отображать их на frontend части.

События и слушатели

Видео: События и слушатели в Magento

Созданная в уроке структура модуля IGN_Siteblocks-8.zip

В Magento можно использовать шаблон «событие-слушатель». Что позволяет в нашем модуле отлавливать какие-то определенные моменты работы сайта. Добавляет динамичности, гибкости и вносит больше автоматизации.
И стандартных событий в Magento реализовано очень много.
Сделайте поиск по файлам Magento текста «Mage::dispatchEvent»
Или загляните по ссылке magento2.atlassian.net/wiki/display/m1wiki/Magento+1.x+Events+Reference
Это из явных событий, еще есть события происходящие с каждой моделью, каждым блоком или экшеном контроллера. Как правило это пред и пост события.
model_save_before, model_save_after, controller_action_predispatch, controller_action_postdispatch, core_block_abstract_to_html_before, core_block_abstract_to_html_after
Плюс, события в которых используется event_prefix ваших классов или ваш route name контроллеров (siteblocks_save_before, controller_action_predispatch_siteblocks…)
Вариаций очень много и благодаря этой системе, можно с легкостью «подловить» нужное событие.

Непосредственно создать событие можно в любом месте в коде:

Mage::dispatchEvent('some_event_name',array('myparam' => $someVar)); 

Слушателей декларируют в config.xml. И там есть 3 варианта: global, admin, frontend. Соответственно это просто разделение, где мы хотим, что бы срабатывал наш слушатель.
Наш конфиг принимает следующий вид:

app/code/local/IGN/Siteblocks/etc/config.xml

<?xml version="1.0" ?> <config>     <modules>         <IGN_Siteblocks>             <version>1.0.0</version>         </IGN_Siteblocks>     </modules>     <global>         <blocks>             <siteblocks>                 <class>IGN_Siteblocks_Block</class>             </siteblocks>         </blocks>         <models>             <siteblocks>                 <class>IGN_Siteblocks_Model</class>                 <resourceModel>siteblocks_resource</resourceModel>             </siteblocks>             <siteblocks_resource>                 <class>IGN_Siteblocks_Model_Resource</class>                 <entities>                     <block>                         <table>ign_siteblock</table>                     </block>                 </entities>             </siteblocks_resource>         </models>         <resources>             <siteblocks_setup>                 <setup>                     <module>IGN_Siteblocks</module>                 </setup>             </siteblocks_setup>         </resources>         <helpers>             <siteblocks>                 <class>IGN_Siteblocks_Helper</class>             </siteblocks>         </helpers>     </global>     <frontend>         <events>             <checkout_cart_product_add_after> <!-- название события говорит само за себя-->                 <observers>                     <siteblocks>                         <class>siteblocks/observer</class>                         <method>checkout_cart_product_add_after</method> <!-- я предпочитаю использовать название метода по названию события -->                         <type>model</type>                     </siteblocks>                 </observers>             </checkout_cart_product_add_after>         </events>         <layout>             <updates>                 <siteblocks module="siteblocks">                     <file>siteblocks.xml</file>                 </siteblocks>             </updates>         </layout>         <routers>             <siteblocks>                 <use>standard</use>                 <args>                     <module>IGN_Siteblocks</module>                     <frontName>siteblocks</frontName>                 </args>             </siteblocks>         </routers>         <translate>             <modules>                 <IGN_Siteblocks>                     <files>                         <default>IGN_Siteblocks.csv</default>                     </files>                 </IGN_Siteblocks>             </modules>         </translate>     </frontend>     <admin>         <routers>             <adminhtml>                 <args>                     <modules>                         <siteblocks before="Mage_Adminhtml">IGN_Siteblocks_Adminhtml</siteblocks>                     </modules>                 </args>             </adminhtml>         </routers>     </admin>     <default>             <siteblocks>                 <settings>                     <enabled>1</enabled>                     <block_count>10</block_count>                 </settings>             </siteblocks>     </default> </config> 

Мы добавили 1 слушателя на события добавления товара в корзину. Теперь необходимо создать класс слушателя. Можно обойтись и без этого класса и добавить логику в какую-нибудь модель. Но это дурной тон. Поэтому Observer.php

app/code/local/IGN/Siteblocks/Model/Observer.php

<?php class IGN_Siteblocks_Model_Observer {     /**      * @param $bserver Varien_Event_Observer      */     public function checkout_cart_product_add_after($observer)     {         var_dump($observer->getEvent()->getData('quote_item')->getData());die;     } } 

В своем методе мы можем произвести все необходимые манипуляции.
Сейчас мы просто распечатаем содержимое айтема из корзины. (позже закомментируйте этот код, иначе не сможете добавлять товары в корзину)

Крон и задачи по расписанию

Видео: Крон и задачи по расписанию в Magento

Созданная в уроке структура модуля IGN_Siteblocks-9.zip

Еще одним поспорьем в автоматизации рабочих процессов нашего модуля, и работы магазина является возможность создавать задачи по расписанию.
В первую очередь необходимо будет настроить запуск крона Magento, а уже запускаемый файл Magento будет сам распределять когда какую задачу запускать.
Настройка Magento cron в консоли:
crontab -e
* */1 * * * php /var/www/magento.dev/cron.php
Больше информации тут: help.ubuntu.ru/wiki/cron
Или вы можете не настраивать, а запускать крон, когда вам это нужно, просто перейдя по ссылке вида example.com/cron.php

Наши задачи мы декларируем в config.xml в отдельном блоке crontab.
И обновленный вид файла:

app/code/local/IGN/Siteblocks/etc/config.xml

<?xml version="1.0" ?> <config>     <modules>         <IGN_Siteblocks>             <version>1.0.0</version>         </IGN_Siteblocks>     </modules>     <global>         <blocks>             <siteblocks>                 <class>IGN_Siteblocks_Block</class>             </siteblocks>         </blocks>         <models>             <siteblocks>                 <class>IGN_Siteblocks_Model</class>                 <resourceModel>siteblocks_resource</resourceModel>             </siteblocks>             <siteblocks_resource>                 <class>IGN_Siteblocks_Model_Resource</class>                 <entities>                     <block>                         <table>ign_siteblock</table>                     </block>                 </entities>             </siteblocks_resource>         </models>         <resources>             <siteblocks_setup>                 <setup>                     <module>IGN_Siteblocks</module>                 </setup>             </siteblocks_setup>         </resources>         <helpers>             <siteblocks>                 <class>IGN_Siteblocks_Helper</class>             </siteblocks>         </helpers>     </global>     <frontend>         <events>             <controller_action_predispatch>              </controller_action_predispatch>             <checkout_cart_product_add_after>                 <observers>                     <siteblocks>                         <class>siteblocks/observer</class>                         <method>checkout_cart_product_add_after</method>                         <type>model</type>                     </siteblocks>                 </observers>             </checkout_cart_product_add_after>         </events>         <layout>             <updates>                 <siteblocks module="siteblocks">                     <file>siteblocks.xml</file>                 </siteblocks>             </updates>         </layout>         <routers>             <siteblocks>                 <use>standard</use>                 <args>                     <module>IGN_Siteblocks</module>                     <frontName>siteblocks</frontName>                 </args>             </siteblocks>         </routers>         <translate>             <modules>                 <IGN_Siteblocks>                     <files>                         <default>IGN_Siteblocks.csv</default>                     </files>                 </IGN_Siteblocks>             </modules>         </translate>     </frontend>     <admin>         <routers>             <adminhtml>                 <args>                     <modules>                         <siteblocks before="Mage_Adminhtml">IGN_Siteblocks_Adminhtml</siteblocks>                     </modules>                 </args>             </adminhtml>         </routers>     </admin>     <default>             <siteblocks>                 <settings>                     <enabled>1</enabled>                     <block_count>10</block_count>                 </settings>             </siteblocks>     </default>     <crontab>         <jobs>             <siteblocks_clear_cache> <!-- Произвольное название задачи-->                 <schedule>                     <cron_expr>*/10 * * * *</cron_expr> <!-- каждые 10 минут -->                 </schedule>                 <run>                     <model>siteblocks/cron::siteblocks_clear_cache</model> <!-- модель и метод, который мы хотим запустить -->                 </run>             </siteblocks_clear_cache>         </jobs>     </crontab> </config> 

Для задач будем использовать отдельный файл Cron.php

app/code/local/IGN/Siteblocks/Model/Cron.php

<?php class IGN_Siteblocks_Model_Cron {     public function siteblocks_clear_cache()     {       //do something here         Mage::app()->cleanCache(array('siteblocks_blocks'));     } } 

Использование рендереров в админке

Видео: Использование рендереров в админке Magento

Созданная в уроке структура модуля IGN_Siteblocks-10.zip

Зачастую стандартных элементов бывает недостаточно для реализации задуманного функционала. Поэтому можно создать рендерер для нужного элемента и этот процесс не требует больших затрат времени.
Рассмотрим создание рендерера для элемента формы.
У нас есть админ форма:

app/code/local/IGN/Siteblocks/Block/Adminhtml/Siteblocks/Edit/Form.php

<?php  class IGN_Siteblocks_Block_Adminhtml_Siteblocks_Edit_Form extends Mage_Adminhtml_Block_Widget_Form {      /**      * Init form      */     public function __construct()     {         parent::__construct();         $this->setId('block_form');         $this->setTitle(Mage::helper('siteblocks')->__('Block Information'));     }      protected function _prepareForm()     {         $model = Mage::registry('siteblocks_block');         $form = new Varien_Data_Form(             array(                 'id' => 'edit_form',                 'action' => $this->getUrl('*/*/save',array('block_id'=>$this->getRequest()->getParam('block_id'))),                 'method' => 'post',                 'enctype' => 'multipart/form-data'             )         );           $form->setHtmlIdPrefix('block_');          $fieldset = $form->addFieldset('base_fieldset',             array(                 'legend'=>Mage::helper('siteblocks')->__('General Information'),                 'class' => 'fieldset-wide')         );          if ($model->getBlockId()) {             $fieldset->addField('block_id', 'hidden', array(                 'name' => 'block_id',             ));         }          $fieldset->addField('title', 'text', array(             'name'      => 'title',             'label'     => Mage::helper('siteblocks')->__('Block Title'),             'title'     => Mage::helper('siteblocks')->__('Block Title'),             'required'  => true,         ));  #1й способ добавления рендерера или редекларации рендера для определенного типа полей #соответсвтенно нам нужно создать класс по пути .../Block/Adminhtml/Siteblocks/Edit/Renderer/Myimage.php         $fieldset->addType('myimage','IGN_Siteblocks_Block_Adminhtml_Siteblocks_Edit_Renderer_Myimage');  #если не пользоваться первым вариантом указания соответсвия типа-класс, то нужно создать файл lib/Varien/Data/Form/Element/Myimage.php         $fieldset->addField('image', 'myimage', array(             'name'      => 'image',             'label'     => Mage::helper('siteblocks')->__('Image'),             'title'     => Mage::helper('siteblocks')->__('Image'),             'required'  => true,         ));          $fieldset->addField('block_status', 'select', array(             'label'     => Mage::helper('siteblocks')->__('Status'),             'title'     => Mage::helper('siteblocks')->__('Status'),             'name'      => 'block_status',             'required'  => true,             'options'   => Mage::getModel('siteblocks/source_status')->toArray(),         ));          $fieldset->addField('content', 'textarea', array(             'name'      => 'content',             'label'     => Mage::helper('siteblocks')->__('Content'),             'title'     => Mage::helper('siteblocks')->__('Content'),             'style'     => 'height:36em',             'required'  => true,          ));          $form->setValues($model->getData());         $form->setUseContainer(true);         $this->setForm($form);          return parent::_prepareForm();     } } 

В нашей форме указано 2 варианта создания рендереров и пользоваться можно любым вариантом, но мне больше импонирует вариант с созданием файла в папке lib/Varien/Data/Form/Element/
Т.к. в этом случае, мы сможем использовать этот же рендерер и в полях для system.xml
спокойно указывая <frontend_type>myimage</frontend_type> по нашему примеру.

Содержимое файлов:

app/code/local/IGN/Siteblocks/Block/Adminhtml/Siteblocks/Edit/Renderer/Myimage.php

<?php  class IGN_Siteblocks_Block_Adminhtml_Siteblocks_Edit_Renderer_Myimage extends Varien_Data_Form_Element_Abstract {     /**      * Constructor      *      * @param array $data      */     public function __construct($data)     {         parent::__construct($data);         $this->setType('file');     }      /**      * Return element html code      *      * @return string      */     public function getElementHtml()     {         $html = '';          if ((string)$this->getValue()) {             $url = $this->_getUrl();              if( !preg_match("/^http\:\/\/|https\:\/\//", $url) ) {                 $url = Mage::getBaseUrl('media') . 'siteblocks' .DS.$url;             }              $html = '<a href="' . $url . '"'                 . ' onclick="imagePreview(\'' . $this->getHtmlId() . '_image\'); return false;">'                 . '<img src="' . $url . '" id="' . $this->getHtmlId() . '_image" title="' . $this->getValue() . '"'                 . ' alt="' . $this->getValue() . '" height="100" width="100" class="small-image-preview v-middle" />'                 . '</a> ';             /*$additional = Mage::app()->getLayout()->createBlock('Mage_Adminhtml_Block_Template');             $additional->setTemplate('siteblocks/image.phtml')                 ->setImageUrl($url);             $html = $additional->toHtml();*/ #закомментированный выше код мы можем использовать для того, что бы html код строился в темплейте, актуально при использовании сложных элементов         }         $this->setClass('input-file');         $html .= parent::getElementHtml();         return $html;     }      /**      * Return html code of hidden element      *      * @return string      */     protected function _getHiddenInput()     {         return '<input type="hidden" name="' . parent::getName() . '[value]" value="' . $this->getValue() . '" />';     }      /**      * Get image preview url      *      * @return string      */     protected function _getUrl()     {         return $this->getValue();     }      /**      * Return name      *      * @return string      */     public function getName()     {         return  $this->getData('name');     } }  

lib/Varien/Data/Form/Element/Myimage.php

<?php  class Varien_Data_Form_Element_Myimage extends Varien_Data_Form_Element_Abstract {     /**      * Constructor      *      * @param array $data      */     public function __construct($data)     {         parent::__construct($data);         $this->setType('file');     }      /**      * Return element html code      *      * @return string      */     public function getElementHtml()     {         $html = '';          if ((string)$this->getValue()) {             $url = $this->_getUrl();              if( !preg_match("/^http\:\/\/|https\:\/\//", $url) ) {                 $url = Mage::getBaseUrl('media') . 'siteblocks' .DS.$url;             }              $html = '<a href="' . $url . '"'                 . ' onclick="imagePreview(\'' . $this->getHtmlId() . '_image\'); return false;">'                 . '<img src="' . $url . '" id="' . $this->getHtmlId() . '_image" title="' . $this->getValue() . '"'                 . ' alt="' . $this->getValue() . '" height="150" width="150" class="small-image-preview v-middle" />'                 . '</a> ';             /*$additional = Mage::app()->getLayout()->createBlock('Mage_Adminhtml_Block_Template');             $additional->setTemplate('siteblocks/image.phtml')                 ->setImageUrl($url);             $html = $additional->toHtml();*/ #закомментированный выше код мы можем использовать для того, что бы html код строился в темплейте, актуально при использовании сложных элементов         }         $this->setClass('input-file');         $html .= parent::getElementHtml();         return $html;     }      /**      * Return html code of hidden element      *      * @return string      */     protected function _getHiddenInput()     {         return '<input type="hidden" name="' . parent::getName() . '[value]" value="' . $this->getValue() . '" />';     }      /**      * Get image preview url      *      * @return string      */     protected function _getUrl()     {         return $this->getValue();     }      /**      * Return name      *      * @return string      */     public function getName()     {         return  $this->getData('name');     } } 

Содержимое этих файлов я скопировал из стандартного lib/Varien/Data/Form/Element/Image.php
И подправил код под свои нужды.

Теперь займемся созданием рендерера для колонки Grid.

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

app/code/local/IGN/Siteblocks/Block/Adminhtml/Siteblocks/Grid.php

<?php class IGN_Siteblocks_Block_Adminhtml_Siteblocks_Grid extends Mage_Adminhtml_Block_Widget_Grid {      public function __construct()     {         parent::__construct();         $this->setId('cmsBlockGrid');         $this->setDefaultSort('block_identifier');         $this->setDefaultDir('ASC');     }      protected function _prepareCollection()     {         $collection = Mage::getModel('siteblocks/block')->getCollection();         /* @var $collection Mage_Cms_Model_Mysql4_Block_Collection */         $this->setCollection($collection);         return parent::_prepareCollection();     }      protected function _prepareColumns()     {          $this->addColumn('title', array(             'header'    => Mage::helper('siteblocks')->__('Title'),             'align'     => 'left',             'index'     => 'title',         ));          $this->addColumn('image', array(             'header'    => Mage::helper('siteblocks')->__('Image'),             'align'     => 'left',             'index'     => 'image',             'filter'    => false, <!-- Картинки мы не сможем фильтровать -->             'sortable'  => false,<!-- и не сможем их сортировать -->             'renderer'  => 'IGN_Siteblocks_Block_Adminhtml_Siteblocks_Grid_Renderer_Image', //         'renderer'  => 'siteblocks/adminhtml_siteblocks_grid_renderer_image' #альтернативный способ         ));          $this->addColumn('block_status', array(             'header'    => Mage::helper('cms')->__('Status'),             'align'     => 'left',             'type'      => 'options',             'options'   => Mage::getModel('siteblocks/source_status')->toArray(),             'index'     => 'block_status'         ));           $this->addColumn('created_at', array(             'header'    => Mage::helper('siteblocks')->__('Created At'),             'index'     => 'created_at',             'type'      => 'date',          ));           return parent::_prepareColumns();     }      protected function _prepareMassaction()     {         $this->setMassactionIdField('block_id');         $this->getMassactionBlock()->setIdFieldName('block_id');         $this->getMassactionBlock()             ->addItem('delete',                 array(                     'label' => Mage::helper('siteblocks')->__('Delete'),                     'url' => $this->getUrl('*/*/massDelete'),                     'confirm' => Mage::helper('siteblocks')->__('Are you sure?')                 )             )             ->addItem('status',                 array(                     'label' => Mage::helper('siteblocks')->__('Update status'),                     'url' => $this->getUrl('*/*/massStatus'),                     'additional' =>                         array('block_status'=>                         array(                             'name' => 'block_status',                             'type' => 'select',                             'class' => 'required-entry',                             'label' => Mage::helper('siteblocks')->__('Status'),                             'values' => Mage::getModel('siteblocks/source_status')->toOptionArray()                         )                     )                 )             );          return $this;     }      /**      * Row click url      *      * @return string      */     public function getRowUrl($row)     {         return $this->getUrl('*/*/edit', array('block_id' => $row->getId()));     }  } 

app/code/local/IGN/Siteblocks/Block/Adminhtml/Siteblocks/Grid/Renderer/Image.php

<?php class IGN_Siteblocks_Block_Adminhtml_Siteblocks_Grid_Renderer_Image     extends Mage_Adminhtml_Block_Widget_Grid_Column_Renderer_Abstract {     public function render(Varien_Object $row) #именно в этом методе необходимо добавлять логику     {         if( ! $row->getImage()) {             return '';         }         $url = Mage::getBaseUrl('media') . 'siteblocks' .DS .$row->getImage();         $html = "<img src='$url' width='100' height='auto'>";         return $html;     } } 

В рендерере мы формируем $src URL и выводим html код картинки. Теперь мы сможем видеть в таблице картинки.

Для того, что бы в модуле можно было загружать картинки, нужно провести некоторые дополнения.
1. Обновить версию в config.xml до 1.0.1
2. Создать файл upgrade-1.0.0-1.0.1.php

app/code/local/IGN/Siteblocks/sql/siteblocks_setup/upgrade-1.0.0-1.0.1.php

<?php /** @var Mage_Core_Model_Resource_Setup $installer */ $installer = $this; $installer->startSetup();  $installer->run(" ALTER TABLE `{$this->getTable('siteblocks/block')}` ADD `image` TEXT NOT NULL; ");  $installer->endSetup(); 

3. В контроллере добавить соответствующий код:

app/code/local/IGN/Siteblocks/controllers/Adminhtml/SiteblocksController.php

<?php class IGN_Siteblocks_Adminhtml_SiteblocksController extends Mage_Adminhtml_Controller_Action {      public function indexAction()     {         $this->loadLayout();         $this->_addContent($this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks'));         $this->renderLayout();     }      public function newAction()     {         $this->_forward('edit');     }      public function editAction()     {         $id = $this->getRequest()->getParam('block_id');         Mage::register('siteblocks_block',Mage::getModel('siteblocks/block')->load($id));         $blockObject = (array)Mage::getSingleton('adminhtml/session')->getBlockObject(true);         if(count($blockObject)) {             Mage::registry('siteblocks_block')->setData($blockObject);         }         $this->loadLayout();         $this->_addContent($this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks_edit'));         $this->renderLayout();     }  // метод загрузки файлов     protected function _uploadFile($fieldName,$model)     {          if( ! isset($_FILES[$fieldName])) {             return false;         }         $file = $_FILES[$fieldName];          if(isset($file['name']) && (file_exists($file['tmp_name']))){             if($model->getId()){                 unlink(Mage::getBaseDir('media').DS.$model->getData($fieldName));             }             try             {                 $path = Mage::getBaseDir('media') . DS . 'siteblocks' . DS;                 $uploader = new Varien_File_Uploader($file);                 $uploader->setAllowedExtensions(array('jpg','png','gif','jpeg'));                 $uploader->setAllowRenameFiles(true);                 $uploader->setFilesDispersion(false);                  $uploader->save($path, $file['name']);                 $model->setData($fieldName,$uploader->getUploadedFileName());                 return true;             }             catch(Exception $e)             {                 return false;             }         }     }      public function saveAction()     {         try {             $id = $this->getRequest()->getParam('block_id');             $block = Mage::getModel('siteblocks/block')->load($id);             /*$block                 ->setTitle($this->getRequest()->getParam('title'))                 ->setContent($this->getRequest()->getParam('content'))                 ->setBlockStatus($this->getRequest()->getParam('block_status'))                 ->save();*/             $block                 ->setData($this->getRequest()->getParams());             $this->_uploadFile('image',$block); //используем метод загрузки файлов             $block                 ->setCreatedAt(Mage::app()->getLocale()->date())                 ->save();              if(!$block->getId()) {                 Mage::getSingleton('adminhtml/session')->addError('Cannot save the block');             }         } catch(Exception $e) {             Mage::logException($e);             Mage::getSingleton('adminhtml/session')->addError($e->getMessage());             Mage::getSingleton('adminhtml/session')->setBlockObject($block->getData());             return  $this->_redirect('*/*/edit',array('block_id'=>$this->getRequest()->getParam('block_id')));         }          Mage::getSingleton('adminhtml/session')->addSuccess('Block was saved successfully!');          $this->_redirect('*/*/'.$this->getRequest()->getParam('back','index'),array('block_id'=>$block->getId()));     }      public function deleteAction()     {         $block = Mage::getModel('siteblocks/block')             ->setId($this->getRequest()->getParam('block_id'))             ->delete();         if($block->getId()) {             Mage::getSingleton('adminhtml/session')->addSuccess('Block was deleted successfully!');         }         $this->_redirect('*/*/');      }      public function massStatusAction()     {         $statuses = $this->getRequest()->getParams();         try {             $blocks= Mage::getModel('siteblocks/block')                 ->getCollection()                 ->addFieldToFilter('block_id',array('in'=>$statuses['massaction']));             foreach($blocks as $block) {                 $block->setBlockStatus($statuses['block_status'])->save();             }         } catch(Exception $e) {             Mage::logException($e);             Mage::getSingleton('adminhtml/session')->addError($e->getMessage());             return $this->_redirect('*/*/');         }         Mage::getSingleton('adminhtml/session')->addSuccess('Blocks were updated!');          return $this->_redirect('*/*/');      }      public function massDeleteAction()     {         $blocks = $this->getRequest()->getParams();         try {             $blocks= Mage::getModel('siteblocks/block')                 ->getCollection()                 ->addFieldToFilter('block_id',array('in'=>$blocks['massaction']));             foreach($blocks as $block) {                 $block->delete();             }         } catch(Exception $e) {             Mage::logException($e);             Mage::getSingleton('adminhtml/session')->addError($e->getMessage());             return $this->_redirect('*/*/');         }         Mage::getSingleton('adminhtml/session')->addSuccess('Blocks were deleted!');          return $this->_redirect('*/*/');      } } 

4. Незабыть создать папку media/siteblocks/ и назначить соответствющие права на запись.

Не забудем и про отображение картинок на frontend.

Отредактируем темплейт:

app/design/frontend/base/default/template/siteblocks/list.phtml

<?php foreach($this->getBlocks() as $block):?>     <div class="siteblock">         <div class="block-title"><?php echo $block->getTitle()?></div>         <div class="block-image">             <?php if($block->getImage()):?>                 <img src="<?php echo $block->getImageSrc()?>" height="150" width="auto" alt="<?php $block->getTitle()?>" title="<?php $block->getTitle()?>">             <?php endif;?>         </div>         <div class="block-content"><?php echo $block->getContent() ?></div>     </div> <?php endforeach;?> 

В модель я добавил новый метод getImageSrc и вот ее листинг:

app/code/local/IGN/Siteblocks/Model/Block.php

<?php  /**  * Class IGN_Siteblocks_Model_Block  * @method getBlockStatus()  * @method getContent()  * @method getImage()  */ class IGN_Siteblocks_Model_Block extends Mage_Core_Model_Abstract {      protected $_eventPrefix = 'siteblocks_block';      public function _construct()     {         parent::_construct();         $this->_init('siteblocks/block');      }      public function getImageSrc()     {         return Mage::getBaseUrl('media') . 'siteblocks' . DS . $this->getImage();     } } 

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

Использование WYSIWYG редактора

Видео: Использование WYSIWYG редактора в админке Magento

Созданная в уроке структура модуля IGN_Siteblocks-11.zip

WYSIWYG — What you see is what you get (то что вы видете, то и получите)
Это удобный редактор для создания контента. И в нашем модуле ему есть применение.
Но его включение не является таким простым, как ожидалось.
Мы подошли к тому, что нам необходимо создать макет для админки.

app/design/adminhtml/default/default/layout/siteblocks.xml

<?xml version="1.0"?> <layout version="1.0.0">     <adminhtml_siteblocks_edit> <!-- это соответствует пути на страницу редактирования -->         <update handle="editor"/> <!-- Благодаря этой строке загрузится handle в котором включены все необходимые js и css ресурсы для редактора, а описан он в макете cms.xml -->     </adminhtml_siteblocks_edit>      <adminhtml_system_config_edit>         <update handle="editor"/>     </adminhtml_system_config_edit> </layout> 

Теперь необходимо обновить форму редактирования.

app/code/local/IGN/Siteblocks/Block/Adminhtml/Siteblocks/Edit/Form.php

<?php  class IGN_Siteblocks_Block_Adminhtml_Siteblocks_Edit_Form extends Mage_Adminhtml_Block_Widget_Form {      /**      * Init form      */     public function __construct()     {         parent::__construct();         $this->setId('block_form');         $this->setTitle(Mage::helper('siteblocks')->__('Block Information'));     }      protected function _prepareForm()     {         $model = Mage::registry('siteblocks_block');         $form = new Varien_Data_Form(             array(                 'id' => 'edit_form',                 'action' => $this->getUrl('*/*/save',array('block_id'=>$this->getRequest()->getParam('block_id'))),                 'method' => 'post',                 'enctype' => 'multipart/form-data'             )         );           $form->setHtmlIdPrefix('block_');          $fieldset = $form->addFieldset('base_fieldset',             array(                 'legend'=>Mage::helper('siteblocks')->__('General Information'),                 'class' => 'fieldset-wide')         );          if ($model->getBlockId()) {             $fieldset->addField('block_id', 'hidden', array(                 'name' => 'block_id',             ));         }          $fieldset->addField('title', 'text', array(             'name'      => 'title',             'label'     => Mage::helper('siteblocks')->__('Block Title'),             'title'     => Mage::helper('siteblocks')->__('Block Title'),             'required'  => true,         ));          //$fieldset->addType('myimage','IGN_Siteblocks_Block_Adminhtml_Siteblocks_Edit_Renderer_Myimage');          $fieldset->addField('image', 'myimage', array(             'name'      => 'image',             'label'     => Mage::helper('siteblocks')->__('Image'),             'title'     => Mage::helper('siteblocks')->__('Image'),             'required'  => true,         ));            $fieldset->addField('block_status', 'select', array(             'label'     => Mage::helper('siteblocks')->__('Status'),             'title'     => Mage::helper('siteblocks')->__('Status'),             'name'      => 'block_status',             'required'  => true,             'options'   => Mage::getModel('siteblocks/source_status')->toArray(),         ));   #модифицируем этот элемент         $fieldset->addField('content', 'editor', array(             'name'      => 'content',             'label'     => Mage::helper('siteblocks')->__('Content'),             'title'     => Mage::helper('siteblocks')->__('Content'),             'style'     => 'height:36em',             'required'  => true,             'config'    => Mage::getSingleton('cms/wysiwyg_config')->getConfig()          ));          $form->setValues($model->getData());         $form->setUseContainer(true);         $this->setForm($form);          return parent::_prepareForm();     }  //добавили этот метод, в которм выставляем флаг в блок head, если редактор включен в настройках     protected function _prepareLayout()     {         parent::_prepareLayout();         if (Mage::getSingleton('cms/wysiwyg_config')->isEnabled()) {             $this->getLayout()->getBlock('head')->setCanLoadTinyMce(true);         }     } }  

После этих действий вместо скучной textarea мы получаем удобное поле редактора.
А если мы хотим такое же проделать для поля на странице конфигурации, то необходимо создать новый рендерер, который, в основном, будет представлять из себя копипаст стандартного Editor элемента.

lib/Varien/Data/Form/Element/Myeditor.php

<?php class Varien_Data_Form_Element_Myeditor extends Varien_Data_Form_Element_Editor {     public function __construct($attributes=array())     {          parent::__construct($attributes); #вся дополнительная логика в блоке ниже         if (Mage::getSingleton('cms/wysiwyg_config')->isEnabled()) {             Mage::app()->getLayout()->getBlock('head')->setCanLoadTinyMce(true);             $this->setData('config',Mage::getSingleton('cms/wysiwyg_config')->getConfig());         }         if($this->isEnabled()) {             $this->setType('wysiwyg');             $this->setExtType('wysiwyg');         } else {             $this->setType('textarea');             $this->setExtType('textarea');         }     } } 

А system.xml теперь выглядит так:

app/code/local/IGN/Siteblocks/etc/system.xml

<?xml version="1.0"?> <config>     <tabs>         <ign translate="label" module="siteblocks">             <label>IGN</label>             <sort_order>2</sort_order>         </ign>     </tabs>     <sections>         <siteblocks module="siteblocks" translate="label">             <label>Siteblocks</label>             <tab>ign</tab>             <frontend>text</frontend>             <sort_order>1</sort_order>             <show_in_default>1</show_in_default>             <show_in_website>1</show_in_website>             <show_in_store>1</show_in_store>             <groups>                 <settings module="siteblocks" translate="label">                     <label>Settings</label>                     <expanded>1</expanded>                     <sort_order>1</sort_order>                     <show_in_default>1</show_in_default>                     <show_in_Website>1</show_in_Website>                     <show_in_store>1</show_in_store>                     <fields>                         <enabled translate="label comment" module="siteblocks">                             <label>Enabled</label>                             <frontend_type>select</frontend_type>                             <source_model>siteblocks/source_status</source_model>                             <sort_order>1</sort_order>                             <show_in_default>1</show_in_default>                             <show_in_Website>1</show_in_Website>                             <show_in_store>1</show_in_store>                             <comment>Is module enabled</comment>                         </enabled>                         <blocks_count>                             <label>Blocks on page</label>                             <frontend_type>text</frontend_type>                             <sort_order>2</sort_order>                             <show_in_default>1</show_in_default>                             <show_in_Website>1</show_in_Website>                             <show_in_store>1</show_in_store>                             <depends><enabled>1</enabled></depends>                         </blocks_count>                         <raw_text>                             <label>Raw text</label>                             <frontend_type>myeditor</frontend_type> <!-- Просто указываем новый frontend_type -->                             <sort_order>3</sort_order>                             <show_in_default>1</show_in_default>                             <show_in_Website>1</show_in_Website>                             <show_in_store>1</show_in_store>                             <depends><enabled>1</enabled></depends>                         </raw_text>                         <myimage>                             <label>Image</label>                             <frontend_type>myimage</frontend_type>                             <sort_order>3</sort_order>                             <show_in_default>1</show_in_default>                             <show_in_Website>1</show_in_Website>                             <show_in_store>1</show_in_store>                             <depends><enabled>1</enabled></depends>                         </myimage>                     </fields>                 </settings>             </groups>         </siteblocks>     </sections> </config> 

Для нашего модуля это не нужное поле и сделано для примера.
А вот таким образом мы будем выводить контент на frontend.

app/design/frontend/base/default/template/siteblocks/list.phtml

<?php foreach($this->getBlocks() as $block):?>     <div class="siteblock">         <div class="block-title"><?php echo $block->getTitle()?></div>         <div class="block-image">             <?php if($block->getImage()):?>                 <img src="<?php echo $block->getImageSrc()?>" height="150" width="auto" alt="<?php $block->getTitle()?>" title="<?php $block->getTitle()?>">             <?php endif;?>         </div>         <div class="block-content"><?php echo $this->getBlockContent($block)?></div>     </div> <?php endforeach;?> 

В блоке для этого был создан новый метод getBlockContent

app/local/IGN/Siteblocks/Block/List.php

<?php class IGN_Siteblocks_Block_List extends Mage_Core_Block_Template {      public function getBlocks()     {         //return Mage::getResourceModel('siteblocks/block_collection');         $items = Mage::getModel('siteblocks/block')->getCollection()             ->addFieldToFilter('block_status',array('eq'=>IGN_Siteblocks_Model_Source_Status::ENABLED));         return $items;     }      public function getBlockContent($block)     {         $processor = Mage::helper('cms')->getBlockTemplateProcessor();         $html = $processor->filter($block->getContent());         return $html;     } } 

Использование Rule Conditions (условий)

Видео: Использование Rule Conditions (условий) в Magento

Созданная в уроке структура модуля IGN_Siteblocks-12.zip

Следующим шагом, мы добавим в наш модуль условия. Такие же используются в Magento Promotional Rules. И тут заготовлено 2 типа условий. В первом, используются аттрибуты товара, во втором корзина. Изложенный ниже рецепт описывает первый случай, но их отличия заключаются лишь в подмене нескольких строк.
Зачем нам нужны условия? Мы будем использовать условия для того, что бы выбирать, где будет выводиться блок. Например, на страницах товаров у которых цена ниже $100 или все телефоны из определенной категории, у которых 16гб памяти и дата производства 2015. Мы тут не о юзкейсах будем разговаривать.

Порядок создания:
1. Обновляем версию модуля и доавляем upgrade скрипт, что бы в таблице добавилось новая колонка conditions_serialized типа TEXT.

app/code/local/IGN/Siteblocks/sql/siteblocks_setup/upgrade-1.0.1-1.0.2.php

<?php /** @var Mage_Core_Model_Resource_Setup $installer */ $installer = $this; $installer->startSetup();  $installer->run(" ALTER TABLE `{$this->getTable('siteblocks/block')}` ADD `conditions_serialized` TEXT NOT NULL; "); $installer->endSetup(); 

2. Модель должна наследоваться от Mage_Rule_Model_Abstract.
И должна декларировать 2 метода: getConditionsInstance и getActionInstance

app/code/local/IGN/Siteblocks/Model/Observer.php

<?php  /**  * Class IGN_Siteblocks_Model_Block  * @method getBlockStatus()  * @method getContent()  * @method getImage()  */ class IGN_Siteblocks_Model_Block extends Mage_Rule_Model_Abstract {      protected $_eventPrefix = 'siteblocks_block';  #этот метод, на самом деле нам не нужен, но интерфейс его требует     public function getActionsInstance()     {         return Mage::getModel('catalogrule/rule_action_collection');     }       public function getConditionsInstance()     {         return Mage::getModel('catalogrule/rule_condition_combine');     }      public function _construct()     {         parent::_construct();         $this->_init('siteblocks/block');      }      public function getImageSrc()     {         return Mage::getBaseUrl('media') . 'siteblocks' . DS . $this->getImage();     } } 

Все внимание на метод getConditionsInstance.
Сейчас мы используем условия как в Catalog Price Rules, т.е. только свойства и аттрибуты товара.
Если мы хотим условия как в Shopping Cart Price Rules, то нужно использовать
Mage::getModel(‘salesrule/rule_condition_combine’);
И если вы хотите решать когда выводить блок на основе данных в корзине, то берем salesrule.
А так же, можно создать собственную модель и в ней реализовать любые условия.
3. Необходимо обновить saveAction в нашем контроллере.

app/code/local/IGN/Siteblocks/controllers/Adminhtml/SiteblocksController.php

<?php class IGN_Siteblocks_Adminhtml_SiteblocksController extends Mage_Adminhtml_Controller_Action {      public function indexAction()     {         $this->loadLayout();         $this->_addContent($this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks'));         $this->renderLayout();     }      public function newAction()     {         $this->_forward('edit');     }      public function editAction()     {         $id = $this->getRequest()->getParam('block_id');         Mage::register('siteblocks_block',Mage::getModel('siteblocks/block')->load($id));         $blockObject = (array)Mage::getSingleton('adminhtml/session')->getBlockObject(true);         if(count($blockObject)) {             Mage::registry('siteblocks_block')->setData($blockObject);         }         $this->loadLayout();         $this->_addContent($this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks_edit'));         $this->renderLayout();     }      protected function _uploadFile($fieldName,$model)     {          if( ! isset($_FILES[$fieldName])) {             return false;         }         $file = $_FILES[$fieldName];          if(isset($file['name']) && (file_exists($file['tmp_name']))){             if($model->getId()){                 unlink(Mage::getBaseDir('media').DS.$model->getData($fieldName));             }             try             {                 $path = Mage::getBaseDir('media') . DS . 'siteblocks' . DS;                 $uploader = new Varien_File_Uploader($file);                 $uploader->setAllowedExtensions(array('jpg','png','gif','jpeg'));                 $uploader->setAllowRenameFiles(true);                 $uploader->setFilesDispersion(false);                  $uploader->save($path, $file['name']);                 $model->setData($fieldName,$uploader->getUploadedFileName());                 return true;             }             catch(Exception $e)             {                 return false;             }         }     }      public function saveAction()     {         try {             $id = $this->getRequest()->getParam('block_id');             /** @var IGN_Siteblocks_Model_Block $block */             $block = Mage::getModel('siteblocks/block')->load($id);             /*$block                 ->setTitle($this->getRequest()->getParam('title'))                 ->setContent($this->getRequest()->getParam('content'))                 ->setBlockStatus($this->getRequest()->getParam('block_status'))                 ->save();*/ #ниже следует участок для сохранения условий             $data = $this->getRequest()->getParams();             if (isset($data['rule']['conditions'])) {                 $data['conditions'] = $data['rule']['conditions'];             }             unset($data['rule']); #вместо setData используем loadPost             $block                 ->loadPost($data);             $this->_uploadFile('image',$block);             $block                 ->setCreatedAt(Mage::app()->getLocale()->date())                 ->save();              if(!$block->getId()) {                 Mage::getSingleton('adminhtml/session')->addError('Cannot save the block');             }         } catch(Exception $e) {             Mage::logException($e);             Mage::getSingleton('adminhtml/session')->addError($e->getMessage());             Mage::getSingleton('adminhtml/session')->setBlockObject($block->getData());             return  $this->_redirect('*/*/edit',array('block_id'=>$this->getRequest()->getParam('block_id')));         }          Mage::getSingleton('adminhtml/session')->addSuccess('Block was saved successfully!');          $this->_redirect('*/*/'.$this->getRequest()->getParam('back','index'),array('block_id'=>$block->getId()));     }      public function deleteAction()     {         $block = Mage::getModel('siteblocks/block')             ->setId($this->getRequest()->getParam('block_id'))             ->delete();         if($block->getId()) {             Mage::getSingleton('adminhtml/session')->addSuccess('Block was deleted successfully!');         }         $this->_redirect('*/*/');      }      public function massStatusAction()     {         $statuses = $this->getRequest()->getParams();         try {             $blocks= Mage::getModel('siteblocks/block')                 ->getCollection()                 ->addFieldToFilter('block_id',array('in'=>$statuses['massaction']));             foreach($blocks as $block) {                 $block->setBlockStatus($statuses['block_status'])->save();             }         } catch(Exception $e) {             Mage::logException($e);             Mage::getSingleton('adminhtml/session')->addError($e->getMessage());             return $this->_redirect('*/*/');         }         Mage::getSingleton('adminhtml/session')->addSuccess('Blocks were updated!');          return $this->_redirect('*/*/');      }      public function massDeleteAction()     {         $blocks = $this->getRequest()->getParams();         try {             $blocks= Mage::getModel('siteblocks/block')                 ->getCollection()                 ->addFieldToFilter('block_id',array('in'=>$blocks['massaction']));             foreach($blocks as $block) {                 $block->delete();             }         } catch(Exception $e) {             Mage::logException($e);             Mage::getSingleton('adminhtml/session')->addError($e->getMessage());             return $this->_redirect('*/*/');         }         Mage::getSingleton('adminhtml/session')->addSuccess('Blocks were deleted!');          return $this->_redirect('*/*/');      } } 

4. Обновить макет admin

app/code/design/adminhtml/default/default/layout/siteblocks.xml

<?xml version="1.0"?> <layout version="1.0.0">     <adminhtml_siteblocks_edit>         <update handle="editor"/> <!-- Что бы подгрузились нужные js ресурсы -->         <reference name="head">             <action method="setCanLoadExtJs"><flag>1</flag></action>             <action method="setCanLoadRulesJs"><flag>1</flag></action>         </reference>     </adminhtml_siteblocks_edit>      <adminhtml_system_config_edit>         <update handle="editor"/>     </adminhtml_system_config_edit>  </layout> 

5. Отредактировать файл admin формы, где мы и добавим дизайнер условий.

app/code/local/IGN/Siteblocks/Block/Adminhtml/Siteblocks/Edit/Form.php

<?php  class IGN_Siteblocks_Block_Adminhtml_Siteblocks_Edit_Form extends Mage_Adminhtml_Block_Widget_Form {      /**      * Init form      */     public function __construct()     {         parent::__construct();         $this->setId('block_form');         $this->setTitle(Mage::helper('siteblocks')->__('Block Information'));     }      protected function _prepareForm()     {         $model = Mage::registry('siteblocks_block');         $form = new Varien_Data_Form(             array(                 'id' => 'edit_form',                 'action' => $this->getUrl('*/*/save',array('block_id'=>$this->getRequest()->getParam('block_id'))),                 'method' => 'post',                 'enctype' => 'multipart/form-data'             )         );           $form->setHtmlIdPrefix('block_');          $fieldset = $form->addFieldset('base_fieldset',             array(                 'legend'=>Mage::helper('siteblocks')->__('General Information'),                 'class' => 'fieldset-wide')         );          if ($model->getBlockId()) {             $fieldset->addField('block_id', 'hidden', array(                 'name' => 'block_id',             ));         }          $fieldset->addField('title', 'text', array(             'name'      => 'title',             'label'     => Mage::helper('siteblocks')->__('Block Title'),             'title'     => Mage::helper('siteblocks')->__('Block Title'),             'required'  => true,         ));          //$fieldset->addType('myimage','IGN_Siteblocks_Block_Adminhtml_Siteblocks_Edit_Renderer_Myimage');          $fieldset->addField('image', 'myimage', array(             'name'      => 'image',             'label'     => Mage::helper('siteblocks')->__('Image'),             'title'     => Mage::helper('siteblocks')->__('Image'),             'required'  => true,         ));            $fieldset->addField('block_status', 'select', array(             'label'     => Mage::helper('siteblocks')->__('Status'),             'title'     => Mage::helper('siteblocks')->__('Status'),             'name'      => 'block_status',             'required'  => true,             'options'   => Mage::getModel('siteblocks/source_status')->toArray(),         ));           $fieldset->addField('content', 'editor', array(             'name'      => 'content',             'label'     => Mage::helper('siteblocks')->__('Content'),             'title'     => Mage::helper('siteblocks')->__('Content'),             'style'     => 'height:36em',             'required'  => true,             'config'    => Mage::getSingleton('cms/wysiwyg_config')->getConfig()          ));  #все для добавления условий         $model->getConditions()->setJsFormObject('block_conditions_fieldset');          $renderer = Mage::getBlockSingleton('adminhtml/widget_form_renderer_fieldset')             ->setTemplate('promo/fieldset.phtml')             ->setNewChildUrl($this->getUrl('*/promo_catalog/newConditionHtml/form/block_conditions_fieldset'));          $conditionsFieldset = $form->addFieldset('conditions_fieldset',             array(                 'legend'=>Mage::helper('siteblocks')->__('Conditions'),                 'class' => 'fieldset-wide')         )->setRenderer($renderer);         $conditionsFieldset->addField('conditions', 'text', array(             'name' => 'conditions',             'label' => Mage::helper('siteblocks')->__('Conditions'),             'title' => Mage::helper('siteblocks')->__('Conditions'),             'required' => true,         ))->setRule($model)->setRenderer(Mage::getBlockSingleton('rule/conditions'));          $form->setValues($model->getData());         $form->setUseContainer(true);         $this->setForm($form);          return parent::_prepareForm();     }      protected function _prepareLayout()     {         parent::_prepareLayout();         if (Mage::getSingleton('cms/wysiwyg_config')->isEnabled()) { #Мы можем не редактировать макет, тогда пишем эти 2 строки тут $this->getLayout()->getBlock('head')->setCanLoadExtJs(true); $this->getLayout()->getBlock('head')->setCanLoadRulesJs(true);              $this->getLayout()->getBlock('head')->setCanLoadTinyMce(true);         }     } }  

Обратим внимание на строчку: $this->getUrl(‘*/promo_catalog/newConditionHtml/form/block_conditions_fieldset’)
если используем Shopping Cart Price Rules то пишем:
$this->getUrl(‘*/promo_quote/newConditionHtml/form/block_conditions_fieldset’)
Посмотрите еще на один важный момент:
block_conditions_fieldset — где block_ должно совпадать с $form->setHtmlIdPrefix(‘block_’);

И это все, что касается admin части. Теперь добавим валидацию условий на frontend.
А для этого отредактируем блок List.php

app/code/local/IGN/Siteblocks/Block/List.php

<?php class IGN_Siteblocks_Block_List extends Mage_Core_Block_Template {      public function getBlocks()     {         //return Mage::getResourceModel('siteblocks/block_collection');         $items = Mage::getModel('siteblocks/block')->getCollection()             ->addFieldToFilter('block_status',array('eq'=>IGN_Siteblocks_Model_Source_Status::ENABLED));         $filteredItems = $items; #валидируем только если вывод на странице товара.         if(Mage::registry('current_product') instanceof Mage_Catalog_Model_Product) {             $filteredItems = array();             /** @var IGN_Siteblocks_Model_Block $item */             foreach ($items as $item) { #в метод validate необходимо передать валидируемый объект, в нашем случае товар                 if($item->validate(Mage::registry('current_product'))) {                     $filteredItems[] = $item;                 }             }         }         return $filteredItems;     }      public function getBlockContent($block)     {         $processor = Mage::helper('cms')->getBlockTemplateProcessor();         $html = $processor->filter($block->getContent());         return $html;     }  } 

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

Использование вкладок на странице редактирования

Видео: Использование вкладок на странице редактирования в Magento

Созданная в уроке структура модуля IGN_Siteblocks-13.zip

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

app/code/local/IGN/Siteblocks/Block/Adminhtml/Siteblocks/Edit/Tabs.php

<?php class IGN_Siteblocks_Block_Adminhtml_Siteblocks_Edit_Tabs extends Mage_Adminhtml_Block_Widget_Tabs {      public function __construct()     {         parent::__construct();         $this->setId('block_tabs');         $this->setDestElementId('edit_form');         $this->setTitle(Mage::helper('siteblocks')->__('Block Information'));     }  #в этом методе мы можем добавлять вкладки. еще их можно добавлять в макете     protected function _prepareLayout()     {         $this->addTab('main_tab',array(             'label' => $this->__('Main'),             'title' => $this->__('Main'),             'content' => $this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks_edit_tab_main')->toHtml()         ));         /*$this->addTab('conditions_tab',array(             'label' => $this->__('Conditions'),             'title' => $this->__('Conditions'),             'content' => $this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks_edit_tab_conditions')->toHtml()         ));*/          $this->addTab('conditions_tab','siteblocks/adminhtml_siteblocks_edit_tab_conditions');          return parent::_prepareLayout();     } } 

Загляните в реализацию метода addTab и увидете, что на вход можно подавать массив, объект, строку. И есть некоторые отличия. Тут я рекомендую, заглянуть в видео, где это я наглядно демонстрирую. Но и тут замолвлю словечко.
Если мы передаем в метод строку, то класс вкладки обязан имплементить интерфейс Mage_Adminhtml_Block_Widget_Tab_Interface
Иначе вы получите ошибку. А интерфейс требует реализации 4 методов.
Поэтому в примере мы используем 2 варианта для демонстрации. На практике, лучше использовать одинаковые способы добавления вкладок.

Посмотрим содержимое наших вкладок, которое мы скопировали из исходного файла Form.php

app/code/local/IGN/Siteblocks/Block/Adminhtml/Siteblocks/Edit/Tab/Main.php

<?php  class IGN_Siteblocks_Block_Adminhtml_Siteblocks_Edit_Tab_Main extends Mage_Adminhtml_Block_Widget_Form {      /**      * Init form      */     public function __construct()     {         parent::__construct();         $this->setId('main_form');         $this->setTitle(Mage::helper('siteblocks')->__('Block Information'));     }      protected function _prepareForm()     {         $model = Mage::registry('siteblocks_block');         $form = new Varien_Data_Form();           $form->setHtmlIdPrefix('main_');          $fieldset = $form->addFieldset('base_fieldset',             array(                 'legend'=>Mage::helper('siteblocks')->__('General Information'),                 'class' => 'fieldset-wide')         );          if ($model->getBlockId()) {             $fieldset->addField('block_id', 'hidden', array(                 'name' => 'block_id',             ));         }          $fieldset->addField('title', 'text', array(             'name'      => 'title',             'label'     => Mage::helper('siteblocks')->__('Block Title'),             'title'     => Mage::helper('siteblocks')->__('Block Title'),             'required'  => true,         ));          //$fieldset->addType('myimage','IGN_Siteblocks_Block_Adminhtml_Siteblocks_Edit_Renderer_Myimage');          $fieldset->addField('image', 'myimage', array(             'name'      => 'image',             'label'     => Mage::helper('siteblocks')->__('Image'),             'title'     => Mage::helper('siteblocks')->__('Image'),             'required'  => true,         ));            $fieldset->addField('block_status', 'select', array(             'label'     => Mage::helper('siteblocks')->__('Status'),             'title'     => Mage::helper('siteblocks')->__('Status'),             'name'      => 'block_status',             'required'  => true,             'options'   => Mage::getModel('siteblocks/source_status')->toArray(),         ));           $fieldset->addField('content', 'editor', array(             'name'      => 'content',             'label'     => Mage::helper('siteblocks')->__('Content'),             'title'     => Mage::helper('siteblocks')->__('Content'),             'style'     => 'height:36em',             'required'  => true,             'config'    => Mage::getSingleton('cms/wysiwyg_config')->getConfig()          ));           $form->setValues($model->getData());         $this->setForm($form);          return parent::_prepareForm();     }      protected function _prepareLayout()     {         parent::_prepareLayout();         if (Mage::getSingleton('cms/wysiwyg_config')->isEnabled()) {             $this->getLayout()->getBlock('head')->setCanLoadTinyMce(true);         }     } }  

app/code/local/IGN/Siteblocks/Block/Adminhtml/Siteblocks/Edit/Tab/Conditions.php

<?php  class IGN_Siteblocks_Block_Adminhtml_Siteblocks_Edit_Tab_Conditions extends Mage_Adminhtml_Block_Widget_Form implements Mage_Adminhtml_Block_Widget_Tab_Interface {  #методы, которые требует интерфейс     public function getTabTitle()     {         return $this->__('Conditions');     }      public function getTabLabel()     {         return $this->__('Conditions');     }      public function canShowTab()     {         return true;     }      public function isHidden()     {         return false;     }      /**      * Init form      */     public function __construct()     {         parent::__construct();         $this->setId('conditions_form');         $this->setTitle(Mage::helper('siteblocks')->__('Block Conditions'));     }      protected function _prepareForm()     {         $model = Mage::registry('siteblocks_block');         $form = new Varien_Data_Form();           $form->setHtmlIdPrefix('block_');           $model->getConditions()->setJsFormObject('block_conditions_fieldset');          $renderer = Mage::getBlockSingleton('adminhtml/widget_form_renderer_fieldset')             ->setTemplate('promo/fieldset.phtml')             ->setNewChildUrl($this->getUrl('*/promo_catalog/newConditionHtml/form/block_conditions_fieldset'));          $conditionsFieldset = $form->addFieldset('conditions_fieldset',             array(                 'legend'=>Mage::helper('siteblocks')->__('Conditions'),                 'class' => 'fieldset-wide')         )->setRenderer($renderer);         $conditionsFieldset->addField('conditions', 'text', array(             'name' => 'conditions',             'label' => Mage::helper('siteblocks')->__('Conditions'),             'title' => Mage::helper('siteblocks')->__('Conditions'),             'required' => true,         ))->setRule($model)->setRenderer(Mage::getBlockSingleton('rule/conditions'));          $form->setValues($model->getData());         $this->setForm($form);          return parent::_prepareForm();     }      protected function _prepareLayout()     {         parent::_prepareLayout();         if (Mage::getSingleton('cms/wysiwyg_config')->isEnabled()) {             $this->getLayout()->getBlock('head')->setCanLoadTinyMce(true);         }     } }  

Мы просто скопировали исходный файл Form.php. Разделили элементы формы. И не забываем убрать флаг $form->setUseContainer(true);
Cоответственно поля из исходного файла формы можно удалить.

app/code/local/IGN/Siteblocks/Block/Adminhtml/Siteblocks/Edit/Form.php

<?php  class IGN_Siteblocks_Block_Adminhtml_Siteblocks_Edit_Form extends Mage_Adminhtml_Block_Widget_Form {      /**      * Init form      */     public function __construct()     {         parent::__construct();         $this->setId('block_form');         $this->setTitle(Mage::helper('siteblocks')->__('Block Information'));     }      protected function _prepareForm()     {         $model = Mage::registry('siteblocks_block');         $form = new Varien_Data_Form(             array(                 'id' => 'edit_form',                 'action' => $this->getUrl('*/*/save',array('block_id'=>$this->getRequest()->getParam('block_id'))),                 'method' => 'post',                 'enctype' => 'multipart/form-data'             )         );           $form->setHtmlIdPrefix('block_');           $form->setValues($model->getData());         $form->setUseContainer(true);         $this->setForm($form);          return parent::_prepareForm();     }      protected function _prepareLayout()     {         parent::_prepareLayout();         if (Mage::getSingleton('cms/wysiwyg_config')->isEnabled()) {             $this->getLayout()->getBlock('head')->setCanLoadTinyMce(true);         }     } }  

Как сделать вывод блока вкладок.
Способ №1 в контроллере:

app/code/local/IGN/Siteblocks/controllers/Adminhtml/SiteblocksController.php

<?php class IGN_Siteblocks_Adminhtml_SiteblocksController extends Mage_Adminhtml_Controller_Action {      public function indexAction()     {         $this->loadLayout();         $this->_addContent($this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks'));         $this->renderLayout();     }      public function newAction()     {         $this->_forward('edit');     }      public function editAction()     {         $id = $this->getRequest()->getParam('block_id');         Mage::register('siteblocks_block',Mage::getModel('siteblocks/block')->load($id));         $blockObject = (array)Mage::getSingleton('adminhtml/session')->getBlockObject(true);         if(count($blockObject)) {             Mage::registry('siteblocks_block')->setData($blockObject);         }         $this->loadLayout(); #вывод блока вкладок на странице         $this->_addLeft($this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks_edit_tabs'));         $this->_addContent($this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks_edit'));         $this->renderLayout();     }      protected function _uploadFile($fieldName,$model)     {          if( ! isset($_FILES[$fieldName])) {             return false;         }         $file = $_FILES[$fieldName];          if(isset($file['name']) && (file_exists($file['tmp_name']))){             if($model->getId()){                 unlink(Mage::getBaseDir('media').DS.$model->getData($fieldName));             }             try             {                 $path = Mage::getBaseDir('media') . DS . 'siteblocks' . DS;                 $uploader = new Varien_File_Uploader($file);                 $uploader->setAllowedExtensions(array('jpg','png','gif','jpeg'));                 $uploader->setAllowRenameFiles(true);                 $uploader->setFilesDispersion(false);                  $uploader->save($path, $file['name']);                 $model->setData($fieldName,$uploader->getUploadedFileName());                 return true;             }             catch(Exception $e)             {                 return false;             }         }     }      public function saveAction()     {         try {             $id = $this->getRequest()->getParam('block_id');             /** @var IGN_Siteblocks_Model_Block $block */             $block = Mage::getModel('siteblocks/block')->load($id);             /*$block                 ->setTitle($this->getRequest()->getParam('title'))                 ->setContent($this->getRequest()->getParam('content'))                 ->setBlockStatus($this->getRequest()->getParam('block_status'))                 ->save();*/             $data = $this->getRequest()->getParams();             if (isset($data['rule']['conditions'])) {                 $data['conditions'] = $data['rule']['conditions'];             }             unset($data['rule']);             $block                 ->loadPost($data);             $this->_uploadFile('image',$block);             $block                 ->setCreatedAt(Mage::app()->getLocale()->date())                 ->save();              if(!$block->getId()) {                 Mage::getSingleton('adminhtml/session')->addError('Cannot save the block');             }         } catch(Exception $e) {             Mage::logException($e);             Mage::getSingleton('adminhtml/session')->addError($e->getMessage());             Mage::getSingleton('adminhtml/session')->setBlockObject($block->getData());             return  $this->_redirect('*/*/edit',array('block_id'=>$this->getRequest()->getParam('block_id')));         }          Mage::getSingleton('adminhtml/session')->addSuccess('Block was saved successfully!');          $this->_redirect('*/*/'.$this->getRequest()->getParam('back','index'),array('block_id'=>$block->getId()));     }      public function deleteAction()     {         $block = Mage::getModel('siteblocks/block')             ->setId($this->getRequest()->getParam('block_id'))             ->delete();         if($block->getId()) {             Mage::getSingleton('adminhtml/session')->addSuccess('Block was deleted successfully!');         }         $this->_redirect('*/*/');      }      public function massStatusAction()     {         $statuses = $this->getRequest()->getParams();         try {             $blocks= Mage::getModel('siteblocks/block')                 ->getCollection()                 ->addFieldToFilter('block_id',array('in'=>$statuses['massaction']));             foreach($blocks as $block) {                 $block->setBlockStatus($statuses['block_status'])->save();             }         } catch(Exception $e) {             Mage::logException($e);             Mage::getSingleton('adminhtml/session')->addError($e->getMessage());             return $this->_redirect('*/*/');         }         Mage::getSingleton('adminhtml/session')->addSuccess('Blocks were updated!');          return $this->_redirect('*/*/');      }      public function massDeleteAction()     {         $blocks = $this->getRequest()->getParams();         try {             $blocks= Mage::getModel('siteblocks/block')                 ->getCollection()                 ->addFieldToFilter('block_id',array('in'=>$blocks['massaction']));             foreach($blocks as $block) {                 $block->delete();             }         } catch(Exception $e) {             Mage::logException($e);             Mage::getSingleton('adminhtml/session')->addError($e->getMessage());             return $this->_redirect('*/*/');         }         Mage::getSingleton('adminhtml/session')->addSuccess('Blocks were deleted!');          return $this->_redirect('*/*/');      } } 

Но мы откажемся от этой затеи и воспользуемся способом №2 в макете:

app/design/adminhtml/default/default/layout/siteblocks.xml

<?xml version="1.0"?> <layout version="1.0.0">     <adminhtml_siteblocks_edit>         <update handle="editor"/>         <reference name="head">             <action method="setCanLoadExtJs"><flag>1</flag></action>             <action method="setCanLoadRulesJs"><flag>1</flag></action>         </reference> <!-- Выводим блок вкладок на странице редактирования -->         <reference name="left">             <block type="siteblocks/adminhtml_siteblocks_edit_tabs" name="siteblocks_tabs"> <!-- 2 cпособа добавления вкладок в макете -->                 <block name="conditions_tab" type="siteblocks/adminhtml_siteblocks_edit_tab_conditions"/>                 <action method="addTab"><name>my_conditions</name><block>conditions_tab</block></action>                  <action method="addTab"><name>my_conditions</name><block>siteblocks/adminhtml_siteblocks_edit_tab_conditions</block></action>             </block>         </reference>     </adminhtml_siteblocks_edit>      <adminhtml_system_config_edit>         <update handle="editor"/>     </adminhtml_system_config_edit>  </layout> 

И на последок один совет: не стоит добавлять вкладки сразу в 2х местах. Одну в макете, другую в блоке. Делайте добавление в одном месте или все в макете или все в блоке.

Вывод таблицы (grid) товаров на странице редактирования и на frontend.

Видео: Использование вкладок на странице редактирования в Magento

Созданная в уроке структура модуля IGN_Siteblocks-14.zip

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

Добавим новую вкладку:

app/code/local/IGN/Siteblocks/Block/Adminhtml/Siteblocks/Edit/Tabs.php

<?php class IGN_Siteblocks_Block_Adminhtml_Siteblocks_Edit_Tabs extends Mage_Adminhtml_Block_Widget_Tabs {      public function __construct()     {         parent::__construct();         $this->setId('block_tabs');         $this->setDestElementId('edit_form');         $this->setTitle(Mage::helper('siteblocks')->__('Block Information'));     }      protected function _prepareLayout()     {         $this->addTab('main_tab',array(             'label' => $this->__('Main'),             'title' => $this->__('Main'),             'content' => $this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks_edit_tab_main')->toHtml()         ));          $this->addTab('conditions_tab',array(             'label' => $this->__('Conditions'),             'title' => $this->__('Conditions'),             'content' => $this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks_edit_tab_conditions')->toHtml()         )); //вкладку с товарами будет использовать AJAX, поэтому не используем массив параметров как примере выше         $this->addTab('products_tab','siteblocks/adminhtml_siteblocks_edit_tab_products');          return parent::_prepareLayout();     } } 

Вкладка использует AJAX. Это можно увидеть в коде. Там же и указан URL для запросов.

app/code/local/IGN/Siteblocks/Block/Adminhtml/Siteblocks/Edit/Tab/Products.php

<?php  class IGN_Siteblocks_Block_Adminhtml_Siteblocks_Edit_Tab_Products extends Mage_Adminhtml_Block_Widget_Form implements Mage_Adminhtml_Block_Widget_Tab_Interface {      public function getTabTitle()     {         return $this->__('Products');     }      public function getTabLabel()     {         return $this->__('Products');     }      public function canShowTab()     {         return true;     }      public function isHidden()     {         return false;     }      public function getClass()     {         return 'ajax';     }      public function getTabClass()     {         return 'ajax';     }  #URL для запросов, ('_current'=>true) передадим в урл все параметры, а значит и текущий block_id там тоже будет     public function getTabUrl()     {         return $this->getUrl('*/*/products',array('_current'=>true));     } } 

Т.к. вкладка использует AJAX, необходимо добавить экшены в контроллер.
И, забегая вперед, можете посмотреть какая логика была добавлена в saveAction, что бы сохранялись отмеченные товары.

app/code/local/IGN/Siteblocks/controllers/Adminhtml/SiteblocksController.php

<?php class IGN_Siteblocks_Adminhtml_SiteblocksController extends Mage_Adminhtml_Controller_Action {      public function indexAction()     {         $this->loadLayout();         $this->_addContent($this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks'));         $this->renderLayout();     }      public function newAction()     {         $this->_forward('edit');     }      public function editAction()     {         $id = $this->getRequest()->getParam('block_id');         Mage::register('siteblocks_block',Mage::getModel('siteblocks/block')->load($id));         $blockObject = (array)Mage::getSingleton('adminhtml/session')->getBlockObject(true);         if(count($blockObject)) {             Mage::registry('siteblocks_block')->setData($blockObject);         }         $this->loadLayout();         //$this->_addLeft($this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks_edit_tabs'));         $this->_addContent($this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks_edit'));         $this->renderLayout();     }      protected function _uploadFile($fieldName,$model)     {          if( ! isset($_FILES[$fieldName])) {             return false;         }         $file = $_FILES[$fieldName];          if(isset($file['name']) && (file_exists($file['tmp_name']))){             if($model->getId()){                 unlink(Mage::getBaseDir('media').DS.$model->getData($fieldName));             }             try             {                 $path = Mage::getBaseDir('media') . DS . 'siteblocks' . DS;                 $uploader = new Varien_File_Uploader($file);                 $uploader->setAllowedExtensions(array('jpg','png','gif','jpeg'));                 $uploader->setAllowRenameFiles(true);                 $uploader->setFilesDispersion(false);                  $uploader->save($path, $file['name']);                 $model->setData($fieldName,$uploader->getUploadedFileName());                 return true;             }             catch(Exception $e)             {                 return false;             }         }     }      public function saveAction()     {         try {             $id = $this->getRequest()->getParam('block_id');             /** @var IGN_Siteblocks_Model_Block $block */             $block = Mage::getModel('siteblocks/block')->load($id);             /*$block                 ->setTitle($this->getRequest()->getParam('title'))                 ->setContent($this->getRequest()->getParam('content'))                 ->setBlockStatus($this->getRequest()->getParam('block_status'))                 ->save();*/             $data = $this->getRequest()->getParams();  #вот такой участок отвечает за сохранение отмеченных чекбоками товаров             $links = $this->getRequest()->getPost('links', array());             if (array_key_exists('products', $links)) {                 $selectedProducts = Mage::helper('adminhtml/js')->decodeGridSerializedInput($links['products']);                 $products = array();                 foreach($selectedProducts as $product => $position) {                     $products[$product] = isset($position['position']) ? $position['position'] : $product;                 }                 $data['products'] = $products;             }              if (isset($data['rule']['conditions'])) {                 $data['conditions'] = $data['rule']['conditions'];             }             unset($data['rule']);             $block                 ->loadPost($data);             $this->_uploadFile('image',$block);             $block                 ->setCreatedAt(Mage::app()->getLocale()->date())                 ->save();              if(!$block->getId()) {                 Mage::getSingleton('adminhtml/session')->addError('Cannot save the block');             }         } catch(Exception $e) {             Mage::logException($e);             Mage::getSingleton('adminhtml/session')->addError($e->getMessage());             Mage::getSingleton('adminhtml/session')->setBlockObject($block->getData());             return  $this->_redirect('*/*/edit',array('block_id'=>$this->getRequest()->getParam('block_id')));         }          Mage::getSingleton('adminhtml/session')->addSuccess('Block was saved successfully!');          $this->_redirect('*/*/'.$this->getRequest()->getParam('back','index'),array('block_id'=>$block->getId()));     }      public function deleteAction()     {         $block = Mage::getModel('siteblocks/block')             ->setId($this->getRequest()->getParam('block_id'))             ->delete();         if($block->getId()) {             Mage::getSingleton('adminhtml/session')->addSuccess('Block was deleted successfully!');         }         $this->_redirect('*/*/');      }      public function massStatusAction()     {         $statuses = $this->getRequest()->getParams();         try {             $blocks= Mage::getModel('siteblocks/block')                 ->getCollection()                 ->addFieldToFilter('block_id',array('in'=>$statuses['massaction']));             foreach($blocks as $block) {                 $block->setBlockStatus($statuses['block_status'])->save();             }         } catch(Exception $e) {             Mage::logException($e);             Mage::getSingleton('adminhtml/session')->addError($e->getMessage());             return $this->_redirect('*/*/');         }         Mage::getSingleton('adminhtml/session')->addSuccess('Blocks were updated!');          return $this->_redirect('*/*/');      }      public function massDeleteAction()     {         $blocks = $this->getRequest()->getParams();         try {             $blocks= Mage::getModel('siteblocks/block')                 ->getCollection()                 ->addFieldToFilter('block_id',array('in'=>$blocks['massaction']));             foreach($blocks as $block) {                 $block->delete();             }         } catch(Exception $e) {             Mage::logException($e);             Mage::getSingleton('adminhtml/session')->addError($e->getMessage());             return $this->_redirect('*/*/');         }         Mage::getSingleton('adminhtml/session')->addSuccess('Blocks were deleted!');          return $this->_redirect('*/*/');      }  #2 наших новых экшена для AJAX запросов     public function productsAction()     {         $this->loadLayout()             ->renderLayout();     }      public function productsgridAction()     {         $this->loadLayout()             ->renderLayout();     } } 

Из кода в контроллере понятно, что необходимо обновить макет.

app/design/adminhtml/default/default/layout/adminhtml.xml

<?xml version="1.0"?> <layout version="1.0.0">     <adminhtml_siteblocks_edit>         <update handle="editor"/>         <reference name="head">             <action method="setCanLoadExtJs"><flag>1</flag></action>             <action method="setCanLoadRulesJs"><flag>1</flag></action>         </reference>         <reference name="left">             <block type="siteblocks/adminhtml_siteblocks_edit_tabs" name="siteblocks_tabs">                <!-- <block name="conditions_tab" type="siteblocks/adminhtml_siteblocks_edit_tab_conditions"/>                 <action method="addTab"><name>my_conditions</name><block>conditions_tab</block></action>-->                 <!--<action method="addTab"><name>my_conditions</name><block>siteblocks/adminhtml_siteblocks_edit_tab_conditions</block></action>-->             </block>         </reference>     </adminhtml_siteblocks_edit>      <adminhtml_system_config_edit>         <update handle="editor"/>     </adminhtml_system_config_edit>  <!-- Тут выводим таблицу товаров, а всякие дополнительные параметры нужны, что бы можно было сохранять отмеченные товары -->     <adminhtml_siteblocks_products>         <block type="core/text_list" name="root" output="toHtml">             <block type="siteblocks/adminhtml_siteblocks_edit_tab_products_grid" name="siteblocks_products"/>             <block type="adminhtml/widget_grid_serializer" name="siteblocks_products_serializer">                 <reference name="siteblocks_products_serializer">                     <action method="initSerializerBlock">                         <grid_block_name>siteblocks_products</grid_block_name>                         <data_callback>getSelectedBlockProducts</data_callback>                         <hidden_input_name>links[products]</hidden_input_name>                         <reload_param_name>siteblocks_products</reload_param_name>                     </action>                     <action method="addColumnInputName">                         <input_name>position</input_name>                     </action>                 </reference>             </block>         </block>     </adminhtml_siteblocks_products>  <!-- Тут просто выводим хтмл таблицы товаров -->     <adminhtml_siteblocks_productsgrid>         <block type="core/text_list" name="root" output="toHtml">             <block type="siteblocks/adminhtml_siteblocks_edit_tab_products_grid" name="block_products"/>         </block>     </adminhtml_siteblocks_productsgrid>  </layout> 

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

Завершающим элементом в admin интерфейсе будет класс таблицы.

app/code/local/IGN/Siteblocks/Block/Adminhtml/Siteblocks/Edit/Tab/Products/Grid.php

 <?php class IGN_Siteblocks_Block_Adminhtml_Siteblocks_Edit_Tab_Products_Grid extends Mage_Adminhtml_Block_Widget_Grid {      protected $_block;     /**      * Set grid params      *      */     public function __construct()     {         parent::__construct();         $this->setId('siteblocks_product_grid');         $this->setDefaultSort('entity_id');         $this->setUseAjax(true);         if ($this->_getBlock()->getId()) {             $this->setDefaultFilter(array('in_products'=>1));         }         if ($this->isReadonly()) {             $this->setFilterVisibility(false);         }     }      protected function _getBlock()     {         if(!$this->_block) {             $this->_block = Mage::getModel('siteblocks/block')->load($this->getRequest()->getParam('block_id'));         }         return $this->_block;     }       protected function _addColumnFilterToCollection($column)     {         // Set custom filter for in product flag         if ($column->getId() == 'in_products') {             $productIds = $this->_getSelectedProducts();             if (empty($productIds)) {                 $productIds = 0;             }             if ($column->getFilter()->getValue()) {                 $this->getCollection()->addFieldToFilter('entity_id', array('in'=>$productIds));             } else {                 if($productIds) {                     $this->getCollection()->addFieldToFilter('entity_id', array('nin'=>$productIds));                 }             }         } else {             parent::_addColumnFilterToCollection($column);         }         return $this;     }      /**      * Checks when this block is readonly      *      * @return boolean      */     public function isReadonly()     {         return $this->_getBlock()->getUpsellReadonly();     }      protected function _prepareCollection()     { #тут можем указать какую коллекцию используем для таблицы         $collection = Mage::getResourceModel('catalog/product_collection')             ->addAttributeToSelect('*');          if ($this->isReadonly()) {             $productIds = $this->_getSelectedProducts();             if (empty($productIds)) {                 $productIds = array(0);             }             $collection->addFieldToFilter('entity_id', array('in'=>$productIds));         }          $this->setCollection($collection);         return parent::_prepareCollection();     }      /**      * Add columns to grid      *      * @return Mage_Adminhtml_Block_Widget_Grid      */     protected function _prepareColumns()     { #колонки добавляются как в любой другой таблице         if (!$this->_getBlock()->getUpsellReadonly()) {             $this->addColumn('in_products', array(                 'header_css_class' => 'a-center',                 'type'      => 'checkbox',                 'name'      => 'in_products',                 'values'    => $this->_getSelectedProducts(),                 'align'     => 'center',                 'index'     => 'entity_id'             ));         }          $this->addColumn('entity_id', array(             'header'    => Mage::helper('catalog')->__('ID'),             'sortable'  => true,             'width'     => 60,             'index'     => 'entity_id'         ));         $this->addColumn('name', array(             'header'    => Mage::helper('catalog')->__('Name'),             'index'     => 'name'         ));          $this->addColumn('type', array(             'header'    => Mage::helper('catalog')->__('Type'),             'width'     => 100,             'index'     => 'type_id',             'type'      => 'options',             'options'   => Mage::getSingleton('catalog/product_type')->getOptionArray(),         ));          $sets = Mage::getResourceModel('eav/entity_attribute_set_collection')             ->setEntityTypeFilter(Mage::getModel('catalog/product')->getResource()->getTypeId())             ->load()             ->toOptionHash();          $this->addColumn('set_name', array(             'header'    => Mage::helper('catalog')->__('Attrib. Set Name'),             'width'     => 130,             'index'     => 'attribute_set_id',             'type'      => 'options',             'options'   => $sets,         ));          $this->addColumn('status', array(             'header'    => Mage::helper('catalog')->__('Status'),             'width'     => 90,             'index'     => 'status',             'type'      => 'options',             'options'   => Mage::getSingleton('catalog/product_status')->getOptionArray(),         ));          $this->addColumn('visibility', array(             'header'    => Mage::helper('catalog')->__('Visibility'),             'width'     => 90,             'index'     => 'visibility',             'type'      => 'options',             'options'   => Mage::getSingleton('catalog/product_visibility')->getOptionArray(),         ));          $this->addColumn('sku', array(             'header'    => Mage::helper('catalog')->__('SKU'),             'width'     => 80,             'index'     => 'sku'         ));          $this->addColumn('price', array(             'header'        => Mage::helper('catalog')->__('Price'),             'type'          => 'currency',             'currency_code' => (string) Mage::getStoreConfig(Mage_Directory_Model_Currency::XML_PATH_CURRENCY_BASE),             'index'         => 'price'         ));          $this->addColumn('position', array(             'header'            => Mage::helper('catalog')->__('Position'),             'name'              => 'position',             'type'              => 'number',             'width'             => 60,             'validate_class'    => 'validate-number',             'index'             => 'position',             'editable'          => true         ));          return parent::_prepareColumns();     }  #этот URL будет использоваться при сортировке и фильтрации     public function getGridUrl()     {         return $this->_getData('grid_url') ? $this->_getData('grid_url') : $this->getUrl('*/*/productsgrid', array('_current'=>true));     }      protected function _getSelectedProducts()     {        return array_keys($this->getSelectedBlockProducts());     }      public function getSelectedBlockProducts()     {         $selected = $this->getRequest()->getParam('siteblocks_products');          $products = array();         foreach ($this->_getBlock()->getProducts() as $product => $position) {             $products[$product] = array('position' => $position);         }         foreach ($selected as $product) {             if(!isset($products[$product])) {                 $products[$product] = array('position'=>$product);             }         }         return $products;     } } 

Что бы у нас успешно сохранялись товары, нам необходимо обновить версию и создать новый апгрейд скрипт, в которм мы добавим новую колонку.

app/code/local/IGN/Siteblocks/sql/siteblocks_setup/upgrade-1.0.2-1.0.3.php

<?php /** @var Mage_Core_Model_Resource_Setup $installer */ $installer = $this; $installer->startSetup(); $installer->run(" ALTER TABLE `{$this->getTable('siteblocks/block')}` ADD `products` TEXT NOT NULL; "); $installer->endSetup(); 

И небольшие преобразования в модели.

app/code/local/IGN/Siteblocks/Model/Block.php

<?php  /**  * Class IGN_Siteblocks_Model_Block  * @method getBlockStatus()  * @method getContent()  * @method getImage()  */ class IGN_Siteblocks_Model_Block extends Mage_Rule_Model_Abstract {      protected $_eventPrefix = 'siteblocks_block';      public function getActionsInstance()     {         return Mage::getModel('catalogrule/rule_action_collection');     }      public function getConditionsInstance()     {         return Mage::getModel('catalogrule/rule_condition_combine');     }      public function _construct()     {         parent::_construct();         $this->_init('siteblocks/block');      }      public function getImageSrc()     {         return Mage::getBaseUrl('media') . 'siteblocks' . DS . $this->getImage();     }  #перед сохранением преобразуем массив в строку     protected function _beforeSave()     {         parent::_beforeSave();         if(is_array($this->getData('products'))) {             $this->setData('products',json_encode($this->getData('products')));         }     } #после загрузки преобразуем строку в массив     protected function _afterLoad()     {         parent::_beforeSave();         if(!empty($this->getData('products'))) {             $this->setData('products',(array)json_decode($this->getData('products')));         }     }  #дополнительный метод, который вернет нам массив всегда     public function getProducts()     {         if(!is_array($this->getData('products'))) {             $this->setData('products',(array)json_decode($this->getData('products')));         }         return $this->getData('products');     } } 

Товары можем назначить. Теперь необходимо их корректно отобразить на frontend’e.
Для этих целей я создал новый темплейт, который я скопировал из upsell’ов и отредактировал под свои нужды:

app/design/frontend/base/default/template/siteblocks/product/list.php

<?php if(count($this->getLoadedProductCollection()->getItems())): ?>     <div class="box-collateral box-up-sell">         <h2><?php echo $this->__('You may also like') ?></h2>         <ul class="products-grid products-grid--max-4-col" id="upsell-product-table">             <?php foreach ($this->getLoadedProductCollection()->getItems() as $_link): ?>                 <li>                     <a href="<?php echo $_link->getProductUrl() ?>" title="<?php echo $this->escapeHtml($_link->getName()) ?>" class="product-image">                         <img src="<?php echo $this->helper('catalog/image')->init($_link, 'small_image')->resize(280) ?>" alt="<?php echo $this->escapeHtml($_link->getName()) ?>" />                     </a>                     <h3 class="product-name"><a href="<?php echo $_link->getProductUrl() ?>" title="<?php echo $this->escapeHtml($_link->getName()) ?>"><?php echo $this->escapeHtml($_link->getName()) ?></a></h3>                     <?php echo $this->getPriceHtml($_link, true, '-upsell') ?>                 </li>             <?php endforeach; ?>         </ul>     </div> <?php endif ?> 

Так же обновим темплейт вывода блоков list.phtml:

app/design/frontend/base/default/template/siteblocks/list.php

<?php foreach($this->getBlocks() as $block):?>     <div class="siteblock">         <div class="block-title"><?php echo $block->getTitle()?></div>         <div class="block-image">             <?php if($block->getImage()):?>                 <img src="<?php echo $block->getImageSrc()?>" height="150" width="auto" alt="<?php $block->getTitle()?>" title="<?php $block->getTitle()?>">             <?php endif;?>         </div>         <div class="block-content"><?php echo $this->getBlockContent($block)?></div>          <div class="block-product-list">             <?php echo $this->getProductsList($block)?>         </div>     </div> <?php endforeach;?> 

И требуемые изменения в блоке List.php:

app/code/local/IGN/Siteblocks/Block/List.php

<?php class IGN_Siteblocks_Block_List extends Mage_Core_Block_Template {      public function getBlocks()     {         //return Mage::getResourceModel('siteblocks/block_collection');         $items = Mage::getModel('siteblocks/block')->getCollection()             ->addFieldToFilter('block_status',array('eq'=>IGN_Siteblocks_Model_Source_Status::ENABLED));         $filteredItems = $items;         if(Mage::registry('current_product') instanceof Mage_Catalog_Model_Product) {             $filteredItems = array();             /** @var IGN_Siteblocks_Model_Block $item */             foreach ($items as $item) {                 if($item->validate(Mage::registry('current_product'))) {                     $filteredItems[] = $item;                 }             }         }         return $filteredItems;     }      public function getBlockContent($block)     {         $processor = Mage::helper('cms')->getBlockTemplateProcessor();         $html = $processor->filter($block->getContent());         return $html;     } //этот метод используем для вывода товаров     public function getProductsList($block)     {         $products = $block->getProducts();         asort($products);         $collection = Mage::getResourceModel('catalog/product_collection')             ->addFieldToFilter('entity_id',array('in'=>array_keys($products)))             ->addAttributeToSelect('*');         /** @var Mage_Catalog_Block_Product_List $list */         $list = $this->getLayout()->createBlock('catalog/product_list');         $list->setCollection($collection);         $list->setTemplate('siteblocks/product/list.phtml');         return $list->toHtml();     }  } 

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

Таким образом мы получили модуль, который может выводить блоки в некоторых местах сайта. Вывод блоков на странице товара осуществляется с проверкой условий (Rule Conditions).
Для ввода контента у нас используется удобный WYSIWYG редактор.
А так же вместе с блоком мы можем вывести несколько товаров.
Модуль, которому легко найти реальное применение с некоторыми доработками под себя.
Публичный репозиторий с созданным модулем: bitbucket.org/dvman8bit/ign_siteblocks
И этот гайд не был бы полноценным, если бы мы не рассмотрели процесс создания собственного способа оплаты и способа доставки.

Создание модуля способа оплаты (Payment Method)

Видео: Разработка модуля платежного метода для Magento

Публичный репозиторий: bitbucket.org/dvman8bit/ign_payment

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

app/code/community/IGN/Payment/etc/system.xml

<?xml version="1.0"?> <config>    <sections>         <payment>             <groups>                 <ignpayment translate="label">                     <label>IGN Payment</label>                     <frontend_type>text</frontend_type>                     <sort_order>30</sort_order>                     <show_in_default>1</show_in_default>                     <show_in_website>1</show_in_website>                     <show_in_store>1</show_in_store>                     <fields>                         <active translate="label">                             <label>Enabled</label>                             <frontend_type>select</frontend_type>                             <source_model>adminhtml/system_config_source_yesno</source_model>                             <sort_order>1</sort_order>                             <show_in_default>1</show_in_default>                             <show_in_website>1</show_in_website>                             <show_in_store>0</show_in_store>                         </active>                         <order_status translate="label">                             <label>New Order Status</label>                             <frontend_type>select</frontend_type>                             <source_model>adminhtml/system_config_source_order_status_newprocessing</source_model>                             <sort_order>2</sort_order>                             <show_in_default>1</show_in_default>                             <show_in_website>1</show_in_website>                             <show_in_store>0</show_in_store>                         </order_status>                         <payment_action translate="label">                             <label>Automatically Invoice All Items</label>                             <frontend_type>select</frontend_type>                             <source_model>payment/source_invoice</source_model>                             <sort_order>3</sort_order>                             <show_in_default>1</show_in_default>                             <show_in_website>1</show_in_website>                             <show_in_store>0</show_in_store>                             <depends>                                 <order_status separator=",">processing,processed_ogone</order_status>                             </depends>                         </payment_action>                         <sort_order translate="label">                             <label>Sort Order</label>                             <frontend_type>text</frontend_type>                             <sort_order>100</sort_order>                             <show_in_default>1</show_in_default>                             <show_in_website>1</show_in_website>                             <show_in_store>0</show_in_store>                             <frontend_class>validate-number</frontend_class>                         </sort_order>                         <title translate="label">                             <label>Title</label>                             <frontend_type>text</frontend_type>                             <sort_order>1</sort_order>                             <show_in_default>1</show_in_default>                             <show_in_website>1</show_in_website>                             <show_in_store>1</show_in_store>                         </title>                         <allowspecific translate="label">                             <label>Payment from Applicable Countries</label>                             <frontend_type>allowspecific</frontend_type>                             <sort_order>50</sort_order>                             <source_model>adminhtml/system_config_source_payment_allspecificcountries</source_model>                             <show_in_default>1</show_in_default>                             <show_in_website>1</show_in_website>                             <show_in_store>0</show_in_store>                         </allowspecific>                         <specificcountry translate="label">                             <label>Payment from Specific Countries</label>                             <frontend_type>multiselect</frontend_type>                             <sort_order>51</sort_order>                             <source_model>adminhtml/system_config_source_country</source_model>                             <show_in_default>1</show_in_default>                             <show_in_website>1</show_in_website>                             <show_in_store>0</show_in_store>                             <can_be_empty>1</can_be_empty>                         </specificcountry>                         <min_order_total translate="label">                             <label>Minimum Order Total</label>                             <frontend_type>text</frontend_type>                             <sort_order>98</sort_order>                             <show_in_default>1</show_in_default>                             <show_in_website>1</show_in_website>                             <show_in_store>0</show_in_store>                         </min_order_total>                         <max_order_total translate="label">                             <label>Maximum Order Total</label>                             <frontend_type>text</frontend_type>                             <sort_order>99</sort_order>                             <show_in_default>1</show_in_default>                             <show_in_website>1</show_in_website>                             <show_in_store>0</show_in_store>                         </max_order_total>                         <secret_code translate="label">                             <label>Secret Code</label>                             <frontend_type>text</frontend_type>                             <sort_order>99</sort_order>                             <show_in_default>1</show_in_default>                             <show_in_website>1</show_in_website>                             <show_in_store>0</show_in_store>                         </secret_code>                     </fields>                 </ignpayment>             </groups>         </payment>     </sections> </config> 

В system.xml практически все поля стандартны. Мы добавили только 1 новое поле, куда мы введем секретный код.

app/code/community/IGN/Payment/etc/config.xml

<?xml version="1.0"?> <config>     <modules>         <IGN_Payment>             <version>1.0.0</version>         </IGN_Payment>     </modules>     <global>         <models>             <ignpayment>                 <class>IGN_Payment_Model</class>             </ignpayment>         </models>         <resources>             <payment_setup>                 <setup>                     <module>IGN_Payment</module>                 </setup>             </payment_setup>         </resources>         <blocks>             <ignpayment>                 <class>IGN_Payment_Block</class>             </ignpayment>         </blocks>         <helpers>             <ignpayment>                 <class>IGN_Payment_Helper</class>             </ignpayment>         </helpers>     </global>     <frontend>         <translate>             <modules>                 <IGN_Payment>                     <files>                         <default>IGN_Payment.csv</default>                     </files>                 </IGN_Payment>             </modules>         </translate>     </frontend>     <adminhtml>         <translate>             <modules>                 <IGN_Payment>                     <files>                         <default>IGN_Payment.csv</default>                     </files>                 </IGN_Payment>             </modules>         </translate>     </adminhtml>     <default>         <payment>             <ignpayment>                 <active>1</active>                 <model>ignpayment/method</model> <!-- Самый важный момент в настройках -->                 <order_status>pending</order_status>                 <title>Secret Code</title>                 <allowspecific>0</allowspecific>                 <sort_order>1</sort_order>                 <group>offline</group>             </ignpayment>         </payment>     </default> </config> 

Теперь перейдем к самой важной части: модели Method.php.

app/code/community/IGN/Payment/Model/Method.php

<?php class IGN_Payment_Model_Method extends Mage_Payment_Model_Method_Abstract {  //нельзя забывать указать код метода     protected $_code = 'ignpayment';  //указываем block type     protected $_formBlockType = 'ignpayment/form';     protected $_infoBlockType = 'ignpayment/info';  //этот метод используется для валидации секретного кода, а так же любых интересующих нас параметров корзины     public function validate()     {         $code = Mage::app()->getRequest()->getParam('secret_code');         if($code != $this->getConfigData('secret_code')) {             Mage::throwException(Mage::helper('ignpayment')->__("This code doesn't work!"));         }         return parent::validate();     } } 

Обязательно наследуемся от класса Mage_Payment_Model_Method_Abstract
Если заглянуть внутрь этого класса, то увидим там кучу свойств с дефолтными значениями и методов. Свойства и методы несут вполне говорящие названия, поэтому, если нам что-то особенно важно, копируем в свой класс и указываем соответсвующее нуждам значение.
Запоминаем, что в модели реализованы методы:
order(), capture(), void(), refund() и тд. И если наш платежный способ должен «общаться» с серверами платежного сервиса, то копируем методы в свой класс и добавляем в них соответствующие сценарии.

Теперь позаботимся о выводе нашего метода на frontend части.
И тут мы создаем 2 класса.
Form.php используется при выводе платежного способа в блоке оформления заказа.

app/code/community/IGN/Payment/Block/Form.php

<?php  /**  * Payment method form base block  */ class IGN_Payment_Block_Form extends Mage_Payment_Block_Form {     public function _construct()     {         parent::_construct(); //самое главное, это указать свой темплейт, остальную логику мы наследуем от родительского класса         $this->setTemplate('ignpayment/form.phtml');     } } 

Этот блок выводится в инфо блоке на странице заказа.

app/code/community/IGN/Payment/Block/Info.php

<?php class IGN_Payment_Block_Info extends Mage_Payment_Block_Info {     protected function _construct()     {         parent::_construct();         $this->setTemplate('ignpayment/info.phtml');     } } 

И соответствующие блокам темплейты:

app/design/frontend/base/default/template/ignpayment/form.phtml

<!-- Следите за id, он должен начинаться с преффикса payment_form_, а сам элемент по-умолчанию скрыт --> <div id="payment_form_ignpayment" style="display: none">     <input type="text" name="secret_code" autocomplete="off"> <!-- Тут может быть форма ввода карточки или других реквизитов --> </div> 

Содержимое файла info.phtml стандартно, но можем его изменить под свои нужды.

app/design/frontend/base/default/template/ignpayment/info.phtml

<p><strong><?php echo $this->escapeHtml($this->getMethod()->getTitle()) ?></strong></p>  <?php if ($_specificInfo = $this->getSpecificInformation()):?> <table>     <tbody>     <?php foreach ($_specificInfo as $_label => $_value):?>         <tr>             <th><strong><?php echo $this->escapeHtml($_label)?>:</strong></th>         </tr>         <tr>             <td><?php echo nl2br(implode($this->getValueAsArray($_value, true), "\n"))?></td>         </tr>     <?php endforeach; ?>     </tbody> </table> <?php endif;?> <?php echo $this->getChildHtml()?> 

Вот это основа нашего платежного способа. Дальнейшие правки сильно упираются в работу конкретного платежного сервиса. Вам, вполне может понадобиться контроллер, на который будет «стучать» платежный сервис, передавая детали транзакции. А создание контроллеров описано выше, как и создание хелпера, который я тут опустил.

Модуль способа доставки (Shipping Method)

Видео: Разработка способа доставки (Shipping Method) для Magento

Публичный репозиторий: bitbucket.org/dvman8bit/ign_shipment

Посмотрим, какие действия нужны для создания собственного способа доставки.
Наш модуль будет работать с Белпочтой. Т.к. я сам из РБ и мне это вполне актуально. У Белпочты нет публичного API. И нет каптчи, поэтому нам не составит труда спрашивать цену.

Для работы способа доставки необходимо минимум 3 файла.
2 xml и одна модель, мы же еще воспользуемся хелпером. Итого 4.

app/code/community/IGN/Shipment/etc/config.xml

<?xml version="1.0"?> <config>     <modules>         <IGN_Shipment>             <version>1.0.0</version>         </IGN_Shipment>     </modules>     <global>         <models>             <ignshipment>                 <class>IGN_Shipment_Model</class>             </ignshipment>         </models>         <helpers>             <ignshipment>                 <class>IGN_Shipment_Helper</class>             </ignshipment>         </helpers>     </global>     <adminhtml>         <translate>             <modules>                 <IGN_Shipment>                     <files>                         <default>IGN_Shipment.csv</default>                     </files>                 </IGN_Shipment>             </modules>         </translate>     </adminhtml>     <frontend>         <translate>             <modules>                 <IGN_Shipment>                     <files>                         <default>IGN_Shipment.csv</default>                     </files>                 </IGN_Shipment>             </modules>         </translate>     </frontend>     <default>         <carriers>             <ignshipment>                 <active>1</active>                 <sallowspecific>0</sallowspecific>                 <model>ignshipment/carrier</model> <!-- Главное указать модель -->                 <name>IGN Shipment</name>                 <price>5.00</price>                 <title>IGN Shipment</title>                 <type>I</type>                 <specificerrmsg>This shipping method is currently unavailable. If you would like to ship using this shipping method, please contact us.</specificerrmsg>                 <handling_type>F</handling_type>                 <packet_max_weight>2000</packet_max_weight>             </ignshipment>         </carriers>     </default> </config> 

app/code/community/IGN/Shipment/etc/system.xml

<?xml version="1.0"?> <config>     <sections>         <carriers>             <groups>                 <ignshipment translate="label">                     <label>IGN Shipping</label>                     <frontend_type>text</frontend_type>                     <sort_order>2</sort_order>                     <show_in_default>1</show_in_default>                     <show_in_website>1</show_in_website>                     <show_in_store>1</show_in_store>                     <fields>                         <active translate="label">                             <label>Enabled</label>                             <frontend_type>select</frontend_type>                             <source_model>adminhtml/system_config_source_yesno</source_model>                             <sort_order>1</sort_order>                             <show_in_default>1</show_in_default>                             <show_in_website>1</show_in_website>                             <show_in_store>0</show_in_store>                         </active>                         <name translate="label">                             <label>Method Name</label>                             <frontend_type>text</frontend_type>                             <sort_order>3</sort_order>                             <show_in_default>1</show_in_default>                             <show_in_website>1</show_in_website>                             <show_in_store>1</show_in_store>                         </name>                         <price translate="label">                             <label>Price</label>                             <frontend_type>text</frontend_type>                             <validate>validate-number validate-zero-or-greater</validate>                             <sort_order>5</sort_order>                             <show_in_default>1</show_in_default>                             <show_in_website>1</show_in_website>                             <show_in_store>0</show_in_store>                         </price>                         <handling_type translate="label">                             <label>Calculate Handling Fee</label>                             <frontend_type>select</frontend_type>                             <source_model>shipping/source_handlingType</source_model>                             <sort_order>7</sort_order>                             <show_in_default>1</show_in_default>                             <show_in_website>1</show_in_website>                             <show_in_store>0</show_in_store>                         </handling_type>                         <handling_fee translate="label">                             <label>Handling Fee</label>                             <frontend_type>text</frontend_type>                             <validate>validate-number validate-zero-or-greater</validate>                             <sort_order>8</sort_order>                             <show_in_default>1</show_in_default>                             <show_in_website>1</show_in_website>                             <show_in_store>0</show_in_store>                         </handling_fee>                         <sort_order translate="label">                             <label>Sort Order</label>                             <frontend_type>text</frontend_type>                             <sort_order>100</sort_order>                             <show_in_default>1</show_in_default>                             <show_in_website>1</show_in_website>                             <show_in_store>0</show_in_store>                         </sort_order>                         <title translate="label">                             <label>Title</label>                             <frontend_type>text</frontend_type>                             <sort_order>2</sort_order>                             <show_in_default>1</show_in_default>                             <show_in_website>1</show_in_website>                             <show_in_store>1</show_in_store>                         </title>                         <type translate="label">                             <label>Type</label>                             <frontend_type>select</frontend_type>                             <source_model>adminhtml/system_config_source_shipping_flatrate</source_model>                             <sort_order>4</sort_order>                             <show_in_default>1</show_in_default>                             <show_in_website>1</show_in_website>                             <show_in_store>0</show_in_store>                         </type>                         <sallowspecific translate="label">                             <label>Ship to Applicable Countries</label>                             <frontend_type>select</frontend_type>                             <sort_order>90</sort_order>                             <frontend_class>shipping-applicable-country</frontend_class>                             <source_model>adminhtml/system_config_source_shipping_allspecificcountries</source_model>                             <show_in_default>1</show_in_default>                             <show_in_website>1</show_in_website>                             <show_in_store>0</show_in_store>                         </sallowspecific>                         <specificcountry translate="label">                             <label>Ship to Specific Countries</label>                             <frontend_type>multiselect</frontend_type>                             <sort_order>91</sort_order>                             <source_model>adminhtml/system_config_source_country</source_model>                             <show_in_default>1</show_in_default>                             <show_in_website>1</show_in_website>                             <show_in_store>0</show_in_store>                             <can_be_empty>1</can_be_empty>                         </specificcountry>                         <showmethod translate="label">                             <label>Show Method if Not Applicable</label>                             <frontend_type>select</frontend_type>                             <sort_order>92</sort_order>                             <source_model>adminhtml/system_config_source_yesno</source_model>                             <show_in_default>1</show_in_default>                             <show_in_website>1</show_in_website>                             <show_in_store>0</show_in_store>                         </showmethod>                         <specificerrmsg translate="label">                             <label>Displayed Error Message</label>                             <frontend_type>textarea</frontend_type>                             <sort_order>80</sort_order>                             <show_in_default>1</show_in_default>                             <show_in_website>1</show_in_website>                             <show_in_store>1</show_in_store>                         </specificerrmsg>                         <packet_max_weight>                             <label>Packet Max Weight</label>                             <frontend_type>text</frontend_type>                             <sort_order>80</sort_order>                             <show_in_default>1</show_in_default>                             <show_in_website>1</show_in_website>                             <show_in_store>1</show_in_store>                         </packet_max_weight>                     </fields>                 </ignshipment>             </groups>         </carriers>     </sections> </config> 

Теперь можно создать модель Carrier.php

app/code/community/IGN/Shipment/Model/Carrier.php

<?php class IGN_Shipment_Model_Carrier extends Mage_Shipping_Model_Carrier_Abstract implements Mage_Shipping_Model_Carrier_Interface {      protected $_code = 'ignshipment';      public function collectRates(Mage_Shipping_Model_Rate_Request $request)     {         /** @var Mage_Shipping_Model_Rate_Result $result */         $result = Mage::getModel('shipping/rate_result');          $weight = $request->getPackageWeight();          /** @var Mage_Shipping_Model_Rate_Result_Method $method */         $method = Mage::getModel('shipping/rate_result_method');          $method->setCarrier($this->_code);         $method->setCarrierTitle($this->getConfigData('title'));  //В зависимости от общего веса узнаем стоимость у соответствующего способа доставки         if($weight > $this->getConfigData('packet_max_weight')) {             $this->_getBoxMethod($weight,$method);         } else {             $this->_getPacketMethod($weight,$method);         }          $result->append($method);          return $result;     }      protected function _getPacketMethod($weight,$method)     {         $method->setMethod('packet');         $method->setMethodTitle('Packet belpost');         $sum = Mage::helper('ignshipment')->getPacketCost($weight);         $method->setPrice($sum/19050);     }      protected function _getBoxMethod($weight,$method)     {         $method->setMethod('box');         $method->setMethodTitle('Box belpost');         $sum = Mage::helper('ignshipment')->getBoxCost($weight);         $method->setPrice($sum/19050);     }  //Мы не будем реализовывать отслеживание по проблеме отсутствия API     public function isTrackingAvailable()     {         return false;     }      public function getAllowedMethods()     { //по задумке у нас 2 способа доставки. Пакет до 2000 граммов, и посылка         return array(             'packet' => 'Packet belpost',             'box'  => 'Box belpost'         );     }  } 

Наследуем модель от класса Mage_Shipping_Model_Carrier_Abstract . Интерфейс имплементить не обязательно. В нашей логике еще не используется возможность подсчета количества коробок, что так же скажется на стоимости доставки. Но в таком случае придется считать каждую коробку по ее весу и суммировать стоимость. Мы же принимаем, что все товары умещаются в одну общую коробку.

Логику «общения» с белпочтой я вынес в хелпер. В техническом плане ведь просто производится HTTP запрос и распаршивание цены, и нечего делать этому коду в модели.

app/code/community/IGN/Shipment/Helper/Data.php

<?php class IGN_Shipment_Helper_Data extends Mage_Core_Helper_Abstract {       public function getPacketCost($weight)     {         $request = new Zend_Http_Client();         $request->setUri('http://tarifikator.belpost.by/forms/international/packet.php');         $request->setParameterPost(array(             'who'=>'ur',             'type'=>'registered',             'priority'=>'priority',             'to'=>'other',             'weight'=>$weight                                          ));         $response = $request->request(Zend_Http_Client::POST);          $html = $response->getBody();          $tag_regex = "/<blockquote>(.*)<\/blockquote>/im";         $sum_reqex = "/(\d+)/is";         preg_match_all($tag_regex,             $html,             $matches,             PREG_PATTERN_ORDER);         if(isset($matches[1]) && isset($matches[1][0])) {             preg_match($sum_reqex,$matches[1][0],$matches);             if(isset($matches[0])) {                 return (float)$matches[0];             }         } 		//делаем вывод стандартной цены, если не удалось узнать на сайте 		//а можно вернуть ошибку и сделать метод недоступным для использования         return Mage::getStoreConfig('carriers/ignshipment/price');     }      public function getBoxCost($weight)     {         $request = new Zend_Http_Client();         $request->setUri('http://tarifikator.belpost.by/forms/international/ems.php');         $request->setParameterPost(array(             'who'=>'ur',             'type'=>'goods',             'to'=>'n10', //тут простая затычка. нужно создавать ассоциативный массив таких кодов с сайта и кодов страны, т.к. в Magento это US, NZ, AU, а на белпочте это n1,n2,n3 и тд.             'weight'=>$weight         ));         $response = $request->request(Zend_Http_Client::POST);          $html = $response->getBody();          $tag_regex = "/<blockquote>(.*)<\/blockquote>/im";         $sum_reqex = "/(\d+)/is";         preg_match_all($tag_regex,             $html,             $matches,             PREG_PATTERN_ORDER);         if(isset($matches[1]) && isset($matches[1][0])) {             preg_match($sum_reqex,$matches[1][0],$matches);             if(isset($matches[0])) {                 return $matches[0];             }         } 				//делаем вывод стандартной цены, если не удалось узнать на сайте 		//а можно вернуть ошибку и сделать метод недоступным для использования         return Mage::getStoreConfig('carriers/ignshipment/price');     } } 

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

В завершении пожелаю всем успехов. А по ошибкам, которых, наверное, много, пишите желательно в ЛС.
p.s. Не могу не воспользоваться моментом и не попиарить свой маленький youtube канальчик. Заходите, там у стримчики бывают и не только по Magento. А скоро и за разбор Magento 2 возьмемся.
Всем благ!
ссылка на оригинал статьи https://habrahabr.ru/post/312322/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *