PHP - - By Arno Slatius

Rendering Data in Yii 2 with GridView and ListView

In my previous article about Yii 2.0, I introduced the ActiveRecord implementation. Once you have some data in your database you’ll want to be able to show it. Yii uses DataProviders to interact with data sources and it provides some widgets to output the data. Of these, the ListView and GridView provide the most functionality.

Yii logo

The example

I’m going to work with the database example that I introduced in my previous article again. Not very original, but quite useful as an example; a blog like SitePoint.

SitePoint Yii 2.0 blog example

A small wrap-up of this database design and its Yii models which I’ll be using in this example:

  • The Authors model has one relation method call getArticles() that provides an Articles[] attribute to the model.
  • The Articles model has one relation method call getAuthor() that provides the Author and a method getTags() that provides the Tags[].
  • The Tags model has one relation method call getArticles that provides Articles[].

I won’t be using the ArticlesTags table since it is just used for an N:M relation. Of course, there is a model available for it which is used in the relation definition of the other models.

DataProviders

There are three different types:

  • The ActiveDataProvider is fed through an ActiveQuery instance and usually holds an array of models. You’ll normally use it to hold and render data that can be built from normal ActiveRecord instances with their relations.
  • The ArrayDataProvider is built using an array of data. This is quite useful when building all sorts of reports based on multiple models and/or custom aggregations.
  • The SqlDataProvider is a very useful one for the SQL masters among us. It is an easy way to get the data of those complex queries.

They don’t have huge differences, except slightly in views. The data provider itself is a base component that holds data and handles pagination and sorting.

ActiveDataProvider

I already explained that this is populated with an ActiveQuery object. Any model::find() will return an ActiveQuery object so creating one is quite simple.

/* A dataprovider with all articles */
$dataProvider = new ActiveDataProvider([
    'query' => Articles::find()->with('tags'),
]);

/* Get all the articles for one author by using the author relation define in Articles */
$dataProvider = new ActiveDataProvider([
    'query' => Articles::find()->with('author')->where(['Name'=>'Arno Slatius']),
]);

/* Be careful with this! */
$dataProvider = new ActiveDataProvider([
    'query' => Articles::find()->with('author')->where(['Name'=>'Arno Slatius'])->orderBy('Published'),
]);

The last example is one to be careful with, you’ll limit the abilities of your DataProvider since it can do sorting on its own. Keep this in mind, I’ll address it later on.

SqlDataProvider

This one will gather your data based on a SQL statement. My example doesn’t really justify the use of SQL but I’ll do it anyway to show something and throw in some aggregation;

$dataProvider = new SqlDataProvider([
    'sql' => 'SELECT Name, Title, COUNT(ArticleTags.ID) AS TagCount ' . 
             'FROM Authors ' .
             'INNER JOIN Articles ON (Authors.ID = Articles.AuthorID) ' .
             'INNER JOIN ArticleTags ON (Articles.ID = ArticleTags.ID) ' .
             'WHERE Name=:author' .
             'GROUP BY ArticleID',
    'params' => [':author' => 'Arno Slatius'],
]);

There are good arguments to use an SQL query as input for your data provider. ActiveRecord is a very nice way to work with databases, but you might find that defining the criteria for larger databases gets somewhat complex. If you are good at writing SQL and you need a lot of joins, unions and/or aggregations you might find it easier to build an SQL query first and simply use that as an input to your DataProvider.

ArrayDataProvider

This might be the most useful component of the bunch. Anything that you’d normally put in a table that requires a bit more work to compose is well suited for an array data provider.
The documentation uses quite a simple example inserting a few models in an array like so:

$dataProvider = new ArrayDataProvider([
    'allModels' => Authors::find()->all(),
]);

That’ll fill the provider with all the authors. Very simple indeed, but let’s make it more practical and do something that this component is quite useful for; I want to output a list of all authors with their article count and last published date. I’m going to do this by extending the ArrayDataProvider and creating my own. I use the init() function which is available on almost all components. This is called when the class is created and used (in this case) to prepare the data in the provider.

namespace app\data;

use \app\models\Authors;
use \app\models\Articles;

class AuthorsReportProvider extends \yii\data\ArrayDataProvider
{
	/**
     * Initialize the dataprovider by filling allModels
     */
    public function init()
    {
        //Get all all authors with their articles
	    $query = Authors::find()->with('articles');
		foreach($query->all() as $author) {
			
			//Get the last published date if there are articles for the author
			if (count($author->articles)) {
				$lastPublished = $query->max('Published');
			} else {
				$lastPublished = null;
			}

			//Add rows with the Author, # of articles and last publishing date
			$this->allModels[] = [
				'name' => $authors->Name,
				'articles' => count($author->articles),
				'last' => $lastPublished,
			];
		}
	}
}

You can see that the variable containing all rows for the table to be shown, $this->allModels, is filled with the data we need.

Visualizing the data

We’ve got a data provider but now we want to render what’s in there, right? Yii provides two components to visualize the data put into the data provider.

  • The GridView will put the data in an HTML table. If properly configured, it’ll automatically add headers that you can click to change the sorting, pagination to limit the number of items shown at once to the user and a summary showing the total number of results available.
  • The ListView allows you to specify a partial view with which you can render each of the models in the data provider in a specific way. A very common way to go about this is using this to render the content of <li> elements of an ordered or unordered list.

It’s probably best illustrated with a few examples. Let’s assume the following data provider:

/* A dataprovider with all articles */
$dataProvider = new ActiveDataProvider([
    'query' => Articles::find()->with('tags','authors'),
]);

