Провайдеры данных

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

Провайдер данных это класс, который реализует yii\data\DataProviderInterface. Такая реализация поддерживает в основном разбивку на страницы и сортировку. Они обычно используются для работы виджетов данных, что позволяет конечным пользователям интерактивно использовать сортировку данных и их разбивку на страницы.

В Yii реализованы следующие классы провайдеров данных:

  • yii\data\ActiveDataProvider: использует yii\db\Query или yii\db\ActiveQuery для запроса данных из базы данных, возвращая их в виде массива или экземпляров Active Record.
  • yii\data\SqlDataProvider: выполняет запрос SQL к базе данных и возвращает результат в виде массива.
  • yii\data\ArrayDataProvider: принимает большой массив и возвращает выборку из него с возможностью сортировки и разбивки на страницы.

Использование всех этих провайдеров данных имеет общую закономерность:

// создание провайдера данных с конфигурацией для сортировки и постраничной разбивки
$provider = new XyzDataProvider([
    'pagination' => [...],
    'sort' => [...],
]);

// Получение данных с разбивкой на страницы и сортировкой.
$models = $provider->getModels();

// получение количества данных на текущей странице
$count = $provider->getCount();

// получение общего количества данных на всех страницах
$totalCount = $provider->getTotalCount();

Определение поведений сортировки и разбивки для провайдера данных устанавливается через его свойства yii\data\BaseDataProvider::pagination и yii\data\BaseDataProvider::sort, которые соответствуют настройкам yii\data\Pagination and yii\data\Sort. Вы можете отключить сортировку и разбивку на страницы путём выставления их настроек в false.

Виджеты данных, такие как yii\grid\GridView, имеют свойство dataProvider, которое может принимать экземпляр провайдера данных для отображения его данных. Например:

echo yii\grid\GridView::widget([
    'dataProvider' => $dataProvider,
]);

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

ActiveDataProvider

Для использования yii\data\ActiveDataProvider, необходимо настроить его свойство query. Оно принимает любой объект yii\db\Query или yii\db\ActiveQuery. Если использовать первый, то данные будут возвращены в виде массивов, если второй - данные также могут быть возвращены в виде массивов, а также в виде экземпляров Active Record. Например:

use yii\data\ActiveDataProvider;

$query = Post::find()->where(['status' => 1]);

$provider = new ActiveDataProvider([
    'query' => $query,
    'pagination' => [
        'pageSize' => 10,
    ],
    'sort' => [
        'defaultOrder' => [
            'created_at' => SORT_DESC,
            'title' => SORT_ASC, 
        ]
    ],
]);

// возвращает массив Post объектов
$posts = $provider->getModels();

Если изменить $query в этом примере на следующий код, то будут возвращены сырые массивы.

use yii\db\Query;

$query = (new Query())->from('post')->where(['status' => 1]); 

Note: Если query содержит условия сортировки в orderBy, то новые условия, полученные от конечных пользователей (через настройки sort) будут добавлены к существующим условиям в orderBy. Любые условия в limit и offset будут переписаны запросом конечного пользователя к различным страницам (через конфигурацию pagination).

По умолчанию yii\data\ActiveDataProvider использует компонент приложения db для подключения к базе данных. Можно использовать разные базы данных, настроив подключение через конфигурацию свойства yii\data\ActiveDataProvider::$db.

SqlDataProvider

yii\data\SqlDataProvider работает с сырыми запросами SQL, которые используются для извлечение необходимых данных. Основываясь на спецификации из yii\data\SqlDataProvider::sort и yii\data\SqlDataProvider::pagination, провайдер данных будет добавлять конструкции ORDER BY и LIMIT к SQL-запросу, для возврата только запрошенной страницы данных с учётом определённой сортировки.

Для использования yii\data\SqlDataProvider, необходимо настроить свойства sql и yii\data\SqlDataProvider::totalCount. Например:

use yii\data\SqlDataProvider;

