Создание виджета популярных материалов в yii2.
  • 615

Создание виджета для показа популярных материалов в yii2. (часть 1)

Автор: admin | 17 июня (Вс.) 2018г. в 22ч.44м.

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

План действий.

Эта статья состоит из 2х частей. 

Первая часть:
- Подготовка верстки (html, css, javascript).
- Создание каркаса виджета.
- Добавление в html, стилей и скриптов.
- Разработка логики виджета.
- Выводим виджет в файле view. Генерация html верстки из виджета.
- Добавление элементарных настроек виджета.

Вторая часть (тут):
-На сколько гибким должен быть виджет. Настройки.
-Как работать с моделью в виджете.
-Начинаем разработку.
-Использование анонимных функций и замыканий.
-Добавление событий в виджет.
-Вызов виджета. Установка параметров.
-Вывод.

Подготовка верстки.

Как должен выглядеть блок для вывода популярных материалов дело вкуса. Я для себя представляю это так:
Виджет популярных материалов в yii2.
Для верстки будет использован bootstrap 3. Для начала пишем html структуру блока прямо во view файле,
в котором и должен распологаться блок со ссылками на популярные материалы. У меня такой блок будет
расположен под статьей блога. Такая первоначальная структура блока получается:
<!--widget-->
<div class="row linkbox">
    <h2 class="h_style_decoration">
        <i class="fa fa-bookmark-o color-salat" aria-hidden="true"></i>
        <span class="h_style_decoration-text">Также по теме:</span>
    </h2> 
    
    
    <div class="col-md-3 col-sm-4 col-xs-6 linkbox-elem"> 
        <!--тут будет содержание-->
        
    </div>
    
    <div class="col-md-3 col-sm-4 col-xs-6 linkbox-elem"> 
        <!--тут будет содержание-->
        
    </div>
    
    <div class="col-md-3 col-sm-4 col-xs-6 linkbox-elem"> 
        <!--тут будет содержание-->
        
    </div>
    
    <div class="col-md-3 col-sm-4 col-xs-6 linkbox-elem"> 
        <!--тут будет содержание-->
        
    </div>
    
</div>
​

Cоздаем разметку отдельного блока linkbox-elem. Пока для наглядности заполняю все статической информацией:

<div class="col-sm-4 col-xs-6 linkbox-elem" data-link="http://myblog.local/some_link_to_this_material"> 
        <div class="linkbox-elem-header">
            <?php echo Html::img("@img-web-blog-path/some_pic.jpg", ['alt' => '', 'title' => '', 'class' => 'linkbox-elem-img']); ?>
        </div>
        <div class="linkbox-elem-footer">
            <p class="linkbox-elem-category">
                <a href="">Yii2</a>
            </p>
            
            <p class="linkbox-elem-title">
                <a href="">Тут будет заголовок материала.</a>
                
            </p>
            <p class="linkbox-elem-data">
                <span>01. МАЯ 2018</span>
            </p>
        </div>
    </div>

Теперь нужно оформить верстку с помощью стилей и скриптов. Чтобы не создавать отдельные файлы
на данном этапе добавляем стили и скрипты прямо тут же под блоком html.

<?php
$css= <<< CSS
.linkbox{
    background-color: #f7f7f7;
    padding-top:3rem; 
    padding-bottom:3rem;    
}        
        
.linkbox-name{
    padding-bottom:3rem; 
}        
   
        
.linkbox-name > h6{
    margin:0 auto;
    font-size: 2rem;
    border-bottom:1px solid #b5563b;    
}

.linkbox-elem{
    margin-bottom: 20px;
}        
        
.linkbox-elem:hover{
    cursor: pointer;
}         
        
.linkbox-elem:hover .linkbox-elem-img{
    opacity:0.5;
}        
  
.linkbox-elem:hover .linkbox-elem-title a{
    border-bottom: 1px solid #d0021b;
}   
    
.linkbox-elem-header{
    display: table;
    background-color: #555;
    position: relative;   
}  
    
.linkbox-elem-header img{
    opacity:1;
    transition: all 0.4s    
}        
 
.linkbox-elem-img{
    width: 100%;
    hight: auto;    
}        
        
.linkbox-elem-footer{
    background-color: #fff;
    padding: 1.5rem 1rem;    
}     
        
.linkbox-elem{
    padding-left:10px;
    padding-right:10px;    
}        
                
.linkbox-elem-category{
    text-align: center;   
}
 
.linkbox-elem-category > a{
    text-transform: uppercase;
    color: #bd0017; 
    font-size: 0.9rem; 
    font-family: Arial, sans-serif;
    font-weight: bold;  
    transition: all 0.4s;    
}   
 
.linkbox-elem-category > a:hover{
    color: #5BC0DE;   
}    
        
.linkbox-elem-title{
    text-align: center;
    min-height: 10px;   
}
        
.linkbox-elem-title > a{
    font-family: Georgia,  Times, Times New Roman, serif; 
    font-variant: small-caps;   
    font-size: 1.5rem;
    text-indent: 0px; 
    border-bottom:1px solid transparent;
    color: #2A3035;   
    transition: all 0.4s;
    line-height: 1.7rem;
}
    
