Data Providers (Provedores de Dados)

Nas seções Paginação e Ordenação, descrevemos como os usuários finais podem escolher uma determinada página de dados para exibir e ordená-los por determinadas colunas. Uma vez que esta tarefa de paginação e ordenação de dados é muito comum, o Yii fornece um conjunto de classes data provider para encapsular estes recursos.

Um data provider é uma classe que implementa yii\data\DataProviderInterface. Ele suporta principalmente a recuperação de dados paginados e ordenados. Geralmente é usado para trabalhar com widgets de dados de modo que os usuários finais possam interativamente paginar e ordenar dados.

O Yii fornece as seguintes classes de data provider:

O uso de todos estes data providers compartilham o seguinte padrão comum:

// cria o data provider configurando suas propriedades de paginação e ordenação
$provider = new XyzDataProvider([
  'pagination' => [...],
  'sort' => [...],
]);

// recupera dados paginados e ordenados
$models = $provider->getModels();

// obtém o número de itens de dados na página atual
$count = $provider->getCount();

// obtém o número total de itens de dados de todas as páginas
$totalCount = $provider->getTotalCount();

Você define o comportamento da paginação e da ordenação do data provider configurando suas propriedades yii\data\BaseDataProvider::pagination e yii\data\BaseDataProvider::sort que correspondem às configurações yii\data\Pagination e yii\data\Sort respectivamente. Você também pode configurá-los como false para desativar os recursos de paginação e/ou ordenação.

Os widgets de dados, assim como yii\grid\GridView, tem uma propriedade chamada dataProvider que pode receber uma instância de data provider e exibir os dados que ele fornece. Por exemplo:

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

Estes data providers variam principalmente conforme a fonte de dados é especificada. Nas subseções seguintes, vamos explicar o uso detalhado de cada um dos data providers.

Active Data Provider

Para usar yii\data\ActiveDataProvider, você deve configurar sua propriedade query. Ele pode receber qualquer um dos objetos yii\db\Query ou yii\db\ActiveQuery. Se for o primeiro, os dados serão retornados em array; se for o último, os dados podem ser retornados em array ou uma instância de Active Record. Por exemplo:

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,
      ]
  ],
]);

// retorna um array de objetos Post
$posts = $provider->getModels();

Se $query no exemplo acima fosse criada usando o código a seguir, então o data provider retornaria um array.

use yii\db\Query;

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