$count = Yii::$app->db->createCommand('
    SELECT COUNT(*) FROM post WHERE status=:status
', [':status' => 1])->queryScalar();

$provider = new SqlDataProvider([
    'sql' => 'SELECT * FROM post WHERE status=:status',
    'params' => [':status' => 1],
    'totalCount' => $count,
    'pagination' => [
        'pageSize' => 10,
    ],
    'sort' => [
        'attributes' => [
            'title',
            'view_count',
            'created_at',
        ],
    ],
]);

// возвращает массив данных
$models = $provider->getModels();

Совет: Свойство yii\data\SqlDataProvider::totalCount обязательно только тогда, когда вам нужна разбивка на страницы. Всё потому, что запрос SQL sql будет изменяться провайдером данных для возврата только текущей запрошенной страницы. Провайдеру необходимо знать общее количество данных в запросе для корректного вычисления разбивки на доступные страницы.

ArrayDataProvider

yii\data\ArrayDataProvider лучше использовать для работы с большим массивом. Этот провайдер помогает вернуть выборку из большого массива с сортировкой по одному или нескольким колонкам. Для использования yii\data\ArrayDataProvider необходимо определить свойство allModels, как большой массив. Элементы в большом массиве могут быть ассоциативными массивами (например, результаты выборки из DAO) или объекты (экземпляры Active Record). Например:

use yii\data\ArrayDataProvider;

$data = [
    ['id' => 1, 'name' => 'name 1', ...],
    ['id' => 2, 'name' => 'name 2', ...],
    ...
    ['id' => 100, 'name' => 'name 100', ...],
];

$provider = new ArrayDataProvider([
    'allModels' => $data,
    'pagination' => [
        'pageSize' => 10,
    ],
    'sort' => [
        'attributes' => ['id', 'name'],
    ],
]);

// получает строки для текущей запрошенной странице
$rows = $provider->getModels();

Note: По сравнению с Active Data Provider и SQL Data Provider, ArrayDataProvider менее эффективный, потому что требует загрузки всех данных в память.

Принципы работы с ключами данных

При возврате данных с помощью провайдера, часто требуется идентификация каждого элемента по уникальному ключу. Например, если данные - это какая-то информация по клиенту, то возможно понадобится использовать ID клиента, как ключ для данных по каждому клиенту. Провайдер данных через yii\data\DataProviderInterface::getModels() может вернуть список из ключей и соответствующего набора данных. Например,

use yii\data\ActiveDataProvider;

$query = Post::find()->where(['status' => 1]);

$provider = new ActiveDataProvider([
    'query' => $query,
]);

// возвращает массив объектов Post
$posts = $provider->getModels();

// возвращает значения первичного ключа в соответствии с $posts
$ids = $provider->getKeys();

В вышеописанном примере, так как yii\data\ActiveDataProvider предоставляется один yii\db\ActiveQuery объект, то в этом случае провайдер достаточно умён, чтобы вернуть значения первичных ключей в качестве идентификатора. Также есть возможность настроить способ вычисления значения идентификатора, через настройку yii\data\ActiveDataProvider::$key, как имя колонки или функцию вычисления значений ключа. Например:

// в качестве ключа используется столбец "slug"
$provider = new ActiveDataProvider([
    'query' => Post::find(),
    'key' => 'slug',
]);

// в качестве ключа используется md5(id)
$provider = new ActiveDataProvider([
    'query' => Post::find(),
    'key' => function ($model) {
        return md5($model->id);
    }
]);

Создание своего провайдера данных

Для создания своих классов провайдера данных, необходимо реализовать yii\data\DataProviderInterface. Простой способ сделать это - наследовать yii\data\BaseDataProvider, который помогает сфокусироваться на логике ядра провайдера данных. В основном необходимо реализовать следующие методы:

  • prepareModels(): подготавливает модели данных, которые будут доступны в текущей странице и возвращает их в виде массива.
  • prepareKeys(): принимает массив имеющихся в настоящее время моделей данных и возвращает ключи, связанные с ними.
  • prepareTotalCount: возвращает значение, указывающее общее количество моделей данных в провайдере данных.

Ниже приведён пример провайдера данных, который эффективно считывает данные из CSV:

<?php
use yii\data\BaseDataProvider;

class CsvDataProvider extends BaseDataProvider
{
    /**
     * @var string имя CSV-файла для чтения
     */
    public $filename;
    
    /**
     * @var string|callable имя столбца с ключом или callback-функция, возвращающие его
     */
    public $key;
    
    /**
     * @var SplFileObject
     */
    protected $fileObject; // с помощью SplFileObject очень удобно искать конкретную строку в файле
    
 
    /**
     * {@inheritdoc}
     */
    public function init()
    {
        parent::init();
        
        // открыть файл
        $this->fileObject = new SplFileObject($this->filename);
    }
 
    /**
     * {@inheritdoc}
     */
    protected function prepareModels()
    {
        $models = [];
        $pagination = $this->getPagination();
 
        if ($pagination === false) {
            // в случае отсутствия разбивки на страницы - прочитать все строки
            while (!$this->fileObject->eof()) {
                $models[] = $this->fileObject->fgetcsv();
                $this->fileObject->next();
            }
        } else {
            // в случае, если разбивка на страницы есть - прочитать только одну страницу
            $pagination->totalCount = $this->getTotalCount();
            $this->fileObject->seek($pagination->getOffset());
            $limit = $pagination->getLimit();
 
            for ($count = 0; $count < $limit; ++$count) {
                $models[] = $this->fileObject->fgetcsv();
                $this->fileObject->next();
            }
        }
 
        return $models;
    }
 
    /**
     * {@inheritdoc}
     */
    protected function prepareKeys($models)
    {
        if ($this->key !== null) {
            $keys = [];
 
            foreach ($models as $model) {
                if (is_string($this->key)) {
                    $keys[] = $model[$this->key];
                } else {
                    $keys[] = call_user_func($this->key, $model);
                }
            }
 
            return $keys;
        } else {
            return array_keys($models);
        }
    }
 
    /**
     * {@inheritdoc}
     */
    protected function prepareTotalCount()
    {
        $count = 0;
 
        while (!$this->fileObject->eof()) {
            $this->fileObject->next();
            ++$count;
        }
 
        return $count;
    }
}