Привет! Несмотря на давно уже выпущеную 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/(versiond*/)?(.*)$ /static.php?resource=$2 last;
}
}
location ~* .(zip|gz|gzip|bz2|csv|xml)$ {
add_header Cache-Control "no-store";
add_header X-Frame-Options "SAMEORIGIN";
expires off;
if (!-f $request_filename) {
rewrite ^/static/(versiond*/)?(.*)$ /static.php?resource=$2 last;
}
}
if (!-f $request_filename) {
rewrite ^/static/(versiond*/)?(.*)$ /static.php?resource=$2 last;
}
add_header X-Frame-Options "SAMEORIGIN";
}
location /media/ {
try_files $uri $uri/ /get.php?$args;
location ~ ^/media/theme_customization/.*.xml {
deny all;
}
location ~* .(ico|jpg|jpeg|png|gif|svg|js|css|swf|eot|ttf|otf|woff|woff2)$ {
add_header Cache-Control "public";
add_header X-Frame-Options "SAMEORIGIN";
expires +1y;
try_files $uri $uri/ /get.php?$args;
}
location ~* .(zip|gz|gzip|bz2|csv|xml)$ {
add_header Cache-Control "no-store";
add_header X-Frame-Options "SAMEORIGIN";
expires off;
try_files $uri $uri/ /get.php?$args;
}
add_header X-Frame-Options "SAMEORIGIN";
}
location /media/customer/ {
deny all;
}
location /media/downloadable/ {
deny all;
}
location /media/import/ {
deny all;
}
location ~ cron.php {
deny all;
}
location ~ (index|get|static|report|404|503).php$ {
try_files $uri =404;
fastcgi_pass fastcgi_backend;
fastcgi_param PHP_FLAG "session.auto_start=off n suhosin.session.cryptua=off";
fastcgi_param PHP_VALUE "memory_limit=256M n max_execution_time=600";
fastcgi_read_timeout 600s;
fastcgi_connect_timeout 600s;
fastcgi_param MAGE_MODE $MAGE_MODE;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
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 = *Entersnews*spassword:* %nn *Retypesnews*spassword:* %nn *passwordsupdatedssuccessfully* .
pam password change = yes
map to guest = bad user
null passwords = Yes
guest account = www-data
[www]
path = /var/www/
comment = WWW folder
guest ok = yes
browseable = yes
read only = no
locking = no
force user = www-data
force group = www-data
Установка тестового магазина
Процесс установки прост и не требует каких-то особых умений. Но для внесения ясности, оставлю видео-инструкцию спрятанную под спойлером.
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.
И сразу решим, что в 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 after="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 after="Mage_Adminhtml">IGN_Siteblocks_Adminhtml</siteblocks>
</modules>
</args>
</adminhtml>
</routers>
</admin>
<defalut>
</defalut>
</config>
А файл IGN_Siteblocks.csv создаем в папке app/locale/en_US/. Содержимое вида: «Some text»,«Some text».
Стараемся выводить текст с использованием своего хелпера и в таком случае, упрощается локализация модуля на разные языки.
Достаточно скопировать файл переводов в соответствующую локаль и перевести второй столбец и нет необходимости копаться в коде.
Конфигурация модуля в админке
Созданная в уроке структура модуля 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 after="Mage_Adminhtml">IGN_Siteblocks_Adminhtml</siteblocks>
</modules>
</args>
</adminhtml>
</routers>
</admin>
<defalut>
<siteblocks>
<settings>
<enabled>1</enabled>
<block_count>10</block_count>
</settings>
</siteblocks>
</defalut>
</config>
<?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». Или загляните по ссылке. Это из явных событий, еще есть события происходящие с каждой моделью, каждым блоком или экшеном контроллера. Как правило это пред и пост события.
model_save_before, model_save_after, controller_action_predispatch, controller_action_postdispatch, core_block_abstract_to_html_before, core_block_abstract_to_html_after
Плюс, события в которых используется event_prefix ваших классов или ваш route name контроллеров (siteblocks_save_before, controller_action_predispatch_siteblocks...)
Вариаций очень много и благодаря этой системе, можно с легкостью «подловить» нужное событие.
Непосредственно создать событие можно в любом месте в коде:
Mage::dispatchEvent('some_event_name',array('myparam' => $someVar));
Слушателей декларируют в config.xml. И там есть 3 варианта: global, adminhtml, frontend. Соответственно это просто разделение, где мы хотим, что бы срабатывал наш слушатель. Наш конфиг принимает следующий вид:
<?xml version="1.0" ?>
<config>
<modules>
<IGN_Siteblocks>
<version>1.0.0</version>
</IGN_Siteblocks>
</modules>
<global>
<blocks>
<siteblocks>
<class>IGN_Siteblocks_Block</class>
</siteblocks>
</blocks>
<models>
<siteblocks>
<class>IGN_Siteblocks_Model</class>
<resourceModel>siteblocks_resource</resourceModel>
</siteblocks>
<siteblocks_resource>
<class>IGN_Siteblocks_Model_Resource</class>
<entities>
<block>
<table>ign_siteblock</table>
</block>
</entities>
</siteblocks_resource>
</models>
<resources>
<siteblocks_setup>
<setup>
<module>IGN_Siteblocks</module>
</setup>
</siteblocks_setup>
</resources>
<helpers>
<siteblocks>
<class>IGN_Siteblocks_Helper</class>
</siteblocks>
</helpers>
</global>
<frontend>
<events>
<checkout_cart_product_add_after> <!-- название события говорит само за себя-->
<observers>
<siteblocks>
<class>siteblocks/observer</class>
<method>checkout_cart_product_add_after</method> <!-- я предпочитаю использовать название метода по названию события -->
<type>model</type>
</siteblocks>
</observers>
</checkout_cart_product_add_after>
</events>
<layout>
<updates>
<siteblocks module="siteblocks">
<file>siteblocks.xml</file>
</siteblocks>
</updates>
</layout>
<routers>
<siteblocks>
<use>standard</use>
<args>
<module>IGN_Siteblocks</module>
<frontName>siteblocks</frontName>
</args>
</siteblocks>
</routers>
<translate>
<modules>
<IGN_Siteblocks>
<files>
<default>IGN_Siteblocks.csv</default>
</files>
</IGN_Siteblocks>
</modules>
</translate>
</frontend>
<admin>
<routers>
<adminhtml>
<args>
<modules>
<siteblocks after="Mage_Adminhtml">IGN_Siteblocks_Adminhtml</siteblocks>
</modules>
</args>
</adminhtml>
</routers>
</admin>
<default>
<siteblocks>
<settings>
<enabled>1</enabled>
<block_count>10</block_count>
</settings>
</siteblocks>
</default>
</config>
Мы добавили 1 слушателя на события добавления товара в корзину. Теперь необходимо создать класс слушателя. Можно обойтись и без этого класса и добавить логику в какую-нибудь модель. Но это дурной тон. Поэтому Observer.php
<?php
class IGN_Siteblocks_Model_Observer {
/**
* @param $bserver Varien_Event_Observer
*/
//Да это не camelCase стиль именования. Моя привычка: название события = название метода
//Но можно назвать метод как угодно, например: checkoutCartProductAddAfter
public function checkout_cart_product_add_after($observer)
{
var_dump($observer->getEvent()->getData('quote_item')->getData());die;
}
}
В своем методе мы можем произвести все необходимые манипуляции. Сейчас мы просто распечатаем содержимое айтема из корзины. (позже закомментируйте этот код, иначе не сможете добавлять товары в корзину).
Крон и задачи по расписанию
Созданная в уроке структура модуля IGN_Siteblocks-9.zip
Еще одним поспорьем в автоматизации рабочих процессов нашего модуля, и работы магазина является возможность создавать задачи по расписанию.
В первую очередь необходимо будет настроить запуск крона Magento, а уже запускаемый файл Magento будет сам распределять когда какую задачу запускать. Настройка Magento cron в консоли:
crontab -e
* * * * * php /var/www/magento.dev/cron.php
Больше информации тут: help.ubuntu.ru/wiki/cron. Или вы можете не настраивать, а запускать крон, когда вам это нужно, просто перейдя по ссылке вида example.com/cron.php
Наши задачи мы декларируем в config.xml в отдельном блоке crontab. И обновленный вид файла:
<?xml version="1.0" ?>
<config>
<modules>
<IGN_Siteblocks>
<version>1.0.0</version>
</IGN_Siteblocks>
</modules>
<global>
<blocks>
<siteblocks>
<class>IGN_Siteblocks_Block</class>
</siteblocks>
</blocks>
<models>
<siteblocks>
<class>IGN_Siteblocks_Model</class>
<resourceModel>siteblocks_resource</resourceModel>
</siteblocks>
<siteblocks_resource>
<class>IGN_Siteblocks_Model_Resource</class>
<entities>
<block>
<table>ign_siteblock</table>
</block>
</entities>
</siteblocks_resource>
</models>
<resources>
<siteblocks_setup>
<setup>
<module>IGN_Siteblocks</module>
</setup>
</siteblocks_setup>
</resources>
<helpers>
<siteblocks>
<class>IGN_Siteblocks_Helper</class>
</siteblocks>
</helpers>
</global>
<frontend>
<events>
<controller_action_predispatch>
</controller_action_predispatch>
<checkout_cart_product_add_after>
<observers>
<siteblocks>
<class>siteblocks/observer</class>
<method>checkout_cart_product_add_after</method>
<type>model</type>
</siteblocks>
</observers>
</checkout_cart_product_add_after>
</events>
<layout>
<updates>
<siteblocks module="siteblocks">
<file>siteblocks.xml</file>
</siteblocks>
</updates>
</layout>
<routers>
<siteblocks>
<use>standard</use>
<args>
<module>IGN_Siteblocks</module>
<frontName>siteblocks</frontName>
</args>
</siteblocks>
</routers>
<translate>
<modules>
<IGN_Siteblocks>
<files>
<default>IGN_Siteblocks.csv</default>
</files>
</IGN_Siteblocks>
</modules>
</translate>
</frontend>
<admin>
<routers>
<adminhtml>
<args>
<modules>
<siteblocks after="Mage_Adminhtml">IGN_Siteblocks_Adminhtml</siteblocks>
</modules>
</args>
</adminhtml>
</routers>
</admin>
<default>
<siteblocks>
<settings>
<enabled>1</enabled>
<block_count>10</block_count>
</settings>
</siteblocks>
</default>
<crontab>
<jobs>
<siteblocks_clear_cache> <!-- Произвольное название задачи-->
<schedule>
<cron_expr>*/10 * * * *</cron_expr> <!-- каждые 10 минут -->
</schedule>
<run>
<model>siteblocks/cron::siteblocks_clear_cache</model> <!-- модель и метод, который мы хотим запустить -->
</run>
</siteblocks_clear_cache>
</jobs>
</crontab>
</config>
Для задач будем использовать отдельный файл Cron.php
<?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 редактор.
А так же вместе с блоком мы можем вывести несколько товаров. Модуль, которому легко найти реальное применение с некоторыми доработками под себя. Публичный репозиторий с созданным модулем. И этот гайд не был бы полноценным, если бы мы не рассмотрели процесс создания собственного способа оплаты и способа доставки.
Создание модуля способа оплаты (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 возьмемся.
Всем благ!
Автор: igentuman