.linkbox-elem-title > a:hover{
    color: #2A3035;    
}        
        
.linkbox-elem-data{
    text-transform: uppercase;
    text-align: center;
    color: #C9C9C9; 
    font-size: 0.8rem;    
}
        
        
CSS;

$this->registerCss($css, ["type" => "text/css"], "myStyles" );


$js= <<< JS
    $('.linkbox-elem').on('click', function(e){
        var link = $(this).attr("data-link");
        window.location.assign(link);
        
    });  
     

    var mHeight = 0;
    $('.linkbox-elem-title').each(function(index,element){
        if ($(this).height() > mHeight) {
            mHeight = $(this).height(); 
        }
    });
        
    $('.linkbox-elem-title').height(mHeight);   
        
JS;

$this->registerJs($js, $position = yii\web\View::POS_READY, "myScripts");        
        
        
?>

Регистрируем css и js с помощью $this->registerJs() и $this->registerCss().
Перезагружаю браузер. Вижу результат. Меня устраивает. Теперь нужно все это оформить в виджет.

Создание каркаса виджета.

Я в данном примере использую advanced каркас yii2. Поэтому создаю папку materialList, которая будет содержать
виджет по пути common/widgets/materialList.
Создаем такую структуру файлов и папок в папке виджета materialList.

materialList
---assets
        ---css
              ---style.css
        ---js
              ---script.js
---view
       ---index.php
MaterialListWidget.php
MaterialListAssets.php​

Добавление в html, стилей и скриптов.

В файл view/index.php переносим написанный выше html код. В файл script.js добавляем скрипт
и в style.css стили описанные выше.

В файлах MaterialListWidget.php и MaterialListAssets.php пространства имен:

 namespace common\widgets\materialList;

В файл вида, в котором планируется разместить виджет (в котором делалась верстка) устанавливаем
код вызова виджета в нужном месте:
<?php
use common\widgets\materialList\MaterialListWidget;

?>

<div>
    <!--тут какой-то html-->
</div>

<?= MaterialListWidget::widget([]); ?>​

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

 use common\widgets\materialList\MaterialListWidget;

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

Разработка логики виджета.

Теперь нужно подключить скрипты и стили в MaterialListWidget и сделать рендеринг html шаблона (пока со статичными данными) на страницу, где подключен виджет. 
Для начала разберемся с файлом класса ресурсов MaterialListAssets.php. Определим пути к стилям
и скриптам, а также обозначим зависимости:
<?php

namespace common\widgets\materialList;

use yii\web\AssetBundle;

/**
 * Main frontend application asset bundle.
 */
class MaterialListAsset extends AssetBundle
{
    public $sourcePath = (__DIR__ . '/assets');
    
    public $css = ["css/style.css"];
    public $js = ["js/script.js"];
    
    public $jsOptions = [
        'position' => \yii\web\View::POS_END,
    ];
    
    public $depends = [
        "frontend\assets\AppAsset"
    ];
    
    public $publishOptions = [
        'forceCopy' => true,
    ];
    
    public function init()
    {
        
        parent::init();
    }

}​

Наш набор MaterialListAsset зависит от frontend\assets\AppAsset, который содержит все базовые
стили сайта, в том числе и bootstrap и jquery. Таким образом стили и скрипты виджета будут выведены в порядке очередности после
основных стилей и скриптов сайта.

Все стили и скрипты, которые не расположены в web дирректории - автоматически попадают в кэш в
папку runtime и далее подключаются к сайту из кэша.
На этапе разработки удобно устанавливать 'forceCopy' => true ,чтобы не работать с кэш и не чистить
его каждый раз, при изменении стилей и js, а работать с первоисточниками.

Теперь займемся файлом MaterialListWidget.

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

<?php
namespace common\widgets\sidebar\materialList;

use Yii;
use yii\base\InvalidConfigException;
use yii\base\Widget;
use yii\helpers\Html;
use yii\helpers\Url;
use yii\web\View;


/**
 * Виджет 
 */

class MaterialListWidget extends Widget
{

    public function init()
    {
        parent::init();

        
        
    }

    public function run()
    {
        
        
        return $this->render('index',[]);

    }
}    ​

Тут подключаем нужные пространства имен. В init() будем проверять и инициализировать все свойства,
пришедшие извне, в run() выводим html, из view/index.php виджета.

Выводим виджет в файле view контроллера. Генерация html верстки из виджета.

Теперь подключим тут и инициализируем файлы css и js виджета и посмотрим на результат. Теперь должен быть
тот же результат, что изначально было, когда была сделана верстка, css и js на странице в файле вида экшэна article.
<?php
namespace common\widgets\materialList;

use Yii;
use yii\base\InvalidConfigException;
use yii\base\Widget;
use yii\helpers\Html;
use yii\helpers\Url;
use yii\web\View;


/**
 * Виджет 
 */

class MaterialListWidget extends Widget
{

    
    public function init()
    {
        parent::init();

    }

