Пошаговое создание модуля в Magento — руководство начинающего разработчика

в 15:49, , рубрики: Без рубрики

Сколько о Magento не пиши, а все равно вопросов много ;) © jeje

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

В данной статье я попытаюсь показать создание модуля пошагово с объяснениями каждого изменения на примере модуля новостей «DS News», где DS — это Namespace (Пространство имён), а News — это название модуля. Данная схема именования модулей является довольно удобной для того, чтобы не бояться конфликта имён в названии модулей. Особый упор постараюсь сделать на объяснение значений, используемых в файле конфигурации — названия узлов и места, где они используются. Сам я пользуюсь данным руководством постоянно при создании нового модуля, т.к. запомнить откуда какие данные идут, какие классы нужно наследовать и т.д… просто невозможно физически. А тут всё в одной статье :)

Не буду описывать установку Magento и заполнение товарами, считая что система уже работает. Однако следует убедиться в том, что кеширование отключено (кеш можно отключить в админке на странице System/Cache Management) — это необходимо для того, чтобы сразу видеть производимые изменения.

Шаг 1. Регистрация модуля в системе

На первом шаге создадим минимальный модуль, который не будет ничего делать, но зато будет учитываться в системе (и показываться в админке):

  1. Создать директорию /app/code/local/DS/News
  2. Создать файл etc/config.xml
    <?xml version="1.0" ?>
    <config>
        <modules>
            <DS_News>
                <version>0.0.1</version>
            </DS_News>
        </modules>
    </config>
    

  3. Создать файл DS_News.xml в директории /app/etc/modules/
    <?xml version="1.0" ?>
    <config>
        <modules>
            <DS_News>
                <active>true</active>
                <codePool>local</codePool>
            </DS_News>
        </modules>
    </config>
    

В результате создания данной структуры директорий и файлов, в системе будет зарегистрирован модуль DS_News. Данный модуль можно увидеть в списке модулей в админке по пути System/Configuration/Advanced/Advanced/Disable Modules Output.

В пункте 1 создаётся директория /app/code/local/DS/News, в которой будет храниться основной код модуля: модели, контроллеры, хелперы и т.д… В пункте 2 создаётся директория etc, в которой хранятся файлы конфигурации модуля, а также создаётся основной файл конфигурации config.xml, в котором пока указывается только версия модуля. В пункте 3 модуль регистрируется в системе.

По факту, для задачи регистрации модуля самым важным является пункт 3 — модуль будет показан в админке даже при отсутствии директории модуля. Название файла DS_News.xml в директории /app/etc/modules/ тоже не является обязательным — при загрузке система парсит все XML файлы из данной директории. Это может быть удобно при разработке в случае, когда устанавливаются несколько взаимосвязанных модулей — все модули можно указать в одном XML файле.

Названия модуля DS_News в xml файлах состоит из двух частей: [название директории namespace]_[название директории модуля]. В связи с данным подходом именования, название директорий желательно составлять только из букв, и, также, важно учитывать регистр — название модуля в файлах конфигурации должно точно совпадать с названием директорий для корректной работы на *nix системах.

В файле /app/etc/modules/DS_News.xml используются два тега: active и codePool. Первый тег отвечает за включение/отключение модуля, а второй — за местоположение в директории /app/code/: core (ядро системы), community (модули, разработанные сообществом Magento) или local (модули, разработанные другими разработчиками).

Шаг 2. Инициализация базы данных

