Пример вывода массива с поиском, сортировкой и пагинацией с помощью виджета GridView

Обсуждение документации второй версии фреймворка. Переводы Cookbook и авторские рецепты.
Ответить
ladserg
Сообщения: 8
Зарегистрирован: 2012.05.04, 12:02

Пример вывода массива с поиском, сортировкой и пагинацией с помощью виджета GridView

Сообщение ladserg »

Введение

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

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

Пример состоит из трёх файлов:
  • protected/models/samples/ArraySample.php — модель работы с массивом. Именно в ней описано, где получать данные, структура данных, как их фильтровать, и в ней же будет формироваться провайдер для виджета GridView.
  • protected/views/test/index.php — представление массива с использованием виджета GridView.
  • protected/controllers/TestController.php — контроллер, связывающий воедино модель и представление.
Я постарался максимально задокументировать код примеров в комментариях в формате разметки doxygen, что бы при просмотре кода была понятна логика решения задачи.

Невнятное описание решения

Вся суть в модели. Создаём модель, которая будет:
  • Получать данные в виде массива, как — не важно, откуда — тоже не важно, зависит от вашей задачи.
  • Фильтровать полученные данные, если источник позволяет в запросе отсылать ему фильтр получаемых данных, то формируем запрос к источнику данных с учетом фильтров, если это возможно.
    Но не во всех случаях получается сформировать запрос к источнику с учетом фильтра. Так, к примеру, дата в источнике может храниться в формате timestamp, тогда фильтровать уже придется полученные из источника данные и возвращать пользователю уже отфильтрованные данные.
  • Формировать провайдер для виджета. В качестве провайдера выбран класс \yii\data\ArrayDataProvider, именно он обеспечит нам как связь массива с виджетом, так и сортировку массива.
Созданная модель и будет использоваться виджетом GridView.

Исходный код файлов

Файл protected/models/samples/ArraySample.php

Файл с описанием модели:

Код: Выделить всё

<?
/**
 * @file protected/models/samples/ArraySample.php
 * @brief Модель @ref app::models::samples::ArraySample "app\\models\\samples\\ArraySample"
 * @author ladserg
 * @date 2021/07/11
 * @version $Id: $
**/

namespace app\models\samples;
use yii\helpers\ArrayHelper;

/**
 * @class ArraySample
 * @ingroup Models
 * @brief Пример модели работы с массивами данных.
 * 
 * Данный пример содержит минимально необходимый код организации вывода
 * массива при помощи 
 * [GridView](https://www.yiiframework.com/doc/api/2.0/yii-grid-gridview),
 * с поддержкой сортировки, фильтрации строк и пагинации.
**/
class ArraySample extends \yii\base\Model
{
    // Для фильтрации необходимо для каждой колонки массива определить атрибут.
    /**
     * @var string $name
     * Название праздника.
     */
    public $name;
    /**
     * @var timestamp $date
     * Дата праздника.
     */
    public $date;
    /**
     * @var integer $holiday
     * Признак выходного дня
     * - 0 - рабочий день
     * - 1 - выходной день
     */
    public $holiday;
    /**
     * @var integer $pageSize
     * Начальное количество строк на странице для пагинации.
     * 
     * Т.к. массив у нас маленький, то для демонстрации пагинации укажем
     * значение поменьше.
     */
    public static $pageSize=5;

    /**
     * @var const DATE_FMT
     * Формат даты.
     * Что бы не путаться, переопределим в качестве константы формат даты, для
     * дальнейшего его использования при фильтрации и выводе значений.
     */
    public const DATE_FMT='Y-m-d';

    /**
     * @brief Получение меток атрибутов массива.
     * @details Этот метод позволяет виджету GridView получить человеко-читаемые
     * названия столбцов. Без этого метода в качестве заголовков столбцов, при
     * выводе таблицы, будут выступать индексы значений.
     * @return array - Ассоциированный массив атрибутов с их метками.
     */
    public function attributeLabels()
    {
        return [
            'name'=>'Праздник',
            'date'=>'Дата',
            'holiday'=>'Выходной',
        ];
    }