    public function run()
    {
        $this->registerAssets();//выносим регистрацию стилей в отдельный метод
        
        return $this->render('index', []);

    }

    /**
     * Register assets.
     */
    protected function registerAssets()
    {
        $view = $this->getView();// получаем объект вида, в который рендерится виджет
        MaterialListAsset::register($view);// регестрируем файл с классом наборов css, js.
        
    }

}        ​

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

 <?= MaterialListWidget::widget([]); ?>

Пока данные в виджете статические исключительно для демонстрации того, как верстка уже
выводится из виджета.

Добавление элементарных настроек виджета.

Теперь нужно добавить настройки виджету, чтобы выводились данные из базы.
Для этого нужно сформировать sql запрос к базе для получения данных, а именно статей блога,
с сортировкой по количеству просмотров.
В моем случае запрос получился такой:
$sql = BlogArticles::find()->where(['status' => 1])->limit(6)->orderBy(['viewCount' => SORT_DESC]);​

По моей задумке в виджет передается запрос без ->all(), чтобы потом можно было далее с ним работать.
А также добавим заголовок всего блока

Итак, добавляем настройки в виджет:
<?= MaterialListWidget::widget([
    'query' => $sql,
    'headerText' => 'Также по теме:',
]); ?> ​

Теперь получаем данные в виджете MaterialListWidget.php:

class MaterialListWidget extends Widget
{
    public $query;
    public $headerText;
    
    public function init()
    {
        parent::init();
        //далее настройки из виджета обробатываем, если надо, тут
        
        if ($this->query === null) {
            throw new InvalidConfigException('The "query" property must be set.');
        }
        
        if ($this->headerText === null) {
            $this->headerText = "Заголовок по умолчанию";
        }
    }

Свойства public $query и public $headerText и содержат те самые настройки из виджета.
Если при инициализации виджета они не установлены, то они будут равны null. Поэтому для
некоторых настроек нужна проверка в function init(). Там можно установить значения по умолчанию
или выкинуть исключение, если настройка обязательна.

Теперь передаем данные в html:
//MaterialListWidget
public function run()
{
    $this->registerAssets();//выносим регистрацию стилей в отдельный метод

    
    if($this->query->count() > 0){
        $header = $this->headerText;
        $query = $this->query->all();
        
        return $this->render('index', compact('header', 'query'));
    }
    
    return '';
}​

В файле вида в виджете (views/index.php) выводим заголовок блока в нужном месте и с помощью цикла проходимся по $query
и выводим данные для каждой записи:

<?php
use yii\helpers\Url;

?>

<!--widget-->
<div class="row linkbox">
    <h2 class="h_style_decoration">
        <i class="fa fa-bookmark-o color-salat" aria-hidden="true"></i>
        <span class="h_style_decoration-text"><?= $header; ?></span>
    </h2> 
    
    <?php foreach($query as $q): ?>
    <div class="col-sm-4 col-xs-6 linkbox-elem" data-link="<?= Url::toRoute(['/blog/article', 'alias' => $q->alias]);?>"> 
        <div class="linkbox-elem-header">
            <?php echo Html::img("@img-web-blog-path/{$q->image}", ['alt' => '', 'title' => '', 'class' => 'linkbox-elem-img']); ?>
        </div>
        <div class="linkbox-elem-footer">
            <p class="linkbox-elem-category">
                <a href="<?= Url::toRoute(['/blog/category', 'alias' => $q->category->alias]);?>"><?= $q->category->title; ?></a>
            </p>
            
            <p class="linkbox-elem-title">
                <a href="<?= Url::toRoute(['/blog/article', 'alias' => $q->alias]);?>">$q->title</a>
                
            </p>
            <p class="linkbox-elem-data">
                <span><?= \Yii::$app->formatter->asDateTime($q->createdAt, $pattern = 'php:d. M Y');?></span>
            </p>
        </div>
    </div>
    <?php endforeach; ?>
    
</div>
Теперь блок популярных материалов выводится с данными из модели. Виджет работает нормально.

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

Вообще, виджет должен быть так спроектирован, чтобы вся его настройка выполнялась на этапе
создания без правки кода виджета:
<!--Тут создается виджет-->
<?= MaterialListWidget::widget([
    'query' => $sql,
    'headerText' => 'Также по теме:',
]); ?> ​

Именно так устроены виджеты в yii2, такие как yii\widgets\DetailView или
yii\widgets\GridView и пр.

Настройка позволяет делать виджет гибким, в определенных рамках конечно. Должна быть какая-то задача, которую решает виджет,его ядро если хотите. Например так,  как это реализовано в том же yii\widgets\GridView (можно модефицировать данные и стили,
но на выходе получаем в любом случае таблицу). Т.е. гибкость должна быть, но в меру необходимости для решения специфических задач.

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

Читайте также из этой серии:

Приветствую!

Меня зовут Сергей. Я - автор этого блога.

Если Вам был полезен материал на моем сайте, поддержите пожалуйста мой проект, чтобы о нем узнали другие люди - кликните plizz :) на иконку в соц. сети, чтобы поделиться материалом с другими.