Observação: Se uma query já especificou a cláusula orderBy, as novas instruções de ordenação dadas por usuários finais (através da configuração sort) será acrescentada a cláusula orderBy existente. Existindo qualquer uma das cláusulas limit e offset será substituído pelo request de paginação dos usuários finais (através da configuração pagination`).

Por padrão, yii\data\ActiveDataProvider utiliza o componente da aplicação db como a conexão de banco de dados. Você pode usar uma conexão de banco de dados diferente, configurando a propriedade yii\data\ActiveDataProvider::$db.

SQL Data Provider

O yii\data\SqlDataProvider trabalha com uma instrução SQL, que é usado para obter os dados necessários. Com base nas especificações de yii\data\SqlDataProvider::sort e yii\data\SqlDataProvider::pagination, o provider ajustará as cláusulas ORDER BY e LIMIT da instrução SQL em conformidade para buscar somente a página de dados solicitada na ordem desejada.

Para usar yii\data\SqlDataProvider, você deve especificar a propriedade sql bem como a propriedade yii\data\SqlDataProvider::totalCount. Por exemplo:

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',
      ],
  ],
]);

// retorna um array de linha de dados
$models = $provider->getModels();

Observação: A propriedade yii\data\SqlDataProvider::totalCount é requerida somente se você precisar paginar os dados. Isto porque a instrução SQL definida por sql será modificada pelo provider para retornar somente a página atual de dados solicitada. O provider ainda precisa saber o número total de dados a fim de calcular corretamente o número de páginas disponíveis.

Array Data Provider

O yii\data\ArrayDataProvider é melhor usado quando se trabalha com um grande array. O provider permite-lhe retornar uma página dos dados do array ordenados por uma ou várias colunas. Para usar yii\data\ArrayDataProvider, você precisa especificar a propriedade allModels como um grande array. Elementos deste array podem ser outros arrays associados (por exemplo, resultados de uma query do DAO) ou objetos (por exemplo, uma instância do Active Record). Por exemplo:

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'],
  ],
]);

// obter as linhas na página corrente
$rows = $provider->getModels();

Observação: Comparando o Active Data Provider com o SQL Data Provider, o array data provider é menos eficiente porque requer o carregamento de todo os dados na memória.

Trabalhando com Chave de Dados

Ao usar os itens de dados retornados por um data provider, muitas vezes você precisa identificar cada item de dados com uma chave única. Por exemplo, se os itens de dados representam as informações do cliente, você pode querer usar o ID do cliente como a chave para cada dado do cliente. Data providers podem retornar uma lista das tais chaves correspondentes aos itens de dados retornados por yii\data\DataProviderInterface::getModels(). Por exemplo:

use yii\data\ActiveDataProvider;

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

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

// retorna uma array de objetos Post
$posts = $provider->getModels();

// retorna os valores de chave primária correspondente a $posts
$ids = $provider->getKeys();

No exemplo abaixo, como você fornece um objeto yii\db\ActiveQuery para o yii\data\ActiveDataProvider, ele é inteligente o suficiente para retornar os valores de chave primária como chaves no resultado. Você também pode especificar explicitamente como os valores das chaves devem ser calculados configurando a propriedade yii\data\ActiveDataProvider::$key com um nome de coluna ou com uma função callback que retorna os valores das chaves. Por exemplo:

// usa a coluna "slug" como valor da chave
$provider = new ActiveDataProvider([
  'query' => Post::find(),
  'key' => 'slug',
]);

// usa o resultados do md5(id) como valor da chave
$provider = new ActiveDataProvider([
  'query' => Post::find(),
  'key' => function ($model) {
      return md5($model->id);
  }
]);

Criado Data Provider Personalizado

Para criar sua própria classe de data provider personalizada, você deve implementar o yii\data\DataProviderInterface. Um caminho fácil é estender de yii\data\BaseDataProvider, o que lhe permite concentrar-se na lógica principal do data provider. Em particular, você precisa principalmente implementar os seguintes métodos:

  • prepareModels(): prepara o data models que será disponibilizado na página atual e as retorna como um array.
  • prepareKeys(): recebe um array de data models disponíveis e retorna chaves que lhes estão associados.
  • prepareTotalCount: retorna um valor que indica o número total de data models no data provider.

Abaixo está um exemplo de um data provider que lê dados em CSV eficientemente:

<?php
use yii\data\BaseDataProvider;

class CsvDataProvider extends BaseDataProvider
{
  /**
   * @var string nome do arquivo CSV que será lido
   */
  public $filename;
  
  /**
   * @var string|nome da coluna chave ou função que a retorne
   */
  public $key;
  
  /**
   * @var SplFileObject
   */
  protected $fileObject; // SplFileObject é muito conveniente para procurar uma linha específica em um arquivo
  
  /**
   * {@inheritdoc}
   */
  public function init()
  {
      parent::init();
      
      // abre o arquivo
      $this->fileObject = new SplFileObject($this->filename);
  }

  /**
   * {@inheritdoc}
   */
  protected function prepareModels()
  {
      $models = [];
      $pagination = $this->getPagination();
      if ($pagination === false) {
          // no caso não há paginação, lê todas as linhas
          while (!$this->fileObject->eof()) {
              $models[] = $this->fileObject->fgetcsv();
              $this->fileObject->next();
          }
      } else {
          // no caso existe paginação, lê somente uma página
          $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;
  }
}