    /**
     * @brief Получение правил валидации значений атрибутов сущности.
     * @details По сути нам валидировать нечего, но если не перечислить поля
     * в правилах, то GridView просто не позволит по неуказанному полю
     * фильтровать.
     * 
     * Достаточно указать тип полей. Можно все поля указать как строки, можно
     * организовать валидацию и посложнее, к примеру проверять, что бы в
     * качестве даты не вводили всякую похабщину, или ограничить количество
     * символов в строках.
     * @return array - Массив правил.
     */
    public function rules()
    {
        return [
            [['name', 'date',],'string'],
            [['holiday', ],'integer'],
        ];
    }

    /**
     * @brief Получение строк массива.
     * @details В реальности, массив может быть получен из любого источника,
     * это может быть и результат хитрого запроса к БД, может быть результат
     * запроса к API веб-сайта возвращенного в любом формате (CSV, JSON, XML,
     * т.д.). Для нашего примера оный метод будет возвращать статический
     * массив данных.
     * 
     * Для примера возьмём список праздников из виртуального личного
     * календаря за 2021, в который добавим поле указывающее выходной день
     * это или нет:
     *   * значение 1 - будет соответствовать выходному дню,
     *   * значение 0 - будет соответствовать рабочему дню,
     * 
     * Наш список будет выглядеть так:
     * name                         | date          | holiday
     * -----------------------------|--------------:|:-------:
     * Новый год                    | 2021.01.01    |   1
     * Рождество Христово           | 2021.01.07    |   1
     * Д.р. тёщи                    | 2021.02.09    |   0
     * День защитника Отечества     | 2021.02.23    |   1
     * Международный женский день   | 2021.03.08    |   1
     * Днюха Ильюхи                 | 2021.04.15    |   0
     * Днюха Таньки                 | 2021.04.21    |   0
     * Праздник весны и труда       | 2021.05.01    |   1
     * День Победы                  | 2021.05.09    |   1
     * День России                  | 2021.06.12    |   1
     * Д.р. жены                    | 2021.07.29    |   0
     * День свадьбы                 | 2021.09.01    |   0
     * День народного единства      | 2021.11.04    |   1
     * Корпоратив                   | 2021.12.30    |   0
     * 
     * Что за праздники и что за личности в таблице фигурируют нам не важно, оную
     * таблицу мы просто возьмём за пример для нашего массива.
     * 
     * При этом дату будем хранить в виде метки Unix (timestamp), для последующих
     * примеров фильтрации.
     * @return array - Массив данных
     */
    public static function getAll()
    {
        return [
            [
                'name'=>'Новый год',
                'date'=>mktime(0, 0, 0,  1,  1, 2021),
                'holiday'=>1,
            ],
            [
                'name'=>'Рождество Христово',
                'date'=>mktime(0, 0, 0,  1,  7, 2021),
                'holiday'=>1,
            ],
            [
                'name'=>'Д.р. тёщи',
                'date'=>mktime(0, 0, 0,  2,  9, 2021),
                'holiday'=>0,
            ],
            [
                'name'=>'День защитника Отечества',
                'date'=>mktime(0, 0, 0,  2, 23, 2021),
                'holiday'=>1,
            ],
            [
                'name'=>'Международный женский день',
                'date'=>mktime(0, 0, 0,  3,  8, 2021),
                'holiday'=>1,
            ],
            [
                'name'=>'Днюха Ильюхи',
                'date'=>mktime(0, 0, 0,  4, 15, 2021),
                'holiday'=>0,
            ],
            [
                'name'=>'Днюха Таньки',
                'date'=>mktime(0, 0, 0,  4, 21, 2021),
                'holiday'=>0,
            ],
            [
                'name'=>'Праздник весны и труда',
                'date'=>mktime(0, 0, 0,  5,  1, 2021),
                'holiday'=>1,
            ],
            [
                'name'=>'День Победы',
                'date'=>mktime(0, 0, 0,  5,  9, 2021),
                'holiday'=>1,
            ],
            [
                'name'=>'День России',
                'date'=>mktime(0, 0, 0,  6, 12, 2021),
                'holiday'=>1,
            ],
            [
                'name'=>'Д.р. жены',
                'date'=>mktime(0, 0, 0,  7, 29, 2021),
                'holiday'=>0,
            ],
            [
                'name'=>'День свадьбы',
                'date'=>mktime(0, 0, 0,  9,  1, 2021),
                'holiday'=>0,
            ],
            [
                'name'=>'День народного единства',
                'date'=>mktime(0, 0, 0, 11,  4, 2021),
                'holiday'=>1,
            ],
            [
                'name'=>'Корпоратив',
                'date'=>mktime(0, 0, 0, 12, 30, 2021),
                'holiday'=>0,
            ],
        ];
    }