Note that I immediately make ActiveRecord execute a join of the Articles on the ArticleTags and Tags table by specifying the with() method. This is known as ‘eager loading’, making the related data available immediately. When done without the with() you’d still be able to use the data available from the author and tags relation in your views, but it would be loaded run time, also known as ‘lazy loading’. If you know you’re going to use related data, use with(). Eager loading performs a lot better than lazy loading.

In the view, we’d render a table with a GridView. I’m showing a few columns in increasing complexity, a simple list of the article first:

<?= GridView::widget([
    'dataProvider' => $dataProvider,
    'columns' => [
        'ID',
        'Author',
        'Title',
        'Published:date',
    ],
]); ?>

Which will look like this:

Yii 2.0 GridView Blog Articles Example

I do admit that I cheated slightly by defining a page size of 6 and three columns by which it can sort; more on that later. Note the summary at the top, the sort links in the column headers and the pager at the bottom. Everything is formatted in a localized format (Dutch for me) where applicable. I needed no additional code to get the handling of links for the sorting or next pages working. The links are all automatically generated by the widget and actions are handled by the DataProvider. This component makes life really easy out of the box.

Since I joined the articles with other tables, we can use that data as well in a very easy table.column manner. Formatting of the data can also be customized using anonymous functions:

<?= GridView::widget([
		'dataProvider' => $dataProvider,
        'columns' => [
	        'Title',
	        'author.Name',
	        [
	            'attribute' => 'author.Twitter',
	            'type' => 'html',
	            'value' => function($model) {
		            return Html::link('@' . $model->author->Twitter, 'http://twitter.com/' . $model->author->Twitter);
	            }
	        ],
	        [
		        'attribute' => 'tags',
		        'value' => function($model) {
			        return implode(', ', ArrayHelper::map($model->tags, 'ID', 'Tag'));
			    }
			],
			'Published:date',
		],
	]); ?>

The key, when defining the output for the columns, is to remember that each extends from yii\grid\Column so always look back at that class to see all the attributes and functions you can use. For instance, yii\grid\DataColumn has a $footer attribute. Add that to the configuration array of a column to set the footer text, and don’t forget to set showFooter to true in the GridView.

The GridView actually has quite a few extra attributes through which you can configure it. I’ll use another example:

<?= GridView::widget([
		'dataProvider' => $dataProvider,
        'columns' => [
        ...
        ],
        'layout' => '{summary}{items}{pager}',
		'filterPosition' => FILTER_POS_FOOTER,
		'rowOptions' => function ($model, $key, $index, $grid) {
		},
		'emptyText' => '-',
		'showFooter' => true,
    ]); ?>

The layout attribute is a very useful one. It allows you to change the way the basic elements appear. You can add {summary}, {errors}, {items}, {sorter}, {pager} some even multiple times; you might want a pager above and below the items list, the same might go for the summary which basically shows the total number of results and the number of currently shown results. Errors are a list of errors that the filter outputs (if any).
filterPosition allows you to move the filter to the bottom, or inside the body. Normally it would be included in the header part of the table, etc.

Customizing Sorting and Paging

The GridView will do sorting and pagination for you by default. Its functionality is actually provided through configuration of the yii\data\BaseDataProvider which can be initialized by supplying additional parameters during the class’ initialization. Let’s start with simple pagination; by default, it is set to defaultPageSize which normally gives 20 results per page. Change it by supplying a pageSize, or use it to display everything and turn off pagination:

/* show 80 items per page */
$dataProvider = new ActiveDataProvider([
    'query' => Articles::find()->with('tags'),
    'pagination' => [
	    'pagesize' => 80,
    ],
]);

/* disable pagination, shows all items at once */
...
    'pagination' => false,
...
    'pagination' => [
	    'pagesize' => -1,    //Alternate method of disabling paging
    ],
...

Sorting has quite a few more options which depend on the data you supply and display. First, you can customize the sorting. You can supply multiple attributes to sort on for each column. When you use the GridView you might find that the default sort handling sometimes fails on more complex attributes or on values that you output using unnamed functions. In these situations, supplying sort attributes also helps.

Let’s go over some code:

$dataProvider = new ActiveDataProvider([
    'query' => Articles::find()->with('authors'),
    'sort' => [
	    'defaultOrder' => [
	        'Published' => SORT_DESC,
	        'LastEdited' => SORT_DESC,
	    ],
	    'attributes' => [
		    'name' => [
                'asc' => [
                    'author.Name' => SORT_ASC, 
                    'Published' => SORT_ASC,
                    'Title' => SORT_ASC,
                ],
                'desc' => [
                    'author.Name' => SORT_DESC
                    'Published' => SORT_DESC,
                    'Title' => SORT_DESC,
                ],
            ],
            ... etc ...
	    ],
    ],
]);

The defaultOrder changed, as you might guess, the initial ordering of the GridView, should it be rendered. In this example the ordering will be on ‘Published’ decending and ‘LastEdited’ descending next.

attributes will contain a list of all sortable attributes from the GridView’s columns (the attribute names here should match the attribute names in the GridView). You can independently define how the ascending and descending result will be sorted – again, this can be done on multiple columns in your DB.

One thing to note that for pagination to work the DataProvider needs to know the total number of results available. The ActiveDataProvider and SqlDataProvider are able to do it by default using database queries. If you use the ArrayDataProvider, you might need to do some additional work depending on how you fill the provider.

Conclusion

The ListView and GridView are very practical widgets which allow you to quickly show the data that you want. If you want even more from your GridView, then be sure to check out the ‘GridView on steroids’ made by Kartik Visweswaran. This adds a lot of additional functionality to your grids allowing the user to fully customize the way the data is shown. Be sure to check out all the other components from Kartik as well!

Are there more Yii 2.0 topics that you would like to see covered? Suggestions are welcome!

Sponsors