Вступление

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

Первый взгляд

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

 

Изображение 1

На изображении 1 (вверху) показана общая архитектурная схема составных частей расширения. Для удобства восприятия все части разделены на 4 категории.

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

Прежде всего, функции — это статические методы, реализованные в основном классе расширения, или трейты, если ваше расширение это позволяет. Обычно методы не предназначены для прямого запроса, но при необходимости бэкэнд позволяет это сделать через специальные расширения контроллера?module=YourExtensionName&action=adminAction. Для фронтенда такой подход считается небезопасным и недоступен.

Следующим элементом основания являются крючки. Это точки доступа, позволяющие выполнять ваш код в разных местах. Хуки могут быть как в файле php, так и в шаблоне tpl. Как правило, подобные файлы размещаются в подкаталогах hooks и описываются в статическом методе getAdminHooks в основном классе или в классе установки, который мы рассмотрим позже. Вы всегда можете найти список доступных хуков в системе, проверив файл /lib/common/extensions/methodology.txt. И последний элемент первой части расширения — рендеринг. Это процессор класса шаблона. Мы не сделали его общим, чтобы обеспечить достаточную свободу для реализации в расширении. Стоит отметить, что обычно этот классРендер находится в корневой папке расширения. В этом случае файлы шаблона должны быть размещены в представлении каталога на одном уровне.

 

Изображение 2

На практике все три элемента организуют цепочку последовательных действий, как на рисунке 2.

Первый шаг

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

Итак, приступим к выполнению этой задачи. Мы предполагаем, что наше новое поле будет называться Rank. Вся задача сводится к тому, чтобы добавить некоторую классификацию этих рангов, назовем наше расширение Customers rank.

Изучив архитектуру системы, вы должны быть знакомы с отправной точкой любого расширения. Это папка /lib/common/extensions/, где находятся все расширения независимо от их назначения. Давайте определим место для нашего нового расширения и создадим новую папку. Следует учитывать, что слова, начинающиеся с заглавной буквы и без пробелов в названии, так называемые CamelCase. Затем создаем папку CustomersRank. В этой папке мы создаем файл CustomersRank.php , содержимое которого будет выглядеть следующим образом:

<?php

namespace common\extensions\CustomersRank;

class CustomersRank extends \common\classes\modules\ModuleExtensions {

}

Важно помнить, что название папки, название основного файла и название класса всегда будут одинаковыми, всегда следует учитывать регистр букв. В нашем файле namespace указывает расширение location, то есть фактически путь к папке. Также необходимо обратить внимание на родительский класс, который мы наследуем. Для всех расширений он будет одинаковым и будет иметь набор готовых инструкций по работе с расширением. На практике это означает, что на данном этапе ваше расширение уже готово к установке и вы сможете увидеть его в списке неустановленных расширений:

 

Изображение 3

Вы можете установить расширение и включить его. Но так как мы не хотели, чтобы наше расширение что-либо делало, мы получаем соответствующий результат. Давайте добавим еще немного контента в наш основной файл. Для этого нам понадобятся элементы из раздела основной функционал. Прежде всего, функции. Это обычный статический метод, расположенный в основном классе. Мы действительно начинаем с этого элемента, так как его работоспособность можно проверить напрямую через контроллер, несмотря на то, что в дальнейшем запросом будет управлять второй элемент из нашего набора — хуки. Добавим в наш класс функцию рендеринга дополнительного поля клиента:

    public static function renderCustomerField() 
    {
        return 'I want render new field';
    }

Теперь, чтобы увидеть возвращенный текст, воспользуемся прямым запросом из бэкенда контроллера через URL-адрес браузера:

extensions?module=CustomersRank&action=renderCustomerField

Это означает, что если URL-адрес вашего магазина http://localhost/oscommerce/, то URL-адрес вашего бэкэнда — http://localhost/oscommerce/admin/, а прямой запрос от бэкэнда контроллера через URL-адрес браузера — http://localhost. /oscommerce/admin/ extensions?module=CustomersRank&action=renderCustomerField    

