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



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

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

Власне, я намагався зробити кожну частину самодостатньою, тобто якщо вас цікавить лише окремий момент, то ви можете взяти всю необхідну інформацію з конкретного розділу і не бігати по всьому гайду. А якщо вже якісь ділянки з розділу у вас реалізовані, то їх можна і пропустити. Таке ж ставлення до відео. Тільки відео уроків достатньо для роботи, але і без відео можна обійтися, порядок дейтсвий і лістинги з коментарями є. Хоча деякі речі краще глянути відео, т. к. там по мимо кодинга ще присутні і демонстрації працездатності. Так і я просто не міг, щось упустити. Так що відео можуть присутні деякі незадокументированные моменти, і в текстовій версії можуть бути доповнення, яких немає у відео. Це було не ізбежно, т. до. все робилося в різний час.

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

Якщо, у вас оточення вже готове — можете перейти до наступного розділу.

Сервер на Ubuntu 16.04 LTS
Викачуємо дистрибутив Ubuntu 16.04, конфігуруємо «віртуалку». І встановлюємо Ubuntu на наш віртуальний комп'ютер. Процес установки в цілому простий і не вимагає документації, але весь процес установки і настройки можна пройти у відео нижче.

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

Встановимо і налаштуємо необхідний софт.

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

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

apt-get install mc nano htop

Налаштуємо статично IP адресу (в принципі це можна і не робити, а статичний адресу призначити на стороні роутера):

nano /etc/network/interfaces

Приклад налаштування:

iface eth0 inet static
address 192.168.0.100
netmask 255.255.255.0
шлюз 192.168.0.1
dns-серверів імен 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-php gd-php mbstring-mcrypt php-curl php-xml

MySQL 5.7 і phpMyAdmin:

apt-get install mysql-server-5.7 phpmyadmin

Змінимо власника та права на папку, де будуть файли магазину:

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

dev:dev — ім'я та група користувача. Я використав це ім'я при установці Ubuntu.
Тепер необхідно налаштувати встановлений ПЗ.