Для работы модуля новостей требуется создать таблицу, в которой будут храниться новости. Magento позволяет при установке модуля создавать/обновлять таблицы.

  1. Создать файл sql/dsnews_setup/install-0.0.1.php
    <?php
    
    die('DS News module setup');
    
    $installer = $this;
    $installer->startSetup();
    $installer->run("CREATE TABLE ds_news_entities (
            `news_id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
            `title` VARCHAR(255) NOT NULL,
            `content` TEXT NOT NULL,
            `created` DATETIME,
    
            PRIMARY KEY (`news_id`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8;");
    
    $installer->endSetup();
    
    

  2. Добавить в файл конфигурации etc/config.xml секцию с ресурсом для установки:
    <?xml version="1.0" ?>
    <config>
        <modules>
            ...
        </modules>
        <global>
            <resources>
                <dsnews_setup>
                    <setup>
                        <module>DS_News</module>
                    </setup>
                </dsnews_setup>
            </resources>
        </global>
    </config>
    

После того, как изменения сделаны, нужно просто открыть сайт в браузере. Если появляется надпись DS News module setup, то всё нормально. Чуть позже можно будет закомментировать строчку с die в файле install-0.0.1.php, и обновить окно браузера, после чего модуль будет зарегистрирован в таблице core_resource, а в базе будет создана таблица ds_news_entities.

Если надпись не появляется, то, скорее всего, модуль был уже установлен в системе, и для установки требуется удалить запись о ресурсе dsnews_setup из таблицы core_resource в базе данных с помощью какого-либо SQL менеджера, после чего заново обновить окно браузера с сайтом:

DELETE FROM `core_resource` WHERE `code` = 'dsnews_setup';

Название узла dsnews_setup в config.xml может быть любым. Главное, чтобы название данного узла совпадало с названием директории, где будут находиться файлы для установки. Также, название данного узла должно быть уникальным в системе, т.к. это название используется в качестве первичного ключа таблицы core_resource, в которой хранятся все установленные ресурсы.

Названия файлов в директории установки должны соответствовать определённой структуре. Для файлов установки это install-[version].[php|sql], а для файлов обновления это upgrade-[version-from]-[version-to].[php|sql]. Более подробно о процессе установки ресурсов, именовании файлов и директорий можно увидеть, исследуя класс Mage_Core_Model_Resource_Setup.

Шаг 3. Получение названия таблицы из config.xml

Так как в дальнейшем в данном модуле работа с базой данных будет происходить через модели и коллекции, названия таблиц должны храниться в файле конфигурации, а не в файле установки. Для этого нужно изменить файл etc/config.xml, а также файл установки sql/dsnews_setup/install-0.0.1.php

  1. Создать узел моделей config/global/models/ в файле etc/config.xml
    <?xml version="1.0" ?>
    <config>
        ...
        <global>
            <models>
                <dsnews>
                    <resourceModel>dsnews_resource</resourceModel>
                </dsnews>
                <dsnews_resource>
                    <entities>
                        <table_news>
                            <table>ds_news_entities</table>
                        </table_news>
                    </entities>
                </dsnews_resource>
            </models>
            <resources>
                ...
            </resources>
        </global>
    </config>
    

  2. Изменить файл sql/dsnews_setup/install-0.0.1.php
    <?php
    
    $installer = $this;
    $tableNews = $installer->getTable('dsnews/table_news');
    
    die($tableNews);
    
    $installer->startSetup();
    $installer->run("DROP TABLE IF EXISTS {$tableNews}");
    $installer->run("CREATE TABLE {$tableNews} (
            `news_id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
            `title` VARCHAR(255) NOT NULL,
            `content` TEXT NOT NULL,
            `created` DATETIME,
    
            PRIMARY KEY (`news_id`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8;");
    
    $installer->endSetup();
    
    

Если в браузере выведется название таблиы ds_news_entities, то всё сделано правильно — можно удалить строчку с die и обновить окно для завершения установки модуля. В противном случае следует удалить запись о ресурсе модуля из таблицы core_resource с помощью какого-либо SQL менеджера:

DELETE FROM `core_resource` WHERE `code` = 'dsnews_setup';

Для того, чтобы у нас не возникло SQL ошибки в случае, если данная таблица была создана на предыдущем этапе, в файл установки был добавлен код предварительного удаления уже существующей таблицы.

Название узла модели dsnews, также как и название узла модели-ресурса dsnews_resource, расположенных в config/global/models/, может быть любым. Единственное требование — уникальность названия узла среди других моделей/ресурсов системы.

Внутри кода инстраллера в пункте 2 запрашивается название таблицы с помощью функции $installer->getTable('dsnews/table_news'), которая принимает в качестве аргумента строку
[название узла модели]/[название узла entities ресурса]. Сам узел модели не хранит названия таблиц, так как низкоуровневое взаимодействие с БД, по логике Magento, должно происходить с помощью моделей-ресурсов. По этой причине модель хранит только ссылку на узел модели-ресурса в теге resourceModel.

Шаг 4. Доступ к модулю через FrontEnd

Теперь выведем надпись «Hello World» на сайте.

  1. Добавить секцию frontend в файл конфигурации etc/config.xml
    <?xml version="1.0" ?>
    <config>
        <modules>
            ...
        </modules>
        <frontend>
            <routers>
                <dsnews>
                    <use>standard</use>
                    <args>
                        <module>DS_News</module>
                        <frontName>news</frontName>
                    </args>
                </dsnews>
            </routers>
        </frontend>
        <global>
            ...
        </global>
    </config>
    

  2. Создать файл controllers/IndexController.php
    <?php
    
    class DS_News_IndexController extends Mage_Core_Controller_Front_Action
    {
    
        public function indexAction()
        {
            echo '<h1>News</h1>';
        }
    
    }
    

Теперь по адресу http://site.com/news/ будет выводиться надпись «News».

Название узла dsnews, расположенного в секции config/frontend/routers/, может быть любым; единственное требование — это уникальность названия среди других роутеров системы. Более важным является значение узла frontName — это значение будет использоваться для определения нужного модуля во время обработки запроса.

Путь определяется по следующей схеме: http://[site]/[router]/[controller]/[action]. При запросе можно не указывать [action] или пару [controller]/[action], тогда вместо недостающих параметров будет использоваться значение по умолчанию index. Таким образом пути http://site.com/news/, http://site.com/news/index/, http://site.com/news/index/index являются эквивалентными — обращаются к модулю DS News, контроллеру IndexController и действию indexAction.

Шаг 5. Использование шаблона для вывода данных

Информацию можно генерировать напрямую в контроллере, а можно использовать специальный шаблон для отделения логики отображения от программной логики

  1. Создать файл шаблона /app/design/frontend/[package]/[theme]/template/ds_news/index.phtml
    <h1>Template ds_news/index.phtml</h1>
    

  2. Создать файл конфигурации макета страницы /app/design/frontend/[package]/[theme]/layout/ds_news.xml
    <?xml version="1.0" ?>
    <layout>
        <dsnews_index_index>
            <reference name="content">
                <block type="core/template" template="ds_news/index.phtml" />
            </reference>
        </dsnews_index_index>
    </layout>
    

  3. Добавить секцию layout в файл конфигурации etc/config.xml
    <?xml version="1.0" ?>
    <config>
        <modules>
            ...
        </modules>
        <frontend>
            <layout>
                <updates>
                    <dsnews>
                        <file>ds_news.xml</file>
                    </dsnews>
                </updates>
            </layout>
            <routers>
                ...
            </routers>
        </frontend>
        <global>
            ...
        </global>
    </config>
    

  4. Изменить контроллер controllers/IndexController.php
    <?php
    
    class DS_News_IndexController extends Mage_Core_Controller_Front_Action
    {
    
        public function indexAction()
        {
            $this->loadLayout();
            $this->renderLayout();
        }
    
    }
    

Теперь по пути http://site.com/news/ должна открываться страница сайта со всеми блоками (header, footer, sidebar), а на месте содержимого страницы будет показана надпись «Template ds_news/index.phtml».

Все шаблоны хранятся в директории template, а настройки макета страницы — в директории текущей темы /app/design/frontend/[package]/[theme]. При работе с Magento необходимо создавать собственные темы, не трогая содержимого базовых тем /app/design/frontend/base/ и /app/design/frontend/default/ из-за того, что содержимое базовых тем может быть перезаписано при обновлении версии Magento.

При инициализации макета страницы $this->loadLayout() в действии контроллера происходит поочерёдная загрузка layout handle-ов страницы, которые могут изменять друг друга. По умолчанию, первым грузится handle default, а одним из последних — handle с названием [router]_[controller]_[action], где [router] — это название узла config/frontend/routers/[router].

В пункте 2 определяется layout handle для нашего контроллера/действия: dsnews_index_index, внутри которого создаётся блок встроенного типа core/template и шаблоном dsnews/index.phtml, созданным в пункте 1. В пункте 3 в файл конфигурации добавляется секция config/frontend/layout/updates, в которой указывается файл обновлений, который нужно применить при инициализации темы.

Если изменений в макете темы не так много и модуль не планируется реализовывать в других проектах, то layout handle можно разместить в файле /app/design/frontend/[package]/[theme]/layout/local.xml,
который грузится всегда после всех обновлений — этот файл нет необходимости прописывать в файле конфигурации модуля config.xml

В случае затруднения с определением правильного названия layout handle, можно посмотреть список применяемых handle-ов в контроллере:

<?php

class DS_News_IndexController extends Mage_Core_Controller_Front_Action
{

    public function indexAction()
    {
        $this->loadLayout();
        $layoutHandles = $this->getLayout()->getUpdate()->getHandles();
        echo '<pre>' . print_r($layoutHandles, true) . '</pre>';
    }

}

В результате можно будет увидеть вывод, наподобие данного:

Array
(
    [0] => default
    [1] => STORE_default
    [2] => THEME_frontend_[package]_[theme]
    [3] => dsnews_index_index
    [4] => customer_logged_out
)
Шаг 6. Показ новостей напрямую из базы данных

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

  1. Добавить в базу тестовые новости для вывода:
    INSERT INTO `ds_news_entities` VALUES
        (NULL, 'News 1', 'News 1 Content', '2013-10-16 17:45'),
        (NULL, 'News 2', 'News 2 Content', '2013-11-07 04:12'),
        (NULL, 'News 3', 'News 3 Content', '2014-01-12 15:55');
    

  2. Изменить шаблон /app/design/frontend/[package]/[theme]/template/dsnews/index.phtml
    <h1>News</h1>
    <?php
    $news = Mage::registry('news');
    foreach ($news as $item) {
        echo '<h2>' . $item['title'] . '</h2>';
    }
    

  3. Изменить контроллер controllers/IndexController.php
    <?php
    
    class DS_News_IndexController extends Mage_Core_Controller_Front_Action
    {
    
        public function indexAction()
        {
            $resource = Mage::getSingleton('core/resource');
            $read = $resource->getConnection('core_read');
            $table = $resource->getTableName('dsnews/table_news');
    
            $select = $read->select()
                    ->from($table, array('news_id', 'title', 'content', 'created'))
                    ->order('created DESC');
    
            $news = $read->fetchAll($select);
            Mage::register('news', $news);
    
            $this->loadLayout();
            $this->renderLayout();
        }
    
    }
    

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

Использование реестра, как и использование глобальных переменных, считается признаком некачественного кода. Прямой доступ к базе данных также не рекомендуется, так как это является потенциальной проблемой совместимости и переностимости кода. Лучше использовать возможности, предоставляемые самой системой для работы с объектами — модели и коллекции, которые будут рассматриваться в следующем разделе.

Шаг 7. Создание и использование моделей

По логике Magento (и многих других ООП систем), обработка данных должна производиться на уровне объектов/моделей и коллекций, не учитывая способы получения и сохранения данных. Операции получения и сохранения данных оставляются для моделей-ресурсов, как более низкоуровневые — тогда, в случае изменения способа хранения данных (БД, файлы и т.д...), не требуется изменять логику обработки, а потребуется лишь изменить модель-ресурс, занимающейся получением и сохранением данных. Переделаем вывод новостей с использованием моделей, а также добавим вывод содержимого новости по айди:

  1. Добавить узел class в узел моделей в файле конфигурации:
    <?xml version="1.0" ?>
    <config>
        <modules>
            ...
        </modules>
        <frontend>
            ...
        </frontend>
        <global>
            <models>
                <dsnews>
                    <class>DS_News_Model</class>
                    <resourceModel>dsnews_resource</resourceModel>
                </dsnews>
                <dsnews_resource>
                    <class>DS_News_Model_Resource</class>
                    <entities>
                        <table_news>
                            <table>ds_news_entities</table>
                        </table_news>
                    </entities>
                </dsnews_resource>
            </models>
            <resources>
                ...
            </resources>
        </global>
    </config>
    

  2. Создать файл модели новостей Model/News.php
    <?php
    
    class DS_News_Model_News extends Mage_Core_Model_Abstract
    {
    
        public function _construct()
        {
            parent::_construct();
            $this->_init('dsnews/news');
        }
    
    }
    

  3. Создать файл ресурса модели новостей Model/Resource/News.php
    <?php
    
    class DS_News_Model_Resource_News extends Mage_Core_Model_Mysql4_Abstract
    {
    
        public function _construct()
        {
            $this->_init('dsnews/table_news', 'news_id');
        }
    
    }
    

  4. Создать файл ресурса коллекции Model/Resource/News/Collection.php
    <?php
    
    class DS_News_Model_Resource_News_Collection extends Mage_Core_Model_Mysql4_Collection_Abstract
    {
    
        public function _construct()
        {
            parent::_construct();
            $this->_init('dsnews/news');
        }
    
    }
    

  5. Изменить контроллер controllers/IndexController.php
    <?php
    
    class DS_News_IndexController extends Mage_Core_Controller_Front_Action
    {
    
        public function indexAction()
        {
            $news = Mage::getModel('dsnews/news')->getCollection()->setOrder('created', 'DESC');
            $viewUrl = Mage::getUrl('news/index/view');
    
            echo '<h1>News</h1>';
            foreach ($news as $item) {
                echo '<h2><a href="' . $viewUrl . '?id=' . $item->getId() . '">' . $item->getTitle() . '</a></h2>';
            }
        }
    
        public function viewAction()
        {
            $newsId = Mage::app()->getRequest()->getParam('id', 0);
            $news = Mage::getModel('dsnews/news')->load($newsId);
    
            if ($news->getId() > 0) {
                echo '<h1>' . $news->getTitle() . '</h1>';
                echo '<div class="content">' . $news->getContent() . '</div>';
            } else {
                $this->_forward('noRoute');
            }
        }
    
    }
    

Теперь по ссылке http://site.com/news откроется список новостей в виде ссылок, при клике на которые будет открываться страница с содержимым новости.

В пункте 1 в файл кофигурации добавляется узел class, в котором прописываются базовые префиксы классов для модели DS_News_Model и ресурса DS_News_Model_Resource.

При запросе модели в контроллере (пункт 5) Mage::getModel('dsnews/news'), функция getModel принимает строку типа [model]/[class], из которой формируется название класса модели, где [model] — это название узла config/global/models/[model], из которой берётся значение узла classDS_News_Model, и к этому префиксу класса добавляется значение [class] (первая буква в каждом слове [class] преобразуется в заглавную). К примеру, из строки dsnews/news получается класс DS_News_Model_News, а из строки dsnews/news_gallery получится класс DS_News_Model_News_Gallery.

В пункте 2 создаётся базовая модель новости DS_News_Model_News, в конструкторе которого происходит инициализация ресурса $this->_init('dsnews/news'): в качестве параметров функция принимает строку [model]/[class], где [model] — это название узла config/global/models/[model], а [class] — это название класса. Однако, в отличие от модели, инициализация класса ресурса в качестве префикса класса использует значение узла config/global/models/[resourceModel]/class ресурса, на который ссылается модель в узле resourceModel. В итоге при инициализации ресурса $this->_init('dsnews/news') будет инициализироваться класс DS_News_Model_Resource_News.

В пункте 3 создаётся класс ресурса DS_News_Model_Resource_News, в котором происходит инициализация таблицы $this->_init('dsnews/table_news', 'news_id'): первым параметром является путь к названию нужной таблицы, а вторым — поле, использующееся в качестве первичного ключа (PRIMARY KEY) таблицы.

В пункте 4 происходит инициализация класса коллекции объектов, в конструкторе которой происходит инициализация исходной модели DS_News_Model_News.

В пункте 5 происходит изменение получения данных из базы. На этот раз используются модели и коллекции. В действии indexAction происходит запрос всех новостей с помощью получения коллекции $news = Mage::getModel('dsnews/news')->getCollection(). Название класса коллекции вычисляется из названия класса ресурса модели DS_News_Model_Resource_News + _Collection.

В действии viewAction происходит загрузка новости по id, полученному в запросе. Если новость с данным айди существует, то выведется название новости и содержимое. В противном случае будет сгенерирована страница 404.

Шаг 8. Создание и использование блоков

Для вывода данных в Magento используется специальные блоки — это объекты кода, отвечающие за рендеринг того или иного участка кода. На шаге 5: Использование шаблона для вывода данных, был использован стандартный блок core/template. На этом шаге для модуля новостей будут созданы блоки для вывода списка и содержимого новости.

  1. Добавить узел блоков config/global/blocks
    <?xml version="1.0" ?>
    <config>
        ...
        <global>
            <blocks>
                <dsnews>
                    <class>DS_News_Block</class>
                </dsnews>
            </blocks>
            ...
        </global>
    </config>
    

  2. Создать класс блока списка новостей Block/News.php
    <?php
    
    class DS_News_Block_News extends Mage_Core_Block_Template
    {
    
        public function getNewsCollection()
        {
            $newsCollection = Mage::getModel('dsnews/news')->getCollection();
            $newsCollection->setOrder('created', 'DESC');
            return $newsCollection;
        }
    
    }
    

  3. Создать класс блока содержимого новости Block/View.php
    <?php
    
    class DS_News_Block_View extends Mage_Core_Block_Template
    {
    
    }
    

  4. Обновить файл конфигурации /app/design/frontend/[package]/[theme]/layout/ds_news.xml
    <layout>
        <dsnews_index_index>
            <reference name="content">
                <block type="dsnews/news" template="ds_news/index.phtml" />
            </reference>
        </dsnews_index_index>
        <dsnews_index_view>
            <reference name="content">
                <block type="dsnews/view" name="news.content" template="ds_news/view.phtml" />
            </reference>
        </dsnews_index_view>
    </layout>
    

  5. Изменить файл шаблона /app/design/frontend/[package]/[theme]/template/ds_news/index.phtml
    <?php
    $news = $this->getNewsCollection();
    $newsViewUrl = Mage::getUrl('news/index/view');
    ?>
    <h1>News</h1>
    <?php foreach ($news as $item): ?>
        <h2>
            <a href="<?php echo $newsViewUrl; ?>?id=<?php echo $item->getId(); ?>">
                <?php echo $item->getTitle(); ?>
            </a>
        </h2>
    <?php endforeach; ?>
    

  6. Создать файл шаблона /app/design/frontend/[package]/[theme]/template/ds_news/view.phtml
    <h1><?php echo $newsItem->getTitle(); ?></h1>
    <div class="content"><?php echo $newsItem->getContent(); ?></div>
    

  7. Изменить контролллер controllers/IndexController.php
    <?php
    
    class DS_News_IndexController extends Mage_Core_Controller_Front_Action
    {
    
        public function indexAction()
        {
            $this->loadLayout();
            $this->renderLayout();
        }
    
        public function viewAction()
        {
            $newsId = Mage::app()->getRequest()->getParam('id', 0);
            $news = Mage::getModel('dsnews/news')->load($newsId);
    
            if ($news->getId() > 0) {
                $this->loadLayout();
                $this->getLayout()->getBlock('news.content')->assign(array(
                    "newsItem" => $news,
                ));
                $this->renderLayout();
            } else {
                $this->_forward('noRoute');
            }
        }
    
    }
    

При открытии страницы http://site.com/news/ теперь будет выводиться список новостей как содержимое страницы сайта, а переход по ссылке новости откроет страницу с содержимым новости.

Механизм блоков позволяет использовать логику отображения вне зависимости от контроллера. Как видно в пункте 3, действие indexAction теперь занимается только загрузкой и отображением шаблонов. Логика получения новостей теперь лежит на блоке DS_News_Block_News, а логика отображения — на шаблоне /app/design/frontend/[package]/[theme]/template/dsnews/index.phtml. Таким образом, к примеру, если список последних новостей должен показываться в сайдбаре какой-либо другой страницы, то достаточно будет подключить нужный блок к странице без задействования контроллера.

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

В пункте 1 в конфигурацию был добавлен узел блоков модуля новостей для того, чтобы при выводе шаблонов Magento знала к какому классу блока обращаться. Классы блоков, используемые для рендеринга шаблонов указываются как тип (type) блока в .xml файле. Аргумент type блока принимает строку вида [module]/[block], где [module] — это название узла config/global/blocks/[module], из которого берётся значение узла class для формирования названия класса блока; a [block] — название класса. В результате type=«dsnews/news» будет преобразован в класс блока DS_News_Block_News.

В пункте 4 произшло добавление в содержимое страницы <reference name=«content» были добавлены созданные блоки. Для того, чтобы к блокам можно было обращаться, каждый блок должен иметь уникальное имя в макете текущей страницы. Для отображения содержимого новости был добавлен новый layout handle dsnews_index_view, внутри которого был добавлен блок <block type=«dsnews/view» name=«news.content» template=«ds_news/view.phtml» />. Внутри контроллера в действии viewAction после загрузки макета страницы происходит обращение к данному блоку и назначение текущей новости через функцию assign, в которую передаётся ассоциативный массив с данными. При генерации шаблона к ключам данного массива можно обращаться как к переменным.

Шаг 9. Создание админки для модуля

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

  1. Создать контроллер controllers/Adminhtml/NewsController.php
    <?php
    
    class DS_News_Adminhtml_NewsController extends Mage_Adminhtml_Controller_Action
    {
    
        public function indexAction()
        {
            echo '<h1>News Module: Admin section</h1>';
        }
    
    }
    

  2. Добавить путь до контроллера в файле etc/config.xml
    <?xml version="1.0" ?>
    <config>
        ...
        <admin>
            <routers>
                <dsnews_admin>
                    <use>admin</use>
                    <args>
                        <module>DS_News</module>
                        <frontName>dsnews_admin</frontName>
                    </args>
                </dsnews_admin>
            </routers>
        </admin>
        ...
    </config>
    

  3. Добавить пункт меню для модуля в админке, изменив файл etc/config.xml
    <?xml version="1.0" ?>
    <config>
        ...
        <adminhtml>
            <menu>
                <dsnews module="dsnews">
                    <title>News</title>
                    <sort_order>77</sort_order>
                    <action>dsnews_admin/adminhtml_news</action>
                </dsnews>
            </menu>
        </adminhtml>
        ...
    </config>
    

  4. Добавить хелпер-заглушку Helper/Data.php
    <?php
    
    class DS_News_Helper_Data extends Mage_Core_Helper_Abstract
    {
    
    }
    

  5. Добавить секцию хелпера в файл конфигурации etc/config.xml
    <?xml version="1.0" ?>
    <config>
        ...
        <global>
            ...
            <helpers>
                <dsnews>
                    <class>DS_News_Helper</class>
                </dsnews>
            </helpers>
            ...
        </global>
        ...
    </config>
    

Теперь, залогинившись в админке, можно увидеть новый пункт меню News, перейдя по которому можно увидеть надпись «News Module: Admin section».

В пункте 1 создаётся контроллер для админки модуля новостей, который регистрируется в пункте 2 — в роутерах config/admin/routers/[router]. После регистрации в админке, контроллер будет доступен по пути http://site.com/index.php/[frontName]/[controller]/index/, где [frontName] — это значение узла config/admin/routers/[router]/args/[frontName], а [controller] — это путь, из которого генерируется класс контроллера из класса модуля DS_News + [controller] + Controller. В результате данной схемы из значения adminhtml_news получится класс контроллера DS_News_Adminhtml_NewsController. Таким образом админка модуля новостей будет доступна по адресу http://site.com/index.php/dsnews_admin/adminhtml_news/index/.

Однако при попытке прямого доступа к данной странице, Magento откажется открывать контроллер модуля из-за политики безопасности. Любой адрес страницы в админке должен содержать секретный ключ, генерируемый отдельно для каждой страницы админки. По этой причине для доступа к админке в пункте 3 создаётся ссылка в меню, которая будет содержащать адрес с нужными параметрами.

Если пропустить пункты 4 и 5, то при попытке зайти на страницу админки, будет сгенерирована ошибка типа «Warning: include(MageDSNewsHelperData.php): failed to open stream: No such file or directory...». Для формирования меню Magento использует хелпер модуля, указанного в качестве параметра module в узле config/adminhtml/menu/[menu]. В качестве значения параметра задаётся название узла модуля хелпера config/global/helpers/[helper], из которого формируется название класса хелпера. По этой причине в пункте 4 и 5 создаётся хелпер-заглушка, который регистрируется в файле конфигурации по необходимому пути.

Ещё один момент, на который стоит обратить внимание — это отличие названий роутеров и frontName в секции config/admin/routers от таких же в секции config/frontend/routers. При совпадении данных значений
(а подобное искушение будет), возможны проблемы с сохранением сообщений в сессии, а также проблемы с редиректами модуля при включении HTTPS на сайте.

Шаг 10. Использование блоков в админке

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

  1. Изменить контроллер controllers/Adminhtml/NewsController.php
    <?php
    
    class DS_News_Adminhtml_NewsController extends Mage_Adminhtml_Controller_Action
    {
    
        public function indexAction()
        {
            $this->loadLayout();
            $this->_setActiveMenu('dsnews');
    
            $contentBlock = $this->getLayout()->createBlock('dsnews/adminhtml_news');
            $this->_addContent($contentBlock);
            $this->renderLayout();
        }
    
    }
    

  2. Создать блок Block/Adminhtml/News.php
    <?php
    
    class DS_News_Block_Adminhtml_News extends Mage_Adminhtml_Block_Abstract
    {
    
        public function _toHtml()
        {
            return '<h1>News Module: Admin section</h1>';
        }
    
    }
    

Magento для генерации любой страницы использует шаблоны, как в frontend (на сайте), так и в backend (админке) части. Можно воспользоваться .xml файлом макета для добавления/изменения блоков на странице (как это сделано на шаге 5: «Использование шаблона для вывода данных»), однако в большинстве случаев возможностей, предоставляемых админ-классами блоков Magento, вполне достаточно. А в редких случаях можно формировать HTML «на лету» прямо в классе своего блока. К тому же, шаблоны для админки и файлы конфигурации модуля будут находиться в директории /app/design/adminhtml/default/default/, что не есть хорошо, т.к. приводит к излишнему разделению кода модуля по разным директориям.

В пункте 1 происходит инициализация макета страницы админки $this->loadLayout() и выставление текущего активного пункта меню. После инициализации макета можно обращаться к уже сформированному макету с помощью функции $this->getLayout(): добавлять/удалять/изменять блоки и выполнять другие программные действия с содержимым страницы. Функция создания блока createBlock в качестве первого параметра принимает строку блока [module]/[block] такого же типа, как и в макете <block type="[module]/[block]" для формирования
класса блока; после чего созданный блок добавляется в макет в качестве содержимого страницы.

Шаг 11. Вывод списка в Data Grid

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

  1. Изменить блок Block/Adminhtml/News.php
    <?php
    
    class DS_News_Block_Adminhtml_News extends Mage_Adminhtml_Block_Widget_Grid_Container
    {
    
        protected function _construct()
        {
            parent::_construct();
    
            $helper = Mage::helper('dsnews');
            $this->_blockGroup = 'dsnews';
            $this->_controller = 'adminhtml_news';
    
            $this->_headerText = $helper->__('News Management');
            $this->_addButtonLabel = $helper->__('Add News');
        }
    
    }
    

  2. Создать блок грида Block/Adminhtml/News/Grid.php
    <?php
    
    class DS_News_Block_Adminhtml_News_Grid extends Mage_Adminhtml_Block_Widget_Grid
    {
    
        protected function _prepareCollection()
        {
            $collection = Mage::getModel('dsnews/news')->getCollection();
            $this->setCollection($collection);
            return parent::_prepareCollection();
        }
    
        protected function _prepareColumns()
        {
    
            $helper = Mage::helper('dsnews');
    
            $this->addColumn('news_id', array(
                'header' => $helper->__('News ID'),
                'index' => 'news_id'
            ));
    
            $this->addColumn('title', array(
                'header' => $helper->__('Title'),
                'index' => 'title',
                'type' => 'text',
            ));
    
            $this->addColumn('created', array(
                'header' => $helper->__('Created'),
                'index' => 'created',
                'type' => 'date',
            ));
    
            return parent::_prepareColumns();
        }
    
    }
    

Название класса блока грида формируется из значений, заданных в пункте 1 в блоке DS_News_Block_Adminhtml_News: [_blockGroup]/[_controller]_grid, где _blockGroup — это название узла блоков модуля config/global/blocks/[_blockGroup]. В результате получится строка типа блока «dsnews/adminhtml_news_grid».

В функции _prepareColumns добавляются колонки, которые будут показываться в гриде. Функция addColumn первым параметром принимает название название колонки, а вторым параметром — массив с данными о колонке, где index — название колонки в базе данных, а type — тип колонки. Список поддерживаемых типов и опций можно увидеть в классе Mage_Adminhtml_Block_Widget_Grid_Column.

Шаг 12. Массовые операции в Data Grid

Теперь добавим возможность удаления сразу нескольких новостей

  1. Добавить в блок грида Block/Adminhtml/News/Grid.php метод _prepareMassaction
    <?php
    
    class DS_News_Block_Adminhtml_News_Grid extends Mage_Adminhtml_Block_Widget_Grid
    {
    
        protected function _prepareCollection(){ ... }
    
        protected function _prepareColumns(){ ... }
    
        protected function _prepareMassaction()
        {
            $this->setMassactionIdField('news_id');
            $this->getMassactionBlock()->setFormFieldName('news');
    
            $this->getMassactionBlock()->addItem('delete', array(
                'label' => $this->__('Delete'),
                'url' => $this->getUrl('*/*/massDelete'),
            ));
            return $this;
        }
    
    }
    

  2. Добавить метод massDeleteAction в контроллер controllers/Adminhtml/NewsController.php
    <?php
    
    class DS_News_Adminhtml_NewsController extends Mage_Adminhtml_Controller_Action
    {
    
        public function indexAction(){ ... }
    
        public function massDeleteAction()
        {
            $news = $this->getRequest()->getParam('news', null);
    
            if (is_array($news) && sizeof($news) > 0) {
                try {
                    foreach ($news as $id) {
                        Mage::getModel('dsnews/news')->setId($id)->delete();
                    }
                    $this->_getSession()->addSuccess($this->__('Total of %d news have been deleted', sizeof($news)));
                } catch (Exception $e) {
                    $this->_getSession()->addError($e->getMessage());
                }
            } else {
                $this->_getSession()->addError($this->__('Please select news'));
            }
            $this->_redirect('*/*');
        }
    
    }
    
    

Теперь в списке новостей первой колонкой должны показываться чекбоксы. Можно выделить сразу несколько новостей, выбрать в правом верхнем углу списка Actions опцию Delete и нажать кнопку Submit для удаления выбранных новостей.

В пункте 1 классу грида был добавлен метод _prepareMassaction, в котором устанавливается id-поле news_id, а также название параметра, которое будет использоваться для получения массива id-полей. Далее происходит добавление опций в список массовых операций — айди операции, название и ссылка, куда отправлять выбранные записи. Можно добавлять также зависимые селекты: в зависимости от выбранной операции будет показываться второй селект. Например, обновление статуса в списке продуктов. Пример зависимых селектов в массовых операциях можно посмотреть в классе грида Mage_Adminhtml_Block_Catalog_Product_Grid.

Шаг 13. CRUD: добавление, редактирование и удаление

Реализуем добавление, редактирование и удаление записей

  1. Изменить контроллер controllers/Adminhtml/NewsController.php
    <?php
    
    class DS_News_Adminhtml_NewsController extends Mage_Adminhtml_Controller_Action
    {
    
        public function indexAction()
        {
            $this->loadLayout()->_setActiveMenu('dsnews');
            $this->_addContent($this->getLayout()->createBlock('dsnews/adminhtml_news'));
            $this->renderLayout();
        }
    
        public function newAction()
        {
            $this->_forward('edit');
        }
    
        public function editAction()
        {
            $id = (int) $this->getRequest()->getParam('id');
            Mage::register('current_news', Mage::getModel('dsnews/news')->load($id));
    
            $this->loadLayout()->_setActiveMenu('dsnews');
            $this->_addContent($this->getLayout()->createBlock('dsnews/adminhtml_news_edit'));
            $this->renderLayout();
        }
    
        public function saveAction()
        {
            if ($data = $this->getRequest()->getPost()) {
                try {
                    $model = Mage::getModel('dsnews/news');
                    $model->setData($data)->setId($this->getRequest()->getParam('id'));
                    if(!$model->getCreated()){
                        $model->setCreated(now());
                    }
                    $model->save();
    
                    Mage::getSingleton('adminhtml/session')->addSuccess($this->__('News was saved successfully'));
                    Mage::getSingleton('adminhtml/session')->setFormData(false);
                    $this->_redirect('*/*/');
                } catch (Exception $e) {
                    Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
                    Mage::getSingleton('adminhtml/session')->setFormData($data);
                    $this->_redirect('*/*/edit', array(
                        'id' => $this->getRequest()->getParam('id')
                    ));
                }
                return;
            }
            Mage::getSingleton('adminhtml/session')->addError($this->__('Unable to find item to save'));
            $this->_redirect('*/*/');
        }
    
        public function deleteAction()
        {
            if ($id = $this->getRequest()->getParam('id')) {
                try {
                    Mage::getModel('dsnews/news')->setId($id)->delete();
                    Mage::getSingleton('adminhtml/session')->addSuccess($this->__('News was deleted successfully'));
                } catch (Exception $e) {
                    Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
                    $this->_redirect('*/*/edit', array('id' => $id));
                }
            }
            $this->_redirect('*/*/');
        }
    
        public function massDeleteAction(){ ... }
    
    }
    

  2. Добавить метод getRowUrl в блок грида Block/Adminhtml/News/Grid.php
    <?php
    
    class DS_News_Block_Adminhtml_News_Grid extends Mage_Adminhtml_Block_Widget_Grid
    {
    
        protected function _prepareCollection(){ ... }
    
        protected function _prepareColumns(){ ... }
    
        protected function _prepareMassaction(){ ... }
    
        public function getRowUrl($model)
        {
            return $this->getUrl('*/*/edit', array(
                        'id' => $model->getId(),
                    ));
        }
    
    }
    

  3. Создать блок для редактирования новости Block/Adminhtml/News/Edit.php
    <?php
    
    class DS_News_Block_Adminhtml_News_Edit extends Mage_Adminhtml_Block_Widget_Form_Container
    {
    
        protected function _construct()
        {
            $this->_blockGroup = 'dsnews';
            $this->_controller = 'adminhtml_news';
        }
    
        public function getHeaderText()
        {
            $helper = Mage::helper('dsnews');
            $model = Mage::registry('current_news');
    
            if ($model->getId()) {
                return $helper->__("Edit News item '%s'", $this->escapeHtml($model->getTitle()));
            } else {
                return $helper->__("Add News item");
            }
        }
    
    }
    

  4. Создать блок для вывода формы Block/Adminhtml/News/Edit/Form.php
    <?php
    
    class DS_News_Block_Adminhtml_News_Edit_Form extends Mage_Adminhtml_Block_Widget_Form
    {
    
        protected function _prepareForm()
        {
            $helper = Mage::helper('dsnews');
            $model = Mage::registry('current_news');
    
            $form = new Varien_Data_Form(array(
                        'id' => 'edit_form',
                        'action' => $this->getUrl('*/*/save', array(
                            'id' => $this->getRequest()->getParam('id')
                        )),
                        'method' => 'post',
                        'enctype' => 'multipart/form-data'
                    ));
    
            $this->setForm($form);
    
            $fieldset = $form->addFieldset('news_form', array('legend' => $helper->__('News Information')));
    
            $fieldset->addField('title', 'text', array(
                'label' => $helper->__('Title'),
                'required' => true,
                'name' => 'title',
            ));
    
            $fieldset->addField('content', 'editor', array(
                'label' => $helper->__('Content'),
                'required' => true,
                'name' => 'content',
            ));
    
            $fieldset->addField('created', 'date', array(
                'format' => Mage::app()->getLocale()->getDateFormat(Mage_Core_Model_Locale::FORMAT_TYPE_SHORT),
                'image' => $this->getSkinUrl('images/grid-cal.gif'),
                'label' => $helper->__('Created'),
                'name' => 'created'
            ));
    
            $form->setUseContainer(true);
    
            if($data = Mage::getSingleton('adminhtml/session')->getFormData()){
                $form->setValues($data);
            } else {
                $form->setValues($model->getData());
            }
    
            return parent::_prepareForm();
        }
    
    }
    

Теперь при клике на новость в Data Grid откроется форма редактирования, а при клике на кнопку Add News — форма добавления записи.

В пункте 1 в контроллер были добавлены методы new, edit, save и delete, отвечающие за соответствующие действия. Так как действие new по большей части повторяет действие edit, то в действии new просто происходит редирект на действие edit. Внутри действия инициализируется модель новости, и добавляется в глобальный реестр (это один из тех случаев, когда использование глобального реестра меньшее из зол), после чего в содержимое добавляется блок редактирования. Стоит обратить внимание в действии save на
строку $model->setData($data)->setId(...) — при сохранении у модели добавление айди происходит после инициализации данных, т.к. если вначале добавить айди, то при вызове setData айди будет перезатёрт. В итоге при сохранении вместо редактирования текущей записи, будет создана новая.

В пункте 2 добавляется метод инициализации ссылки для каждой из строк Data Grid — реакция на клик пользователя по строке. В пункте 3 инициализируется блок редактирования новости, в конструкторе которого происходит инициализация переменных формы, которые будут использоваться для построения класса самой формы по схеме [_blockGroup]/[_controller]_[_mode]_form, в результате получится
dsnews/adminhtml_news_edit_form (_mode по умолчанию имеет значение edit).

В последнем пункте происходит непосредственно инициализация самой формы и добавление нужных полей. Следует обратить внимание, что при создании формы new Varien_Data_Form в качестве параметра id должен указываться уникальный HTML id формы на странице, т.к. отправка и изменение формы происходит JavaScript-ом, который обращается по данному айди. При добавлении полей на форму нужно, чтобы значение name совпадало с названиями полей в базе данных.

Шаг 14. Использование вкладок (tabs)

При редактировании модели можно разбить форму на несколько частей, и показывать их отдельно в разных TAB-ах.

  1. Изменить контроллер controllers/Adminhtml/NewsController.php
    <?php
    
    class DS_News_Adminhtml_NewsController extends Mage_Adminhtml_Controller_Action
    {
    
        public function indexAction(){ ... }
    
        public function newAction(){ ... }
    
        public function editAction()
        {
            $id = (int) $this->getRequest()->getParam('id');
            $model = Mage::getModel('dsnews/news');
    
            if($data = Mage::getSingleton('adminhtml/session')->getFormData()){
                $model->setData($data)->setId($id);
            } else {
                $model->load($id);
            }
            Mage::register('current_news', $model);
    
            $this->loadLayout()->_setActiveMenu('dsnews');
            $this->_addLeft($this->getLayout()->createBlock('dsnews/adminhtml_news_edit_tabs'));
            $this->_addContent($this->getLayout()->createBlock('dsnews/adminhtml_news_edit'));
            $this->renderLayout();
        }
    
        public function saveAction(){ ... }
    
        public function deleteAction() { ... }
    
        public function massDeleteAction(){ ... }
    
    }
    

  2. Создать блок с вкладками Block/Adminhtml/News/Edit/Tabs.php
    <?php
    
    class DS_News_Block_Adminhtml_News_Edit_Tabs extends Mage_Adminhtml_Block_Widget_Tabs
    {
    
        public function __construct()
        {
            $helper = Mage::helper('dsnews');
    
            parent::__construct();
            $this->setId('news_tabs');
            $this->setDestElementId('edit_form');
            $this->setTitle($helper->__('News Information'));
        }
    
        protected function _prepareLayout()
        {
            $helper = Mage::helper('dsnews');
    
            $this->addTab('general_section', array(
                'label' => $helper->__('General Information'),
                'title' => $helper->__('General Information'),
                'content' => $this->getLayout()->createBlock('dsnews/adminhtml_news_edit_tabs_general')->toHtml(),
            ));
            $this->addTab('custom_section', array(
                'label' => $helper->__('Custom Fields'),
                'title' => $helper->__('Custom Fields'),
                'content' => $this->getLayout()->createBlock('dsnews/adminhtml_news_edit_tabs_custom')->toHtml(),
            ));
            return parent::_prepareLayout();
        }
    
    }
    

  3. Изменить блок формы Block/Adminhtml/News/Edit/Form.php
    <?php
    
    class DS_News_Block_Adminhtml_News_Edit_Form extends Mage_Adminhtml_Block_Widget_Form
    {
    
        protected function _prepareForm()
        {
            $form = new Varien_Data_Form(array(
                        'id' => 'edit_form',
                        'action' => $this->getUrl('*/*/save', array(
                            'id' => $this->getRequest()->getParam('id')
                        )),
                        'method' => 'post',
                        'enctype' => 'multipart/form-data'
                    ));
    
            $form->setUseContainer(true);
            $this->setForm($form);
    
            return parent::_prepareForm();
        }
    
    }
    

  4. Добавить блок вкладки General Block/Adminhtml/News/Edit/Tabs/General.php
    <?php
    
    class DS_News_Block_Adminhtml_News_Edit_Tabs_General extends Mage_Adminhtml_Block_Widget_Form
    {
    
        protected function _prepareForm()
        {
    
            $helper = Mage::helper('dsnews');
            $model = Mage::registry('current_news');
    
    
            $form = new Varien_Data_Form();
            $fieldset = $form->addFieldset('general_form', array(
                        'legend' => $helper->__('General Information')
                    ));
    
            $fieldset->addField('title', 'text', array(
                'label' => $helper->__('Title'),
                'required' => true,
                'name' => 'title',
            ));
    
            $fieldset->addField('content', 'editor', array(
                'label' => $helper->__('Content'),
                'required' => true,
                'name' => 'content',
            ));
    
            $fieldset->addField('created', 'date', array(
                'format' => Mage::app()->getLocale()->getDateFormat(Mage_Core_Model_Locale::FORMAT_TYPE_SHORT),
                'image' => $this->getSkinUrl('images/grid-cal.gif'),
                'label' => $helper->__('Created'),
                'name' => 'created'
            ));
    
            $form->setValues($model->getData());
            $this->setForm($form);
    
            return parent::_prepareForm();
        }
    
    }
    

  5. Добавить блок вкладки Custom
    Block/Adminhtml/News/Edit/Tabs/Custom.php

    <?php
    
    class DS_News_Block_Adminhtml_News_Edit_Tabs_Custom extends Mage_Adminhtml_Block_Widget
    {
    
        protected function _toHtml()
        {
            return '<h2>Custom Fields</h2>';
        }
    
    }
    

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

Можно обратить внимание, что теперь инициализация значений формы вынесена в контроллер, там же добавляется блок вкладок. В пункте 2 создаётся блок-контейнер для вкладок, внутри которого происходит инициализация вкладок в методе _prepareLayout. Инициализация полей и их значений теперь вынесена из основной формы в форму вкладки. Не смотря на то, что во вкладке General происходит создание новой формы $form = new Varien_Data_Form(), при генерации страницы будет сгенерирована только один элемент формы, где вкладки — это просто контейнеры элементов.

Шаг 15. Добавление поля загрузки изображения

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

  1. Добавить поле изображения в блок General Block/Adminhtml/News/Edit/Tabs/General.php
    <?php
    
    class DS_News_Block_Adminhtml_News_Edit_Tabs_General extends Mage_Adminhtml_Block_Widget_Form
    {
    
        protected function _prepareForm()
        {
    
            $helper = Mage::helper('dsnews');
            $model = Mage::registry('current_news');
    
    
            $form = new Varien_Data_Form();
            $fieldset = $form->addFieldset('general_form', array('legend' => $helper->__('General Information')));
    
            $fieldset->addField('title', 'text', array(
                'label' => $helper->__('Title'),
                'required' => true,
                'name' => 'title',
            ));
    
            $fieldset->addField('content', 'editor', array(
                'label' => $helper->__('Content'),
                'required' => true,
                'name' => 'content',
            ));
    
            $fieldset->addField('image', 'image', array(
                'label' => $helper->__('Image'),
                'name' => 'image',
            ));
    
            $fieldset->addField('created', 'date', array(
                'format' => Mage::app()->getLocale()->getDateFormat(Mage_Core_Model_Locale::FORMAT_TYPE_SHORT),
                'image' => $this->getSkinUrl('images/grid-cal.gif'),
                'label' => $helper->__('Created'),
                'name' => 'created'
            ));
    
            $formData = array_merge($model->getData(), array('image' => $model->getImageUrl()));
            $form->setValues($formData);
            $this->setForm($form);
    
            return parent::_prepareForm();
        }
    
    }
    

  2. Добавить дополнительные функции в хелпер Helper/Data.php
    <?php
    
    class DS_News_Helper_Data extends Mage_Core_Helper_Abstract
    {
    
        public function getImagePath($id = 0)
        {
            $path = Mage::getBaseDir('media') . '/ds_news';
            if ($id) {
                return "{$path}/{$id}.jpg";
            } else {
                return $path;
            }
        }
    
        public function getImageUrl($id = 0)
        {
            $url = Mage::getBaseUrl(Mage_Core_Model_Store::URL_TYPE_MEDIA) . 'ds_news/';
            if ($id) {
                return $url . $id . '.jpg';
            } else {
                return $url;
            }
        }
    
    }
    

  3. Добавить функции к модели Model/News.php
    <?php
    
    class DS_News_Model_News extends Mage_Core_Model_Abstract
    {
    
        protected function _construct()
        {
            parent::_construct();
            $this->_init('dsnews/news');
        }
    
        protected function _afterDelete()
        {
            $helper = Mage::helper('dsnews');
            @unlink($helper->getImagePath($this->getId()));
            return parent::_afterDelete();
        }
    
        public function getImageUrl()
        {
            $helper = Mage::helper('dsnews');
            if ($this->getId() && file_exists($helper->getImagePath($this->getId()))) {
                return $helper->getImageUrl();
            }
            return null;
        }
    
    }
    

  4. Изменить контроллер controllers/Adminhtml/NewsController.php
    <?php
    
    class DS_News_Adminhtml_NewsController extends Mage_Adminhtml_Controller_Action
    {
    
        public function indexAction(){ ... }
    
        public function newAction(){ ... }
    
        public function editAction(){ ... }
    
        public function saveAction()
        {
            $id = $this->getRequest()->getParam('id');
            if ($data = $this->getRequest()->getPost()) {
                try {
                    $helper = Mage::helper('dsnews');
                    $model = Mage::getModel('dsnews/news');
    
                    $model->setData($data)->setId($id);
                    if (!$model->getCreated()) {
                        $model->setCreated(now());
                    }
                    $model->save();
                    $id = $model->getId();
    
                    if (isset($_FILES['image']['name']) && $_FILES['image']['name'] != '') {
                        $uploader = new Varien_File_Uploader('image');
                        $uploader->setAllowedExtensions(array('jpg', 'jpeg'));
                        $uploader->setAllowRenameFiles(false);
                        $uploader->setFilesDispersion(false);
                        $uploader->save($helper->getImagePath(), $id . '.jpg'); // Upload the image
                    } else {
                        if (isset($data['image']['delete']) && $data['image']['delete'] == 1) {
                            @unlink($helper->getImagePath($id));
                        }
                    }
    
                    Mage::getSingleton('adminhtml/session')->addSuccess($this->__('News was saved successfully'));
                    Mage::getSingleton('adminhtml/session')->setFormData(false);
                    $this->_redirect('*/*/');
                } catch (Exception $e) {
                    Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
                    Mage::getSingleton('adminhtml/session')->setFormData($data);
                    $this->_redirect('*/*/edit', array(
                        'id' => $id
                    ));
                }
                return;
            }
            Mage::getSingleton('adminhtml/session')->addError($this->__('Unable to find item to save'));
            $this->_redirect('*/*/');
        }
    
        public function deleteAction(){ ... }
    
        public function massDeleteAction(){ ... }
    
    }
    

К каждой новости можно прикрепить только одно изображение в формате JPG, поэтому нет необходимости изменять структуру таблицы новостей в базе данных. Изображение будет храниться в директории /media/ds_news, в качестве имени файла будет выступать айди изображения, что обеспечит уникальность названия в файловой системе. В модель были добавлены две функции — одна для получения URL изображения, если изображение имеется, а вторая — удаление изображения при удалении новости. В форму было добавлено поле загрузки изображения, а ниже изменено назначение данных для формы — в массив данных было добавлено поле image: $formData = array_merge($model->getData(), array('image' => $model->getImageUrl())).

Шаг 16. Подключение JavaScript/CSS файлов в админке

Теперь подключим необохдимые скрипты и стили в админке

  1. Создать файл /skin/adminhtml/default/default/ds_news/adminhtml/scripts.js
    console.log('DS News admin');
    

  2. Создать файл /skin/adminhtml/default/default/ds_news/adminhtml/styles.css
    #general_form label {
        color: #FF0000;
        font-weight: bold;
    }
    

  3. Подключить скрипты и стили в контроллере controllers/Adminhtml/NewsController.php
    <?php
    
    class DS_News_Adminhtml_NewsController extends Mage_Adminhtml_Controller_Action
    {
    
        public function indexAction(){ ... }
    
        public function newAction(){ ... }
    
        public function editAction()
        {
            $id = (int) $this->getRequest()->getParam('id');
            $model = Mage::getModel('dsnews/news');
    
            if ($data = Mage::getSingleton('adminhtml/session')->getFormData()) {
                $model->setData($data)->setId($id);
            } else {
                $model->load($id);
            }
            Mage::register('current_news', $model);
    
            $this->loadLayout()->_setActiveMenu('dsnews');
    
            $this->getLayout()->getBlock('head')->addItem('skin_js', 'ds_news/adminhtml/scripts.js');
            $this->getLayout()->getBlock('head')->addItem('skin_css', 'ds_news/adminhtml/styles.css');
    
            $this->_addLeft($this->getLayout()->createBlock('dsnews/adminhtml_news_edit_tabs'));
            $this->_addContent($this->getLayout()->createBlock('dsnews/adminhtml_news_edit'));
            $this->renderLayout();
        }
    
        public function saveAction(){ ... }
    
        public function deleteAction(){ ... }
    
        public function massDeleteAction(){ ... }
    
    }
    

В результате при открытии формы редактирования новости в консоли отладки браузера будет отображена надпись DS News admin, а шрифт названия полей станет жирным и красным.

По умолчанию скрипты и стили, как на сайте, так и в админке, можно подгружать из двух мест: либо директория JS, либо директория текущей темы. Где хранить скрипты и стили — в текущей теме админки /skin/adminhtml/default/default/ или в директории /js/ — дело вкуса каждого разработчика. Однако, по мнению автора, библиотеки и плагины лучше хранить в директории /js/,
а исполняемые скрипты и стили — в директории темы.

Для подключения скриптов и стилей не из темы, а из директории /js/ нужно перенести файлы скриптов и стилей в директорию /js/ и поменять код

$this->getLayout()->getBlock('head')->addItem('skin_js', 'ds_news/adminhtml/scripts.js');
$this->getLayout()->getBlock('head')->addItem('skin_css', 'ds_news/adminhtml/styles.css');

на

$this->getLayout()->getBlock('head')->addJs('ds_news/adminhtml/scripts.js');
$this->getLayout()->getBlock('head')->addItem('js_css', 'ds_news/adminhtml/styles.css');
Шаг 17. Обновление модуля: добавление категорий для новостей

Теперь займёмся довольно большой задачей — обновлением модуля: добавление категорий к новостям. Для упрощения задачи у каждой новости может быть только одна категория, либо новость может быть без категории.

  1. Создать файл обновления версии sql/dsnews_setup/upgrade-0.0.1-0.0.2.php
    <?php
    
    echo '<h1>Upgrade DS News to version 0.0.2</h1>';
    exit;
    
    $installer = $this;
    $tableCategories = $installer->getTable('dsnews/table_categories');
    $tableNews = $installer->getTable('dsnews/table_news');
    
    $installer->startSetup();
    $installer->run("DROP TABLE IF EXISTS {$tableCategories}");
    $installer->run("CREATE TABLE {$tableCategories} (
            `category_id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
            `title` VARCHAR(255) NOT NULL,
    
            PRIMARY KEY (`category_id`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8;");
    
    $installer->run("ALTER TABLE {$tableNews}
            ADD COLUMN `category_id` INT(11) UNSIGNED NOT NULL DEFAULT 0;");
    
    $installer->endSetup();
    
    

  2. Изменить файл конфигурации etc/config.xml
    <?xml version="1.0" ?>
    <config>
        <modules>
            <DS_News>
                <version>0.0.2</version>
            </DS_News>
        </modules>
        ...
        <global>
            ...
            <models>
                <dsnews>
                    <class>DS_News_Model</class>
                    <resourceModel>dsnews_resource</resourceModel>
                </dsnews>
                <dsnews_resource>
                    <class>DS_News_Model_Resource</class>
                    <entities>
                        <table_categories>
                            <table>ds_news_categories</table>
                        </table_categories>
                        <table_news>
                            <table>ds_news_entities</table>
                        </table_news>
                    </entities>
                </dsnews_resource>
            </models>
            ...
        </global>
    </config>
    

  3. Создать модель категории Model/Category.php
    <?php
    
    class DS_News_Model_Category extends Mage_Core_Model_Abstract
    {
    
        protected function _construct()
        {
            parent::_construct();
            $this->_init('dsnews/category');
        }
    
        protected function _afterDelete()
        {
            $resource = Mage::getSingleton('core/resource');
            $connection = $resource->getConnection('core_write');
    
            $id = intval($this->getId());
            $tableNews = $resource->getTableName('dsnews/table_news');
            $sql = "UPDATE {$tableNews} SET `category_id` = NULL WHERE `category_id` = {$id}";
    
            $connection->multiQuery($sql);
    
            return parent::_afterDelete();
        }
    
    }
    

  4. Добавить поле категории на форму редактирования новости Block/Adminhtml/News/Edit/Tabs/General.php
    <?php
    
    class DS_News_Block_Adminhtml_News_Edit_Tabs_General extends Mage_Adminhtml_Block_Widget_Form
    {
    
        protected function _prepareForm()
        {
            ...
    
            $fieldset->addField('category_id', 'select', array(
                'label' => $helper->__('Category'),
                'name' => 'category_id',
                'values' => $helper->getCategoriesOptions(),
            ));
    
            ...
        }
    }
    

  5. Добавить в хелпер Helper/Data.php формирование списка категорий для селекта на форме редактирования, а также для списка новостей
    <?php
    
    class DS_News_Helper_Data extends Mage_Core_Helper_Abstract
    {
    
        public function getImagePath($id = 0){ ... }
    
        public function getImageUrl($id = 0){ ... }
    
        public function getCategoriesList()
        {
            $categories = Mage::getModel('dsnews/category')->getCollection()->load();
            $output = array();
            foreach($categories as $category){
                $output[$category->getId()] = $category->getTitle();
            }
            return $output;
        }
    
        public function getCategoriesOptions()
        {
            $categories = Mage::getModel('dsnews/category')->getCollection()->load();
            $options = array();
            $options[] = array(
                'label' => '',
                'value' => ''
            );
            foreach ($categories as $category) {
                $options[] = array(
                    'label' => $category->getTitle(),
                    'value' => $category->getId(),
                );
            }
            return $options;
        }
    
    }
    

  6. Добавить вывод категории в списке новостей Block/News/Grid.php
    <?php
    
    class DS_News_Block_Adminhtml_News_Grid extends Mage_Adminhtml_Block_Widget_Grid
    {
    
        protected function _prepareCollection(){ ... }
    
        protected function _prepareColumns()
        {
    
            $helper = Mage::helper('dsnews');
    
            $this->addColumn('news_id', array(
                'header' => $helper->__('News ID'),
                'index' => 'news_id',
                'width' => '100px',
            ));
    
            $this->addColumn('title', array(
                'header' => $helper->__('Title'),
                'index' => 'title',
                'type' => 'text',
            ));
    
            $this->addColumn('category', array(
                'header' => $helper->__('Category'),
                'index' => 'category_id',
                'options' => $helper->getCategoriesList(),
                'type'  => 'options',
                'width' => '150px',
            ));
    
            $this->addColumn('created', array(
                'header' => $helper->__('Created'),
                'index' => 'created',
                'type' => 'date',
            ));
    
            return parent::_prepareColumns();
        }
    
        protected function _prepareMassaction(){ ... }
    
        public function getRowUrl($model){ ... }
    
    }
    

  7. Изменить меню админки в файле конфигурации etc/config.xml
    <?xml version="1.0" ?>
    <config>
        ...
        <adminhtml>
            <menu>
                <dsnews module="dsnews">
                    <title>News</title>
                    <sort_order>77</sort_order>
                    <children>
                        <dsnews_news translate="title" module="dsnews">
                            <title>News</title>
                            <sort_order>10</sort_order>
                            <action>dsnews_admin/adminhtml_news</action>
                        </dsnews_news>
                        <dsnews_category translate="title" module="dsnews">
                            <title>Categories</title>
                            <sort_order>20</sort_order>
                            <action>dsnews_admin/adminhtml_category</action>
                        </dsnews_category>
                    </children>
                </dsnews>
            </menu>
        </adminhtml>
        ...
    </config>
    

Если при открытии сайта в браузере появляется надпись «Upgrade DS News to version 0.0.2», то значит всё нормально — можно удалить две строчки из файла обновления и обновить окно с сайтом. В противном случае нужно смотреть на версию модуля в таблице core_resource — для строки dsnews_setup значение версии должно быть 0.0.1.

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

Шаг 18. Отображение Data Grid во вкладке

Если на предыдущем шаге всё прошло нормально, то теперь можно приступить к отображению списка новостей во вкладке при редактировании категории

  1. Добавить в модель категории Model/Category.php метод получения всех новостей данной категории
    <?php
    
    class DS_News_Model_Category extends Mage_Core_Model_Abstract
    {
    
        protected function _construct(){ ... }
    
        protected function _afterDelete(){ ... }
    
        public function getNewsCollection()
        {
            $collection = Mage::getModel('dsnews/news')->getCollection();
            $collection->addFieldToFilter('category_id', $this->getId());
            return $collection;
        }
    
    }
    

  2. Добавить вкладку Block/Adminhtml/Category/Edit/Tabs/News.php
    <?php
    
    class DS_News_Block_Adminhtml_Category_Edit_Tabs_News extends Mage_Adminhtml_Block_Widget_Grid
    {
    
        public function __construct()
        {
            parent::__construct();
            $this->setId('categoryNewsGrid');
            $this->setUseAjax(true);
        }
    
        protected function _prepareCollection()
        {
            $collection = Mage::registry('current_category')->getNewsCollection();
            $this->setCollection($collection);
            return parent::_prepareCollection();
        }
    
        protected function _prepareColumns()
        {
    
            $helper = Mage::helper('dsnews');
    
            $this->addColumn('ajax_grid_news_id', array(
                'header' => $helper->__('News ID'),
                'index' => 'news_id',
                'width' => '100px',
            ));
    
            $this->addColumn('ajax_grid_title', array(
                'header' => $helper->__('Title'),
                'index' => 'title',
                'type' => 'text',
            ));
    
            $this->addColumn('ajax_grid_created', array(
                'header' => $helper->__('Created'),
                'index' => 'created',
                'type' => 'date',
            ));
    
            return parent::_prepareColumns();
        }
    
        public function getGridUrl()
        {
            return $this->getUrl('*/*/news', array('_current' => true));
        }
    
    }
    

  3. Добавить вкладку на форму редактирования категории Block/Adminhtml/Category/Edit/Tabs.php
    <?php
    
    class DS_News_Block_Adminhtml_Category_Edit_Tabs extends Mage_Adminhtml_Block_Widget_Tabs
    {
    
        public function __construct(){ ... }
    
        protected function _prepareLayout()
        {
            $helper = Mage::helper('dsnews');
    
            $this->addTab('general_section', array(
                'label' => $helper->__('General Information'),
                'title' => $helper->__('General Information'),
                'content' => $this->getLayout()->createBlock('dsnews/adminhtml_category_edit_tabs_general')->toHtml(),
            ));
            $this->addTab('news_section', array(
                'class' => 'ajax',
                'label' => $helper->__('News'),
                'title' => $helper->__('News'),
                'url' => $this->getUrl('*/*/news', array('_current' => true)),
            ));
    
            return parent::_prepareLayout();
        }
    
    }
    

  4. Добавить в контроллер категории controllers/Adminhtml/CategoryController.php действие, которое будет генерировать список новостей для текущей категории
    <?php
    
    class DS_News_Adminhtml_CategoryController extends Mage_Adminhtml_Controller_Action
    {
    
        public function indexAction(){ ... }
    
        public function newAction(){ ... }
    
        public function editAction(){ ... }
    
        public function saveAction(){ ... }
    
        public function deleteAction(){ ... }
    
        public function newsAction()
        {
            $id = (int) $this->getRequest()->getParam('id');
            $model = Mage::getModel('dsnews/category')->load($id);
            Mage::register('current_category', $model);
    
            if (Mage::app()->getRequest()->isAjax()) {
                $this->loadLayout();
                echo $this->getLayout()->createBlock('dsnews/adminhtml_category_edit_tabs_news')->toHtml();
            }
        }
    
    }
    

В результате при открытии формы редактирования категории, можно будет увидеть все новости данной категории в новой вкладке «News». Для новой категории в данной вкладке будут показываться новости, не имеющие категории. Если данное поведение не устраивает, то при добавлении новой категории, вкладку новостей можно не показывать, немного подкорректировав блок табов в пункте 3:

<?php

class DS_News_Block_Adminhtml_Category_Edit_Tabs extends Mage_Adminhtml_Block_Widget_Tabs
{

    public function __construct(){ ... }

    protected function _prepareLayout()
    {
        $helper = Mage::helper('dsnews');
        $category = Mage::registry('current_category');

        $this->addTab('general_section', array(
            'label' => $helper->__('General Information'),
            'title' => $helper->__('General Information'),
            'content' => $this->getLayout()->createBlock('dsnews/adminhtml_category_edit_tabs_general')->toHtml(),
        ));
        if($category->getId()){
            $this->addTab('news_section', array(
                'class' => 'ajax',
                'label' => $helper->__('News'),
                'title' => $helper->__('News'),
                'url' => $this->getUrl('*/*/news', array('_current' => true)),
            ));
        }

        return parent::_prepareLayout();
    }

}

Следует иметь в виду, что айди, добавляемый для грида в пункте 2 $this->setId('categoryNewsGrid'), будет использоваться для формирования имени JavaScript переменной, поэтому в айди грида можно использовать только символы, разрешённые для названия переменных в JavaScript (например, нельзя использовать символ дефиса).

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

При добавлении вкладки списка, используется механизм аякс-получения данных, для чего вкладке добавляется класс ajax (этот класс обязателен) и ссылка на контроллер/действие, которое будет возвращать список на аякс запрос. Эта же ссылка используется в методе getGridUrl в классе грида для фильтрации данных аякс-запросом. При этом в параметре ссылки используется значение array('_current' => true), которое говорит, что при формировании ссылки необходимо оставить текущие параметры, таким образом сохраняется ссылка id категории при аякс запросе.

Шаг 19. Сохранение выбранных в Data Grid записей

Использование отображения новостей во вкладке категории позволяет не только показать новости текущей категории, но и выбрать/снять выделение с тех или иных новостей прямо во вкладке. А во время сохранения категории, у выбранных новостей изменится категория на текущую. Необходимо учитывать, что новости фильтруются аякс-запросом, также работает пагинация. В результате не все новости могут быть видимы при сохранении категории, а хранить айди выбранных новостей где-то нужно. Для этого будет использоваться специальный блок сериализации данных Mage_Adminhtml_Block_Widget_Grid_Serializer.

  1. Изменить файл Block/Adminhtml/Category/Edit/Tabs/News.php
    <?php
    
    class DS_News_Block_Adminhtml_Category_Edit_Tabs_News extends Mage_Adminhtml_Block_Widget_Grid
    {
    
        public function __construct()
        {
            parent::__construct();
            $this->setDefaultFilter(array('ajax_grid_in_category' => 1));
            $this->setId('categoryNewsGrid');
            $this->setSaveParametersInSession(false);
            $this->setUseAjax(true);
        }
    
        protected function _prepareCollection()
        {
            $collection = Mage::getModel('dsnews/news')->getCollection();
            $this->setCollection($collection);
            return parent::_prepareCollection();
        }
    
        protected function _prepareColumns()
        {
            $helper = Mage::helper('dsnews');
    
            $this->addColumn('ajax_grid_in_category', array(
                'align' => 'center',
                'header_css_class' => 'a-center',
                'index' => 'news_id',
                'type' => 'checkbox',
                'values' => $this->getSelectedNews(),
            ));
    
            $this->addColumn('ajax_grid_news_id', array(
                'header' => $helper->__('News ID'),
                'index' => 'news_id',
                'width' => '100px',
            ));
    
            $this->addColumn('ajax_grid_title', array(
                'header' => $helper->__('Title'),
                'index' => 'title',
                'type' => 'text',
            ));
    
            $this->addColumn('ajax_grid_created', array(
                'header' => $helper->__('Created'),
                'index' => 'created',
                'type' => 'date',
            ));
    
            return parent::_prepareColumns();
        }
    
        protected function _addColumnFilterToCollection($column)
        {
            if ($column->getId() == 'ajax_grid_in_category') {
                $collection = $this->getCollection();
                $selectedNews = $this->getSelectedNews();
                if ($column->getFilter()->getValue()) {
                    $collection->addFieldToFilter('news_id', array('in' => $selectedNews));
                } elseif (!empty($selectedNews)) {
                    $collection->addFieldToFilter('news_id', array('nin' => $selectedNews));
                }
            } else {
                parent::_addColumnFilterToCollection($column);
            }
            return $this;
        }
    
        public function getGridUrl()
        {
            return $this->getUrl('*/*/news', array('_current' => true, 'grid_only' => 1));
        }
    
        public function getSelectedNews()
        {
            if (!isset($this->_data['selected_news'])) {
                $selectedNews = Mage::app()->getRequest()->getParam('selected_news', null);
                if(is_null($selectedNews) || !is_array($selectedNews)){
                    $category = Mage::registry('current_category');
                    $selectedNews = $category->getNewsCollection()->getAllIds();
                }
                $this->_data['selected_news'] = $selectedNews;
            }
            return $this->_data['selected_news'];
        }
    
    }
    

  2. Изменить контроллер controllers/Adminhtml/CategoryController.php
    <?php
    
    class DS_News_Adminhtml_CategoryController extends Mage_Adminhtml_Controller_Action
    {
    
        public function indexAction(){ ... }
    
    
        public function newAction(){ ... }
    
        public function editAction(){ ... }
    
        public function saveAction()
        {
            $id = $this->getRequest()->getParam('id');
            if ($data = $this->getRequest()->getPost()) {
                try {
                    $helper = Mage::helper('dsnews');
                    $model = Mage::getModel('dsnews/category');
    
                    $model->setData($data)->setId($id);
                    $model->save();
    
                    $categoryId = $model->getId();
                    $categoryNews = $model->getNewsCollection()->getAllIds();
                    if ($selectedNews = $this->getRequest()->getParam('selected_news', null)) {
                        $selectedNews = Mage::helper('adminhtml/js')->decodeGridSerializedInput($selectedNews);
                    } else {
                        $selectedNews = array();
                    }
    
                    $setCategory = array_diff($selectedNews, $categoryNews);
                    $unsetCategory = array_diff($categoryNews, $selectedNews);
    
                    foreach($setCategory as $id){
                        Mage::getModel('dsnews/news')->setId($id)->setCategoryId($categoryId)->save();
                    }
                    foreach($unsetCategory as $id){
                        Mage::getModel('dsnews/news')->setId($id)->setCategoryId(0)->save();
                    }
    
                    Mage::getSingleton('adminhtml/session')->addSuccess($this->__('Category was saved successfully'));
                    Mage::getSingleton('adminhtml/session')->setFormData(false);
                    $this->_redirect('*/*/');
                } catch (Exception $e) {
                    Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
                    Mage::getSingleton('adminhtml/session')->setFormData($data);
                    $this->_redirect('*/*/edit', array(
                        'id' => $id
                    ));
                }
                return;
            }
            Mage::getSingleton('adminhtml/session')->addError($this->__('Unable to find item to save'));
            $this->_redirect('*/*/');
        }
    
        public function deleteAction(){ ... }
    
        public function newsAction()
        {
            $id = (int) $this->getRequest()->getParam('id');
            $model = Mage::getModel('dsnews/category')->load($id);
            $request = Mage::app()->getRequest();
    
            Mage::register('current_category', $model);
    
            if ($request->isAjax()) {
    
                $this->loadLayout();
                $layout = $this->getLayout();
    
                $root = $layout->createBlock('core/text_list', 'root', array('output' => 'toHtml'));
    
                $grid = $layout->createBlock('dsnews/adminhtml_category_edit_tabs_news');
                $root->append($grid);
    
                if (!$request->getParam('grid_only')) {
                    $serializer = $layout->createBlock('adminhtml/widget_grid_serializer');
                    $serializer->initSerializerBlock($grid, 'getSelectedNews', 'selected_news', 'selected_news');
                    $root->append($serializer);
                }
    
                $this->renderLayout();
            }
        }
    
    }
    

Как видно из пункта 1, впереди был добавлен ещё одна колонка ajax_grid_in_category, которая будет отвечать за выделение/снятие выделения с новостей, а также за фильтрацию выделенных новостей (новости текущей категории/других категорий/все новости). По этой причине теперь коллекция новостей загружается полностью в методе _prepareCollection, а фильтрация происходит в методе _addColumnFilterToCollection. Код $this->setDefaultFilter(array('ajax_grid_in_category' => 1)); включает фильтрацию по умолчанию (чтобы показывались только новости текущей категории). Функция getSelectedNews возвращает массив айди выделенных новостей, если имеются. Если не имеются, то вначале пытается возвратиться массив данных, пришедших POST-ом (если пользователь менял выделение), а затем — айди новостей текущей категории.

Контроллер тоже претерпевает достаточно изменений. Особенно это касается метода загрузки новостей. Строка $root = $layout->createBlock(...) перезатирает основной блок шаблона root, убирая все ненужные вложенные блоки. Затем создаётся блок грида, и, если запрос пришёл в первый раз, то ещё и блок для сериализации данных. После этого происходит генерация всего шаблона.

При вызове функции initSerializerBlock блока сериализации, происходит привязка блока к гриду. Данная функция принимает 4 параметра: блок или имя грида в шаблоне, имя метода блока для получения выделенных элементов, имя скрытого инпута (это имя используется в контроллере при сохранении), и последним параметром идём название поля, которое используется в методе getSelectedNews в блоке грида для получения изменений выделения новостей.

Шаг 20. Использование красивых URL

Текущие новости выводятся на сайте довольно некрасиво — это поправимо, если есть знания HTML/CSS и немного вкуса дизайнера. Другое дело — ссылки. Ссылки должны быть красивыми, в них должен отображаться заголовок новости. Для этого создадим свой роутер. Однако перед тем, как создавать свой роутер, нужно обновить версию модуля, добавив новостям поле для ссылки.

  1. Создать файл обновления sql/dsnews_setup/upgrade-0.0.2-0.0.3.php
    <?php
    
    echo '<h1>Upgrade DS News to version 0.0.3</h1>';
    exit;
    
    $installer = $this;
    $tableNews = $installer->getTable('dsnews/table_news');
    
    $installer->run("ALTER TABLE {$tableNews}
            ADD COLUMN `link` VARCHAR(255) AFTER `title`;");
    $installer->run("ALTER TABLE {$tableNews}
            ADD UNIQUE KEY (`link`);");
    
    foreach (Mage::getModel('dsnews/news')->getCollection() as $news) {
        try {
            $news->load($news->getId())->setDataChanges(true)->save();
        } catch (Exception $e) {
            $news->setId($news->getId())->setLink($news->getId())->save();
        }
    }
    
    $installer->endSetup();
    

  2. Изменить файл конфигурации модуля etc/config.xml
    <?xml version="1.0" ?>
    <config>
        <modules>
            <DS_News>
                <version>0.0.3</version>
            </DS_News>
        </modules>
        ...
    </config>
    

  3. Изменить хелпер Helper/Data.php
    <?php
    class DS_News_Helper_Data extends Mage_Core_Helper_Abstract
    {
    
        public function getImagePath($id = 0){ ... }
    
        public function getImageUrl($id = 0){ ... }
    
        public function getCategoriesList(){ ... }
    
        public function getCategoriesOptions(){ ... }
    
        public function prepareUrl($url)
        {
            return trim(preg_replace('/-+/', '-', preg_replace('/[^a-z0-9]/sUi', '-', strtolower(trim($url)))), '-');
        }
    
    }
    

  4. Изменить модель новости Model/News.php
    <?php
    
    class DS_News_Model_News extends Mage_Core_Model_Abstract
    {
    
        protected function _construct(){ ... }
    
        protected function _afterDelete(){ ... }
    
        protected function _beforeSave()
        {
            $helper = Mage::helper('dsnews');
    
            if (!$this->getData('link')) {
                $this->setData('link', $helper->prepareUrl($this->getTitle()));
            } else {
                $this->setData('link', $helper->prepareUrl($this->getData('link')));
            }
            return parent::_beforeSave();
        }
    
        public function getImageUrl(){ ... }
    
    }
    

  5. Добавить поле ссылки на форму редактирования новости Block/Adminhtml/News/Edit/Tabs/General.php
    <?php
    
    class DS_News_Block_Adminhtml_News_Edit_Tabs_General extends Mage_Adminhtml_Block_Widget_Form
    {
    
        protected function _prepareForm()
        {
            ...
    
            $fieldset->addField('link', 'text', array(
                'label' => $helper->__('Link'),
                'name' => 'link',
            ));
    
            ...
        }
    }
    

  6. Изменить файл конфигурации модуля etc/config.xml
    <?xml version="1.0" ?>
    <config>
        ...
        <global>
            ...
            <events>
                <controller_front_init_routers>
                    <observers>
                        <dsnews>
                            <class>DS_News_Controller_Router</class>
                            <method>initControllerRouters</method>
                        </dsnews>
                    </observers>
                </controller_front_init_routers>
            </events>
            ...
        </global>
    </config>
    

  7. Создать файл Controller/Router.php
    <?php
    
    class DS_News_Controller_Router extends Mage_Core_Controller_Varien_Router_Abstract
    {
    
        public function initControllerRouters($observer)
        {
            $front = $observer->getEvent()->getFront();
            $front->addRouter('dsnews', $this);
        }
    
        public function match(Zend_Controller_Request_Http $request)
        {
            $identifier = trim($request->getPathInfo(), '/');
            $cmd = explode('/', $identifier);
    
            if ($cmd[0] == 'news') {
                if (count($cmd) == 1) {
                    return $this->_fillRequest($request);
                } else {
                    $model = Mage::getModel('dsnews/news')->load($cmd[1], 'link');
                    if ($model->getId()) {
                        $params = array(
                            'id' => $model->getId()
                        );
                        return $this->_fillRequest($request, $params, 'index', 'view');
                    }
                }
            }
            return false;
        }
    
        protected function _fillRequest($request, $cmd = array(), $controller = 'index', $action = 'index')
        {
            $request->setModuleName('news')
                    ->setControllerName($controller)
                    ->setActionName($action)
                    ->setParam('is_routed', 1);
            if (is_array($cmd) && count($cmd)) {
                foreach ($cmd as $key => $value) {
                    $request->setParam($key, $value);
                }
            }
    
            $request->setAlias(Mage_Core_Model_Url_Rewrite::REWRITE_REQUEST_PATH_ALIAS, $request->getPathInfo());
            return true;
        }
    
    }
    

Если при обновлении окна сайта в браузере видна надпись Upgrade DS News to version 0.0.3, значит всё нормально — можно закомментировать надпись, и установить обновление. Пункты с 1 по 5 — это обновление модуля: добавление поля ссылки для новостей. Сам роутер создаётся в пункте 7, а в пункте 6 прописывается на событие инициализации роутеров сайта. Теперь на страницу новости можно перейти по ссылке http://site.com/news/{news-link}

По большей части данный код довольно прост. В файле конфигурации создаётся слушатель для события инициализации роутеров controller_front_init_routers, во время которого к списку обработчиков добавляется объект самого роутера $this: $front->addRouter('dsnews', $this). Данный объект должен наследоваться от класса Mage_Core_Controller_Varien_Router_Abstract, в котором будет вызываться метод match. Для того, чтобы не создавать лишнюю модель обсервера, слушатель события реализуется в том же самом объекте. Вся основная логика работы происходит в методе match, который занимается разбором URL. Тут можно делать всё, что угодно. Если этот метод возвращает true, то дальнейшая обработка прекращается, возвращение значения false ведёт к продолжению обработки другими роутерами. Метод _fillRequest — это просто вспомогательный код, вынесенный отдельно для сокращения общего объёма кода.

Поддержка крассивых ссылок для категорий, а также ссылки на новости с учётом категорий, остаётся как самостоятельная работа.

Исходные коды для каждого шага доступны тут

Автор: Gromo

Источник

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


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