В зависимости от того, включаете вы расширение или отключаете, результат будет отличаться:

You have not rights for this extension: CustomersRank

или же

I want render new field

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

Следующим шагом, как и было обещано, будет подключение расширения к странице редактирования клиента. Список возможных значений можно увидеть в файле lib/common/extensions/methodology.txt в соответствующем разделе Хуки. Разберемся, что это за список и как понять, какое именно место нужно использовать. Откройте любую страницу редактирования клиента в бэкэнде и обратите внимание на URL-адрес страницы.

admin/customers/customeredit?customers_id=XXX

А теперь обратим внимание на список запросов. Тот, который нас интересует, будет похож на адрес страницы. Выберем те, которые соответствуют нашему требованию:

'customers/customeredit', ''
'customers/customeredit/before-render', ''
'customers/customeredit', 'personal-block'
'customers/customeredit', 'left-column'
'customers/customeredit', 'right-column'

Мы поняли первую ключевую часть, но как понять, что такое второй ключ? Это очень просто! Если второй параметр отсутствует, то это контроллер в момент поста. Мы будем использовать его для сохранения данных в будущем. Остальные четыре значения указывают расположение на странице. Обязательно попробуйте их все, но в нашем примере мы пробуем персональный блок , который соответствует блоку с названием Личные данные. Итак, мы знаем, какой ключ использовать, давайте разберемся, как его использовать. Все точки запроса описываются в функции getAdminHooks как массив. Добавьте новую функцию в свой основной класс:

public static function getAdminHooks() 
    {
        $path = \Yii::getAlias('@common'). DIRECTORY_SEPARATOR. 'extensions'. DIRECTORY_SEPARATOR. 'CustomersRank'. DIRECTORY_SEPARATOR. 'hooks'. DIRECTORY_SEPARATOR;
        return [
            [
                'sort_order' => 10,
                'page_name' => 'customers/customeredit',
                'page_area' => 'personal-block',
                'extension_file' => $path. 'customers.customeredit.personal-block.tpl',
            ],
        ];
    }

Данные о точке перехвата передаются в виде массива. Мы выбрали название файла не случайно, а объединили заголовки страницы и блока. Расширение файла указывает на то, что это шаблон и Smarty используется для обработки этих файлов. Вы можете найти более подробную информацию о синтаксисе и функциях в официальной документации Smarty. Это позволит избежать путаницы в будущем, когда подобных перехватов будет много. Хуки подкатегории обычно выбираются как место для хранения наших файлов перехвата . Давайте создадим каталог и файл в нем. Содержимое файла будет выглядеть следующим образом:

{if $ext = \common\helpers\Acl::checkExtensionAllowed('CustomersRank', 'allowed')}
    {$ext::renderCustomerField()}
{/if}

Мы все сделали правильно, но после обновления страницы мы не видим наш текст. Это происходит потому, что хуки атрибутируются в процессе установки, и нам придется очищать их вручную или переустанавливать расширение. К счастью, очистку кеша можно легко выполнить, и вы будете часто использовать эту страницу в процессе разработки, так как система кэширует много данных. Например, системный кэш кэширует константы и даже структуру таблицы, smarty кэширует предварительно скомпилированные шаблоны, opcache кэширует php-файлы. Теперь нас интересует очистка кеша от хуков :

 

Изображение 4

Нажимаем Flush , ждем всплывающее окно с сообщением об успешной очистке и возвращаемся на страницу редактирования клиента. В нижней части блока мы увидим наш текст:

 

Изображение 5

Но мы еще не достигли нашей цели. Нам придется использовать третий элемент из раздела рендеринга. Это также очень важная часть расширения, которая позволяет использовать смарт-шаблоны для вывода контента. В отличие от функции это отдельный класс, обычно мы размещаем его на одном уровне с основным классом. Создайте файл Render.php в корневой папке расширения.

<?php

namespace common\extensions\CustomersRank;

