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

в 9:57, , рубрики: cms, Magento, php, модуль для magento, ооп, разработка для magento, Разработка под e-commerce, метки:

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

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

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

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

Подготовка

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

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

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

Сервер на 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/(versiond*/)?(.*)$ /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/(versiond*/)?(.*)$ /static.php?resource=$2 last;
    }
    }
    if (!-f $request_filename) {
    rewrite ^/static/(versiond*/)?(.*)$ /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 = *Entersnews*spassword:* %nn *Retypesnews*spassword:* %nn *passwordsupdatedssuccessfully* .

    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 1.x — большой гайд + видео - 22

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

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

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

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


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

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

Видео: Структура и конфигурация модуля 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, core.

И сразу решим, что в 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>
    

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

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

Отладка кода 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 1.x — большой гайд + видео - 25

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

Видео: Модели, коллекции. Работа с базой данных 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 1.x — большой гайд + видео - 26

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

Видео: Контроллеры и роутинг в 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 after="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 1.x — большой гайд + видео - 27

Хелперы

Видео: Хелперы в 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 after="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 1.x — большой гайд + видео - 28

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

Видео: Конфигурация модуля в админке 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 after="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'),
        );
    }


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

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

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

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

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

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

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

Поэтому из темплейта к блоку обращаемся через $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. Чтобы посмотреть на вывод записей, необходимо добавить их на прямую в базу или перейти к следующему шагу разработки формы редактирования.

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

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:

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

Таким образом, формируется 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 1.x — большой гайд + видео - 33

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

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

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

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

И стандартных событий в Magento реализовано очень много. Сделайте поиск по файлам Magento текста «Mage::dispatchEvent». Или загляните по ссылке. Это из явных событий, еще есть события происходящие с каждой моделью, каждым блоком или экшеном контроллера. Как правило это пред и пост события.

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, adminhtml, 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 after="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
     */
//Да это не camelCase стиль именования. Моя привычка: название события = название метода
//Но можно назвать метод как угодно, например: checkoutCartProductAddAfter
    public function checkout_cart_product_add_after($observer)
    {
        var_dump($observer->getEvent()->getData('quote_item')->getData());die;
    }
}

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

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

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

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

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

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

В первую очередь необходимо будет настроить запуск крона Magento, а уже запускаемый файл Magento будет сам распределять когда какую задачу запускать. Настройка Magento cron в консоли:

crontab -e
* * * * * 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 after="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 1.x — большой гайд + видео - 35

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

Видео: Использование рендереров в админке 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();
    }
}

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

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

Использование 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;
    }
}


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

Использование 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 1.x — большой гайд + видео - 38

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

Видео: Использование вкладок на странице редактирования в 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х местах. Одну в макете, другую в блоке. Делайте добавление в одном месте или все в макете или все в блоке.

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

Вывод таблицы (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 редактор.

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


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

Создание модуля способа оплаты (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()?>

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

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

Модуль способа доставки (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 возьмемся.

Всем благ!

Автор: igentuman

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js