Привет, Хабр!
Несмотря на давно уже выпущеную Magento 2, Magento первой версии еще живее всех живых и пока еще не собирается нас покидать. Команда Magento будет поддерживать первую версию продукта 3 года с даты выпуска версии 2, т.е. примерно до ноября 2018. Рынок пестрит широчайшим выбором тем, модулей и сервисов заточеных под Magento 1.x версии. И большое количество сайтов, которые сейчас на Magento 1.x, не торопятся обновляться. Работы много — выхлопа мало. А значит, разработка под Magento первых версий еще актуальна и так будет несколько лет.
Но не о перспективах развития e-commerce решений пойдет речь в этой статье. Тут я решил собрать своеобразный гайд по созданию модулей для Magento 1.x (далее просто Magento). Но не простой гайд, в котором надо всего лишь следовать инструкциям, а с небольшими пояснениями «почему пишем так, а не иначе». Я старался найти золотую середину между краткостью и достаточностью. И в первую очередь, гайд несет пользу новичкам в деле разработки модулей для Magento. Но и более опытным пользователям данный материал может принести пользу.
Собственно, я старался сделать каждую часть самодостаточной, т.е. если вас интересует только отдельный момент, то вы можете взять всю необходимую информацию из конкретного раздела и не бегать по всему гайду. А если уже какие-то участки из раздела у вас реализованы, то их можно и пропустить. Такое же и отношение к видео. Только видео уроков достаточно для работы, но и без видео можно обойтись, порядок дейтсвий и листинги с комментариями есть. Хотя некоторые вещи лучше глянуть в видео, т.к. там по мимо кодинга еще присутствуют и демонстрации работоспособности. Да и я просто мог, что-то упустить. Так что в видео могут присутствуют некоторые незадокументированные моменты, и в текстовой версии могут быть дополнения, которых нет в видео. Это было не избежно, т.к. все делалось в разное время.
- Сервер на Ubuntu 16.04 LTS
- Установка тестового магазина
- Структура и конфигурация модуля
- Отладка кода XDEBUG + PHPSTORM
- Модели, коллекции. Работа с базой данных
- Контроллеры и роутинг
- Хелперы
- Конфигурация модуля в админке
- Frontend блоки. Макеты. Темплейты
- Admin интерфейс. Грид. Форма редактирования
- События и слушатели
- Крон и задачи по расписанию
- Использование рендереров в админке
- Использование WYSIWYG редактора
- Использование Rule Conditions (условий)
- Использование вкладок на странице редактирования
- Вывод таблицы (grid) товаров на странице редактирования и на frontend.
- Создание модуля способа оплаты (Payment Method)
- Модуль способа доставки (Shipping Method)
Подготовка
Все начинается с подготовки рабочего места, а в нашем случае — сервера с установленым тестовым магазином.
Если, у вас окружение уже готово — можете перейти к следующему разделу.
Сервер на Ubuntu 16.04 LTS
Скачиваем дистрибутив Ubuntu 16.04, конфигурируем «виртуалку». И устанавливаем Ubuntu на наш виртуальный компьютер. Процесс установки в целом простой и не требует документации, но весь процесс установки и настройки можно пройти в видео ниже.
Установим и настроим необходимый софт.
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
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; } }
# Magento Vars # # Example configuration: upstream fastcgi_backend { server unix:unix:/run/php/php7.0-fpm.sock; } server { set $MAGE_ROOT /var/www/m2.dev; set $MAGE_MODE default; # or production or developer listen 80; server_name m2.dev; root /var/www/m2.dev/pub; index index.php; autoindex off; charset off; add_header 'X-Content-Type-Options' 'nosniff'; add_header 'X-XSS-Protection' '1; mode=block'; location /setup { root $MAGE_ROOT; location ~ ^/setup/index.php { fastcgi_pass fastcgi_backend; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } location ~ ^/setup/(?!pub/). { deny all; } location ~ ^/setup/pub/ { add_header X-Frame-Options "SAMEORIGIN"; } } location /update { root $MAGE_ROOT; location ~ ^/update/index.php { fastcgi_split_path_info ^(/update/index.php)(/.+)$; fastcgi_pass fastcgi_backend; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; include fastcgi_params; } # deny everything but index.php location ~ ^/update/(?!pub/). { deny all; } location ~ ^/update/pub/ { add_header X-Frame-Options "SAMEORIGIN"; } } location / { try_files $uri $uri/ /index.php?$args; } location /pub { location ~ ^/pub/media/(downloadable|customer|import|theme_customization/.*\.xml) { deny all; } alias $MAGE_ROOT/pub; add_header X-Frame-Options "SAMEORIGIN"; } location /static/ { if ($MAGE_MODE = "production") { expires max; } location ~* \.(ico|jpg|jpeg|png|gif|svg|js|css|swf|eot|ttf|otf|woff|woff2)$ { add_header Cache-Control "public"; add_header X-Frame-Options "SAMEORIGIN"; expires +1y; if (!-f $request_filename) { rewrite ^/static/(version\d*/)?(.*)$ /static.php?resource=$2 last; } } location ~* \.(zip|gz|gzip|bz2|csv|xml)$ { add_header Cache-Control "no-store"; add_header X-Frame-Options "SAMEORIGIN"; expires off; if (!-f $request_filename) { rewrite ^/static/(version\d*/)?(.*)$ /static.php?resource=$2 last; } } if (!-f $request_filename) { rewrite ^/static/(version\d*/)?(.*)$ /static.php?resource=$2 last; } add_header X-Frame-Options "SAMEORIGIN"; } location /media/ { try_files $uri $uri/ /get.php?$args; location ~ ^/media/theme_customization/.*\.xml { deny all; } location ~* \.(ico|jpg|jpeg|png|gif|svg|js|css|swf|eot|ttf|otf|woff|woff2)$ { add_header Cache-Control "public"; add_header X-Frame-Options "SAMEORIGIN"; expires +1y; try_files $uri $uri/ /get.php?$args; } location ~* \.(zip|gz|gzip|bz2|csv|xml)$ { add_header Cache-Control "no-store"; add_header X-Frame-Options "SAMEORIGIN"; expires off; try_files $uri $uri/ /get.php?$args; } add_header X-Frame-Options "SAMEORIGIN"; } location /media/customer/ { deny all; } location /media/downloadable/ { deny all; } location /media/import/ { deny all; } location ~ cron\.php { deny all; } location ~ (index|get|static|report|404|503)\.php$ { try_files $uri =404; fastcgi_pass fastcgi_backend; fastcgi_param PHP_FLAG "session.auto_start=off \n suhosin.session.cryptua=off"; fastcgi_param PHP_VALUE "memory_limit=256M \n max_execution_time=600"; fastcgi_read_timeout 600s; fastcgi_connect_timeout 600s; fastcgi_param MAGE_MODE $MAGE_MODE; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }
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
Мне нравится работать через самбу, подмонтировать себе сетевой диск и спокойно копировать файлы. Но вам она может и не понадобиться. На вкус и цвет, как говорится…
Мой конфиг таков:
[global] workgroup = WORKGROUP server string = %h server (Samba, Ubuntu) dns proxy = no log file = /var/log/samba/log.%m max log size = 1000 syslog = 0 panic action = /usr/share/samba/panic-action %d server role = standalone server passdb backend = tdbsam obey pam restrictions = yes unix password sync = yes passwd program = /usr/bin/passwd %u passwd chat = *Enter\snew\s*\spassword:* %n\n *Retype\snew\s*\spassword:* %n\n *password\supdated\ssuccessfully* . pam password change = yes map to guest = bad user null passwords = Yes guest account = www-data [www] path = /var/www/ comment = WWW folder guest ok = yes browseable = yes read only = no locking = no force user = www-data force group = www-data
Установка тестового магазина
Процесс установки прост и не требует каких-то особых умений. Но для внесения ясности, оставлю видео-инструкцию спрятанную под спойлером.
Cоздание модуля
Структура и конфигурация
Созданная в уроке структура модуля IGN_Siteblocks-1.zip
Учиться создавать модули будем на примере модуля для вывода блоков на страницах магазина (его frontend части). И первым делом мы придумываем название модуля. Название должно быть коротким и нести смысл.
А еще нам нужно выбрать неймспейс (обычно название компании разработчика или его ФИО). И финальное наименование принимает вид Namespace_Modulename. В нашем случае я назвал 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 (и enterprise в Enterprise версии Magento)
И сразу решим, что в core мы ничего изменять не будем, там базовые файлы системы и если их надо изменить, то есть другие способы, помимо их непосредственного редактирования.
Мы можем спокойно использовать local и community (На самом деле, лучше сразу взять community, но в этом примере будет local)
Зайдем в админку магазина, в раздел System > Configuration > Advanced > Disable Modules Output и увидим наш IGN_Siteblocks.
Создадим папки для нашего модуля:
app/code/local/IGN/Siteblocks/
- Block — классы блоков, отвечают за рендеринг страниц
- controllers — контроллеры принимают запросы
- etc — тут всякие конфигурационные файлы
- Helper — дополнительные классы помощники
- Model — модели
- sql — инсталяционные скрипты
Модули в Magento реализуют паттерн MVC
У нас есть модели, вид (блоки, темплейты и макеты) и контроллеры.
В папке etc создадим config.xml
<?xml version="1.0" ?> <config> <modules> <IGN_Siteblocks> <version>1.0.0</version> </IGN_Siteblocks> </modules> <global> <!-- Тут будут модели, блоки, хелперы, реврайты, глобальные обсерверы --> </global> <frontend> <!-- Все касательно frontend части магазина: роуты, макеты, переводы, обсерверы --> </frontend> <admin> <!-- Все касательно admin части магазина: роуты, макеты, переводы--> </admin> <adminhtml> <!-- Все касательно admin части магазина: макеты, переводы, обсерверы --> </adminhtml> <defalut> <!-- Все касательно admin части магазина: макеты, переводы, обсерверы --> </defalut> </config>
Тут мы будем декларировать наши блоки, модели, контроллеры, хелперы, обсерверы, реврайты, макеты, переводы и стандартные значения некоторых настроек модуля.
Отладка кода XDEBUG + PHPSTORM
Тут я бы все-таки рекомендовал посмотреть на видеоинструкцию.
Сначала настроим сервер:
apt-get install php-xdebug
Отредактируем настройки в php.ini или 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.
Модели, коллекции. Работа с базой данных.
Созданная в уроке структура модуля IGN_Siteblocks-2.zip
Модели представляют собой классы для работы с данными и только данными. Никаких тонкостей со способом сохранения этих данных в базе. Никакого кода связанного с рендерингом этих данных. В Magento это: Customer, Product, Order и тд.
Что бы наш модуль мог использовать модели, необходимо отконфигурировать config.xml
Напомню, что модели, блоки и хелперы добавляются в global секцию.
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:
<?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.
<?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 } }
<?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 не обязательно.
Не забываем создать инсталяционный скрипт, который будет создавать таблицу для нашей модели.
<?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');
Контроллеры и роутинг
Созданная в уроке структура модуля IGN_Siteblocks-3.zip
Контроллеры, согласно паттерну MVC, отвечают за обработку запросов. Принимают на себя так называемый входной сигнал в виде HTTP запроса. Перешел по ссылке — отработал соответствующий контроллер.
Перед созданием контроллеров сконфигурируем роутинг в config.xml. Роутинг для frontend и admin части настраивается отдельно. А значит добавляем routers в секцию frontend и admin.
config.xml принимает вид:
<?xml version="1.0" ?> <config> <modules> <IGN_Siteblocks> <version>1.0.0</version> </IGN_Siteblocks> </modules> <global> <models> <siteblocks> <class>IGN_Siteblocks_Model</class> <resourceModel>siteblocks_resource</resourceModel> </siteblocks> <siteblocks_resource> <class>IGN_Siteblocks_Resource</class> <entities> <block> <table>ign_siteblock</table> </block> </entities> </siteblocks_resource> </models> <resources> <siteblocks_setup> <setup> <module>IGN_Siteblocks</module> </setup> </siteblocks_setup> </resources> </global> <frontend> <routers> <siteblocks> <use>standard</use> <args> <module>IGN_Siteblocks</module> <frontName>siteblocks</frontName><!-- любое название, не конфликтуйте с существующими роутерами --> </args> </siteblocks> </routers> </frontend> <admin> <routers> <adminhtml> <args> <modules> <siteblocks before="Mage_Adminhtml">IGN_Siteblocks_Adminhtml</siteblocks> </modules> </args> </adminhtml> </routers> </admin> <default> </default> </config>
Теперь можно создавать свои контроллеры в папке controllers нашего модуля.
Класс контроллера для frontend части должен наследоваться от класса Mage_Core_Controller_Front_Action
Создадим тестовый контроллер TestController.php
<?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мя способами:
- Классическим способом: example.com/siteblocks/test/mytest?param=val¶m2=val
- Через слэши: example.com/siteblocks/test/mytest/param/val/param2/val
Контроллеры для админки создаются в папке controllers/Adminhtml.
Класс контроллера для frontend части должен наследоваться от класса Mage_Adminhtml_Controller_Action
Создадим тестовый контроллер 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) или складываем контроллеры в подпапку.
<?php class IGN_Siteblocks_Adminhtml_Siteblocks_TestController extends Mage_Adminhtml_Controller_Action { public function mytestAction() { die('admin'); } }
Теперь наш URL принимает вид: example.com/admin/siteblocks_test/mytest
Хелперы
Созданная в уроке структура модуля IGN_Siteblocks-4.zip
Классы хелперов в Magento используются как дополнительные классы. В них стоит реализовывать стороннюю логику, которая не вписывается в функционал моделей, блоков или контроллеров. Но модуль нуждается как минимум в одном классе хелпера Data.php.
Этот хелпер используется по-умолчанию для перевода текста (лейблов, пунктов меню и тд) и другой логики.
В хелпере рекомендуется декларировать методы чтения настроек из конфига.
Хелперы должны наследоваться от класса Mage_Core_Helper_Abstract
<?php class IGN_Siteblocks_Helper_Data extends Mage_Core_Helper_Abstract { }
Для переводов текста в хелпере существует метод __(), а его применение выглядит так
echo Mage::helper('siteblocks')->__('Some text')
Файлы переводов мы декларируем в config.xml.
<?xml version="1.0" ?> <config> <modules> <IGN_Siteblocks> <version>1.0.0</version> </IGN_Siteblocks> </modules> <global> <models> <siteblocks> <class>IGN_Siteblocks_Model</class> <resourceModel>siteblocks_resource</resourceModel> </siteblocks> <siteblocks_resource> <class>IGN_Siteblocks_Resource</class> <entities> <block> <table>ign_siteblock</table> </block> </entities> </siteblocks_resource> </models> <resources> <siteblocks_setup> <setup> <module>IGN_Siteblocks</module> </setup> </siteblocks_setup> </resources> <helpers> <siteblocks> <class>IGN_Siteblocks_Helper</class> </siteblocks> </helpers> </global> <frontend> <routers> <siteblocks> <use>standard</use> <args> <module>IGN_Siteblocks</module> <frontName>siteblocks</frontName> </args> </siteblocks> </routers> <translate> <modules> <IGN_Siteblocks> <files> <default>IGN_Siteblocks.csv</default> </files> </IGN_Siteblocks> </modules> </translate> </frontend> <admin> <routers> <adminhtml> <args> <modules> <siteblocks before="Mage_Adminhtml">IGN_Siteblocks_Adminhtml</siteblocks> </modules> </args> </adminhtml> </routers> </admin> <defalut> </defalut> </config>
А файл IGN_Siteblocks.csv создаем в папке app/locale/en_US/
Содержимое вида:
«Some text»,«Some text»
Стараемся выводить текст с использованием своего хелпера и в таком случае, упрощается локализация модуля на разные языки.
Достаточно скопировать файл переводов в соответствующую локаль и перевести второй столбец и нет необходимости копаться в коде.
Конфигурация модуля в админке
Созданная в уроке структура модуля IGN_Siteblocks-5.zip
Для придания модулю гибкости, мы создадим страницу с настройками модуля.
Делается это сугубо через xml файлы.
Нам необходимо создать 2 файла:
system.xml — где будут добавлены поля
adminhtml.xml — где будут указаны разделы и права доступа
А стандартные значения настроек мы можем указать в секции default в файле config.xml
<?xml version="1.0" ?> <config> <modules> <IGN_Siteblocks> <version>1.0.0</version> </IGN_Siteblocks> </modules> <global> <models> <siteblocks> <class>IGN_Siteblocks_Model</class> <resourceModel>siteblocks_resource</resourceModel> </siteblocks> <siteblocks_resource> <class>IGN_Siteblocks_Resource</class> <entities> <block> <table>ign_siteblock</table> </block> </entities> </siteblocks_resource> </models> <resources> <siteblocks_setup> <setup> <module>IGN_Siteblocks</module> </setup> </siteblocks_setup> </resources> <helpers> <siteblocks> <class>IGN_Siteblocks_Helper</class> </siteblocks> </helpers> </global> <frontend> <routers> <siteblocks> <use>standard</use> <args> <module>IGN_Siteblocks</module> <frontName>siteblocks</frontName> </args> </siteblocks> </routers> <translate> <modules> <IGN_Siteblocks> <files> <default>IGN_Siteblocks.csv</default> </files> </IGN_Siteblocks> </modules> </translate> </frontend> <admin> <routers> <adminhtml> <args> <modules> <siteblocks before="Mage_Adminhtml">IGN_Siteblocks_Adminhtml</siteblocks> </modules> </args> </adminhtml> </routers> </admin> <defalut> <siteblocks> <settings> <enabled>1</enabled> <block_count>10</block_count> </settings> </siteblocks> </defalut> </config>
<?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>
<?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>
В наших настройках выводится дропдаун с опциями, и используется собственная модель для этих опций:
<?php class IGN_Siteblocks_Model_Source_Status { const ENABLED = '1'; const DISABLED = '0'; /** * Options getter * * @return array */ public function toOptionArray() { return array( array('value' => self::ENABLED, 'label'=>Mage::helper('siteblocks')->__('Enabled')), array('value' => self::DISABLED, 'label'=>Mage::helper('siteblocks')->__('Disabled')), ); } /** * Get options in "key-value" format * * @return array */ public function toArray() { return array( self::DISABLED => Mage::helper('siteblocks')->__('Disabled'), self::ENABLED => Mage::helper('siteblocks')->__('Enabled'), ); }
Frontend блоки. Макеты. Темплейты
Созданная в уроке структура модуля IGN_Siteblocks-6.zip
Займемся выводом информации на frontend части магазина.
И, как не сложно догадаться из заголовка, у нас будут задействованы 3 типа файлов: блоки, макеты и темплейты.
Блоки это классы, отвечающие за подготовку и вывод информации. Блоки используют для вывода темплейты, но не всегда. Если используется темплейт, то он просто инклюдится в методе fetchView
Поэтому из темплейта к блоку обращаемся через $this
<?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
Макеты используются для построения структуры страницы, какие элементы выводить на странице и в каком порядке.
Создадим файл макетов:
<?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'; }
Создадим темплейт:
<?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
<?php class IGN_Siteblocks_IndexController extends Mage_Core_Controller_Front_Action { public function indexAction() { $this->loadLayout(); #загружаем макеты $this->renderLayout(); #выводим html } }
URL по которому мы увидим вывод имеет вид: example.com/siteblocks/index/index или example.com/siteblocks, т.к. index/index можно опустить.
А handle в макете будет использоваться такой: siteblocks_index_index
Что бы посмотреть на вывод записей, необходимо добавить их на прямую в базу или перейти к следующему шагу разработки формы редактирования.
Admin интерфейс. Грид. Форма редактирования.
Созданная в уроке структура модуля IGN_Siteblocks-7.zip
Процесс создания Admin интерфейса состоит из нескольких этапов:
- Добавляем пункты в меню
- Создаем блоки
- Создаем контроллеры
Добавляем пункты в меню:
<?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 экшен для начала.
<?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 записей.
Теперь, можно перейти к созданию блоков.
<?php class IGN_Siteblocks_Block_Adminhtml_Siteblocks extends Mage_Adminhtml_Block_Widget_Grid_Container { public function __construct() { $this->_controller = 'adminhtml_siteblocks'; $this->_blockGroup = 'siteblocks'; $this->_headerText = Mage::helper('siteblocks')->__('Siteblocks'); $this->_addButtonLabel = Mage::helper('siteblocks')->__('Add New Block'); parent::__construct(); } }
Почему мы прописали такие значения свойств, сейчас увидим в методе класса Mage_Adminhtml_Block_Widget_Grid_Container
Таким образом, формируется block type блока grid.
<?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х блоков: блок контейнер и блок формы.
<?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
Класс формы:
<?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’); и добавим оставшиеся экшены в контроллер.
Нам нужны экшены, редактирования, сохранения, удаления записей. Так же у нас будут добавлены экшены массового удаления и изменения статуса, когда пользователь в таблице может отметить несколко строчек и нажать кнопку удаления этих отмеченых записей.
Контроллер принимает следующий вид:
<?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 части.
События и слушатели
Созданная в уроке структура модуля IGN_Siteblocks-8.zip
В Magento можно использовать шаблон «событие-слушатель». Что позволяет в нашем модуле отлавливать какие-то определенные моменты работы сайта. Добавляет динамичности, гибкости и вносит больше автоматизации.
И стандартных событий в Magento реализовано очень много.
Сделайте поиск по файлам Magento текста «Mage::dispatchEvent»
Или загляните по ссылке magento2.atlassian.net/wiki/display/m1wiki/Magento+1.x+Events+Reference
Это из явных событий, еще есть события происходящие с каждой моделью, каждым блоком или экшеном контроллера. Как правило это пред и пост события.
model_save_before, model_save_after, controller_action_predispatch, controller_action_postdispatch, core_block_abstract_to_html_before, core_block_abstract_to_html_after
Плюс, события в которых используется event_prefix ваших классов или ваш route name контроллеров (siteblocks_save_before, controller_action_predispatch_siteblocks…)
Вариаций очень много и благодаря этой системе, можно с легкостью «подловить» нужное событие.
Непосредственно создать событие можно в любом месте в коде:
Mage::dispatchEvent('some_event_name',array('myparam' => $someVar));
Слушателей декларируют в config.xml. И там есть 3 варианта: global, admin, frontend. Соответственно это просто разделение, где мы хотим, что бы срабатывал наш слушатель.
Наш конфиг принимает следующий вид:
<?xml version="1.0" ?> <config> <modules> <IGN_Siteblocks> <version>1.0.0</version> </IGN_Siteblocks> </modules> <global> <blocks> <siteblocks> <class>IGN_Siteblocks_Block</class> </siteblocks> </blocks> <models> <siteblocks> <class>IGN_Siteblocks_Model</class> <resourceModel>siteblocks_resource</resourceModel> </siteblocks> <siteblocks_resource> <class>IGN_Siteblocks_Model_Resource</class> <entities> <block> <table>ign_siteblock</table> </block> </entities> </siteblocks_resource> </models> <resources> <siteblocks_setup> <setup> <module>IGN_Siteblocks</module> </setup> </siteblocks_setup> </resources> <helpers> <siteblocks> <class>IGN_Siteblocks_Helper</class> </siteblocks> </helpers> </global> <frontend> <events> <checkout_cart_product_add_after> <!-- название события говорит само за себя--> <observers> <siteblocks> <class>siteblocks/observer</class> <method>checkout_cart_product_add_after</method> <!-- я предпочитаю использовать название метода по названию события --> <type>model</type> </siteblocks> </observers> </checkout_cart_product_add_after> </events> <layout> <updates> <siteblocks module="siteblocks"> <file>siteblocks.xml</file> </siteblocks> </updates> </layout> <routers> <siteblocks> <use>standard</use> <args> <module>IGN_Siteblocks</module> <frontName>siteblocks</frontName> </args> </siteblocks> </routers> <translate> <modules> <IGN_Siteblocks> <files> <default>IGN_Siteblocks.csv</default> </files> </IGN_Siteblocks> </modules> </translate> </frontend> <admin> <routers> <adminhtml> <args> <modules> <siteblocks before="Mage_Adminhtml">IGN_Siteblocks_Adminhtml</siteblocks> </modules> </args> </adminhtml> </routers> </admin> <default> <siteblocks> <settings> <enabled>1</enabled> <block_count>10</block_count> </settings> </siteblocks> </default> </config>
Мы добавили 1 слушателя на события добавления товара в корзину. Теперь необходимо создать класс слушателя. Можно обойтись и без этого класса и добавить логику в какую-нибудь модель. Но это дурной тон. Поэтому Observer.php
<?php class IGN_Siteblocks_Model_Observer { /** * @param $bserver Varien_Event_Observer */ public function checkout_cart_product_add_after($observer) { var_dump($observer->getEvent()->getData('quote_item')->getData());die; } }
В своем методе мы можем произвести все необходимые манипуляции.
Сейчас мы просто распечатаем содержимое айтема из корзины. (позже закомментируйте этот код, иначе не сможете добавлять товары в корзину)
Крон и задачи по расписанию
Созданная в уроке структура модуля IGN_Siteblocks-9.zip
Еще одним поспорьем в автоматизации рабочих процессов нашего модуля, и работы магазина является возможность создавать задачи по расписанию.
В первую очередь необходимо будет настроить запуск крона Magento, а уже запускаемый файл Magento будет сам распределять когда какую задачу запускать.
Настройка Magento cron в консоли:
crontab -e
* */1 * * * php /var/www/magento.dev/cron.php
Больше информации тут: help.ubuntu.ru/wiki/cron
Или вы можете не настраивать, а запускать крон, когда вам это нужно, просто перейдя по ссылке вида example.com/cron.php
Наши задачи мы декларируем в config.xml в отдельном блоке crontab.
И обновленный вид файла:
<?xml version="1.0" ?> <config> <modules> <IGN_Siteblocks> <version>1.0.0</version> </IGN_Siteblocks> </modules> <global> <blocks> <siteblocks> <class>IGN_Siteblocks_Block</class> </siteblocks> </blocks> <models> <siteblocks> <class>IGN_Siteblocks_Model</class> <resourceModel>siteblocks_resource</resourceModel> </siteblocks> <siteblocks_resource> <class>IGN_Siteblocks_Model_Resource</class> <entities> <block> <table>ign_siteblock</table> </block> </entities> </siteblocks_resource> </models> <resources> <siteblocks_setup> <setup> <module>IGN_Siteblocks</module> </setup> </siteblocks_setup> </resources> <helpers> <siteblocks> <class>IGN_Siteblocks_Helper</class> </siteblocks> </helpers> </global> <frontend> <events> <controller_action_predispatch> </controller_action_predispatch> <checkout_cart_product_add_after> <observers> <siteblocks> <class>siteblocks/observer</class> <method>checkout_cart_product_add_after</method> <type>model</type> </siteblocks> </observers> </checkout_cart_product_add_after> </events> <layout> <updates> <siteblocks module="siteblocks"> <file>siteblocks.xml</file> </siteblocks> </updates> </layout> <routers> <siteblocks> <use>standard</use> <args> <module>IGN_Siteblocks</module> <frontName>siteblocks</frontName> </args> </siteblocks> </routers> <translate> <modules> <IGN_Siteblocks> <files> <default>IGN_Siteblocks.csv</default> </files> </IGN_Siteblocks> </modules> </translate> </frontend> <admin> <routers> <adminhtml> <args> <modules> <siteblocks before="Mage_Adminhtml">IGN_Siteblocks_Adminhtml</siteblocks> </modules> </args> </adminhtml> </routers> </admin> <default> <siteblocks> <settings> <enabled>1</enabled> <block_count>10</block_count> </settings> </siteblocks> </default> <crontab> <jobs> <siteblocks_clear_cache> <!-- Произвольное название задачи--> <schedule> <cron_expr>*/10 * * * *</cron_expr> <!-- каждые 10 минут --> </schedule> <run> <model>siteblocks/cron::siteblocks_clear_cache</model> <!-- модель и метод, который мы хотим запустить --> </run> </siteblocks_clear_cache> </jobs> </crontab> </config>
Для задач будем использовать отдельный файл Cron.php
<?php class IGN_Siteblocks_Model_Cron { public function siteblocks_clear_cache() { //do something here Mage::app()->cleanCache(array('siteblocks_blocks')); } }
Использование рендереров в админке
Созданная в уроке структура модуля IGN_Siteblocks-10.zip
Зачастую стандартных элементов бывает недостаточно для реализации задуманного функционала. Поэтому можно создать рендерер для нужного элемента и этот процесс не требует больших затрат времени.
Рассмотрим создание рендерера для элемента формы.
У нас есть админ форма:
<?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> по нашему примеру.
Содержимое файлов:
<?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'); } }
<?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.
Вместе с этим я выполнил некоторые дополнения в функционале модуля. Нужно было сделать функционал загрузки и сохранения картинок.
<?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())); } }
<?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
<?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. В контроллере добавить соответствующий код:
<?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.
Отредактируем темплейт:
<?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 и вот ее листинг:
<?php /** * Class IGN_Siteblocks_Model_Block * @method getBlockStatus() * @method getContent() * @method getImage() */ class IGN_Siteblocks_Model_Block extends Mage_Core_Model_Abstract { protected $_eventPrefix = 'siteblocks_block'; public function _construct() { parent::_construct(); $this->_init('siteblocks/block'); } public function getImageSrc() { return Mage::getBaseUrl('media') . 'siteblocks' . DS . $this->getImage(); } }
Выводить полноразмерные загруженные картинки это не хорошая идея, но главной задачей сейчас было описание рендереров.
Использование WYSIWYG редактора
Созданная в уроке структура модуля IGN_Siteblocks-11.zip
WYSIWYG — What you see is what you get (то что вы видете, то и получите)
Это удобный редактор для создания контента. И в нашем модуле ему есть применение.
Но его включение не является таким простым, как ожидалось.
Мы подошли к тому, что нам необходимо создать макет для админки.
<?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>
Теперь необходимо обновить форму редактирования.
<?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 элемента.
<?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 теперь выглядит так:
<?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.
<?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
<?php class IGN_Siteblocks_Block_List extends Mage_Core_Block_Template { public function getBlocks() { //return Mage::getResourceModel('siteblocks/block_collection'); $items = Mage::getModel('siteblocks/block')->getCollection() ->addFieldToFilter('block_status',array('eq'=>IGN_Siteblocks_Model_Source_Status::ENABLED)); return $items; } public function getBlockContent($block) { $processor = Mage::helper('cms')->getBlockTemplateProcessor(); $html = $processor->filter($block->getContent()); return $html; } }
Использование Rule Conditions (условий)
Созданная в уроке структура модуля IGN_Siteblocks-12.zip
Следующим шагом, мы добавим в наш модуль условия. Такие же используются в Magento Promotional Rules. И тут заготовлено 2 типа условий. В первом, используются аттрибуты товара, во втором корзина. Изложенный ниже рецепт описывает первый случай, но их отличия заключаются лишь в подмене нескольких строк.
Зачем нам нужны условия? Мы будем использовать условия для того, что бы выбирать, где будет выводиться блок. Например, на страницах товаров у которых цена ниже $100 или все телефоны из определенной категории, у которых 16гб памяти и дата производства 2015. Мы тут не о юзкейсах будем разговаривать.
Порядок создания:
1. Обновляем версию модуля и доавляем upgrade скрипт, что бы в таблице добавилось новая колонка conditions_serialized типа TEXT.
<?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
<?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 в нашем контроллере.
<?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
<?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 формы, где мы и добавим дизайнер условий.
<?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
<?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; } }
Использование валидатора предельно простое. В нашем случае мы игнорируем условия, если вывод блока происходит не на странице твара и валидировать в таком случае нечего.
Использование вкладок на странице редактирования
Созданная в уроке структура модуля IGN_Siteblocks-13.zip
Вкладки удобно и полезно использовать когда у вас становится много полей. Вы разделяете поля по группам и каждой группе создаете свою вкладку. Существует несколько вариантов добавления вкладок.
Сначала нам необходимо создать класс вкладок и добавить его вывод на странице редактирования.
Сам класс выглядит так:
<?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
<?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); } } }
<?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оответственно поля из исходного файла формы можно удалить.
<?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 в контроллере:
<?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 в макете:
<?xml version="1.0"?> <layout version="1.0.0"> <adminhtml_siteblocks_edit> <update handle="editor"/> <reference name="head"> <action method="setCanLoadExtJs"><flag>1</flag></action> <action method="setCanLoadRulesJs"><flag>1</flag></action> </reference> <!-- Выводим блок вкладок на странице редактирования --> <reference name="left"> <block type="siteblocks/adminhtml_siteblocks_edit_tabs" name="siteblocks_tabs"> <!-- 2 cпособа добавления вкладок в макете --> <block name="conditions_tab" type="siteblocks/adminhtml_siteblocks_edit_tab_conditions"/> <action method="addTab"><name>my_conditions</name><block>conditions_tab</block></action> <action method="addTab"><name>my_conditions</name><block>siteblocks/adminhtml_siteblocks_edit_tab_conditions</block></action> </block> </reference> </adminhtml_siteblocks_edit> <adminhtml_system_config_edit> <update handle="editor"/> </adminhtml_system_config_edit> </layout>
И на последок один совет: не стоит добавлять вкладки сразу в 2х местах. Одну в макете, другую в блоке. Делайте добавление в одном месте или все в макете или все в блоке.
Вывод таблицы (grid) товаров на странице редактирования и на frontend.
Созданная в уроке структура модуля IGN_Siteblocks-14.zip
Теперь мы добавим в модуль финальную фичу — возможность отметить товары, которые будут выводиться на фронтенд вместе с блоком.
Этакая альтернатива сопутствующих товаров. В уме складываются долольно полезные юзкейсы вывода блока с текстом и товарами на страницах товаров с подходящими для блока условиями.
Добавим новую вкладку:
<?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 для запросов.
<?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, что бы сохранялись отмеченные товары.
<?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(); } }
Из кода в контроллере понятно, что необходимо обновить макет.
<?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 интерфейсе будет класс таблицы.
<?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; } }
Что бы у нас успешно сохранялись товары, нам необходимо обновить версию и создать новый апгрейд скрипт, в которм мы добавим новую колонку.
<?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();
И небольшие преобразования в модели.
<?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’ов и отредактировал под свои нужды:
<?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:
<?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:
<?php class IGN_Siteblocks_Block_List extends Mage_Core_Block_Template { public function getBlocks() { //return Mage::getResourceModel('siteblocks/block_collection'); $items = Mage::getModel('siteblocks/block')->getCollection() ->addFieldToFilter('block_status',array('eq'=>IGN_Siteblocks_Model_Source_Status::ENABLED)); $filteredItems = $items; if(Mage::registry('current_product') instanceof Mage_Catalog_Model_Product) { $filteredItems = array(); /** @var IGN_Siteblocks_Model_Block $item */ foreach ($items as $item) { if($item->validate(Mage::registry('current_product'))) { $filteredItems[] = $item; } } } return $filteredItems; } public function getBlockContent($block) { $processor = Mage::helper('cms')->getBlockTemplateProcessor(); $html = $processor->filter($block->getContent()); return $html; } //этот метод используем для вывода товаров public function getProductsList($block) { $products = $block->getProducts(); asort($products); $collection = Mage::getResourceModel('catalog/product_collection') ->addFieldToFilter('entity_id',array('in'=>array_keys($products))) ->addAttributeToSelect('*'); /** @var Mage_Catalog_Block_Product_List $list */ $list = $this->getLayout()->createBlock('catalog/product_list'); $list->setCollection($collection); $list->setTemplate('siteblocks/product/list.phtml'); return $list->toHtml(); } }
Мы бы могли и свой блок для товаров создать, но под наши задачи можем использовать стандартный.
Таким образом мы получили модуль, который может выводить блоки в некоторых местах сайта. Вывод блоков на странице товара осуществляется с проверкой условий (Rule Conditions).
Для ввода контента у нас используется удобный WYSIWYG редактор.
А так же вместе с блоком мы можем вывести несколько товаров.
Модуль, которому легко найти реальное применение с некоторыми доработками под себя.
Публичный репозиторий с созданным модулем: bitbucket.org/dvman8bit/ign_siteblocks
И этот гайд не был бы полноценным, если бы мы не рассмотрели процесс создания собственного способа оплаты и способа доставки.
Создание модуля способа оплаты (Payment Method)
Публичный репозиторий: bitbucket.org/dvman8bit/ign_payment
Это будет платежный способ которым можно будет оплатить заказ, введя секретный код.
Давайте представим, что это ввод каких-то реквизитов для оплаты заказа. Тему можно развить и сделать полноценную форму. Наша же задача — понять минимум действий для создания основы будущего полноценного способа оплаты.
Способ оплаты включает в себя несколько файлов: 2 блока, 2 темплейта, 2 xml файла и 1 модель.
Начнем с system.xml, в нем мы добавим новую секцию в уже существующей вкладке Payment Methods.
<?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 новое поле, куда мы введем секретный код.
<?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.
<?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 используется при выводе платежного способа в блоке оформления заказа.
<?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'); } }
Этот блок выводится в инфо блоке на странице заказа.
<?php class IGN_Payment_Block_Info extends Mage_Payment_Block_Info { protected function _construct() { parent::_construct(); $this->setTemplate('ignpayment/info.phtml'); } }
И соответствующие блокам темплейты:
<!-- Следите за id, он должен начинаться с преффикса payment_form_, а сам элемент по-умолчанию скрыт --> <div id="payment_form_ignpayment" style="display: none"> <input type="text" name="secret_code" autocomplete="off"> <!-- Тут может быть форма ввода карточки или других реквизитов --> </div>
Содержимое файла info.phtml стандартно, но можем его изменить под свои нужды.
<p><strong><?php echo $this->escapeHtml($this->getMethod()->getTitle()) ?></strong></p> <?php if ($_specificInfo = $this->getSpecificInformation()):?> <table> <tbody> <?php foreach ($_specificInfo as $_label => $_value):?> <tr> <th><strong><?php echo $this->escapeHtml($_label)?>:</strong></th> </tr> <tr> <td><?php echo nl2br(implode($this->getValueAsArray($_value, true), "\n"))?></td> </tr> <?php endforeach; ?> </tbody> </table> <?php endif;?> <?php echo $this->getChildHtml()?>
Вот это основа нашего платежного способа. Дальнейшие правки сильно упираются в работу конкретного платежного сервиса. Вам, вполне может понадобиться контроллер, на который будет «стучать» платежный сервис, передавая детали транзакции. А создание контроллеров описано выше, как и создание хелпера, который я тут опустил.
Модуль способа доставки (Shipping Method)
Публичный репозиторий: bitbucket.org/dvman8bit/ign_shipment
Посмотрим, какие действия нужны для создания собственного способа доставки.
Наш модуль будет работать с Белпочтой. Т.к. я сам из РБ и мне это вполне актуально. У Белпочты нет публичного API. И нет каптчи, поэтому нам не составит труда спрашивать цену.
Для работы способа доставки необходимо минимум 3 файла.
2 xml и одна модель, мы же еще воспользуемся хелпером. Итого 4.
<?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>
<?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
<?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 запрос и распаршивание цены, и нечего делать этому коду в модели.
<?php class IGN_Shipment_Helper_Data extends Mage_Core_Helper_Abstract { public function getPacketCost($weight) { $request = new Zend_Http_Client(); $request->setUri('http://tarifikator.belpost.by/forms/international/packet.php'); $request->setParameterPost(array( 'who'=>'ur', 'type'=>'registered', 'priority'=>'priority', 'to'=>'other', 'weight'=>$weight )); $response = $request->request(Zend_Http_Client::POST); $html = $response->getBody(); $tag_regex = "/<blockquote>(.*)<\/blockquote>/im"; $sum_reqex = "/(\d+)/is"; preg_match_all($tag_regex, $html, $matches, PREG_PATTERN_ORDER); if(isset($matches[1]) && isset($matches[1][0])) { preg_match($sum_reqex,$matches[1][0],$matches); if(isset($matches[0])) { return (float)$matches[0]; } } //делаем вывод стандартной цены, если не удалось узнать на сайте //а можно вернуть ошибку и сделать метод недоступным для использования return Mage::getStoreConfig('carriers/ignshipment/price'); } public function getBoxCost($weight) { $request = new Zend_Http_Client(); $request->setUri('http://tarifikator.belpost.by/forms/international/ems.php'); $request->setParameterPost(array( 'who'=>'ur', 'type'=>'goods', 'to'=>'n10', //тут простая затычка. нужно создавать ассоциативный массив таких кодов с сайта и кодов страны, т.к. в Magento это US, NZ, AU, а на белпочте это n1,n2,n3 и тд. 'weight'=>$weight )); $response = $request->request(Zend_Http_Client::POST); $html = $response->getBody(); $tag_regex = "/<blockquote>(.*)<\/blockquote>/im"; $sum_reqex = "/(\d+)/is"; preg_match_all($tag_regex, $html, $matches, PREG_PATTERN_ORDER); if(isset($matches[1]) && isset($matches[1][0])) { preg_match($sum_reqex,$matches[1][0],$matches); if(isset($matches[0])) { return $matches[0]; } } //делаем вывод стандартной цены, если не удалось узнать на сайте //а можно вернуть ошибку и сделать метод недоступным для использования return Mage::getStoreConfig('carriers/ignshipment/price'); } }
Возможно у вас есть вопросы к моим регуляркам. У меня тоже есть к ним вопросы, но оставим это по принципу «работает — не трогай».
Мы можем не углубляться в процесс «узнавания» цены. Все это приведено лишь для примера. В продакшн версии такой код не сгодится. И вообще, такое стоит разрабатывать в виде сервиса, плюс добавлять кеширование, а еще, было бы хорошо высчитать формулу рассчета стоимости. Иначе, возникнут проблемы при недоступности сервера белпочты или когда они обновят дизайн. Можно поискать формулу рассчета стоимости где-то на сайте или спросить на почте у какого-нибудь дружелюбного сотрудника почты.
Подведем итог. Метод доставки умеет подсчитывать стоимость исходя из общего веса. Вес берется из стандартного аттрибута товаров. И если администратор не поленился его указать каждому товару, то все заработает.
В завершении пожелаю всем успехов. А по ошибкам, которых, наверное, много, пишите желательно в ЛС.
p.s. Не могу не воспользоваться моментом и не попиарить свой маленький youtube канальчик. Заходите, там у стримчики бывают и не только по Magento. А скоро и за разбор Magento 2 возьмемся.
Всем благ!
ссылка на оригинал статьи https://habrahabr.ru/post/312322/
Добавить комментарий