class Render extends \common\classes\extended\Widget {

    public $params;
    public $template;

    public function run() {
        return $this->render($this->template, $this->params);
    }
}

Поскольку файл и основной файл находятся в одном месте, наше пространство имен остается прежним. Мы унаследуем его от виджета, объявим пару параметров, которые будем рендерить и простенькую функцию рендеринга контента. Также создайте папку с титровальными представлениями там же, где находится файл Render.php. В этой папке мы будем делать наш первый шаблон. Создайте файл со следующим содержимым:

{use class="\yii\helpers\Html"}
<div class="w-line-row w-line-row-1">
    <div class="wl-td">
        <label>Rank</label>
        {Html::input('text', 'rank', $rank, ['class' => 'form-control'])}
    </div>
</div>

Помимо обычного html мы будем использовать фреймворковую функцию yii. Подробнее ознакомиться с работой с полями в yii можно в официальной документации. И, наконец, последнее действие — запрос рендеринга вместо возврата текста в нашей функции renderCustomerField. Давайте изменим его на следующее:

public static function renderCustomerField() 
    {
        $rank = 'I want render new field';
        return \common\extensions\CustomersRank\Render::widget(['template' => 'customer-field', 'params' => ['rank' => $rank]]);
    }

Если вы используете кэширование файлов php, возможно, вам потребуется очистить OPcache, как было описано выше:

 

Изображение 6

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

 

Изображение 7

Подведем итоги. На данный момент наше расширение не может сохранять данные. Мы объясним, как это сделать в будущем. Но уже сейчас мы можем создать основной класс, определяемый системой как расширение и позволяющий выполнять установку/удаление. Также мы использовали все элементы раздела базовой функциональности, а именно статические методы, перехват, рендер. Если с вашей стороны что-то не получилось, вы можете скачать пример с результатом, который должен получиться здесь.

Посмотрите на инструмент

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

Сегодня мы познакомимся с большей частью раздела Инструменты. Его основная роль заключается в манипулировании данными. Прежде всего, это модели. Это объектно-ориентированный интерфейс для доступа и управления данными, хранящимися в базах данных. Т.к. это заготовленный механизм работы с данными Active Record в yii, то и наши модели мы унаследуем от них. Мы будем использовать модели одноименной подкатегории для хранения моделей .

Еще один элемент — хелперы. Это классы, содержащие статические методы запросов с часто используемыми фрагментами кода. Используя хелперы, мы придерживаемся принципа повторного использования кода, что позволяет избежать избыточности. Думаем, вы уже догадались, что директория для объектов этого типа будет называться helpers.

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

Более конкретным элементом являются виджеты. Это многократно используемые строительные блоки, используемые в представлениях для создания сложных и настраиваемых элементов пользовательского интерфейса с помощью встроенного редактора тем. Из всего, что мы ранее рассмотрели, виджеты похожи на элементы рендера , но в отличие от них виджеты действуют по-другому.

Завершить простое расширение

Ранее мы создали неполное расширение, которое еще не может сохранять данные. Теперь мы создадим таблицу и научимся использовать наши собственные модели. Модели — это наши вспомогательные элементы из раздела Tools (см. рис. 1), которые, как правило, размещаются в подпапке models. Для разделения таблиц по логическому принципу рекомендуется использовать префикс таблицы. В нашем примере мы будем использовать таблицы рангов. Мы назовем файл модели Ranks.php и поместим его в подкаталог моделей. И таблица получит префикс cr_- это будет означать Customer Ranks. Это удобно, если ваше расширение использует несколько таблиц, к тому же вы наверняка избегаете возможности пересечения со странными таблицами, называемыми rank. В результате наша модель будет выглядеть следующим образом:

<?php

namespace common\extensions\CustomersRank\models;

use yii\db\ActiveRecord;

class Ranks extends ActiveRecord
{
    public static function tableName()
    {
        return 'cr_ranks';
    }
    
}

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