    /**
     * @brief Получение отфильтрованных строк массива.
     * @details Это как раз и есть основной метод получения данных. И именно
     * в этом методе мы и опишем как фильтровать наши строки.
     * 
     * В реальности источники данных могут поддерживать фильтрацию, к примеру
     * LDAP в запросах позволяют указывать фильтры, различные API так же
     * позволяют в своих запросах указывать параметры фильтрации. Именно в этом
     * методе и нужно формировать запросы с учётом фильтров и получать данные
     * из нужного источника с использованием сформированного запроса.
     * 
     * В нашем случае мы имеем обычный массив, поэтому мы без всяких затей в
     * цикле пройдёмся по нему, то что не подходит под наш фильтр мы отбросим,
     * а то что подходит добавим в результирующий массив, который вернём в
     * качестве результата.
     * @param array $params - параметры фильтрации, обычно их генерирует GridView.
     * Параметры фильтрации содержатся в подмассиве $params[Имя_модели]
     * в виде значений 'поле'=>'фильтр'.
     * @return array - Массив данных.
     */
    public function find($params=NULL)
    {
        /**
         * Загрузим параметры фильтрации в модель. Если не сделать этого, то при
         * применении фильтра поля фильтров останутся пустыми.
         * 
         * Так же это позволит нам использовать значения атрибутов объекта,
         * не прибегая к массиву параметров, для выяснения значений полей
         * фильтрации.
         */
        $this->load($params);

        // Получим наш массив
        $request=static::getAll();

        // Если параметры фильтрации пусты, то просто вернём весь массив
        if(empty($params)) return $request;

        // Определим пустой массив результатов, в него мы будем добавлять
        // отфильтрованные строки.
        $res=[];

        // Распарсим наш массив и без всяких затей проверим каждую строку на
        // соответствие условиям фильтра
        foreach($request as $row) {
            // Отбросим все строки, по попадающие под фильтр имени
            if($this->name != "") {
                /*
                 * Тут отражена проблема кириллицы. Для регистронезависимого
                 * поиска нужно оба значения (фильтр и значение поля) привести
                 * к одному регистру, функция strtolower не умеет делать этого
                 * для кириллических букв, поэтому пришлось к нижнему регистру
                 * привести текст при помощи функции mb_strtolower.
                 * 
                 * Если вам не нужен регистронезависимый поиск.. то преобразование
                 * регистра можно убрать.
                 * 
                 * Сам по себе фильтр по полю name применяется простым поиском
                 * вхождение строки, если строка не найдена.. то пропускаем
                 * итерацию цикла, и данная строка не попадёт в результирующий
                 * массив.
                */
                if(strpos(mb_strtolower($row["name"]),mb_strtolower($this->name))===false)
                    continue;
            };
            // Отбросим все строки, не попадающие под фильтр выходного дня
            if($this->holiday != "") {
                if($row["holiday"]!=$this->holiday) continue;
            };
            // Фильтрация даты... мы без всяких экивоков преобразуем дату в строку и
            // поищем фильтруемое значение в уже полученной строке.
            if($this->date!="") {
                if(strpos(date(static::DATE_FMT, $row["date"]),$this->date)===false)
                    continue;
            };

            // Если все проверки прошли, то добавим строку к результирующему
            // массиву.
            $res[]=$row;
        }

        // Вернём то что осталось от фильтрации.
        return $res;
    }