Nginx

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

  • Ми налаштовуємо у себе відповідність домен — IP. Як ми це робимо, не важливо, я прописую в файлі hosts. Наприклад,magento.dev 192.168.0.100
  • Коли Nginx отримує запит, він робить server_root шлях виду /var/www/(доменне ім'я). Приклад: пишемо в браузері magento.dev — server_root /var/www/magento.dev
  • Ну а наш магазин необхідно розмістити у теці /var/www/magento.dev
dynamic.conf
server {
listen 80;
server_name $http_host;
root /var/www/$http_host;

location / {
index index.html index.php;
try_files $uri $uri/ @handler;
expires 30d;
}

location /. {
return 404;
}

location @handler {
rewrite / /index.php;
}

location ~ .php/ {
rewrite ^(.*.php)/ $1 last;
}

location ~ .php$ {
if (!-e $request_filename) { rewrite / /index.php last; }

expires off;
fastcgi_pass unix:/run/php/php7.0-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SCRIPT_NAME $document_root$fastcgi_script_name;
fastcgi_param MAGE_RUN_TYPE store;
include fastcgi_params;
}
}



m2.conf
# Magento Vars

#
# Example configuration:
upstream fastcgi_backend {
server unix:unix:/run/php/php7.0-fpm.sock;
}
server {
set $MAGE_ROOT /var/www/m2.dev;
set $MAGE_MODE default; # or production or developer
listen 80;
server_name m2.dev;
root /var/www/m2.dev/pub;

index index.php;
autoindex off;
charset off;

add_header 'X-Content-Type-Options' 'nosniff';
add_header 'X-XSS-Protection' '1; mode=block';

location /setup {
root $MAGE_ROOT;
location ~ ^/setup/index.php {
fastcgi_pass fastcgi_backend;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}

location ~ ^/setup/(?!pub/). {
deny all;
}

location ~ ^/setup/pub/ {
add_header X-Frame-Options "SAMEORIGIN";
}
}

location /update {
root $MAGE_ROOT;

location ~ ^/update/index.php {
fastcgi_split_path_info ^(/update/index.php)(/.+)$;
fastcgi_pass fastcgi_backend;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
include fastcgi_params;
}

# deny everything but index.php
location ~ ^/update/(?!pub/). {
deny all;
}

location ~ ^/update/pub/ {
add_header X-Frame-Options "SAMEORIGIN";
}
}

location / {
try_files $uri $uri/ /index.php?$args;
}

location /pub {
location ~ ^/pub/media/(downloadable|customer|import|theme_customization/.*\.xml) {
deny all;
}
alias $MAGE_ROOT/pub;
add_header X-Frame-Options "SAMEORIGIN";
}

location /static/ {
if ($MAGE_MODE = "production") {
expires max;
}
location ~* \.(ico|jpg|jpeg|png|gif|svg|js|css|swf|eot|ttf|otf|woff|woff2)$ {
add_header Cache-Control "public";
add_header X-Frame-Options "SAMEORIGIN";
expires +1y;

if (!-f $request_filename) {
rewrite ^/static/(version\d*/)?(.*)$ /static.php?resource=$2 last;
}
}
location ~* \.(zip|gz|gzip|bz2|csv|xml)$ {
add_header Cache-Control "no-store";
add_header X-Frame-Options "SAMEORIGIN";
expires off;

if (!-f $request_filename) {
rewrite ^/static/(version\d*/)?(.*)$ /static.php?resource=$2 last;
}
}
if (!-f $request_filename) {
rewrite ^/static/(version\d*/)?(.*)$ /static.php?resource=$2 last;
}
add_header X-Frame-Options "SAMEORIGIN";
}

location /media/ {
try_files $uri $uri/ /get.php?$args;

location ~ ^/media/theme_customization/.*\.xml {
deny all;
}

location ~* \.(ico|jpg|jpeg|png|gif|svg|js|css|swf|eot|ttf|otf|woff|woff2)$ {
add_header Cache-Control "public";
add_header X-Frame-Options "SAMEORIGIN";
expires +1y;
try_files $uri $uri/ /get.php?$args;
}
location ~* \.(zip|gz|gzip|bz2|csv|xml)$ {
add_header Cache-Control "no-store";
add_header X-Frame-Options "SAMEORIGIN";
expires off;
try_files $uri $uri/ /get.php?$args;
}
add_header X-Frame-Options "SAMEORIGIN";
}

location /media/customer/ {
deny all;
}

location /media/downloadable/ {
deny all;
}

location /media/import/ {
deny all;
}

location ~ cron\.php {
deny all;
}

location ~ (index|get|static|report|404|503)\.php$ {
try_files $uri =404;
fastcgi_pass fastcgi_backend;

fastcgi_param PHP_FLAG "session.auto_start=off \n suhosin.session.cryptua=off";
fastcgi_param PHP_VALUE "memory_limit=256M \n max_execution_time=600";
fastcgi_read_timeout 600s;
fastcgi_connect_timeout 600s;
fastcgi_param MAGE_MODE $MAGE_MODE;

fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}



phpmyadmin.conf
server {
listen 80;
server_name pma myadmin;
root /usr/share/phpmyadmin/;
index index.php;

location /setup/index.php {
deny all;
}
location ~ .php$ {
if (!-e $request_filename) { rewrite / /index.php last; }

expires off;
fastcgi_pass unix:/run/php/php7.0-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SCRIPT_NAME $document_root$fastcgi_script_name;
fastcgi_param MAGE_RUN_TYPE store;
include fastcgi_params;
}
include fastcgi_params;
}

Кладемо конфіги в папці /etc/nginx/sites-availiable/ і робимо симлинки на них в папці /etc/nginx/sites-enabled/. Або просто складаємо їх в папці /etc/nginx/sites-enabled/

PHP 7.0 FPM

Редагуємо /etc/php/7.0/fpm/php.ini. Нас хвилюють тільки деякі параметри, які в принципі можна налаштувати на свій смак.

max_execution_time = 300
max_input_time = 160
memory_limit = 512M
display_errors = On
log_errors = On
html_errors = On
date.timezone = (тут свою таймзону вказати)

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

smb.conf

[global]
workgroup = WORKGROUP

server string = %h server (Samba, Ubuntu)

dns proxy = no

log file = /var/log/samba/log.%m

max log size = 1000

syslog = 0

panic action = /usr/share/samba/panic-action %d

server role = standalone server
passdb backend = tdbsam

obey pam restrictions = yes

unix password sync = yes

passwd program = /usr/bin/passwd %u
passwd chat = *Enter\snew\s*\spassword:* %n\n *Retype\snew\s*\spassword:* %n\n *password\supdated\ssuccessfully* .

pam password change = yes

map to guest = bad user
null passwords = Yes
guest account = www-data

[www]
path = /var/www/
comment = WWW folder
guest ok = yes
browseable = yes
read only = no
locking = no
force user = www-data
force group = www-data


Установка тестового магазину
Процес установки простий і не вимагає якихось особливих умінь. Але для внесення ясності, залишу відео-інструкцію заховану під спойлером.

Відео: Встановлюємо тестовий магазин Magento

Створення модуля

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

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

Вчитися створювати модулі будемо на прикладі модуля для виведення блоків на сторінках магазину (його frontend частини). І першим ділом ми придумуємо назву модуля. Назва повинна бути короткою і нести сенс. А ще нам потрібно вибрати неймспейс (зазвичай назва компанії розробника або його ПІБ). І фінальне найменування приймає вигляд Namespace_Modulename. У нашому випадку я назвав IGN_Siteblocks.

Створимо реєстраційний XML файл:

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

Поговоримо про codePool. Всього їх 3: local, community, core (enterprise Enterprise версії Magento).

І відразу вирішимо, що в core ми нічого змінювати не будемо, там базові файли системи і якщо їх треба змінити, то є інші способи, крім їхнього безпосереднього редагування.
Ми можемо спокійно використовувати local community (насправді, краще відразу взяти community, але в цьому прикладі буде local).

Зайдемо в адмінку магазину, в розділ System > Configuration > Advanced > Disable Modules Output і побачимо наш IGN_Siteblocks.

Створимо папку для нашого модуля:

app/code/local/IGN/Siteblocks/
  1. Block — класи блоків, що відповідають за рендеринг сторінок
  2. controllers — контролери беруть запити
  3. etc — тут всякі конфігураційні файли
  4. Helper — додаткові класи помічники
  5. Model — моделі
  6. sql — инсталяционные скрипти
Модулі в Magento реалізують патерн MVC. У нас є моделі, вид (блоки, темплейти і макети) і контролери. В папці etc створимо config.xml

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


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

Налагодження коду XDEBUG + PHPSTORM
Відео: Налагодження коду XDEBUG + PHPSTORM

Тут я б все-таки рекомендував подивитися на відеоінструкцію. Спочатку налаштуємо сервер:

apt-get install php-xdebug

Відредагуємо налаштування php.ini або xdebug.ini

/etc/php/7.0/conf.d/20-xdebug.ini
zend_extension = xdebug.so
xdebug.idekey = "PHPSTORM"
xdebug.remote_autostart = 1
xdebug.remote_connect_back = 1
xdebug.remote_enable = 1
xdebug.remote_port = 9000


Зберігаємо і не забуваємо перезавантажити сервіс service php7.0-fpm restart. У PHPSTORM створюємо новий Remote Debug конфіг.

Додаємо сервер з соответсвтующим адресою і портом. У полі IDE key вводимо слово PHPSTORM.

Моделі, колекції. Робота з базою даних.
Відео: Моделі, колекції. Робота з базою даних Magento

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

Моделі являють собою класи для роботи з даними і тільки даними. Ніяких тонкощів зі способом збереження цих даних у базі. Ніякого коду пов'язаного з рендерингом цих даних. У Magento це: Customer, Product, Order і тд.

Що б наш модуль міг використовувати моделі, необхідно отконфигурировать config.xml
Нагадаю, що моделі, блоки та хелпери додаються в global секцію. config.xml приймає наступний вигляд:

app/code/local/IGN/Siteblocks/etc/config.xml
<?xml version="1.0" ?>
<config>
<modules>
<IGN_Siteblocks>
<version>1.0.0</version>
</IGN_Siteblocks>
</modules>
<global>
<models>
<siteblocks> <!-- Як правило тут namespace_modulename або просто modulename -->
<class>IGN_Siteblocks_Model</class>
<resourceModel>siteblocks_resource</resourceModel>
</siteblocks>
<siteblocks_resource>
<class>IGN_Siteblocks_Resource</class>
<entities>
<block> <!-- найменування моделі -->
<table>ign_siteblock</table> <!-- назву таблиці, до якої буде "прив'язана" модель -->
</block>
</entities>
</siteblocks_resource>
</models>
<resources>
<siteblocks_setup> <!-- саме в папку з такою назвою потрібно складати install upgrade скрипти -->
<setup>
<module>IGN_Siteblocks</module>
</setup>
</siteblocks_setup>
</resources>
</global>
<frontend>

</frontend>
<admin>

</admin>
<defalut>

</defalut>
</config>


Важливо визначитися з назвою префікса (не знаю який термін тут підійде краще). Я вибрав siteblocks. Це довільну назву і, як правило, формується з неймспейса і ім'я модуля або тільки ім'я модуля. Ну або для заплутування розробників, можна вибрати абсолютно довільну рядок, заздалегідь купивши оберіг від проклять.

Вибирайте чітко і бажано без використання великих символів. Одна помилка, і будете довго копатися, шукати проблему. Назва моделі і прив'язка до таблиці. Ім'я моделі відповідає назві файлу моделі. Назва таблиці в базі довільне. У моєму випадку, що б звернутися до моделі, потрібно написати так:

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

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

app/code/local/IGN/Siteblocks/Model/Block.php
<?php
class IGN_Siteblocks_Model_Block extends Mage_Core_Model_Abstract {

public function _construct()
{
parent::_construct();
$this->_init('siteblocks/block'); //Все у відповідності з зазначеними в config.xml параметрами
}
}


Моделі успадковується від Mage_Core_Model_Abstract. Ресурсні моделі зберігаємо в папці Model/Resource.

app/code/local/IGN/Siteblocks/Model/Resource/Block.php
<?php
class IGN_Siteblocks_Model_Resource_Block extends 
Mage_Core_Model_Mysql4_Abstract {

public function _construct()
{
$this->_init('siteblocks/block','block_id'); //block_id це наш PRIMARY KEY таблиці, за замовчуванням entity_id
}

}


app/code/local/IGN/Siteblocks/Model/Resource/Block/Collection.php
<?php
class IGN_Siteblocks_Model_Resource_Block_collection extends Mage_Core_Model_Mysql4_Collection_abstract {

public function _construct()
{
parent::_construct();
$this->_init('siteblocks/block');
}
}


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

Не забуваємо створити інсталяційний скрипт, який буде створювати таблицю для нашої моделі.

app/code/local/IGN/Siteblocks/sql/siteblocks_setup/install-1.0.0.sql
<?php
/** @var Mage_Core_Model_Resource_Setup $installer */
$installer = $this;
$installer->startSetup();

$installer->run("
CREATE TABLE IF NOT EXISTS `{$this->getTable('siteblocks/block')}` (
`block_id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(500) NOT NULL,
`content` text NOT NULL,
`block_status` tinyint(4) NOT NULL,
`created_at` datetime NOT NULL,
PRIMARY KEY (`block_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;
");

//Альтернативний спосіб створення таблиці
$table = $installer->getConnection()
->newTable($this->getTable('siteblocks/block'))
->addColumn('block_id',Varien_Db_Ddl_Table::TYPE_INTEGER,null,array(
'identity' => true,
'unsigned' => true,
'nullable' => false,
'primary' => true
))
->addColumn('title',Varien_Db_Ddl_Table::TYPE_VARCHAR,null,array(
'nullable' => false
))
->addColumn('content',Varien_Db_Ddl_Table::TYPE_TEXT,null,array(
'nullable' => false
))
->addColumn('block_status',Varien_Db_Ddl_Table::TYPE_TINYINT,null,array(
'nullable' => false
))
->addColumn('created_at',Varien_Db_Ddl_Table::TYPE_DATETIME,null,array(
'nullable' => false
));

$installer->endSetup();


ВАЖЛИВИЙ МОМЕНТ!

Якщо ви вже пробували зайти в адмінку, при встановлені модулі, коли ще не було інсталяційного скрипта. Швидше за все ваш інсталл скрипт більше ніколи не запуститься. У цьому випадку необхідно знайти і видалити запис siteblocks_setup з таблиці core_resource в базі магазину.

При оновленні версії модуля. Ми вказуємо нову версію config.xml, наприклад: 1.0.1. І створюємо апгрейд скрипт: upgrade-1.0.0-1.0.1.php. І в такому ж дусі при наступних апгрейдах.

Говорячи про моделі і колекціях, не можна не згадати про самих базових методи цих класів.

Трохи прикладів використання моделей
//Завантажити об'єкт з таблиці block_id = 1
$block = Mage::getModel('siteblocks/block')->load(1);

//Видалити блок
$block->delete();

//Зберегти
$block->save();

//видалити блок не роблячи його завантаження з бази
Mage::getModel('siteblocks/block')->setId(1)->delete();

//Завантажити колекцію блоків з таблиці
$blocks = Mage::getModel('siteblocks/block')->getCollection();

//Колекція блоків де block_id = 1, 2 і 3
$blocks->addFieldToFilter('block_id',array('in'=>array(1,2,3))) ;

echo $blocks->getSelect(); //виведе сформувався SQL запит

//Альтернативний спосіб завантаження колекції
$blocks = Mage::getResourceModel('siteblocks/block_collection');



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

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

Контролери, згідно паттерну MVC, відповідають за обробку запитів. Беруть на себе так званий вхідний сигнал у вигляді HTTP-запиту. Перейшов по посиланню — відпрацював відповідний контролер.

Перед створенням контролерів сконфігуріруем роутинг config.xml. Роутинг для frontend і admin частини настроюється окремо. А значить додаємо routers в секцію frontend admin.

config.xml приймає вигляд:

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

</default>
</config>


Тепер можна створювати свої контролери в папці controllers нашого модуля. Клас контролера для frontend частини повинен спадкуватися від класу Mage_Core_Controller_Front_Action.

Створимо тестовий контролер TestController.php

app/code/local/IGN/Siteblocks/controllers/TestController.php
<?php
class IGN_Siteblocks_TestController extends Mage_Core_Controller_Front_Action {

public function mytestAction()
{
die('test');
}
}


Якщо зараз перейти URL виду example.com/siteblocks/test/mytest. Ви побачите білий екран з написом «test». Якщо цього не відбулося, значить на якомусь етапі виникла помилка.

Перевіряйте код і читайте логи. URL складається з router (siteblocks) / controller (TestController) / action (mytestAction)

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

Контролери для адмінки створюються в папці controllers/Adminhtml. Клас контролера для frontend частини повинен спадкуватися від класу Mage_Adminhtml_Controller_Action.

Створимо тестовий контролер TestController.php:

app/code/local/IGN/Siteblocks/controllers/Adminhtml/TestController.php
<?php
class IGN_Siteblocks_Adminhtml_TestController extends Mage_Adminhtml_Controller_Action {

public function mytestAction()
{
die('admin');
}
}


На нього можна зайти за URL: example.com/admin/test/mytestadmin це ваш шлях в адмінку.

І тут є нюанс: такий урл вже може бути зайнятий іншим модулем. Виходу тут 2: міняємо назву контролера на завідомо неконфліктний (наприклад IgntestController.php) або складаємо контролери в підпапку.

app/code/local/IGN/Siteblocks/controllers/Adminhtml/Siteblocks/TestController.php
<?php
class IGN_Siteblocks_Adminhtml_Siteblocks_testcontroller extends Mage_Adminhtml_Controller_Action {

public function mytestAction()
{
die('admin');
}
}


Тепер наш URL приймає вигляд: example.com/admin/siteblocks_test/mytest

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

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

Класи хелперів в Magento використовуються як додаткові класи. У них варто реалізовувати сторонню логіку, яка не вписується в функціонал моделей, блоків або контролерів. Але модуль потребує як мінімум в одному класі хелперу Data.php.

Цей хелпер використовується за замовчуванням для перекладу тексту (лейблів, пунктів меню і тд) і іншої логіки.

У хелпере рекомендується декларувати методи читання налаштувань з конфига. Хелпери повинні успадковуватися від класу Mage_Core_Helper_Abstract.

app/code/loca/IGN/Siteblocks/Helper/Data.php
<?php
class IGN_Siteblocks_Helper_Data extends Mage_Core_Helper_Abstract {

}


Для перекладів тексту в хелпере існує метод __(), а його застосування виглядає так:

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

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

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

</defalut>
</config>


А файл IGN_Siteblocks.csv створюємо в папці app/locale/en_US/. Вміст виду: «Some text»,«Some text».

Намагаємося виводити текст з використанням свого хелперу і в такому випадку, спрощується локалізація модуля на різні мови.

Достатньо скопіювати файл переказів у відповідну локаль і перевести другий стовпець і немає необхідності копатися в коді.

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

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

Для додання модулю гнучкості, ми створимо сторінку з налаштуваннями модуля. Робиться це суто через xml файли. Нам необхідно створити 2 файлу:

system.xml — де будуть додані поля
adminhtml.xml — де будуть вказані розділи та права доступу

А стандартні значення параметрів ми можемо вказати в секції default файлі config.xml

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


app/code/local/IGN/Siteblocks/etc/adminhtml.xml
<?xml version="1.0"?>
<config>
<acl>
<resources>
<admin>
<children>
<system>
<children>
<config>
<children>
<siteblocks translate="title" module="siteblocks">
<title>Siteblocks</title>
</siteblocks>
</children>
</config>
</children>
</system>
</children>
</admin>
</resources>
</acl>
</config>


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


У наших налаштуваннях виводиться дропдаун з опціями, і використовується власна модель для цих опцій:

app/code/local/IGN/Siteblocks/Model/Source/Status.php
<?php
class IGN_Siteblocks_Model_Source_Status
{
const ENABLED = '1';
const DISABLED = '0';

/**
* Options getter
*
* @return array
*/
public function toOptionArray()
{
return array(
array('value' => self::ENABLED, 'label'=>Mage::helper('siteblocks')->__('Enabled')),
array('value' => self::DISABLED, 'label'=>Mage::helper('siteblocks')->__('Disabled')),
);
}

/**
* Get options in "key-value" format
*
* @return array
*/
public function toArray()
{
return array(
self::DISABLED => Mage::helper('siteblocks')->__('Disabled'),
self::ENABLED => Mage::helper('siteblocks')->__('Enabled'),
);
}




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

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

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

Блоки це класи, що відповідають за підготовку та виведення інформації. Блоки використовують для виведення темплейти, але не завжди. Якщо використовується темплейт, то він просто инклюдится в методі fetchView:



Тому з темплейта до блоку звертаємося через $this.

app/code/local/IGN/Siteblocks/Block/List.php
<?php
class IGN_Siteblocks_Block_List extends Mage_Core_Block_Template {

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


Блок успадковується від класу Mage_Core_Block_Template . Але це залежить від того, що наш блок буде виводити. Так, наприклад, при виводі списку товарів, бажано успадковуватися від блоку Mage_Catalog_Block_Product_List. Макети використовуються для побудови структури сторінки, які елементи виводити на сторінці і в якому порядку.

Створимо файл макетів:

app/design/frontend/base/default/layout/siteblocks.xml
<?xml version="1.0"?>
<layout version="1.0.0">
<siteblocks_index_index> <!-- це відповідає URL example.com/siteblocks/index/index -->
<reference name="head">
<action method="setTitle">
<title>My Siteblocks</title>
</action>
</reference>
<reference name="content">
<block name="siteblocks.list" as="siteblocks" type="siteblocks/list" template="siteblocks/list.phtml"/>
</reference>
</siteblocks_index_index>

<catalog_category_default> <!-- це вже існуючий handle і ми можемо додати свій блок для виводу на цій сторінці -->
<reference name="left">
<block name="siteblocks.list" as="siteblocks" type="siteblocks/list" template="siteblocks/list.phtml"/>
</reference>
<reference name="right">
<block name="siteblocks.list" as="siteblocks" type="siteblocks/list" template="siteblocks/list.phtml"/>
</reference>
</catalog_category_default>

<catalog_product_view> <!-- Додамо висновок нашого блоку на сторінці товару -->
<reference name="product.info.extrahint"> <!-- цей блок вже задекларовано в іншому макеті catalog.xml і ми додаємо свій блок для виведення всередині цього -->
<block name="siteblocks.list" before="-" as="siteblocks" type="siteblocks/list" template="siteblocks/list.phtml"/>
</reference>
</catalog_product_view>
</layout>


У макеті ми можемо додавати js, css файли в head, ми можемо додати або видалити блок на якийсь цієї сторінці. Тема макетів досить обширна і зверху я навів мінімально простий макет, який додасть наш блок у кількох місцях сайту.

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

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

І буде виведений HTML код тільки цього блоку. Таке часто потрібно, наприклад при використанні AJAX запитів.

У макеті у нас згадується файл siteblocks/list.phtml. Його можна і не вказувати, якщо в темплейте вказати його за замовчуванням.

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

Створимо темплейт:

app/design/base/default/template/siteblocks/list.phtml
<?php foreach($this->getBlocks() as $block):?>
<div class="siteblock">
<div class="block-title"><?php echo $block->getTitle()?></div>
<div class="block-content"><?php echo $block->getContent()?></div>
</div>
<?php endforeach;?>


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

app/code/local/IGN/Siteblocks/controllers/IndexController.php
<?php
class IGN_Siteblocks_IndexController extends Mage_Core_Controller_Front_Action {

public function indexAction()
{
$this->loadLayout(); #завантажуємо макети
$this->renderLayout(); #виводимо html
}
}


URL по якому ми побачимо висновок має вигляд: example.com/siteblocks/index/index або example.com/siteblocks, т. к. index/index можна опустити.

А handle в макеті буде використовуватися такий: siteblocks_index_index. Щоб подивитися на виведення записів, необхідно додати їх на пряму в базу або перейти до наступного кроку розробки форми редагування.

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

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

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

  • Додаємо пункти у меню
  • Створюємо блоки
  • Створюємо контролери
Додаємо пункти меню:

app/code/local/IGN/Siteblocks/etc/adminhtml.xml
<?xml version="1.0"?>
<config>
<acl>
<resources>
<admin>
<children>
<system>
<children>
<config>
<children>
<siteblocks translate="title" module="siteblocks">
<title>Siteblocks</title>
</siteblocks>
</children>
</config>
</children>
</system>
<cms>
<children>
<siteblocks translate="title" module="siteblocks">
<title>Siteblocks</title>
</siteblocks>
</children>
</cms>
</children>
</admin>
</resources>
</acl>
<menu>
<cms> <!-- Розділ в якому ми додамо свій пункт -->
<children>
<siteblocks translate="title" module="siteblocks">
<title>Siteblocks</title>
<action>adminhtml/siteblocks</action> <!-- На який контролер веде цей пункт меню, index в цьому випадку я опустив -->
<sort_order>20</sort_order>
</siteblocks>
</children>
</cms>
</menu>
</config>


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

Створимо контролер і 1 екшен для початку.

app/code/local/IGN/Siteblocks/controllers/Adminhtml/SiteblocksController.php
<?php
class IGN_Siteblocks_Adminhtml_Siteblockscontroller extends Mage_Adminhtml_Controller_Action {

public function indexAction()
{
$this->loadLayout();
$this->_addContent($this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks'));
$this->renderLayout();
}
}


Ми могли б створити макет для адмінки, але потрібні блоки можна додавати прямо в контролері. Тут ми контент додали нашу сторінку. Index екшен буде виводити сторінку з Grid записів.

Тепер можна перейти до створення блоків.

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

class IGN_Siteblocks_Block_Adminhtml_Siteblocks extends Mage_Adminhtml_Block_Widget_Grid_container
{

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


Чому ми прописали такі значення властивостей, зараз побачимо в методі класу Mage_Adminhtml_Block_Widget_Grid_container:



Таким чином, формується block type блоку grid.

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

class IGN_Siteblocks_Block_Adminhtml_Siteblocks_grid extends Mage_Adminhtml_Block_Widget_Grid
{

public function __construct()
{
parent::__construct();
$this->setId('cmsBlockGrid');
$this->setDefaultSort('block_identifier');
$this->setDefaultDir('ASC');
}

protected function _prepareCollection()
{
$collection = Mage::getModel('siteblocks/block')->getCollection();
/* @var $collection Mage_Cms_Model_Mysql4_Block_Collection */
$this->setCollection($collection);
return parent::_prepareCollection();
}

protected function _prepareColumns()
{

$this->addColumn('title', array(
'header' => Mage::helper('siteblocks')->__('Title'),
'align' => 'left',
'index' => 'title',
));

$this->addColumn('block_status', array(
'header' => Mage::helper('cms')->__('Status'),
'align' => 'left',
'type' => 'options',
'options' => Mage::getModel('siteblocks/source_status')->toArray(),
'index' => 'block_status'
));


$this->addColumn('created_at', array(
'header' => Mage::helper('siteblocks')->__('Created At'),
'index' => 'created_at',
'type' => 'date',

));


return parent::_prepareColumns();
}

protected function _prepareMassaction()
{
$this->setMassactionIdField('block_id');
$this->getMassactionBlock()->setIdFieldName('block_id');
$this->getMassactionBlock()
->addItem('delete',
array(
'label' => Mage::helper('siteblocks')->__('Delete'),
'url' => $this->getUrl('*/*/massDelete'),
'confirm' => Mage::helper('siteblocks')->__('Are you sure?')
)
)
->addItem('status',
array(
'label' => Mage::helper('siteblocks')->__('Update status'),
'url' => $this->getUrl('*/*/massStatus'),
'additional' =>
array('block_status'=>
array(
'name' => 'block_status',
'type' => 'select',
'class' => 'required-entry',
'label' => Mage::helper('siteblocks')->__('Status'),
'values' => Mage::getModel('siteblocks/source_status')->toOptionArray()
)
)
)
);

return $this;
}

/**
* Row click url
*
* @return string
*/
public function getRowUrl($row)
{
return $this->getUrl('*/*/edit', array('block_id' => $row->getId()));
}

}


Блок Grid в нашому випадку приймає такий вигляд. В принципі, за назвою методів і властивостей, можна зрозуміти як відбувається додавання колонок, формується URL на сторінку редагування і підготовку колекції записів для відображення в таблиці.

Важливо відзначити, що доступні за замовчуванням типи колонок та принципи їх побудови можна показати в папці app/code/core/Mage/Adminhtml/Block/Widget/Grid/Column/Renderer/.

Сторінка редагування так само буде складатися з 2-х блоків: блок контейнер і блок форми.

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

class IGN_Siteblocks_Block_Adminhtml_Siteblocks_edit extends Mage_Adminhtml_Block_Widget_Form_container
{
public function __construct()
{
$this->_objectId = 'block_id';
$this->_controller = 'adminhtml_siteblocks';
$this->_blockGroup = 'siteblocks';

parent::__construct();

$this->_updateButton('save', 'label', Mage::helper('siteblocks')->__('Save Block'));
$this->_updateButton('delete', 'label', Mage::helper('siteblocks')->__('Delete Block'));

$this->_addButton('saveandcontinue', array(
'label' => Mage::helper('adminhtml')->__('Save Edit and Continue'),
'onclick' => 'saveAndContinueEdit()',
'class' => 'save',
), -100);

$this->_formScripts[] = "


function saveAndContinueEdit(){
editForm.submit($('edit_form').action+'back/edit/');
}
";
}

/**
* Get edit-form container header text
*
* @return string
*/
public function getHeaderText()
{
if (Mage::registry('siteblocks_block')->getId()) {
return Mage::helper('siteblocks')->__("Edit Block '%s'", $this->escapeHtml(Mage::registry('siteblocks_block')->getTitle()));
}
else {
return Mage::helper('siteblocks')->__('New Block');
}
}
}


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

Клас форми:

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

class IGN_Siteblocks_Block_Adminhtml_Siteblocks_edit_form extends Mage_Adminhtml_Block_Widget_Form
{

/**
* Init form
*/
public function __construct()
{
parent::__construct();
$this->setId('block_form');
$this->setTitle(Mage::helper('siteblocks')->__('Block Information'));
}

protected function _prepareForm()
{
$model = Mage::registry('siteblocks_block');
$form = new Varien_Data_Form(
array(
'id' => 'edit_form',
'action' => $this->getUrl('*/*/save',array('block_id'=>$this->getRequest()->getParam('block_id'))),
'method' => 'post'
)
);

$form->setHtmlIdPrefix('block_');

$fieldset = $form->addFieldset('base_fieldset', array('legend'=>Mage::helper('siteblocks')->__('General Information'), 'class' => 'fieldset-wide'));

if ($model->getBlockId()) {
$fieldset->addField('block_id', 'hidden', array(
'name' => 'block_id',
));
}

$fieldset->addField('title', 'text', array(
'name' => 'title',
'label' => Mage::helper('siteblocks')->__('Block Title'),
'title' => Mage::helper('siteblocks')->__('Block Title'),
'required' => true,
));


$fieldset->addField('block_status', 'select', array(
'label' => Mage::helper('siteblocks')->__('Status'),
'title' => Mage::helper('siteblocks')->__('Status'),
'name' => 'block_status',
'required' => true,
'options' => Mage::getModel('siteblocks/source_status')->toArray(),
));


$fieldset->addField('content', 'textarea', array(
'name' => 'content',
'label' => Mage::helper('siteblocks')->__('Content'),
'title' => Mage::helper('siteblocks')->__('Content'),
'style' => 'height:36em',
'required' => true,

));

$form->setValues($model->getData());
$form->setUseContainer(true);
$this->setForm($form);

return parent::_prepareForm();
}
}


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

Контролер приймає наступний вигляд:

app/code/local/IGN/Siteblocks/controllers/Adminhtml/SiteblocksController.php
<?php
class IGN_Siteblocks_Adminhtml_Siteblockscontroller extends Mage_Adminhtml_Controller_Action {

public function indexAction()
{
$this->loadLayout();
$this->_addContent($this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks'));
$this->renderLayout();
}

public function newAction()
{
$this->_forward('edit');
}

public function editAction()
{
$id = $this->getRequest()->getParam('block_id');
Mage::register('siteblocks_block',Mage::getModel('siteblocks/block')->load($id));
$blockObject = (array)Mage::getSingleton('adminhtml/session')->getBlockObject(true);
if(count($blockObject)) {
Mage::registry('siteblocks_block')->setData($blockObject);
}
$this->loadLayout();
$this->_addContent($this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks_edit'));
$this->renderLayout();
}

public function saveAction()
{
try {
$id = $this->getRequest()->getParam('block_id');
$block = Mage::getModel('siteblocks/block')->load($id);
/*$block
->setTitle($this->getRequest()->getParam('title'))
->setContent($this->getRequest()->getParam('content'))
->setBlockStatus($this->getRequest()->getParam('block_status'))
->save();*/
$block
->setData($this->getRequest()->getParams())
->setCreatedAt(Mage::app()->getLocale()->date())
->save();

if(!$block->getId()) {
Mage::getSingleton('adminhtml/session')->addError('Cannot save the block');
}
} catch(Exception $e) {
Mage::logException($e);
Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
Mage::getSingleton('adminhtml/session')->setBlockObject($block->getData());
return $this->_redirect('*/*/edit',array('block_id'=>$this->getRequest()->getParam('block_id')));
}

Mage::getSingleton('adminhtml/session')->addSuccess('Block was saved successfully!');

$this->_redirect('*/*/'.$this->getRequest()->getParam('back','index'),array('block_id'=>$block->getId()));
}


public function deleteAction()
{
$block = Mage::getModel('siteblocks/block')
->setId($this->getRequest()->getParam('block_id'))
->delete();
if($block->getId()) {
Mage::getSingleton('adminhtml/session')->addSuccess('Block was deleted successfully!');
}
$this->_redirect('*/*/');

}


public function massStatusAction()
{
$statuses = $this->getRequest()->getParams();
try {
$blocks= Mage::getModel('siteblocks/block')
->getCollection()
->addFieldToFilter('block_id',array('in'=>$statuses['massaction']));
foreach($blocks as $block) {
$block->setBlockStatus($statuses['block_status'])->save();
}
} catch(Exception $e) {
Mage::logException($e);
Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
return $this->_redirect('*/*/');
}
Mage::getSingleton('adminhtml/session')->addSuccess('Blocks were updated!');

return $this->_redirect('*/*/');

}

public function massDeleteAction()
{
$blocks = $this->getRequest()->getParams();
try {
$blocks= Mage::getModel('siteblocks/block')
->getCollection()
->addFieldToFilter('block_id',array('in'=>$blocks['massaction']));
foreach($blocks as $block) {
$block->delete();
}
} catch(Exception $e) {
Mage::logException($e);
Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
return $this->_redirect('*/*/');
}
Mage::getSingleton('adminhtml/session')->addSuccess('Blocks were deleted!');

return $this->_redirect('*/*/');

}
}


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

Події і слухачі
Відео: Події і слухачі в Magento

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

У Magento можна використовувати шаблон «подія-слухач». Що дозволяє в нашому модулі відловлювати якісь певні моменти роботи сайту. Додає динамічності, гнучкості і вносить більше автоматизації.

І стандартних подій в Magento реалізовано дуже багато. Зробіть пошук файлів Magento тексту «Mage::dispatchEvent». Або загляньте за адресою. Це з явних подій, ще є події відбуваються з кожною моделлю, кожним блоком або екшеном контролера. Як правило це перед-і пост події.

model_save_before, model_save_after, controller_action_predispatch, controller_action_postdispatch, core_block_abstract_to_html_before, core_block_abstract_to_html_after

Плюс, події в яких використовується event_prefix ваших класів або ваш route name контролерів (siteblocks_save_before, controller_action_predispatch_siteblocks...)

Варіацій дуже багато і завдяки цій системі, можна з легкістю «підловити» потрібну подію.

Безпосередньо створити подію можна в будь-якому місці в коді:

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

Слухачів декларують в config.xml. І там є 3 варіанти: global, admin, frontend. Відповідно це просто поділ, де ми хочемо, що б спрацьовував наш слухач. Наш конфіг приймає наступний вигляд:

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


Ми додали 1 слухача на події додавання товару в корзину. Тепер необхідно створити клас слухача. Можна обійтися і без цього класу і додати логіку в яку-небудь модель. Але це поганий тон. Тому Observer.php

app/code/local/IGN/Siteblocks/Model/Observer.php
<?php
class IGN_Siteblocks_Model_Observer {
/**
* @param $bserver Varien_Event_Observer
*/
public function checkout_cart_product_add_after($observer)
{
var_dump($observer->getEvent()->getData('quote_item')->getData());die;
}
}


У своєму методі ми можемо зробити всі необхідні маніпуляції. Зараз ми просто роздрукуємо вміст айтема з кошика. (пізніше закоментуйте цей код, інакше не зможете додавати товари в корзину).

Крон і завдання за розкладом
Відео: Крон і завдання за розкладом в Magento

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

Ще одним поспорьем в автоматизації робочих процесів нашого модуля, і роботи магазину є можливість створювати завдання за розкладом.

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

crontab -e
* */1 * * * php /var/www/magento.dev/cron.php

Більше інформації тут: help.ubuntu.ru/wiki/cron. Або ви не можете налаштовувати, а запускати крон, коли вам це потрібно, просто перейшовши за посиланням виду example.com/cron.php

Наші завдання ми декларуємо в config.xml в окремому блоці crontab. І оновлений вигляд файлу:

app/code/local/IGN/Siteblocks/etc/config.xml
<?xml version="1.0" ?>
<config>
<modules>
<IGN_Siteblocks>
<version>1.0.0</version>
</IGN_Siteblocks>
</modules>
<global>
<blocks>
<siteblocks>
<class>IGN_Siteblocks_Block</class>
</siteblocks>
</blocks>
<models>
<siteblocks>
<class>IGN_Siteblocks_Model</class>
<resourceModel>siteblocks_resource</resourceModel>
</siteblocks>
<siteblocks_resource>
<class>IGN_Siteblocks_Model_Resource</class>
<entities>
<block>
<table>ign_siteblock</table>
</block>
</entities>
</siteblocks_resource>
</models>
<resources>
<siteblocks_setup>
<setup>
<module>IGN_Siteblocks</module>
</setup>
</siteblocks_setup>
</resources>
<helpers>
<siteblocks>
<class>IGN_Siteblocks_Helper</class>
</siteblocks>
</helpers>
</global>
<frontend>
<events>
<controller_action_predispatch>

</controller_action_predispatch>
<checkout_cart_product_add_after>
<observers>
<siteblocks>
<class>siteblocks/observer</class>
<method>checkout_cart_product_add_after</method>
<type>model</type>
</siteblocks>
</observers>
</checkout_cart_product_add_after>
</events>
<layout>
<updates>
<siteblocks module="siteblocks">
<file>siteblocks.xml</file>
</siteblocks>
</updates>
</layout>
<routers>
<siteblocks>
<use>standard</use>
<args>
<module>IGN_Siteblocks</module>
<frontName>siteblocks</frontName>
</args>
</siteblocks>
</routers>
<translate>
<modules>
<IGN_Siteblocks>
<files>
<default>IGN_Siteblocks.csv</default>
</files>
</IGN_Siteblocks>
</modules>
</translate>
</frontend>
<admin>
<routers>
<adminhtml>
<args>
<modules>
<siteblocks before="Mage_Adminhtml">IGN_Siteblocks_Adminhtml</siteblocks>
</modules>
</args>
</adminhtml>
</routers>
</admin>
<default>
<siteblocks>
<settings>
<enabled>1</enabled>
<block_count>10</block_count>
</settings>
</siteblocks>
</default>
<crontab>
<jobs>
<siteblocks_clear_cache> <!-- Довільну назву завдання-->
<schedule>
<cron_expr>*/10 * * * *</cron_expr> <!-- кожні 10 хвилин -->
</schedule>
<run>
<model>siteblocks/cron::siteblocks_clear_cache</model> <!-- модель та метод, який ми хочемо запустити -->
</run>
</siteblocks_clear_cache>
</jobs>
</crontab>
</config>


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

app/code/local/IGN/Siteblocks/Model/Cron.php
<?php
class IGN_Siteblocks_Model_Cron {
public function siteblocks_clear_cache()
{
//do something here
Mage::app()->cleanCache(array('siteblocks_blocks'));
}
}



Використання рендереров в адмінці
Відео: Використання рендереров в адмінці Magento

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

Найчастіше стандартних елементів буває недостатньо для реалізації задуманого функціоналу. Тому можна створити рендерер для потрібного елемента і цей процес не вимагає великих витрат часу.

Розглянемо створення рендерера для елемента форми. У нас є адмін форма:

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

class IGN_Siteblocks_Block_Adminhtml_Siteblocks_edit_form extends Mage_Adminhtml_Block_Widget_Form
{

/**
* Init form
*/
public function __construct()
{
parent::__construct();
$this->setId('block_form');
$this->setTitle(Mage::helper('siteblocks')->__('Block Information'));
}

protected function _prepareForm()
{
$model = Mage::registry('siteblocks_block');
$form = new Varien_Data_Form(
array(
'id' => 'edit_form',
'action' => $this->getUrl('*/*/save',array('block_id'=>$this->getRequest()->getParam('block_id'))),
'method' => 'post',
'enctype' => 'multipart/form-data'
)
);


$form->setHtmlIdPrefix('block_');

$fieldset = $form->addFieldset('base_fieldset',
array(
'legend'=>Mage::helper('siteblocks')->__('General Information'),
'class' => 'fieldset-wide')
);

if ($model->getBlockId()) {
$fieldset->addField('block_id', 'hidden', array(
'name' => 'block_id',
));
}

$fieldset->addField('title', 'text', array(
'name' => 'title',
'label' => Mage::helper('siteblocks')->__('Block Title'),
'title' => Mage::helper('siteblocks')->__('Block Title'),
'required' => true,
));

#1й спосіб додавання рендерера або редекларации фонового для певного типу полів
#выдповыдно нам потрібно створити клас по дорозі .../Block/Adminhtml/Siteblocks/Edit/Renderer/Myimage.php
$fieldset->addType('myimage','IGN_Siteblocks_Block_Adminhtml_Siteblocks_edit_renderer_myimage');

#якщо не користуватися першим варіантом вказівки відповідності типу-клас, то потрібно створити файл lib/Varien/Data/Form/Element/Myimage.php
$fieldset->addField('image', 'myimage', array(
'name' => 'image',
'label' => Mage::helper('siteblocks')->__('Image'),
'title' => Mage::helper('siteblocks')->__('Image'),
'required' => true,
));

$fieldset->addField('block_status', 'select', array(
'label' => Mage::helper('siteblocks')->__('Status'),
'title' => Mage::helper('siteblocks')->__('Status'),
'name' => 'block_status',
'required' => true,
'options' => Mage::getModel('siteblocks/source_status')->toArray(),
));

$fieldset->addField('content', 'textarea', array(
'name' => 'content',
'label' => Mage::helper('siteblocks')->__('Content'),
'title' => Mage::helper('siteblocks')->__('Content'),
'style' => 'height:36em',
'required' => true,

));

$form->setValues($model->getData());
$form->setUseContainer(true);
$this->setForm($form);

return parent::_prepareForm();
}
}


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

Вміст файлів:

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

class IGN_Siteblocks_Block_Adminhtml_Siteblocks_edit_renderer_myimage extends Varien_Data_Form_Element_Abstract
{
/**
* Constructor
*
* @param array $data
*/
public function __construct($data)
{
parent::__construct($data);
$this->setType('file');
}

/**
* Return element html code
*
* @return string
*/
public function getElementHtml()
{
$html = ";

if ((string)$this->getValue()) {
$url = $this->_getUrl();

if( !preg_match("/^http\:\/\/|https\:\/\//", $url) ) {
$url = Mage::getBaseUrl('media') . 'siteblocks' .DS.$url;
}

$html = '<a href="' . $url . '"'
. ' onclick="imagePreview(\" . $this->getHtmlId() . '_image\'); return false;">'
. '<img src="' . $url . '" id="' . $this->getHtmlId() . '_image" title="' . $this->getValue() . '"'
. ' alt="' . $this->getValue() . '" height="100" width="100" class="small-image-preview v-middle" />'
. '</a> ';
/*$additional = Mage::app()->getLayout()->createBlock('Mage_Adminhtml_Block_Template');
$additional->setTemplate('siteblocks/image.phtml')
->setImageUrl($url);
$html = $additional->toHtml();*/
#закомментированный код вище ми можемо використовувати для того, що б html код будувався темплейте, актуально при використанні складних елементів
}
$this->setClass('input-file');
$html .= parent::getElementHtml();
return $html;
}

/**
* Return html code of hidden element
*
* @return string
*/
protected function _getHiddenInput()
{
return '<input type="hidden" name="' . parent::getName() . '[value]" value="' . $this->getValue() . '" />';
}

/**
* Get image preview url
*
* @return string
*/
protected function _getUrl()
{
return $this->getValue();
}

/**
* Return name
*
* @return string
*/
public function getName()
{
return $this->getData('name');
}
}




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

class Varien_Data_Form_Element_Myimage extends Varien_Data_Form_Element_Abstract
{
/**
* Constructor
*
* @param array $data
*/
public function __construct($data)
{
parent::__construct($data);
$this->setType('file');
}

/**
* Return element html code
*
* @return string
*/
public function getElementHtml()
{
$html = ";

if ((string)$this->getValue()) {
$url = $this->_getUrl();

if( !preg_match("/^http\:\/\/|https\:\/\//", $url) ) {
$url = Mage::getBaseUrl('media') . 'siteblocks' .DS.$url;
}

$html = '<a href="' . $url . '"'
. ' onclick="imagePreview(\" . $this->getHtmlId() . '_image\'); return false;">'
. '<img src="' . $url . '" id="' . $this->getHtmlId() . '_image" title="' . $this->getValue() . '"'
. ' alt="' . $this->getValue() . '" height="150" width="150" class="small-image-preview v-middle" />'
. '</a> ';
/*$additional = Mage::app()->getLayout()->createBlock('Mage_Adminhtml_Block_Template');
$additional->setTemplate('siteblocks/image.phtml')
->setImageUrl($url);
$html = $additional->toHtml();*/
#закомментированный код вище ми можемо використовувати для того, що б html код будувався темплейте, актуально при використанні складних елементів
}
$this->setClass('input-file');
$html .= parent::getElementHtml();
return $html;
}

/**
* Return html code of hidden element
*
* @return string
*/
protected function _getHiddenInput()
{
return '<input type="hidden" name="' . parent::getName() . '[value]" value="' . $this->getValue() . '" />';
}

/**
* Get image preview url
*
* @return string
*/
protected function _getUrl()
{
return $this->getValue();
}

/**
* Return name
*
* @return string
*/
public function getName()
{
return $this->getData('name');
}
}


Вміст цих файлів я скопіював із стандартного lib/Varien/Data/Form/Element/Image.php
І підправив код під свої потреби.

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

Разом з цим я виконав деякі доповнення у функціоналі модуля. Потрібно було зробити функціонал завантаження і збереження картинок.

app/code/local/IGN/Siteblocks/Block/Adminhtml/Siteblocks/Grid.php
<?php
class IGN_Siteblocks_Block_Adminhtml_Siteblocks_grid extends Mage_Adminhtml_Block_Widget_Grid
{

public function __construct()
{
parent::__construct();
$this->setId('cmsBlockGrid');
$this->setDefaultSort('block_identifier');
$this->setDefaultDir('ASC');
}

protected function _prepareCollection()
{
$collection = Mage::getModel('siteblocks/block')->getCollection();
/* @var $collection Mage_Cms_Model_Mysql4_Block_Collection */
$this->setCollection($collection);
return parent::_prepareCollection();
}

protected function _prepareColumns()
{

$this->addColumn('title', array(
'header' => Mage::helper('siteblocks')->__('Title'),
'align' => 'left',
'index' => 'title',
));

$this->addColumn('image', array(
'header' => Mage::helper('siteblocks')->__('Image'),
'align' => 'left',
'index' => 'image',
'filter' => false, <!-- Картинки ми не зможемо фільтрувати -->
'sortable' => false,<!-- і не зможемо їх сортувати -->
'для' => 'IGN_Siteblocks_Block_Adminhtml_Siteblocks_grid_renderer_image',
// 'renderer' => 'siteblocks/adminhtml_siteblocks_grid_renderer_image' #альтернативний спосіб
));

$this->addColumn('block_status', array(
'header' => Mage::helper('cms')->__('Status'),
'align' => 'left',
'type' => 'options',
'options' => Mage::getModel('siteblocks/source_status')->toArray(),
'index' => 'block_status'
));


$this->addColumn('created_at', array(
'header' => Mage::helper('siteblocks')->__('Created At'),
'index' => 'created_at',
'type' => 'date',

));


return parent::_prepareColumns();
}

protected function _prepareMassaction()
{
$this->setMassactionIdField('block_id');
$this->getMassactionBlock()->setIdFieldName('block_id');
$this->getMassactionBlock()
->addItem('delete',
array(
'label' => Mage::helper('siteblocks')->__('Delete'),
'url' => $this->getUrl('*/*/massDelete'),
'confirm' => Mage::helper('siteblocks')->__('Are you sure?')
)
)
->addItem('status',
array(
'label' => Mage::helper('siteblocks')->__('Update status'),
'url' => $this->getUrl('*/*/massStatus'),
'additional' =>
array('block_status'=>
array(
'name' => 'block_status',
'type' => 'select',
'class' => 'required-entry',
'label' => Mage::helper('siteblocks')->__('Status'),
'values' => Mage::getModel('siteblocks/source_status')->toOptionArray()
)
)
)
);

return $this;
}

/**
* Row click url
*
* @return string
*/
public function getRowUrl($row)
{
return $this->getUrl('*/*/edit', array('block_id' => $row->getId()));
}

}


app/code/local/IGN/Siteblocks/Block/Adminhtml/Siteblocks/Grid/Renderer/Image.php
<?php
class IGN_Siteblocks_Block_Adminhtml_Siteblocks_grid_renderer_image
extends Mage_Adminhtml_Block_Widget_Grid_column_renderer_abstract
{
public function render(Varien_Object $row) #саме в цьому методі необхідно додавати логіку
{
if( ! $row->getImage()) {
return ";
}
$url = Mage::getBaseUrl('media') . 'siteblocks' .DS .$row->getImage();
$html = "<img src='$url' width='100' height='auto'>";
return $html;
}
}


У рендерере ми формуємо $src URL і виводимо html код картинки. Тепер ми зможемо бачити в таблиці картинки.

Для того, що б у модулі можна було завантажувати картинки, потрібно провести деякі доповнення.

1. Оновити версію config.xml до 1.0.1
2. Створити файл upgrade-1.0.0-1.0.1.php

app/code/local/IGN/Siteblocks/sql/siteblocks_setup/upgrade-1.0.0-1.0.1.php
<?php
/** @var Mage_Core_Model_Resource_Setup $installer */
$installer = $this;
$installer->startSetup();

$installer->run("
ALTER TABLE `{$this->getTable('siteblocks/block')}` ADD `image` TEXT NOT NULL;
");

$installer->endSetup();


3. У контролері додати відповідний код:

app/code/local/IGN/Siteblocks/controllers/Adminhtml/SiteblocksController.php
<?php
class IGN_Siteblocks_Adminhtml_Siteblockscontroller extends Mage_Adminhtml_Controller_Action {

public function indexAction()
{
$this->loadLayout();
$this->_addContent($this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks'));
$this->renderLayout();
}

public function newAction()
{
$this->_forward('edit');
}

public function editAction()
{
$id = $this->getRequest()->getParam('block_id');
Mage::register('siteblocks_block',Mage::getModel('siteblocks/block')->load($id));
$blockObject = (array)Mage::getSingleton('adminhtml/session')->getBlockObject(true);
if(count($blockObject)) {
Mage::registry('siteblocks_block')->setData($blockObject);
}
$this->loadLayout();
$this->_addContent($this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks_edit'));
$this->renderLayout();
}

// метод завантаження файлів
protected function _uploadFile($fieldName,$model)
{

if( ! isset($_FILES[$fieldName])) {
return false;
}
$file = $_FILES[$fieldName];

if(isset($file['name']) && (file_exists($file['tmp_name']))){
if($model->getId()){
unlink(Mage::getBaseDir('media').DS.$model->getData($fieldName));
}
try
{
$path = Mage::getBaseDir('media') . DS . 'siteblocks' . DS;
$uploader = new Varien_File_Uploader($file);
$uploader->setAllowedExtensions(array('jpg','png','gif','jpeg'));
$uploader->setAllowRenameFiles(true);
$uploader->setFilesDispersion(false);

$uploader->save($path, $file['name']);
$model->setData($fieldName,$uploader->getUploadedFileName());
return true;
}
catch(Exception $e)
{
return false;
}
}
}

public function saveAction()
{
try {
$id = $this->getRequest()->getParam('block_id');
$block = Mage::getModel('siteblocks/block')->load($id);
/*$block
->setTitle($this->getRequest()->getParam('title'))
->setContent($this->getRequest()->getParam('content'))
->setBlockStatus($this->getRequest()->getParam('block_status'))
->save();*/
$block
->setData($this->getRequest()->getParams());
$this->_uploadFile('image',$block); //використовуємо метод завантаження файлів
$block
->setCreatedAt(Mage::app()->getLocale()->date())
->save();

if(!$block->getId()) {
Mage::getSingleton('adminhtml/session')->addError('Cannot save the block');
}
} catch(Exception $e){
Mage::logException($e);
Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
Mage::getSingleton('adminhtml/session')->setBlockObject($block->getData());
return $this->_redirect('*/*/edit',array('block_id'=>$this->getRequest()->getParam('block_id')));
}

Mage::getSingleton('adminhtml/session')->addSuccess('Block was saved successfully!');

$this->_redirect('*/*/'.$this->getRequest()->getParam('back','index'),array('block_id'=>$block->getId()));
}

public function deleteAction()
{
$block = Mage::getModel('siteblocks/block')
->setId($this->getRequest()->getParam('block_id'))
->delete();
if($block->getId()) {
Mage::getSingleton('adminhtml/session')->addSuccess('Block was deleted successfully!');
}
$this->_redirect('*/*/');

}

public function massStatusAction()
{
$statuses = $this->getRequest()->getParams();
try {
$blocks= Mage::getModel('siteblocks/block')
->getCollection()
->addFieldToFilter('block_id',array('in'=>$statuses['massaction']));
foreach($blocks as $block) {
$block->setBlockStatus($statuses['block_status'])->save();
}
} catch(Exception $e) {
Mage::logException($e);
Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
return $this->_redirect('*/*/');
}
Mage::getSingleton('adminhtml/session')->addSuccess('Blocks were updated!');

return $this->_redirect('*/*/');

}

public function massDeleteAction()
{
$blocks = $this->getRequest()->getParams();
try {
$blocks= Mage::getModel('siteblocks/block')
->getCollection()
->addFieldToFilter('block_id',array('in'=>$blocks['massaction']));
foreach($blocks as $block) {
$block->delete();
}
} catch(Exception $e) {
Mage::logException($e);
Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
return $this->_redirect('*/*/');
}
Mage::getSingleton('adminhtml/session')->addSuccess('Blocks were deleted!');

return $this->_redirect('*/*/');

}
}


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

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

Відредагуємо темплейт:

app/design/frontend/base/default/template/siteblocks/list.phtml
<?php foreach($this->getBlocks() as $block):?>
<div class="siteblock">
<div class="block-title"><?php echo $block->getTitle()?></div>
<div class="block-image">
<?php if($block->getImage()):?>
<img src="<?php echo $block->getImageSrc()?>" height="150" width="авто" alt="<?php $block->getTitle()?>" title="<?php $block->getTitle()?>">
<?php endif;?>
</div>
<div class="block-content"><?php echo $block->getContent() ?></div>
</div>
<?php endforeach;?>


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

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

/**
* Class IGN_Siteblocks_Model_Block
* @method getBlockStatus()
* @method getContent()
* @method getImage()
*/
class IGN_Siteblocks_Model_Block extends Mage_Core_Model_Abstract {

protected $_eventPrefix = 'siteblocks_block';

public function _construct()
{
parent::_construct();
$this->_init('siteblocks/block');

}

public function getImageSrc()
{
return Mage::getBaseUrl('media') . 'siteblocks' . DS . $this->getImage();
}
}


Виводити повнорозмірні завантажені картинки це не гарна ідея, але головним завданням зараз було опис рендереров.

Використання WYSIWYG редактора
Відео: Використання WYSIWYG редактора в адмінці Magento

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

WYSIWYG — What you see is what you get (те що ви бачите, те й отримаєте). Це зручний редактор для створення контенту. І в нашому модулі йому є застосування. Але його включення не є таким простим, як очікувалося. Ми підійшли до того, що нам необхідно створити макет для адмінки.

app/design/adminhtml/default/default/layout/siteblocks.xml
<?xml version="1.0"?>
<layout version="1.0.0">
<adminhtml_siteblocks_edit> <!-- це відповідає шляху на сторінку редагування -->
<update handle="editor"/> <!-- Завдяки цьому рядку завантажиться handle в якому включені всі необхідні js і css ресурси для редактора, а він описаний в макеті cms.xml -->
</adminhtml_siteblocks_edit>

<adminhtml_system_config_edit>
<update handle="editor"/>
</adminhtml_system_config_edit>
</layout>


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

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

class IGN_Siteblocks_Block_Adminhtml_Siteblocks_edit_form extends Mage_Adminhtml_Block_Widget_Form
{

/**
* Init form
*/
public function __construct()
{
parent::__construct();
$this->setId('block_form');
$this->setTitle(Mage::helper('siteblocks')->__('Block Information'));
}

protected function _prepareForm()
{
$model = Mage::registry('siteblocks_block');
$form = new Varien_Data_Form(
array(
'id' => 'edit_form',
'action' => $this->getUrl('*/*/save',array('block_id'=>$this->getRequest()->getParam('block_id'))),
'method' => 'post',
'enctype' => 'multipart/form-data'
)
);


$form->setHtmlIdPrefix('block_');

$fieldset = $form->addFieldset('base_fieldset',
array(
'legend'=>Mage::helper('siteblocks')->__('General Information'),
'class' => 'fieldset-wide')
);

if ($model->getBlockId()) {
$fieldset->addField('block_id', 'hidden', array(
'name' => 'block_id',
));
}

$fieldset->addField('title', 'text', array(
'name' => 'title',
'label' => Mage::helper('siteblocks')->__('Block Title'),
'title' => Mage::helper('siteblocks')->__('Block Title'),
'required' => true,
));

//$fieldset->addType('myimage','IGN_Siteblocks_Block_Adminhtml_Siteblocks_edit_renderer_myimage');

$fieldset->addField('image', 'myimage', array(
'name' => 'image',
'label' => Mage::helper('siteblocks')->__('Image'),
'title' => Mage::helper('siteblocks')->__('Image'),
'required' => true,
));



$fieldset->addField('block_status', 'select', array(
'label' => Mage::helper('siteblocks')->__('Status'),
'title' => Mage::helper('siteblocks')->__('Status'),
'name' => 'block_status',
'required' => true,
'options' => Mage::getModel('siteblocks/source_status')->toArray(),
));


#модифікуємо цей елемент
$fieldset->addField('content', 'editor', array(
'name' => 'content',
'label' => Mage::helper('siteblocks')->__('Content'),
'title' => Mage::helper('siteblocks')->__('Content'),
'style' => 'height:36em',
'required' => true,
'config' => Mage::getSingleton('cms/wysiwyg_config')->getConfig()

));

$form->setValues($model->getData());
$form->setUseContainer(true);
$this->setForm($form);

return parent::_prepareForm();
}

//додали цей метод, в котрому виставляємо прапор в блок head, якщо редактор включений в налаштуваннях
protected function _prepareLayout()
{
parent::_prepareLayout();
if (Mage::getSingleton('cms/wysiwyg_config')->isEnabled()) {
$this->getLayout()->getBlock('head')->setCanLoadTinyMce(true);
}
}
}


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

lib/Varien/Data/Form/Element/Myeditor.php
<?php
class Varien_Data_Form_Element_Myeditor extends Varien_Data_Form_Element_Editor
{
public function __construct($attributes=array())
{

parent::__construct($attributes);
#вся додаткова логіка в блоці нижче
if (Mage::getSingleton('cms/wysiwyg_config')->isEnabled()) {
Mage::app()->getLayout()->getBlock('head')->setCanLoadTinyMce(true);
$this->setData('config',Mage::getSingleton('cms/wysiwyg_config')->getConfig());
}
if($this->isEnabled()) {
$this->setType('wysiwyg');
$this->setExtType('wysiwyg');
} else {
$this->setType('textarea');
$this->setExtType('textarea');
}
}
}


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

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


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

app/design/frontend/base/default/template/siteblocks/list.phtml
<?php foreach($this->getBlocks() as $block):?>
<div class="siteblock">
<div class="block-title"><?php echo $block->getTitle()?></div>
<div class="block-image">
<?php if($block->getImage()):?>
<img src="<?php echo $block->getImageSrc()?>" height="150" width="авто" alt="<?php $block->getTitle()?>" title="<?php $block->getTitle()?>">
<?php endif;?>
</div>
<div class="block-content"><?php echo $this->getBlockContent($block)?></div>
</div>
<?php endforeach;?>


У блоці для цього був створений новий метод getBlockContent

app/local/IGN/Siteblocks/Block/List.php
<?php
class IGN_Siteblocks_Block_List extends Mage_Core_Block_Template {

public function getBlocks()
{
//return Mage::getResourceModel('siteblocks/block_collection');
$items = Mage::getModel('siteblocks/block')->getCollection()
->addFieldToFilter('block_status',array('eq'=>IGN_Siteblocks_Model_Source_Status::ENABLED));
return $items;
}

public function getBlockContent($block)
{
$processor = Mage::helper('cms')->getBlockTemplateProcessor();
$html = $processor->filter($block->getContent());
return $html;
}
}



Використання Rule Conditions (умови)
Відео: Використання Rule Conditions (умови) в Magento

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

Наступним кроком, ми додамо в наш модуль умови. Такі ж використовуються в Magento Promotional Rules. І тут заготовлено 2 типу умов. У першому, використовуються атрибут товару, у другому кошик. Викладений нижче рецепт описує перший випадок, але їх відмінності полягають лише в підміні декількох рядків.

Навіщо нам потрібні умови? Ми будемо використовувати умови для того, що б вибирати, де буде виводитися блок. Наприклад, на сторінках товарів у яких ціна нижче $100 або всі телефони з певної категорії, у яких 16гб пам'яті і дата виробництва 2015. Ми тут не про юзкейсах будемо розмовляти.

Порядок створення:

1. Оновлюємо версію модуля і доавляем upgrade скрипт, що б в таблиці додалася нова колонка conditions_serialized типу TEXT.

app/code/local/IGN/Siteblocks/sql/siteblocks_setup/upgrade-1.0.1-1.0.2.php
<?php
/** @var Mage_Core_Model_Resource_Setup $installer */
$installer = $this;
$installer->startSetup();

$installer->run("
ALTER TABLE `{$this->getTable('siteblocks/block')}` ADD `conditions_serialized` TEXT NOT NULL;
");
$installer->endSetup();


2. Модель повинна успадковуватися від Mage_Rule_Model_Abstract. І повинна декларувати 2 методу: getConditionsInstance та getActionInstance

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

/**
* Class IGN_Siteblocks_Model_Block
* @method getBlockStatus()
* @method getContent()
* @method getImage()
*/
class IGN_Siteblocks_Model_Block extends Mage_Rule_Model_Abstract {

protected $_eventPrefix = 'siteblocks_block';

#цей метод, насправді нам не потрібен, але його інтерфейс вимагає
public function getActionsInstance()
{
return Mage::getModel('catalogrule/rule_action_collection');
}


public function getConditionsInstance()
{
return Mage::getModel('catalogrule/rule_condition_combine');
}

public function _construct()
{
parent::_construct();
$this->_init('siteblocks/block');

}

public function getImageSrc()
{
return Mage::getBaseUrl('media') . 'siteblocks' . DS . $this->getImage();
}
}


Всі увагу на метод getConditionsInstance. Зараз ми використовуємо умови як в Catalog Price Rules, тобто тільки властивості і атрибут товару. Якщо ми хочемо умови як в Shopping Cart Price Rules, то потрібно використовувати Mage::getModel('salesrule/rule_condition_combine');

І якщо ви хочете вирішувати коли виводити блок на основі даних в кошику, то беремо salesrule. А також, можна створити власну модель і в ній реалізувати будь-які умови.

3. Необхідно оновити saveAction у нашому контролері.

app/code/local/IGN/Siteblocks/controllers/Adminhtml/SiteblocksController.php
<?php
class IGN_Siteblocks_Adminhtml_Siteblockscontroller extends Mage_Adminhtml_Controller_Action {

public function indexAction()
{
$this->loadLayout();
$this->_addContent($this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks'));
$this->renderLayout();
}

public function newAction()
{
$this->_forward('edit');
}

public function editAction()
{
$id = $this->getRequest()->getParam('block_id');
Mage::register('siteblocks_block',Mage::getModel('siteblocks/block')->load($id));
$blockObject = (array)Mage::getSingleton('adminhtml/session')->getBlockObject(true);
if(count($blockObject)) {
Mage::registry('siteblocks_block')->setData($blockObject);
}
$this->loadLayout();
$this->_addContent($this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks_edit'));
$this->renderLayout();
}

protected function _uploadFile($fieldName,$model)
{

if( ! isset($_FILES[$fieldName])) {
return false;
}
$file = $_FILES[$fieldName];

if(isset($file['name']) && (file_exists($file['tmp_name']))){
if($model->getId()){
unlink(Mage::getBaseDir('media').DS.$model->getData($fieldName));
}
try
{
$path = Mage::getBaseDir('media') . DS . 'siteblocks' . DS;
$uploader = new Varien_File_Uploader($file);
$uploader->setAllowedExtensions(array('jpg','png','gif','jpeg'));
$uploader->setAllowRenameFiles(true);
$uploader->setFilesDispersion(false);

$uploader->save($path, $file['name']);
$model->setData($fieldName,$uploader->getUploadedFileName());
return true;
}
catch(Exception $e)
{
return false;
}
}
}

public function saveAction()
{
try {
$id = $this->getRequest()->getParam('block_id');
/** @var IGN_Siteblocks_Model_Block $block */
$block = Mage::getModel('siteblocks/block')->load($id);
/*$block
->setTitle($this->getRequest()->getParam('title'))
->setContent($this->getRequest()->getParam('content'))
->setBlockStatus($this->getRequest()->getParam('block_status'))
->save();*/
#нижче слід ділянку для збереження умов
$data = $this->getRequest()->getParams();
if (isset($data['rule']['conditions'])) {
$data['conditions'] = $data['rule']['conditions'];
}
unset($data['rule']);
#замість setData використовуємо loadPost
$block
->loadPost($data);
$this->_uploadFile('image',$block);
$block
->setCreatedAt(Mage::app()->getLocale()->date())
->save();

if(!$block->getId()) {
Mage::getSingleton('adminhtml/session')->addError('Cannot save the block');
}
} catch(Exception $e) {
Mage::logException($e);
Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
Mage::getSingleton('adminhtml/session')->setBlockObject($block->getData());
return $this->_redirect('*/*/edit',array('block_id'=>$this->getRequest()->getParam('block_id')));
}

Mage::getSingleton('adminhtml/session')->addSuccess('Block was saved successfully!');

$this->_redirect('*/*/'.$this->getRequest()->getParam('back','index'),array('block_id'=>$block->getId()));
}

public function deleteAction()
{
$block = Mage::getModel('siteblocks/block')
->setId($this->getRequest()->getParam('block_id'))
->delete();
if($block->getId()) {
Mage::getSingleton('adminhtml/session')->addSuccess('Block was deleted successfully!');
}
$this->_redirect('*/*/');

}

public function massStatusAction()
{
$statuses = $this->getRequest()->getParams();
try {
$blocks= Mage::getModel('siteblocks/block')
->getCollection()
->addFieldToFilter('block_id',array('in'=>$statuses['massaction']));
foreach($blocks as $block) {
$block->setBlockStatus($statuses['block_status'])->save();
}
} catch(Exception $e) {
Mage::logException($e);
Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
return $this->_redirect('*/*/');
}
Mage::getSingleton('adminhtml/session')->addSuccess('Blocks were updated!');

return $this->_redirect('*/*/');

}

public function massDeleteAction()
{
$blocks = $this->getRequest()->getParams();
try {
$blocks= Mage::getModel('siteblocks/block')
->getCollection()
->addFieldToFilter('block_id',array('in'=>$blocks['massaction']));
foreach($blocks as $block) {
$block->delete();
}
} catch(Exception $e) {
Mage::logException($e);
Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
return $this->_redirect('*/*/');
}
Mage::getSingleton('adminhtml/session')->addSuccess('Blocks were deleted!');

return $this->_redirect('*/*/');

}
}


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

app/code/design/adminhtml/default/default/layout/siteblocks.xml
<?xml version="1.0"?>
<layout version="1.0.0">
<adminhtml_siteblocks_edit>
<update handle="editor"/>
<!-- Що б подгрузились потрібні js ресурси -->
<reference name="head">
<action method="setCanLoadExtJs"><flag>1</flag></action>
<action method="setCanLoadRulesJs"><flag>1</flag></action>
</reference>
</adminhtml_siteblocks_edit>

<adminhtml_system_config_edit>
<update handle="editor"/>
</adminhtml_system_config_edit>

</layout>


5. Відредагувати файл admin форми, де ми і додамо дизайнер умов.

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

class IGN_Siteblocks_Block_Adminhtml_Siteblocks_edit_form extends Mage_Adminhtml_Block_Widget_Form
{

/**
* Init form
*/
public function __construct()
{
parent::__construct();
$this->setId('block_form');
$this->setTitle(Mage::helper('siteblocks')->__('Block Information'));
}

protected function _prepareForm()
{
$model = Mage::registry('siteblocks_block');
$form = new Varien_Data_Form(
array(
'id' => 'edit_form',
'action' => $this->getUrl('*/*/save',array('block_id'=>$this->getRequest()->getParam('block_id'))),
'method' => 'post',
'enctype' => 'multipart/form-data'
)
);


$form->setHtmlIdPrefix('block_');

$fieldset = $form->addFieldset('base_fieldset',
array(
'legend'=>Mage::helper('siteblocks')->__('General Information'),
'class' => 'fieldset-wide')
);

if ($model->getBlockId()) {
$fieldset->addField('block_id', 'hidden', array(
'name' => 'block_id',
));
}

$fieldset->addField('title', 'text', array(
'name' => 'title',
'label' => Mage::helper('siteblocks')->__('Block Title'),
'title' => Mage::helper('siteblocks')->__('Block Title'),
'required' => true,
));

//$fieldset->addType('myimage','IGN_Siteblocks_Block_Adminhtml_Siteblocks_edit_renderer_myimage');

$fieldset->addField('image', 'myimage', array(
'name' => 'image',
'label' => Mage::helper('siteblocks')->__('Image'),
'title' => Mage::helper('siteblocks')->__('Image'),
'required' => true,
));



$fieldset->addField('block_status', 'select', array(
'label' => Mage::helper('siteblocks')->__('Status'),
'title' => Mage::helper('siteblocks')->__('Status'),
'name' => 'block_status',
'required' => true,
'options' => Mage::getModel('siteblocks/source_status')->toArray(),
));


$fieldset->addField('content', 'editor', array(
'name' => 'content',
'label' => Mage::helper('siteblocks')->__('Content'),
'title' => Mage::helper('siteblocks')->__('Content'),
'style' => 'height:36em',
'required' => true,
'config' => Mage::getSingleton('cms/wysiwyg_config')->getConfig()

));

#все для додавання умов
$model->getConditions()->setJsFormObject('block_conditions_fieldset');

$renderer = Mage::getBlockSingleton('adminhtml/widget_form_renderer_fieldset')
->setTemplate('promo/fieldset.phtml')
->setNewChildUrl($this->getUrl('*/promo_catalog/newConditionHtml/form/block_conditions_fieldset'));

$conditionsFieldset = $form->addFieldset('conditions_fieldset',
array(
'legend'=>Mage::helper('siteblocks')->__('Conditions'),
'class' => 'fieldset-wide')
)->setRenderer($renderer);
$conditionsFieldset->addField('conditions', 'text', array(
'name' => 'conditions',
'label' => Mage::helper('siteblocks')->__('Conditions'),
'title' => Mage::helper('siteblocks')->__('Conditions'),
'required' => true,
))->setRule($model)->setRenderer(Mage::getBlockSingleton('rule/conditions'));

$form->setValues($model->getData());
$form->setUseContainer(true);
$this->setForm($form);

return parent::_prepareForm();
}

protected function _prepareLayout()
{
parent::_prepareLayout();
if (Mage::getSingleton('cms/wysiwyg_config')->isEnabled()) {
#Ми не можемо відредагувати макет, тоді пишемо ці 2 рядки тут
$this->getLayout()->getBlock('head')->setCanLoadExtJs(true);
$this->getLayout()->getBlock('head')->setCanLoadRulesJs(true);

$this->getLayout()->getBlock('head')->setCanLoadTinyMce(true);
}
}
}



Звернемо увагу на рядок:

$this->getUrl('*/promo_catalog/newConditionHtml/form/block_conditions_fieldset')

якщо використовуємо Shopping Cart Price Rules то пишемо:

$this->getUrl('*/promo_quote/newConditionHtml/form/block_conditions_fieldset')

Подивіться ще на один важливий момент:

block_conditions_fieldset block_ повинно збігатися з $form->setHtmlIdPrefix('block_');

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

app/code/local/IGN/Siteblocks/Block/List.php
<?php
class IGN_Siteblocks_Block_List extends Mage_Core_Block_Template {

public function getBlocks()
{
//return Mage::getResourceModel('siteblocks/block_collection');
$items = Mage::getModel('siteblocks/block')->getCollection()
->addFieldToFilter('block_status',array('eq'=>IGN_Siteblocks_Model_Source_Status::ENABLED));
$filteredItems = $items;
#валидируем тільки якщо висновок на сторінці товару.
if(Mage::registry('current_product') instanceof Mage_Catalog_Model_Product) {
$filteredItems = array();
/** @var IGN_Siteblocks_Model_Block $item */
foreach ($items as $item) {
#метод validate необхідно передати валидируемый об'єкт, в нашому випадку товар
if($item->validate(Mage::registry('current_product'))) {
$filteredItems[] = $item;
}
}
}
return $filteredItems;
}

public function getBlockContent($block)
{
$processor = Mage::helper('cms')->getBlockTemplateProcessor();
$html = $processor->filter($block->getContent());
return $html;
}

}


Використання валідатора гранично просте. У нашому випадку ми ігноруємо умови, якщо висновок блоку відбувається не на сторінці твара та валідувати в такому разі нічого.

Використання вкладок на сторінці редагування
Відео: Використання вкладок на сторінці редагування в Magento

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

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

app/code/local/IGN/Siteblocks/Block/Adminhtml/Siteblocks/Edit/Tabs.php
<?php
class IGN_Siteblocks_Block_Adminhtml_Siteblocks_edit_tabs extends Mage_Adminhtml_Block_Widget_Tabs
{

public function __construct()
{
parent::__construct();
$this->setId('block_tabs');
$this->setDestElementId('edit_form');
$this->setTitle(Mage::helper('siteblocks')->__('Block Information'));
}

#у цьому методі ми можемо додавати вкладки. ще їх можна додавати в макеті
protected function _prepareLayout()
{
$this->addTab('main_tab',array(
'label' => $this->__('Main'),
'title' => $this->__('Main'),
'content' => $this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks_edit_tab_main')->toHtml()
));

/*$this->addTab('conditions_tab',array(
'label' => $this->__('Conditions'),
'title' => $this->__('Conditions'),
'content' => $this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks_edit_tab_conditions')->toHtml()
));*/

$this->addTab('conditions_tab','siteblocks/adminhtml_siteblocks_edit_tab_conditions');

return parent::_prepareLayout();
}
}


Загляньте в реалізацію методу addTab і побачите, що на вхід можна подавати масив, об'єкт, рядок. І є деякі відмінності. Тут я рекомендую заглянути у відео, де це я наочно демонструють. Але і тут замолвлю слівце.

Якщо ми передаємо в метод рядок, то клас вкладки зобов'язаний имплементить інтерфейс Mage_Adminhtml_Block_Widget_Tab_interface.

Інакше ви отримаєте помилку. А інтерфейс вимагає реалізації 4 методів. Тому у прикладі ми використовуємо 2 варіанти для демонстрації. На практиці, краще використовувати однакові способи додавання вкладок.

Подивимося вміст наших вкладок, яке ми скопіювали з вихідного файлу Form.php

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

class IGN_Siteblocks_Block_Adminhtml_Siteblocks_edit_tab_main extends Mage_Adminhtml_Block_Widget_Form
{

/**
* Init form
*/
public function __construct()
{
parent::__construct();
$this->setId('main_form');
$this->setTitle(Mage::helper('siteblocks')->__('Block Information'));
}

protected function _prepareForm()
{
$model = Mage::registry('siteblocks_block');
$form = new Varien_Data_Form();


$form->setHtmlIdPrefix('main_');

$fieldset = $form->addFieldset('base_fieldset',
array(
'legend'=>Mage::helper('siteblocks')->__('General Information'),
'class' => 'fieldset-wide')
);

if ($model->getBlockId()) {
$fieldset->addField('block_id', 'hidden', array(
'name' => 'block_id',
));
}

$fieldset->addField('title', 'text', array(
'name' => 'title',
'label' => Mage::helper('siteblocks')->__('Block Title'),
'title' => Mage::helper('siteblocks')->__('Block Title'),
'required' => true,
));

//$fieldset->addType('myimage','IGN_Siteblocks_Block_Adminhtml_Siteblocks_edit_renderer_myimage');

$fieldset->addField('image', 'myimage', array(
'name' => 'image',
'label' => Mage::helper('siteblocks')->__('Image'),
'title' => Mage::helper('siteblocks')->__('Image'),
'required' => true,
));



$fieldset->addField('block_status', 'select', array(
'label' => Mage::helper('siteblocks')->__('Status'),
'title' => Mage::helper('siteblocks')->__('Status'),
'name' => 'block_status',
'required' => true,
'options' => Mage::getModel('siteblocks/source_status')->toArray(),
));


$fieldset->addField('content', 'editor', array(
'name' => 'content',
'label' => Mage::helper('siteblocks')->__('Content'),
'title' => Mage::helper('siteblocks')->__('Content'),
'style' => 'height:36em',
'required' => true,
'config' => Mage::getSingleton('cms/wysiwyg_config')->getConfig()

));


$form->setValues($model->getData());
$this->setForm($form);

return parent::_prepareForm();
}

protected function _prepareLayout()
{
parent::_prepareLayout();
if (Mage::getSingleton('cms/wysiwyg_config')->isEnabled()) {
$this->getLayout()->getBlock('head')->setCanLoadTinyMce(true);
}
}
}



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

class IGN_Siteblocks_Block_Adminhtml_Siteblocks_edit_tab_conditions extends Mage_Adminhtml_Block_Widget_Form implements Mage_Adminhtml_Block_Widget_Tab_interface
{

#методи, які вимагає інтерфейс
public function getTabTitle()
{
return $this->__('Conditions');
}

public function getTabLabel()
{
return $this->__('Conditions');
}

public function canShowTab()
{
return true;
}

public function isHidden()
{
return false;
}

/**
* Init form
*/
public function __construct()
{
parent::__construct();
$this->setId('conditions_form');
$this->setTitle(Mage::helper('siteblocks')->__('Block Conditions'));
}

protected function _prepareForm()
{
$model = Mage::registry('siteblocks_block');
$form = new Varien_Data_Form();


$form->setHtmlIdPrefix('block_');


$model->getConditions()->setJsFormObject('block_conditions_fieldset');

$renderer = Mage::getBlockSingleton('adminhtml/widget_form_renderer_fieldset')
->setTemplate('promo/fieldset.phtml')
->setNewChildUrl($this->getUrl('*/promo_catalog/newConditionHtml/form/block_conditions_fieldset'));

$conditionsFieldset = $form->addFieldset('conditions_fieldset',
array(
'legend'=>Mage::helper('siteblocks')->__('Conditions'),
'class' => 'fieldset-wide')
)->setRenderer($renderer);
$conditionsFieldset->addField('conditions', 'text', array(
'name' => 'conditions',
'label' => Mage::helper('siteblocks')->__('Conditions'),
'title' => Mage::helper('siteblocks')->__('Conditions'),
'required' => true,
))->setRule($model)->setRenderer(Mage::getBlockSingleton('rule/conditions'));

$form->setValues($model->getData());
$this->setForm($form);

return parent::_prepareForm();
}

protected function _prepareLayout()
{
parent::_prepareLayout();
if (Mage::getSingleton('cms/wysiwyg_config')->isEnabled()) {
$this->getLayout()->getBlock('head')->setCanLoadTinyMce(true);
}
}
}



Ми просто скопіювали файл Form.php. Розділили елементи форми. І не забуваємо прибрати прапор $form->setUseContainer(true);. Відповідно поля з вихідного файлу форми можна видалити.

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

class IGN_Siteblocks_Block_Adminhtml_Siteblocks_edit_form extends Mage_Adminhtml_Block_Widget_Form
{

/**
* Init form
*/
public function __construct()
{
parent::__construct();
$this->setId('block_form');
$this->setTitle(Mage::helper('siteblocks')->__('Block Information'));
}

protected function _prepareForm()
{
$model = Mage::registry('siteblocks_block');
$form = new Varien_Data_Form(
array(
'id' => 'edit_form',
'action' => $this->getUrl('*/*/save',array('block_id'=>$this->getRequest()->getParam('block_id'))),
'method' => 'post',
'enctype' => 'multipart/form-data'
)
);


$form->setHtmlIdPrefix('block_');


$form->setValues($model->getData());
$form->setUseContainer(true);
$this->setForm($form);

return parent::_prepareForm();
}

protected function _prepareLayout()
{
parent::_prepareLayout();
if (Mage::getSingleton('cms/wysiwyg_config')->isEnabled()) {
$this->getLayout()->getBlock('head')->setCanLoadTinyMce(true);
}
}
}



Як зробити висновок блоку вкладок.

Спосіб №1 в контролері:

app/code/local/IGN/Siteblocks/controllers/Adminhtml/SiteblocksController.php
<?php
class IGN_Siteblocks_Adminhtml_Siteblockscontroller extends Mage_Adminhtml_Controller_Action {

public function indexAction()
{
$this->loadLayout();
$this->_addContent($this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks'));
$this->renderLayout();
}

public function newAction()
{
$this->_forward('edit');
}

public function editAction()
{
$id = $this->getRequest()->getParam('block_id');
Mage::register('siteblocks_block',Mage::getModel('siteblocks/block')->load($id));
$blockObject = (array)Mage::getSingleton('adminhtml/session')->getBlockObject(true);
if(count($blockObject)) {
Mage::registry('siteblocks_block')->setData($blockObject);
}
$this->loadLayout();
#виведення блоку вкладок на сторінці
$this->_addLeft($this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks_edit_tabs'));
$this->_addContent($this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks_edit'));
$this->renderLayout();
}

protected function _uploadFile($fieldName,$model)
{

if( ! isset($_FILES[$fieldName])) {
return false;
}
$file = $_FILES[$fieldName];

if(isset($file['name']) && (file_exists($file['tmp_name']))){
if($model->getId()){
unlink(Mage::getBaseDir('media').DS.$model->getData($fieldName));
}
try
{
$path = Mage::getBaseDir('media') . DS . 'siteblocks' . DS;
$uploader = new Varien_File_Uploader($file);
$uploader->setAllowedExtensions(array('jpg','png','gif','jpeg'));
$uploader->setAllowRenameFiles(true);
$uploader->setFilesDispersion(false);

$uploader->save($path, $file['name']);
$model->setData($fieldName,$uploader->getUploadedFileName());
return true;
}
catch(Exception $e)
{
return false;
}
}
}

public function saveAction()
{
try {
$id = $this->getRequest()->getParam('block_id');
/** @var IGN_Siteblocks_Model_Block $block */
$block = Mage::getModel('siteblocks/block')->load($id);
/*$block
->setTitle($this->getRequest()->getParam('title'))
->setContent($this->getRequest()->getParam('content'))
->setBlockStatus($this->getRequest()->getParam('block_status'))
->save();*/
$data = $this->getRequest()->getParams();
if (isset($data['rule']['conditions'])) {
$data['conditions'] = $data['rule']['conditions'];
}
unset($data['rule']);
$block
->loadPost($data);
$this->_uploadFile('image',$block);
$block
->setCreatedAt(Mage::app()->getLocale()->date())
->save();

if(!$block->getId()) {
Mage::getSingleton('adminhtml/session')->addError('Cannot save the block');
}
} catch(Exception $e) {
Mage::logException($e);
Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
Mage::getSingleton('adminhtml/session')->setBlockObject($block->getData());
return $this->_redirect('*/*/edit',array('block_id'=>$this->getRequest()->getParam('block_id')));
}

Mage::getSingleton('adminhtml/session')->addSuccess('Block was saved successfully!');

$this->_redirect('*/*/'.$this->getRequest()->getParam('back','index'),array('block_id'=>$block->getId()));
}

public function deleteAction()
{
$block = Mage::getModel('siteblocks/block')
->setId($this->getRequest()->getParam('block_id'))
->delete();
if($block->getId()) {
Mage::getSingleton('adminhtml/session')->addSuccess('Block was deleted successfully!');
}
$this->_redirect('*/*/');

}

public function massStatusAction()
{
$statuses = $this->getRequest()->getParams();
try {
$blocks= Mage::getModel('siteblocks/block')
->getCollection()
->addFieldToFilter('block_id',array('in'=>$statuses['massaction']));
foreach($blocks as $block) {
$block->setBlockStatus($statuses['block_status'])->save();
}
} catch(Exception $e) {
Mage::logException($e);
Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
return $this->_redirect('*/*/');
}
Mage::getSingleton('adminhtml/session')->addSuccess('Blocks were updated!');

return $this->_redirect('*/*/');

}

public function massDeleteAction()
{
$blocks = $this->getRequest()->getParams();
try {
$blocks= Mage::getModel('siteblocks/block')
->getCollection()
->addFieldToFilter('block_id',array('in'=>$blocks['massaction']));
foreach($blocks as $block) {
$block->delete();
}
} catch(Exception $e) {
Mage::logException($e);
Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
return $this->_redirect('*/*/');
}
Mage::getSingleton('adminhtml/session')->addSuccess('Blocks were deleted!');

return $this->_redirect('*/*/');

}
}


Але ми відмовимося від цієї затії і скористаємося способом №2 в макеті:

app/design/adminhtml/default/default/layout/siteblocks.xml
<?xml version="1.0"?>
<layout version="1.0.0">
<adminhtml_siteblocks_edit>
<update handle="editor"/>
<reference name="head">
<action method="setCanLoadExtJs"><flag>1</flag></action>
<action method="setCanLoadRulesJs"><flag>1</flag></action>
</reference>
<!-- Виводимо блок вкладок на сторінці редагування -->
<reference name="left">
<block type="siteblocks/adminhtml_siteblocks_edit_tabs" name="siteblocks_tabs">
<!-- 2 способи додавання вкладок в макеті -->

<block name="conditions_tab" type="siteblocks/adminhtml_siteblocks_edit_tab_conditions"/>
<action method="addTab"><name>my_conditions</name><block>conditions_tab</block></action>

<action method="addTab"><name>my_conditions</name><block>siteblocks/adminhtml_siteblocks_edit_tab_conditions</block></action>
</block>
</reference>
</adminhtml_siteblocks_edit>

<adminhtml_system_config_edit>
<update handle="editor"/>
</adminhtml_system_config_edit>

</layout>


І на останок одна порада: не варто додавати вкладки відразу в 2х місцях. Одну в макеті, іншу в блоці. Робіть додавання в одному місці чи все в макеті або все в блоці.

Висновок таблиці (grid) товарів на сторінці редагування і на frontend.
Відео: Використання вкладок на сторінці редагування в Magento

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

Тепер ми додамо в модуль фінальну фічу — можливість відзначити товари, які будуть виводитися на фронтенд разом з блоком.

Така собі альтернатива супутніх товарів. В голові складаються долольно корисні юзкейсы виведення блоку з текстом і товарами на сторінках товарів з підходящими для блоку умовами.

Додамо нову вкладку:

app/code/local/IGN/Siteblocks/Block/Adminhtml/Siteblocks/Edit/Tabs.php
<?php
class IGN_Siteblocks_Block_Adminhtml_Siteblocks_edit_tabs extends Mage_Adminhtml_Block_Widget_Tabs
{

public function __construct()
{
parent::__construct();
$this->setId('block_tabs');
$this->setDestElementId('edit_form');
$this->setTitle(Mage::helper('siteblocks')->__('Block Information'));
}

protected function _prepareLayout()
{
$this->addTab('main_tab',array(
'label' => $this->__('Main'),
'title' => $this->__('Main'),
'content' => $this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks_edit_tab_main')->toHtml()
));

$this->addTab('conditions_tab',array(
'label' => $this->__('Conditions'),
'title' => $this->__('Conditions'),
'content' => $this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks_edit_tab_conditions')->toHtml()
));
//вкладку з товарами буде використовувати AJAX, тому не використовуємо масив параметрів як вище прикладі
$this->addTab('products_tab','siteblocks/adminhtml_siteblocks_edit_tab_products');

return parent::_prepareLayout();
}
}


Вкладка використовує AJAX. Це можна побачити в коді. Там же і вказаний URL для запитів.

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

class IGN_Siteblocks_Block_Adminhtml_Siteblocks_edit_tab_products extends Mage_Adminhtml_Block_Widget_Form implements Mage_Adminhtml_Block_Widget_Tab_interface
{

public function getTabTitle()
{
return $this->__('Products');
}

public function getTabLabel()
{
return $this->__('Products');
}

public function canShowTab()
{
return true;
}

public function isHidden()
{
return false;
}

public function getClass()
{
return 'ajax';
}

public function getTabClass()
{
return 'ajax';
}

#URL для запитів, ('_current'=>true) передамо в урл всі параметри, а значить і поточний block_id там теж буде
public function getTabUrl()
{
return $this->getUrl('*/*/products',array('_current'=> 'true'));
}
}


Т. к. вкладка використовує AJAX, необхідно додати дій в контролер. І, забігаючи вперед, можете подивитися яка логіка була додана в saveAction, що б зберігалися зазначені товари.

app/code/local/IGN/Siteblocks/controllers/Adminhtml/SiteblocksController.php
<?php
class IGN_Siteblocks_Adminhtml_Siteblockscontroller extends Mage_Adminhtml_Controller_Action {

public function indexAction()
{
$this->loadLayout();
$this->_addContent($this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks'));
$this->renderLayout();
}

public function newAction()
{
$this->_forward('edit');
}

public function editAction()
{
$id = $this->getRequest()->getParam('block_id');
Mage::register('siteblocks_block',Mage::getModel('siteblocks/block')->load($id));
$blockObject = (array)Mage::getSingleton('adminhtml/session')->getBlockObject(true);
if(count($blockObject)) {
Mage::registry('siteblocks_block')->setData($blockObject);
}
$this->loadLayout();
//$this->_addLeft($this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks_edit_tabs'));
$this->_addContent($this->getLayout()->createBlock('siteblocks/adminhtml_siteblocks_edit'));
$this->renderLayout();
}

protected function _uploadFile($fieldName,$model)
{

if( ! isset($_FILES[$fieldName])) {
return false;
}
$file = $_FILES[$fieldName];

if(isset($file['name']) && (file_exists($file['tmp_name']))){
if($model->getId()){
unlink(Mage::getBaseDir('media').DS.$model->getData($fieldName));
}
try
{
$path = Mage::getBaseDir('media') . DS . 'siteblocks' . DS;
$uploader = new Varien_File_Uploader($file);
$uploader->setAllowedExtensions(array('jpg','png','gif','jpeg'));
$uploader->setAllowRenameFiles(true);
$uploader->setFilesDispersion(false);

$uploader->save($path, $file['name']);
$model->setData($fieldName,$uploader->getUploadedFileName());
return true;
}
catch(Exception $e)
{
return false;
}
}
}

public function saveAction()
{
try {
$id = $this->getRequest()->getParam('block_id');
/** @var IGN_Siteblocks_Model_Block $block */
$block = Mage::getModel('siteblocks/block')->load($id);
/*$block
->setTitle($this->getRequest()->getParam('title'))
->setContent($this->getRequest()->getParam('content'))
->setBlockStatus($this->getRequest()->getParam('block_status'))
->save();*/
$data = $this->getRequest()->getParams();

#ось такий ділянка відповідає за збереження зазначених товарів чекбоками
$links = $this->getRequest()->getPost('links', array());
if (array_key_exists('products', $links)) {
$selectedProducts = Mage::helper('adminhtml/js')->decodeGridSerializedInput($links['products']);
$products = array();
foreach($selectedProducts as $product => $position) {
$products[$product] = isset($position['position']) ? $position['position'] : $product;
}
$data['products'] = $products;
}

if (isset($data['rule']['conditions'])) {
$data['conditions'] = $data['rule']['conditions'];
}
unset($data['rule']);
$block
->loadPost($data);
$this->_uploadFile('image',$block);
$block
->setCreatedAt(Mage::app()->getLocale()->date())
->save();

if(!$block->getId()) {
Mage::getSingleton('adminhtml/session')->addError('Cannot save the block');
}
} catch(Exception $e) {
Mage::logException($e);
Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
Mage::getSingleton('adminhtml/session')->setBlockObject($block->getData());
return $this->_redirect('*/*/edit',array('block_id'=>$this->getRequest()->getParam('block_id')));
}

Mage::getSingleton('adminhtml/session')->addSuccess('Block was saved successfully!');

$this->_redirect('*/*/'.$this->getRequest()->getParam('back','index'),array('block_id'=>$block->getId()));
}

public function deleteAction()
{
$block = Mage::getModel('siteblocks/block')
->setId($this->getRequest()->getParam('block_id'))
->delete();
if($block->getId()) {
Mage::getSingleton('adminhtml/session')->addSuccess('Block was deleted successfully!');
}
$this->_redirect('*/*/');

}

public function massStatusAction()
{
$statuses = $this->getRequest()->getParams();
try {
$blocks= Mage::getModel('siteblocks/block')
->getCollection()
->addFieldToFilter('block_id',array('in'=>$statuses['massaction']));
foreach($blocks as $block) {
$block->setBlockStatus($statuses['block_status'])->save();
}
} catch(Exception $e) {
Mage::logException($e);
Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
return $this->_redirect('*/*/');
}
Mage::getSingleton('adminhtml/session')->addSuccess('Blocks were updated!');

return $this->_redirect('*/*/');

}

public function massDeleteAction()
{
$blocks = $this->getRequest()->getParams();
try {
$blocks= Mage::getModel('siteblocks/block')
->getCollection()
->addFieldToFilter('block_id',array('in'=>$blocks['massaction']));
foreach($blocks as $block) {
$block->delete();
}
} catch(Exception $e) {
Mage::logException($e);
Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
return $this->_redirect('*/*/');
}
Mage::getSingleton('adminhtml/session')->addSuccess('Blocks were deleted!');

return $this->_redirect('*/*/');

}

#2 наших нових екшену для AJAX запитів
public function productsAction()
{
$this->loadLayout()
->renderLayout();
}

public function productsgridAction()
{
$this->loadLayout()
->renderLayout();
}
}


З коду в контролері зрозуміло, що необхідно оновити макет.

app/design/adminhtml/default/default/layout/adminhtml.xml
<?xml version="1.0"?>
<layout version="1.0.0">
<adminhtml_siteblocks_edit>
<update handle="editor"/>
<reference name="head">
<action method="setCanLoadExtJs"><flag>1</flag></action>
<action method="setCanLoadRulesJs"><flag>1</flag></action>
</reference>
<reference name="left">
<block type="siteblocks/adminhtml_siteblocks_edit_tabs" name="siteblocks_tabs">
<!-- <block name="conditions_tab" type="siteblocks/adminhtml_siteblocks_edit_tab_conditions"/>
<action method="addTab"><name>my_conditions</name><block>conditions_tab</block></action>-->
<!--<action method="addTab"><name>my_conditions</name><block>siteblocks/adminhtml_siteblocks_edit_tab_conditions</block></action>-->
</block>
</reference>
</adminhtml_siteblocks_edit>

<adminhtml_system_config_edit>
<update handle="editor"/>
</adminhtml_system_config_edit>

<!-- Тут виводимо таблицю товарів, а всякі додаткові параметри потрібні, що б можна було зберігати зазначені товари -->
<adminhtml_siteblocks_products>
<block type="core/text_list" name="root" output="toHtml">
<block type="siteblocks/adminhtml_siteblocks_edit_tab_products_grid" name="siteblocks_products"/>
<block type="adminhtml/widget_grid_serializer" name="siteblocks_products_serializer">
<reference name="siteblocks_products_serializer">
<action method="initSerializerBlock">
<grid_block_name>siteblocks_products</grid_block_name>
<data_callback>getSelectedBlockProducts</data_callback>
<hidden_input_name>links[products]</hidden_input_name>
<reload_param_name>siteblocks_products</reload_param_name>
</action>
<action method="addColumnInputName">
<input_name>position</input_name>
</action>
</reference>
</block>
</block>
</adminhtml_siteblocks_products>

<!-- Тут просто виводимо хтмл таблиці товарів -->
<adminhtml_siteblocks_productsgrid>
<block type="core/text_list" name="root" output="toHtml">
<block type="siteblocks/adminhtml_siteblocks_edit_tab_products_grid" name="block_products"/>
</block>
</adminhtml_siteblocks_productsgrid>

</layout>


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

Завершальним елементом у admin інтерфейсі буде клас таблиці.

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

<?php
class IGN_Siteblocks_Block_Adminhtml_Siteblocks_edit_tab_products_grid extends Mage_Adminhtml_Block_Widget_Grid
{

protected $_block;
/**
* Set grid params
*
*/
public function __construct()
{
parent::__construct();
$this->setId('siteblocks_product_grid');
$this->setDefaultSort('entity_id');
$this->setUseAjax(true);
if ($this->_getBlock()->getId()) {
$this->setDefaultFilter(array('in_products'=>1));
}
if ($this->isReadonly()) {
$this->setFilterVisibility(false);
}
}

protected function _getBlock()
{
if(!$this->_block) {
$this->_block = Mage::getModel('siteblocks/block')->load($this->getRequest()->getParam('block_id'));
}
return $this->_block;
}


protected function _addColumnFilterToCollection($column)
{
// Set filter for custom in product flag
if ($column->getId() == 'in_products') {
$productIds = $this->_getSelectedProducts();
if (empty($productIds)) {
$productIds = 0;
}
if ($column->getFilter()->getValue()) {
$this->getCollection()->addFieldToFilter('entity_id', array('in'=>$productIds));
} else {
if($productIds) {
$this->getCollection()->addFieldToFilter('entity_id', array('nin'=>$productIds));
}
}
} else {
parent::_addColumnFilterToCollection($column);
}
return $this;
}

/**
* Checks when this block is readonly
*
* @return boolean
*/
public function isReadonly()
{
return $this->_getBlock()->getUpsellReadonly();
}

protected function _prepareCollection()
{
#тут можемо вказати яку колекцію використовуємо для таблиці
$collection = Mage::getResourceModel('catalog/product_collection')
->addAttributeToSelect('*');

if ($this->isReadonly()) {
$productIds = $this->_getSelectedProducts();
if (empty($productIds)) {
$productIds = array(0);
}
$collection->addFieldToFilter('entity_id', array('in'=>$productIds));
}

$this->setCollection($collection);
return parent::_prepareCollection();
}

/**
* Add columns to grid
*
* @return Mage_Adminhtml_Block_Widget_Grid
*/
protected function _prepareColumns()
{
#колонки додаються як в будь-якій іншій таблиці
if (!$this->_getBlock()->getUpsellReadonly()) {
$this->addColumn('in_products', array(
'header_css_class' => 'a-center',
'type' => 'checkbox',
'name' => 'in_products',
'values' => $this->_getSelectedProducts(),
'align' => 'center',
'index' => 'entity_id'
));
}

$this->addColumn('entity_id', array(
'header' => Mage::helper('catalog')->__('ID'),
'sortable' => true,
'width' => 60,
'index' => 'entity_id'
));
$this->addColumn('name', array(
'header' => Mage::helper('catalog')->__('Name'),
'index' => 'name'
));

$this->addColumn('type', array(
'header' => Mage::helper('catalog')->__('Type'),
'width' => 100,
'index' => 'type_id',
'type' => 'options',
'options' => Mage::getSingleton('catalog/product_type')->getOptionArray(),
));

$sets = Mage::getResourceModel('eav/entity_attribute_set_collection')
->setEntityTypeFilter(Mage::getModel('catalog/product')->getResource()->getTypeId())
->load()
->toOptionHash();

$this->addColumn('set_name', array(
'header' => Mage::helper('catalog')->__('Attrib. Set Name'),
'width' => 130,
'index' => 'attribute_set_id',
'type' => 'options',
'options' => $sets,
));

$this->addColumn('status', array(
'header' => Mage::helper('catalog')->__('Status'),
'width' => 90,
'index' => 'status',
'type' => 'options',
'options' => Mage::getSingleton('catalog/product_status')->getOptionArray(),
));

$this->addColumn('visibility', array(
'header' => Mage::helper('catalog')->__('Visibility'),
'width' => 90,
'index' => 'visibility',
'type' => 'options',
'options' => Mage::getSingleton('catalog/product_visibility')->getOptionArray(),
));

$this->addColumn('sku', array(
'header' => Mage::helper('catalog')->__('SKU'),
'width' => 80,
'index' => 'sku'
));

$this->addColumn('price', array(
'header' => Mage::helper('catalog')->__('Price'),
'type' => 'currency',
'currency_code' => (string) Mage::getStoreConfig(Mage_Directory_Model_Currency::XML_PATH_CURRENCY_BASE),
'index' => 'price'
));

$this->addColumn('position', array(
'header' => Mage::helper('catalog')->__('Position'),
'name' => 'position',
'type' => 'number',
'width' => 60,
'validate_class' => 'validate-number',
'index' => 'position',
'editable' => true
));

return parent::_prepareColumns();
}

#цей URL буде використовуватися під час сортування і фільтрації
public function getGridUrl()
{
return $this->_getData('grid_url') ? $this->_getData('grid_url') : $this->getUrl('*/*/productsgrid', array('_current'=> 'true'));
}

protected function _getSelectedProducts()
{
return array_keys($this->getSelectedBlockProducts());
}

public function getSelectedBlockProducts()
{
$selected = $this->getRequest()->getParam('siteblocks_products');

$products = array();
foreach ($this->_getBlock()->getProducts() as $product => $position) {
$products[$product] = array('position' => $position);
}
foreach ($selected as $product) {
if(!isset($products[$product])) {
$products[$product] = array('position'=>$product);
}
}
return $products;
}
}


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

app/code/local/IGN/Siteblocks/sql/siteblocks_setup/upgrade-1.0.2-1.0.3.php
<?php
/** @var Mage_Core_Model_Resource_Setup $installer */
$installer = $this;
$installer->startSetup();
$installer->run("
ALTER TABLE `{$this->getTable('siteblocks/block')}` ADD `products` TEXT NOT NULL;
");
$installer->endSetup();


І невеликі перетворення в моделі.

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

/**
* Class IGN_Siteblocks_Model_Block
* @method getBlockStatus()
* @method getContent()
* @method getImage()
*/
class IGN_Siteblocks_Model_Block extends Mage_Rule_Model_Abstract {

protected $_eventPrefix = 'siteblocks_block';

public function getActionsInstance()
{
return Mage::getModel('catalogrule/rule_action_collection');
}

public function getConditionsInstance()
{
return Mage::getModel('catalogrule/rule_condition_combine');
}

public function _construct()
{
parent::_construct();
$this->_init('siteblocks/block');

}

public function getImageSrc()
{
return Mage::getBaseUrl('media') . 'siteblocks' . DS . $this->getImage();
}

#перед збереженням перетворимо масив в рядок
protected function _beforeSave()
{
parent::_beforeSave();
if(is_array($this->getData('products'))) {
$this->setData('products',json_encode($this->getData('products')));
}
}
#після завантаження перетворимо рядок в масив
protected function _afterLoad()
{
parent::_beforeSave();
if(!empty($this->getData('products'))) {
$this->setData('products',(array)json_decode($this->getData('products')));
}
}

#додатковий метод, який поверне нам масив завжди
public function getProducts()
{
if(!is_array($this->getData('products'))) {
$this->setData('products',(array)json_decode($this->getData('products')));
}
return $this->getData('products');
}
}


Товари можемо призначити. Тепер необхідно їх коректно відобразити на frontend'e. Для цих цілей я створив новий темплейт, який я скопіював з upsell'ів і відредагував під свої потреби:

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


Так само оновимо темплейт виведення блоків list.phtml:

app/design/frontend/base/default/template/siteblocks/list.php
<?php foreach($this->getBlocks() as $block):?>
<div class="siteblock">
<div class="block-title"><?php echo $block->getTitle()?></div>
<div class="block-image">
<?php if($block->getImage()):?>
<img src="<?php echo $block->getImageSrc()?>" height="150" width="авто" alt="<?php $block->getTitle()?>" title="<?php $block->getTitle()?>">
<?php endif;?>
</div>
<div class="block-content"><?php echo $this->getBlockContent($block)?></div>

<div class="block-product-list">
<?php echo $this->getProductsList($block)?>
</div>
</div>
<?php endforeach;?>


І необхідні зміни в блоці List.php:

app/code/local/IGN/Siteblocks/Block/List.php
<?php
class IGN_Siteblocks_Block_List extends Mage_Core_Block_Template {

public function getBlocks()
{
//return Mage::getResourceModel('siteblocks/block_collection');
$items = Mage::getModel('siteblocks/block')->getCollection()
->addFieldToFilter('block_status',array('eq'=>IGN_Siteblocks_Model_Source_Status::ENABLED));
$filteredItems = $items;
if(Mage::registry('current_product') instanceof Mage_Catalog_Model_Product) {
$filteredItems = array();
/** @var IGN_Siteblocks_Model_Block $item */
foreach ($items as $item) {
if($item->validate(Mage::registry('current_product'))) {
$filteredItems[] = $item;
}
}
}
return $filteredItems;
}

public function getBlockContent($block)
{
$processor = Mage::helper('cms')->getBlockTemplateProcessor();
$html = $processor->filter($block->getContent());
return $html;
}
//цей метод використовуємо для виведення товарів
public function getProductsList($block)
{
$products = $block->getProducts();
asort($products);
$collection = Mage::getResourceModel('catalog/product_collection')
->addFieldToFilter('entity_id',array('in'=>array_keys($products)))
->addAttributeToSelect('*');
/** @var Mage_Catalog_Block_Product_List $list */
$list = $this->getLayout()->createBlock('catalog/product_list');
$list->setCollection($collection);
$list->setTemplate('siteblocks/product/list.phtml');
return $list->toHtml();
}

}


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

Таким чином ми отримали модуль, який може виводити блоки в деяких місцях сайту. Висновок блоків на сторінці товару здійснюється з перевіркою умов (Rule Conditions). Для введення вмісту у нас використовується зручний WYSIWYG редактор.

Так само разом з блоком ми можемо вивести кілька товарів. Модуль, якого легко знайти реальне застосування з деякими доробками під себе. Публічний репозиторій з створеним модулем. І цей гайд не був би повноцінним, якщо б ми не розглянули процес створення власного способу оплати і способу доставки.


Створення модуля способу оплати (Payment Method)
Відео: Розробка модуля платіжного методу для Magento

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

Це буде платіжний спосіб, яким можна буде оплатити замовлення, ввівши секретний код. Давайте уявимо, що це введення якихось реквізитів для оплати замовлення. Тему можна розвинути і зробити повноцінну форму. Наше ж завдання — зрозуміти мінімум дій для створення основи майбутнього повноцінного способу оплати.

Спосіб оплати включає в себе кілька файлів: 2 блоку, 2 темплейта, 2 xml файлу і 1 модель.
Почнемо з system.xml у ньому ми додамо нову секцію вже існуючої вкладці Payment Methods.

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


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

app/code/community/IGN/Payment/etc/config.xml
<?xml version="1.0"?>
<config>
<modules>
<IGN_Payment>
<version>1.0.0</version>
</IGN_Payment>
</modules>
<global>
<models>
<ignpayment>
<class>IGN_Payment_Model</class>
</ignpayment>
</models>
<resources>
<payment_setup>
<setup>
<module>IGN_Payment</module>
</setup>
</payment_setup>
</resources>
<blocks>
<ignpayment>
<class>IGN_Payment_Block</class>
</ignpayment>
</blocks>
<helpers>
<ignpayment>
<class>IGN_Payment_Helper</class>
</ignpayment>
</helpers>
</global>
<frontend>
<translate>
<modules>
<IGN_Payment>
<files>
<default>IGN_Payment.csv</default>
</files>
</IGN_Payment>
</modules>
</translate>
</frontend>
<adminhtml>
<translate>
<modules>
<IGN_Payment>
<files>
<default>IGN_Payment.csv</default>
</files>
</IGN_Payment>
</modules>
</translate>
</adminhtml>
<default>
<payment>
<ignpayment>
<active>1</active>
<model>ignpayment/method</model> <!-- Найважливіший момент у налаштуваннях -->
<order_status>pending</order_status>
<title>Secret Code</title>
<allowspecific>0</allowspecific>
<sort_order>1</sort_order>
<group>offline</group>
</ignpayment>
</payment>
</default>
</config>


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

app/code/community/IGN/Payment/Model/Method.php
<?php
class IGN_Payment_Model_Method extends Mage_Payment_Model_Method_Abstract {

//не можна забувати вказати код методу
protected $_code = 'ignpayment';

//вказуємо block type
protected $_formBlockType = 'ignpayment/form';
protected $_infoBlockType = 'ignpayment/info';

//цей метод використовується для валідації секретного коду, а так само будь-яких цікавлять нас параметри кошика
public function validate()
{
$code = Mage::app()->getRequest()->getParam('secret_code');
if($code != $this->getConfigData('secret_code')) {
Mage::throwException(Mage::helper('ignpayment')->__("This code doesn't work!"));
}
return parent::validate();
}
}


Обов'язково наследуемся від класу Mage_Payment_Model_Method_Abstract . Якщо заглянути всередину цього класу, то побачимо там купу властивостей з дефолтними значеннями і методів. Властивості і методи несуть цілком говорять назви, тому, якщо нам щось особливо важливо, копіюємо в свій клас і вказуємо відповідне потребам значення.

Запам'ятовуємо, що в моделі реалізовані методи:

order(), capture(), void(), refund() і тд. І якщо наш платіжний засіб повинен «спілкуватись» із серверами платіжного сервісу, то копіюємо методи в свій клас і додаємо в них відповідні сценарії.

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

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

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


Цей блок виводиться в інформаційному блоці на сторінку замовлення.

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


І відповідають блокам темплейти:

app/design/frontend/base/default/template/ignpayment/form.phtml
<!-- Слідкуйте за id, він повинен починатися з преффикса payment_form_, а сам елемент за замовчуванням прихований -->
<div id="payment_form_ignpayment" style="display: none">
<input type="text" name="secret_code" autocomplete="off">
<!-- Тут може бути форма введення картки або інших реквізитів -->
</div>


Вміст файлу info.phtml стандартно, але можемо його змінити під свої потреби.

app/design/frontend/base/default/template/ignpayment/info.phtml
<p><strong><?php echo $this->escapeHtml($this->getMethod()->getTitle()) ?></strong></p>

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


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

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

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

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

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

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



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


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

app/code/community/IGN/Shipment/Model/Carrier.php
<?php
class IGN_Shipment_Model_Carrier extends Mage_Shipping_Model_Carrier_Abstract implements Mage_Shipping_Model_Carrier_Interface {

protected $_code = 'ignshipment';

public function collectRates(Mage_Shipping_Model_Rate_Request $request)
{
/** @var Mage_Shipping_Model_Rate_Result $result */
$result = Mage::getModel('shipping/rate_result');

$weight = $request->getPackageWeight();

/** @var Mage_Shipping_Model_Rate_Result_method $method */
$method = Mage::getModel('shipping/rate_result_method');

$method->setCarrier($this->_code);
$method->setCarrierTitle($this->getConfigData('title'));

//В залежності від загальної ваги дізнаємося вартість у відповідного способу доставки
if($weight > $this->getConfigData('packet_max_weight')) {
$this->_getBoxMethod($weight,$method);
} else {
$this->_getPacketMethod($weight,$method);
}

$result->append($method);

return $result;
}

protected function _getPacketMethod($weight,$method)
{
$method->setMethod('packet');
$method->setMethodTitle('Packet belpost');
$sum = Mage::helper('ignshipment')->getPacketCost($weight);
$method->setPrice($sum/19050);
}

protected function _getBoxMethod($weight,$method)
{
$method->setMethod('box');
$method->setMethodTitle('Box belpost');
$sum = Mage::helper('ignshipment')->getBoxCost($weight);
$method->setPrice($sum/19050);
}

//Ми не будемо реалізовувати відстеження по проблемі відсутності API
public function isTrackingAvailable()
{
return false;
}

public function getAllowedMethods()
{
//за задумом у нас 2 способу доставки. Пакет до 2000 грамів, і посилка
return array(
'packet' => 'Packet belpost',
'box' => 'Box belpost'
);
}

}


Успадковуємо модель від класу Mage_Shipping_Model_Carrier_Abstract . Інтерфейс имплементить не обов'язково. У нашій логіці ще не використовується можливість підрахунку кількості коробок, що так само позначиться на вартості доставки. Але в такому випадку доведеться рахувати кожну коробку по її вазі і підсумовувати вартість. Ми ж приймаємо, що всі товари поміщаються в одну коробку.

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

app/code/community/IGN/Shipment/Helper/Data.php
<?php
class IGN_Shipment_Helper_Data extends Mage_Core_Helper_Abstract {


public function getPacketCost($weight)
{
$request = new Zend_Http_Client();
$request->setUri('http://tarifikator.belpost.by/forms/international/packet.php');
$request->setParameterPost(array(
'who'=>'ur',
'type'=>'registered',
'priority'=>'priority',
'to'=>'other',
'weight'=>$weight
));
$response = $request->request(Zend_Http_Client::POST);

$html = $response->getBody();

$tag_regex = "/<blockquote>(.*)<\/blockquote>/im";
$sum_reqex = "/(\d+)/is";
preg_match_all($tag_regex,
$html,
$matches,
PREG_PATTERN_ORDER);
if(isset($matches[1]) && isset($matches[1][0])) {
preg_match($sum_reqex,$matches[1][0],$matches);
if(isset($matches[0])) {
return (float)$matches[0];
}
}
//робимо висновок стандартної ціни, якщо не вдалося дізнатися на сайті
//а чи можна повернути помилку і зробити метод недоступним для використання
return Mage::getStoreConfig('carriers/ignshipment/price');
}

public function getBoxCost($weight)
{
$request = new Zend_Http_Client();
$request->setUri('http://tarifikator.belpost.by/forms/international/ems.php');
$request->setParameterPost(array(
'who'=>'ur',
'type'=>'goods',
'to'=>'n10', //тут проста затичка. потрібно створювати асоціативний масив таких кодів з сайту і кодів країни, т. к. в Magento це US NZ, AU, а на белпочте це n1,n2,n3 і тд.
'weight'=>$weight
));
$response = $request->request(Zend_Http_Client::POST);

$html = $response->getBody();

$tag_regex = "/<blockquote>(.*)<\/blockquote>/im";
$sum_reqex = "/(\d+)/is";
preg_match_all($tag_regex,
$html,
$matches,
PREG_PATTERN_ORDER);
if(isset($matches[1]) && isset($matches[1][0])) {
preg_match($sum_reqex,$matches[1][0],$matches);
if(isset($matches[0])) {
return $matches[0];
}
}
//робимо висновок стандартної ціни, якщо не вдалося дізнатися на сайті
//а чи можна повернути помилку і зробити метод недоступним для використання
return Mage::getStoreConfig('carriers/ignshipment/price');
}
}


Можливо у вас є питання до моїх регуляркам. У мене теж є до них запитання, але залишимо це за принципом «працює — не чіпай».

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

Підіб'ємо підсумок. Спосіб доставки вміє підраховувати вартість виходячи з загальної ваги. Вага береться із стандартного атрибуту товарів. І якщо адміністратор не полінувався його вказати кожному товару, то все запрацює.

На завершення бажаю всім успіхів. А щодо помилок, яких, напевно, багато, пишіть бажано в ЛС.

p.s. Не можу не скористатися моментом і не попіарити свій маленький youtube канальчиках. Заходьте, там у стримчики бувають і не тільки по Magento. А скоро і за розбір Magento 2 візьмемося.

Всіх благ!

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.