Как заставить расширение создавать таблицу, мы узнаем позже, когда будем рассматривать раздел Настройка. Теперь ограничимся добавлением таблицы в базу средствами SQL:

CREATE TABLE IF NOT EXISTS `cr_ranks` (
  `customers_id` int(11) NOT NULL,
  `customer_rank` varchar(32) DEFAULT NULL,
  PRIMARY KEY (`customers_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

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

public static function saveCustomerField($id) 
    {
        $rank = filter_var( \Yii::$app->request->post('rank', ''), FILTER_SANITIZE_STRING);
        $ranksRecord = \common\extensions\CustomersRank\models\Ranks::find()
                ->where(['customers_id' => $id])
                ->one();
        if (!($ranksRecord instanceof \common\extensions\CustomersRank\models\Ranks)) {
            $ranksRecord = new \common\extensions\CustomersRank\models\Ranks();
            $ranksRecord->customers_id = $id;
        }
        $ranksRecord->customer_rank = $rank;
        $ranksRecord->save();
    }

Также нам понадобится новый хук, смотрящий не на блок, а на контроллер при сохранении данных формы. Сам файл назовем customers.customeredit.php и он будет выглядеть следующим образом:

<?php
if ($CustomersRank = \common\helpers\Acl::checkExtensionAllowed('CustomersRank', 'allowed')) {
  $CustomersRank::saveCustomerField((int) $cInfo->customers_id);
}

Важно не забыть внести изменения в функцию getAdminHooks. Теперь он будет содержать два элемента. Приведем содержимое обновленной функции:

public static function getAdminHooks() 
    {
        $path = \Yii::getAlias('@common'). DIRECTORY_SEPARATOR. 'extensions'. DIRECTORY_SEPARATOR. 'CustomersRank'. DIRECTORY_SEPARATOR. 'hooks'. DIRECTORY_SEPARATOR;
        return [
            [
                'sort_order' => 10,
                'page_name' => 'customers/customeredit',
                'page_area' => 'personal-block',
                'extension_file' => $path. 'customers.customeredit.personal-block.tpl',
            ],
            [
                'sort_order' => 20,
                'page_name' => 'customers/customeredit',
                'page_area' => '',
                'extension_file' => $path. 'customers.customeredit.php',
            ],
        ];
    }

Как видно из кода нового хука, мы использовали параметр для отрисовки отредактированного вами идентификатора объекта. При необходимости мы могли бы отрендерить весь объект клиента, но в данном случае в этом нет необходимости. Теперь давайте очистим opcache и хуки, как мы делали это раньше, и попробуем сохранить слово sheriff в нашем новом поле. Мы все сделали правильно, но результата не видно. И тем не менее, если база проверена, вы найдете запись в новой таблице. Причина в том, что мы не извлекаем данные из старой функции renderCustomerField. Прежде чем мы это сделаем, давайте создадим хелпер, который возвращает рейтинг клиента. Создаем директорию helpers и в ней файл Customer.php следующего содержания:

<?php

namespace common\extensions\CustomersRank\helpers;

class Customer {

    public static function getRank($id) {
       $ranksRecord = \common\extensions\CustomersRank\models\Ranks::find()
                ->where(['customers_id' => $id])
                ->one();
       if ($ranksRecord instanceof \common\extensions\CustomersRank\models\Ranks) {
           return $ranksRecord->customer_rank;
       }
        return '';
    }
       
}

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

public static function renderCustomerField($id = 0) 
    {
        $rank = \common\extensions\CustomersRank\helpers\Customer::getRank($id);
        return \common\extensions\CustomersRank\Render::widget(['template' => 'customer-field', 'params' => ['rank' => $rank]]);
    }

Важно не забывать, что наша функция теперь требует параметр. Хотя мы использовали значение по умолчанию, чтобы не ошибиться, для полноценной работы нам потребуется изменить старый файл customers.customeredit.personal-block.tpl таким образом, чтобы функция получила идентификатор клиента. Процедура аналогична той, что мы делали при сохранении:

{if $ext = \common\helpers\Acl::checkExtensionAllowed('CustomersRank', 'allowed')}
    {$ext::renderCustomerField($cInfo->customers_id)}
{/if}

Очистим кеш и попробуем обновить страницу. Мы надеемся, что вы получите следующий результат:

  

Изображение 8

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

Мы уже упоминали, что виджет очень похож на рендер, поэтому это будет отдельный класс и отдельный шаблон. Давайте создадим виджеты категорий в нашей папке расширения. Теперь давайте придумаем название нашего виджета. Его задача — отображать присвоенный администратором ранг, поэтому мы называем виджет ShowRank. Создаем одноименную подкатегорию и новый файл в этом каталоге. Как вы уже догадались, файл называется ShowRank.php и будет выглядеть следующим образом:

<?php

namespace common\extensions\CustomersRank\widgets\ShowRank;

class ShowRank extends \yii\base\Widget
{
    public $name;
    public $params;
    public $settings;

    public function run()
    {
        if (\Yii::$app->user->isGuest) {
            return '';
        }
        $customer = \Yii::$app->user->getIdentity();
        $rank = \common\extensions\CustomersRank\helpers\Customer::getRank($customer->customers_id);
        return self::begin()->render('customer-field', [
            'rank' => $rank,
        ]);
    }
}

В первую очередь мы запрещаем работу виджета для незарегистрированных клиентов, так как им нечего отображать. Затем мы получили данные о текущем пользователе и использовали его идентификатор для получения ранга с помощью написанного ранее хелпера. И мы придаем значение шаблону, которого еще не существует. Прежде чем создать шаблон, на уровне нашего виджета мы создаем представления подкаталога , а там создаем простой шаблон customer-field.tpl , который будет выводить текст:

<p>You rank: {$rank}</p>

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

public static function getWidgets($type = 'general') {
        if ( !self::allowed() ) return [];

        $widgets = [];
        if ($type == 'account') {
            $widgets[] = ['name' => 'CustomersRank\widgets\ShowRank', 'title' => 'Show Rank', 'description' => '', 'type' => 'account', 'class' => ''];
        }
        return $widgets;
    }

Это позволит дизайнеру понять, что мы предоставляем виджет для страницы учетной записи клиента. Очистим кеш и ознакомимся с внутренним дизайнером темы.

Прежде чем мы откроем дизайнер тем, войдите в систему как пользователь на внешнем интерфейсе. Без активного пользователя мы могли видеть только страницу входа. Теперь давайте выберем в меню «Дизайн и CMS» -> «Темы», нажмите кнопку «Настроить» -> «Рабочий стол» на теме, которую мы собираемся изменить. Мы видим индексную страницу. Чтобы изменить страницу, мы выбираем Страницы в правом меню, категорию Аккаунт и страницу Аккаунт. Скорее всего, вы увидите страницу, похожую на следующую:

  

Изображение 9

Не будем создавать отдельные блоки, а просто нажмем «Добавить виджеты» в существующем блоке (см. рис. 9). Во всплывающем окне вы увидите полный список доступных виджетов. Воспользуемся поиском:

 

Изображение 10

Находим наш виджет и просто нажимаем на него. Система автоматически добавит его перед блоком и в редакторе мы сможем увидеть результат:

 

Изображение 11

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

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

Что такое бутстрап

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

Улучшить мое расширение

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

Как вы уже догадались, мы используем bootstrap. Для создания предзагрузки в Yii2 обычно создается одноименный файл Bootstrap.php с классом, который должен выполнять интерфейс BootstrapInterface и его метод bootstrap. Создадим этот файл в корневой папке расширения, его содержимое будет выглядеть следующим образом:

<?php

namespace common\extensions\CustomersRank;

use yii\base\BootstrapInterface;

class Bootstrap implements BootstrapInterface {

    public function bootstrap($app) {
        if (!CustomersRank::enabled()) {
            return;
        }
        if ($app instanceof \yii\web\Application) {
            if ($app->id == 'app-backend') {
                $app->controllerMap = array_merge($app->controllerMap, [
                    'manage-rank' => ['class' => __NAMESPACE__. '\backend\controllers\ManageRankController'],
                ]);
            }
        }
    }

}

Как видите, наш контроллер предназначен только для бэкенда. На данный момент его нет, поэтому давайте создадим бэкенд и контроллеры каталогов так же, как это было указано в бутстрапе. Теперь есть место, где мы можем найти наш первый контроллер. Как всегда, название файла ManageRankController.php совпадает с названием класса. Для того, чтобы проверить, правильно ли настроен бутстрап, достаточно будет класса с одной функцией:

<?php

namespace common\extensions\CustomersRank\backend\controllers;

class ManageRankController extends \common\classes\modules\SceletonExtensionsBackend {

    public $acl = ['BOX_HEADING_CUSTOMERS'];
    
    public function actionIndex() {
        die('my controller');
    }
}

Мы уже хорошо знакомы с понятием namespace, и оно в точности повторяет путь к файлу. Мы унаследовали сам класс от SceletonExtensionsBackend , чтобы включить автоматическую проверку владения. Поэтому в переменной $acl необходимо указать, к какому элементу меню мы относимся. Поскольку своего элемента у нас пока нет, сообщим системе, что это Customers. Функция actionIndex — это событие, выполняемое по умолчанию. Это означает, что запрос к URL -адресу management-rank или management-rank/indexбудет идентичным, но первый более удобен для пользователя. Попробуйте оба способа запроса к странице (стоит помнить, что сейчас мы работаем с бэкендом, поэтому ваш URL должен быть похож на https://localhost/admin/manage-rank/index).

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

public function actionIndex() 
    {
        $this->navigation[] = array('link' => \Yii::$app->urlManager->createUrl('manage-rank/'), 'title' => 'Customers Rank');
        $table = [
            [
                'title' => 'Customer name',
                'not_important' => 0,
            ],
            [
                'title' => 'Rank',
                'not_important' => 0,
            ],
            [
                'title' => 'Action',
                'not_important' => 0,
            ],
        ];
        return $this->render('index', [
            'table' => $table,
        ]);
    }

Теперь давайте создадим шаблон, которого еще не существует. Путь к файлу относительно корневой папки расширения будет backend/views/manage-rank/index.tpl. Как видите, все, что касается бэкенда , находится в одном каталоге и по сути повторяет работу системы в целом. Ниже мы демонстрируем содержимое файла, которое включает в себя заголовок, таблицу функций на javascript, которую мы собираемся использовать при нажатии кнопки в столбце «Действие»:

{use class="common\helpers\Html"}
<div class="page-header">
    <div class="page-title">
        <h3>{$app->controller->view->headingTitle}</h3>
    </div>
</div>
<div class="rank-wrap">
    <table class="table table-bordered table-hover table-responsive table-checkable datatable" checkable_list="0, 1" data_ajax="manage-rank/list">
        <thead>
            <tr>
                {foreach $table as $tableItem}
                    <th{if isset($tableItem['not_important']) && $tableItem['not_important'] == 1} class="hidden-xs"{/if}>{$tableItem['title']}</th>
                {/foreach}
            </tr>
        </thead>
    </table> 
</div>
<script type="text/javascript">
function newCustomerRank(id) {
    bootbox.dialog({
        message: '<div class="new-rank">Rank: {Html::input('text', 'rank', '', ['class' => 'form-control'])|escape:javascript}</div>',
        title: "New rank",
        buttons: {
            done:{
                label: "{$smarty.const.TEXT_BTN_OK}",
                className: "btn-cancel",
                callback: function() {
                    var rank = $('input[name="rank"]').val();
                    $.get("manage-rank/change", { 
                        'id': id,
                        'rank': rank
                    }, function () {
                        var table = $('.table').DataTable();
                        table.draw(false);
                    }, "html");
                }
            }
        }
    });
    
    
    return false;
}
</script>

Как видите, мы собираемся использовать еще два дополнительных действия в контроллере. Первый требуется для таблицы:

public function actionList() 
    {
        \Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
        
        $draw = \Yii::$app->request->get('draw', 1);
        $start = \Yii::$app->request->get('start', 0);
        $length = \Yii::$app->request->get('length', 10);
        if ($length == -1) {
            $length = 10000;
        }
        
        
        
        $customersQuery = \common\models\Customers::find()
                ->select(['c.customers_id', 'c.customers_firstname', 'c.customers_lastname', 'r.customer_rank'])
                ->from(\common\models\Customers::tableName(). ' c')
                ->leftJoin(\common\extensions\CustomersRank\models\Ranks::tableName(). ' r', 'c.customers_id=r.customers_id')
                ;
        $search = \Yii::$app->request->get('search');
        if (isset($search['value']) && tep_not_null($search['value'])) {
            $keywords = tep_db_input(filter_var($search['value'], FILTER_SANITIZE_STRING));
            $customersQuery->andWhere("c.customers_firstname like '%". $keywords. "%' or c.customers_lastname like '%". $keywords. "%'");
        }
        
        $order = \Yii::$app->request->get('order');
        if (isset($order[0]['column']) && $order[0]['dir']) {
            switch ($order[0]['column']) {
                case 0:
                    $customersQuery->orderBy("c.customers_firstname ". tep_db_input(tep_db_prepare_input($order[0]['dir'])));
                    break;
                case 1:
                    $customersQuery->orderBy("r.customer_rank ". tep_db_input(tep_db_prepare_input($order[0]['dir'])));
                    break;
                default:
                    $customersQuery->orderBy("c.customers_firstname, c.customers_lastname");
                    break;
            }
        } else {
            $customersQuery->orderBy('c.customers_firstname, c.customers_lastname');
        }
        
        $responseList = [];
        $rowsShow = 0;
        $rowsTotal = $customersQuery->count();
        $customersQuery->limit($length)->offset($start);
        
        foreach ($customersQuery->asArray()->all() as $customersRow) {
            $rowsShow++;
            $responseList[] = [
                $customersRow['customers_firstname']. ' '. $customersRow['customers_lastname'],
                $customersRow['customer_rank'],
                '<a class="btn" href="javascript:void(0);" onclick="return newCustomerRank('. $customersRow['customers_id']. ')">Change rank</a>'
            ];
        }
        
        $response = [
            'draw' => $draw,
            'recordsTotal' => $rowsTotal,
            'recordsFiltered' => $rowsShow,
            'data' => $responseList
        ];
        return $response;
    }

А второй потребуется для изменения значения нашего поля:

public function actionChange() 
    {
        $id = (int)\Yii::$app->request->get('id');
        $rank = filter_var(htmlentities(\Yii::$app->request->get('rank')), FILTER_SANITIZE_STRING);
        
        $ranksRecord = \common\extensions\CustomersRank\models\Ranks::find()
                ->where(['customers_id' => $id])
                ->one();
       if ($ranksRecord instanceof \common\extensions\CustomersRank\models\Ranks) {
           $ranksRecord->customer_rank = $rank;
           $ranksRecord->save();
       } else {
           $customer = \common\models\Customers::find()
                   ->where(['customers_id' => $id])
                   ->one();
           if ($customer instanceof \common\models\Customers) {
            $ranksRecord = new \common\extensions\CustomersRank\models\Ranks();
            $ranksRecord->customers_id = $id;
            $ranksRecord->customer_rank = $rank;
            $ranksRecord->save();
           }
       }
    }

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

Настройка нужна или нет

Теперь у нас уже есть полное расширение. Прежде чем мы начнем делиться им, мы должны коснуться вопроса его установки и удаления из системы. Именно поэтому существует последний раздел настройки расширения (см. изображение 1). Создание таблиц , если таблицы должны быть удалены при деинсталляции, создание переводов для разных языков, как добавить новый элемент в меню, разграничение доступа для администраторов через acl и другие полезные мелочи, которые необходимы для полной автоматизации применяется в следующей главе о практике.

Закончим расширение

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

<?php

namespace common\extensions\CustomersRank;

class Setup extends \common\classes\modules\SetupExtensions {

    public static function getVersionHistory() 
    {
        return [
            '1.0.0' => ['whats_new' => "Customers Rank first version"],
        ];
    }

    public static function getDescription()
    {
        return 'This extension allows you to assign ranks to customers';
    }
}

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

public static function install($platform_id, $migrate)
    {
        $migrate->createTableIfNotExists('cr_ranks', [
            'customers_id' => $migrate->primaryKey(),
            'customer_rank' => $migrate->string(32)
        ]);
    }
    
    public static function getDropDatabasesArray()
    {
        return ['cr_ranks'];
    }

Для создания таблиц мы используем подготовленный класс \common\classes\Migration, все его возможности вы можете просмотреть самостоятельно. А для удаления будет достаточно вернуть массив таблиц, который можно удалить.

Теперь займемся переводами. Для этого нам нужно будет создать метод getTranslationArray, который будет возвращать массив массивов. Чтобы понять, о чем идет речь, рекомендуем посмотреть страницу перевода в бэкенде. Первый ключ — сущность, второй — константа. Есть общедоступные сущности, такие как main и admin/main, мы используем их для будущего элемента меню. Перевод меню должен быть доступен всегда вне зависимости от того, заходили мы на нашу страницу или нет, поэтому используем admin/main. Нам понадобятся другие константы локально, и мы используем их во внутреннем контроллере.

public static function getTranslationArray() {
        return [
            'admin/main' => [
                'BOX_CUSTOMERS_RANK' => 'Customers Rank',
            ],
            'extensions/customers-rank' => [
                'CR_NAME' => 'Customer name',
                'CR_RANK' => 'Rank',
                'CR_ACTION' => 'Action'
            ],
        ];
    }

А так как мы добавили перевод для меню, давайте создадим сам элемент и правила видимости:

public static function getAdminMenu()
    {
        return [
            [
                'parent' => 'BOX_HEADING_CUSTOMERS',
                'sort_order' => '100',
                'acl_check' => 'CustomersRank,allowed',
                'path' => 'manage-rank',
                'title' => 'BOX_CUSTOMERS_RANK',
            ],
        ];
    }   
    
    public static function getAclArray()
    {
        return ['default' => ['BOX_HEADING_CUSTOMERS', 'BOX_CUSTOMERS_RANK']];
    }

Теперь немного подкорректируем наш контроллер, добавив привязку к новому элементу меню и заменив тексты на константы:

public $acl = ['BOX_HEADING_CUSTOMERS', 'BOX_CUSTOMERS_RANK'];
    
    public function actionIndex() 
    {
        $this->navigation[] = array('link' => \Yii::$app->urlManager->createUrl('manage-rank/'), 'title' => 'Customers Rank');
        $table = [
            [
                'title' => CR_NAME,
                'not_important' => 0,
            ],
            [
                'title' => CR_RANK,
                'not_important' => 0,
            ],
            [
                'title' => CR_ACTION,
                'not_important' => 0,
            ],
        ];
        return $this->render('index', [
            'table' => $table,
        ]);
    }

На этом мы закончили создание полнофункционального расширения. Теперь вы можете спокойно удалить и установить расширение через бэкенд. Если вы не уверены в правильности своего кода или что-то не работает как надо, вы можете скачать урок здесь.

Сделай мою жизнь проще

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

Генерация шаблона выполняется в три этапа. Первый этап – ввод общей информации:

Мы уже сделали это вручную, создав папки и классы, заполнив версии и описание.

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

И третий этап позволяет скачать подготовленный архив, выполнив небольшую настройку:

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

Эпилог

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

Создано разработчиком Юрием Нечитайло