    /**
     * @brief Подготовка фильтрации строк, возвращает готовый к употреблению
     *   провайдер.
     * @details Именно этот метод будет возвращать сформированный провайдер для
     * значения dataProvider виджета GridView.
     * @param array $params - параметры фильтрации, обычно их генерирует
     *   GridView.
     * @return mixed - Провайдер данных.
     */
    public function search($params)
    {
        // Сформируем провайдер
        $dataProvider = new \yii\data\ArrayDataProvider([
            // В качестве модели укажем метод, который вернёт нам нужный массив,
            // с уже отфильтрованными строками.
            'allModels' => $this->find($params),
            // Включим пагинацию.
            'pagination' => [
                'pageSize' => static::$pageSize,
            ],
            // При желании пагинацию можно отключить совсем
            // 'pagination' => false,
            // Если пагинация отключена, то можно отключить и подсчёт количества
            // строк
            // 'totalCount' => 0,
            /*
             * Опишем, как сортировать наш массив. Саму сортировку на себя возьмёт
             * провайдер, нам же нужно сказать ему по каким правилам, какие поля
             * сортировать. Без этого описания GridView не предоставит нам
             * возможности сортировать наши данные.
             */
            'sort' => [
                'defaultOrder'=>[ 'name'=> SORT_ASC, ],
                'attributes' => [
                    'name' => [
                        'asc' => ['name' => SORT_ASC],
                        'desc' => ['name' => SORT_DESC],
                        'default' => SORT_DESC,
                    ],
                    'date' => [
                        'asc' => ['date' => SORT_ASC],
                        'desc' => ['date' => SORT_DESC],
                        'default' => SORT_DESC,
                    ],
                    'holiday' => [
                        'asc' => ['holiday' => SORT_ASC, 'name' => SORT_ASC],
                        'desc' => ['holiday' => SORT_DESC, 'name' => SORT_ASC],
                        'default' => SORT_DESC,
                    ],
                ],
            ],
        ]);

        /*
         * Это проверка значений полей фильтров, если что-то введено неверно, то
         * GridView выведет ошибку в соответствующем поле. Это нужно если вы
         * проверяете, что пользователь вводит в качеств фильтра. К примеру в
         * нашем случае в поле holiday требуется вводить число, и если будет
         * введено что-то иное, то пользователь получит сообщение об ошибочном
         * значении.
         * 
         * Если вам не требуется проверка вводимых значений, то ниже приведенное
         * условие можно закомментировать.
         */
        if (!$this->validate()) return $dataProvider;

        return $dataProvider;
    }
};
Файл protected/views/test/index.php

Файл представления нашего массива:

Код: Выделить всё

<?
/**
 * @file protected/views/test/index.php
 * @ingroup Views
 * @brief Представление 
 * @author ladserg
 * @date 2021/07/11
 * @version $Id: $
*/
use app\models\samples\ArraySample;?>
<?=\yii\grid\GridView::widget([
    /**
     * Модель фильтрации. Если её не указать, то поля с фильтрами не отобразятся,
     * При этом параметры фильтрации будут передаваться модели.
     * 
     * Если не указать модель фильтрации, то виджет не сможет получить метки
     * атрибутов, и в заголовках колонок будет использоваться имена полей массива,
     * а не человекочитаемые названия.
     */
    'filterModel' => $searchModel,
    'dataProvider' => $provider,
    // Показывать грид, когда нет результатов?
    // П.С. если колонки не определить, то при отсутствии результатов
    // не отображаются названия колонок и поля фильтров.
    'showOnEmpty' => true,
    // Отображать заголовок таблицы?
    'showHeader' => true,
    // Отображать футер таблицы?
    'showFooter' => false,
    /*
     * Строка-шаблон вывода элементов грида. Например:
     *   'layout' => "{pager}\n{summary}\n{items}\n{pager}",
     * 
     * При отключенной пагинации, и подсчёте количества строк, можно убрать
     * лишнее раскомментировав строку ниже.
     */
    // 'layout' => '{items}',
    // Или переместить информацию о показанных записях вниз, для личного удобства:
    'layout' => "{items}\n{summary}\n{pager}",
    'caption' => '<h2>Пример вывода и фильтрации строк массива</h2>',
    'columns' => [
        [
            'class' => \yii\grid\SerialColumn::className(), 
            'header' => '№',
            'contentOptions' => [
                'style' => 'min-width:30px;width:30px;',
                'align' => 'right'
            ],

        ],
        ['attribute' => 'name',],
        ['attribute' => 'date',
            'contentOptions' => [
                'align' => 'right',
                'style' => 'width: 100px; white-space: normal;',
            ],
            'value' => function($data) {
                return date(ArraySample::DATE_FMT, $data['date']);
            },
        ],
        ['attribute' => 'holiday',
            // Что бы не путать пользователя ноликами и единичками, дадим ему в
            // качестве фильтра выпадающее меню с человекочитаемыми значениями.
            'filter' => [0 => 'Нет', 1 => 'Да',],
            'contentOptions' => [
                'align' => 'center',
                'style' => 'width: 20px; white-space: normal;',
            ],
            'value' => function($data) {
                return $data['holiday']==1?'Да':'Нет';
            },
        ],
    ],
]);?>
Файл protected/controllers/TestController.php

Контроллер:

Код: Выделить всё

<?
/**
 * @file protected/controllers/TestController.php
 * @brief Класс @ref app::controllers::TestController "TestController"
 * @author ladserg
 * @date 2021/07/11
 * @version $Id: $
*/

namespace app\controllers;

use yii\web\Controller;
use app\models\samples\ArraySample;
use Yii;

/**
 * @class TestController
 * @ingroup Controllers
 * @brief Контроллер @ref app::controllers::TestController "/test"
*/
class TestController extends Controller
{
    /**
    * @brief Событие по умолчанию.
    */
    public function actionIndex()
    {
        $data['queryParams']=Yii::$app->request->queryParams;
        // В качестве модели поиска будет выступать объект созданной нами модели
        // работы с массивами.
        $data['searchModel']=new ArraySample();
        // Провайдер для GridView сформирует метод search исходя из параметров
        // запроса
        $data['provider'] = $data['searchModel']->search($data['queryParams']);
        return $this->render('index', $data);
    }
}

unknownby
Сообщения: 689
Зарегистрирован: 2019.11.05, 16:34
Контактная информация:

Re: Пример вывода массива с поиском, сортировкой и пагинацией с помощью виджета GridView

Сообщение unknownby »

Зачем всё это? Можно ведь в GridView от kartik-v передавать массив, вместо объекта. Фильтрация, пагинация и сортировка отлично работает. :)

ladserg
Сообщения: 8
Зарегистрирован: 2012.05.04, 12:02

Re: Пример вывода массива с поиском, сортировкой и пагинацией с помощью виджета GridView

Сообщение ladserg »

unknownby писал(а):
2021.07.31, 19:29
Зачем всё это? Можно ведь в GridView от kartik-v передавать массив, вместо объекта. Фильтрация, пагинация и сортировка отлично работает. :)
В основном для себя. Просто выложил для других.

И тут не про GridView, тут про модель.

unknownby
Сообщения: 689
Зарегистрирован: 2019.11.05, 16:34
Контактная информация:

Re: Пример вывода массива с поиском, сортировкой и пагинацией с помощью виджета GridView

Сообщение unknownby »

ladserg писал(а):
2021.07.31, 20:19
В основном для себя. Просто выложил для других.
И тут не про GridView, тут про модель.
Лично моё мнение - полезности не увидел.
Модель берёт данные из самой модели, а не из БД это первое.
Во вторых есть метод asArray(), который преобразует возврат поиска в массив.
С сортировками, фильтрацией и пагинацией все намного проще можно было бы сделать, используя готовые решения. Посмотрите как это реализовано от картика, надеюсь вам понравится. Сам использую его решения.

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

ladserg
Сообщения: 8
Зарегистрирован: 2012.05.04, 12:02

Re: Пример вывода массива с поиском, сортировкой и пагинацией с помощью виджета GridView

Сообщение ladserg »

Нет, так нет. Данные в модели только для примера. В реальности они берутся из внешних источников.

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

Ответить