Yii2 RESTful search endpoints

REST services can be a little boring when writing simple CRUD endpoint boilerplate. Yii2’s ActiveController can help get the boring stuff out of the way so that you can start working on more interesting code.

ActiveController to the rescue!

ActiveController is a way to setup resource endpoints quickly and easily. Lets say that you need to manage a list of superheroes and their super teams. (Maybe because you’re part of an evil government project on a tight deadline?)

Getting the team together

First you need to create your Superhero and Superteam classes as follows:

<?php
namespace app\models;

use yii\db\ActiveRecord;
use yii\helpers\Url;
use yii\web\Link;
use yii\web\Linkable;

class Superhero extends ActiveRecord implements Linkable
{
    public function getLinks()
    {
        return [
            Link::REL_SELF => Url::to(['superhero/view', 'id' => $this->id], true),
        ];
    }
    
    public static function tableName()
    {
        return 'superheroes';
    }

    public function extraFields()
    {
        return ['superteam'];
    }

    public function getSuperteam()
    {
        return $this->hasOne(Superteam::class, ['id' => 'team_id']);
    }
}

And the Superteam class

<?php
namespace app\models;

use yii\db\ActiveRecord;
use yii\helpers\Url;
use yii\web\Link;
use yii\web\Linkable;

class Superteam extends ActiveRecord implements Linkable
{

    public function getLinks()
    {
        return [
            Link::REL_SELF => Url::to(['superteam/view', 'id' => $this->id], true),
        ];
    }

    public static function tableName()
    {
        return 'superteams';
    }

    public function extraFields()
    {
        return ['superheroes'];
    }

    public function getSuperheroes()
    {
        return $this->hasMany(Superhero::class, ['team_id' => 'id']);
    }
}

This is a pretty common One-to-Many setup. A team can have many heroes, but heroes can only belong to one team. (True they should be able to belong to many teams, but that would complicate things for a simple example.)

You’ll also need to run these database migrations with the yii migrate command.

You’ll notice that these models implement the Linkable interface to enable HATEOAS resource links in the response. And extraFields() to help separate more database-intensive properties from simple object properties.

Superhero Lookup

We don’t always want a list of all superheroes, maybe we want to list only the mutant superheroes, or the ones with a specific superpower. To do so, you’ll need to set up a search model to handle any search parameter that you want to implement.

<?php
namespace app\models\searches;

use app\models\Superhero;
use yii\data\ActiveDataProvider;

class SuperheroSearch extends Superhero
{
    public $name;
    public $origin;
    public $team;

    public function rules()
    {
        return [
            [['name', 'origin', 'team'], 'safe']
        ];
    }
    public function search($params)
    {
        $query = Superhero::find()->joinWith('superteam');
        $dataProvider = new ActiveDataProvider([
           'query' => $query
        ]);

        $this->load($params);
        if (!$this->validate()) {
            return $dataProvider; // If validation fails, just return the unfiltered list
        }

        $query->andFilterWhere(['like', 'codename', $this->name])
            ->andFilterWhere(['like', 'team.name', $this->team])
            ->andFilterWhere(['origin' => $this->origin]);

        return $dataProvider;
    }
}

You’ll see that you can search by codename, team name or even origin. andFilterWhere() is nice because it will only apply the query where clause only if the search parameter is not null. ActiveDataProvider is an interface that can be used to process request query parameters to filter search results, as well as paginate and sort them.

Wiring up ActiveController

Active controller is really easy to setup. Here is the superhero controller.

<?php
namespace app\controllers;

use app\models\searches\SuperheroSearch;
use app\models\Superhero;
use yii\rest\ActiveController;

class SuperheroController extends ActiveController
{
    public $modelClass = Superhero::class;

    public function actions()
    {
        $actions = parent::actions();
        $actions['index']['prepareDataProvider'] = [$this, 'prepareDataProvider'];
        return $actions;
    }

    public function prepareDataProvider()
    {
        $search = new SuperheroSearch();
        return $search->search(\Yii::$app->request->getQueryParams());
    }
}

All the normal REST actions become available out-of-the-box when ActiveController is used. Headers are also properly set and contain pagination metadata. To implement the search parameters, all you need to do is define the prepareDataProvider method for the index endpoint, which in this case is:

GET /superheroes

which would return the following response:

[
 {
 "id":1,
 "team_id":null,
 "codename":"daredevil",
 "powers":"sonar, danger sense",
 "origin":"accident",
 "_links":{
 "self":{
 "href":"http://yii-examples.localhost.dev/superheroes/1"
 }
 }
 },
 {
 "id":2,
 "team_id":1,
 "codename":"thor",
 "powers":"god of thunder",
 "origin":"divine",
 "_links":{
 "self":{
 "href":"http://yii-examples.localhost.dev/superheroes/2"
 }
 }
 },
 {
 "id":3,
 "team_id":1,
 "codename":"iron man",
 "powers":"power armor",
 "origin":"super smart",
 "_links":{
 "self":{
 "href":"http://yii-examples.localhost.dev/superheroes/3"
 }
 }
 },
 {
 "id":4,
 "team_id":2,
 "codename":"professor x",
 "powers":"telepathy",
 "origin":"mutant",
 "_links":{
 "self":{
 "href":"http://yii-examples.localhost.dev/superheroes/4"
 }
 }
 },
 {
 "id":5,
 "team_id":2,
 "codename":"wolverine",
 "powers":"healing factor, adamantium skeleton",
 "origin":"mutant",
 "_links":{
 "self":{
 "href":"http://yii-examples.localhost.dev/superheroes/5"
 }
 }
 },
 {
 "id":6,
 "team_id":3,
 "codename":"guardian",
 "powers":"power armor",
 "origin":"super smart",
 "_links":{
 "self":{
 "href":"http://yii-examples.localhost.dev/superheroes/6"
 }
 }
 }
]

The combination of ActiveController and ActiveDataProvider is a very powerful dynamic duo, since it gives great power and flexibility to the endpoint. Here are some request examples:

A filtered list

GET /superheroes?origin=mutant&fields=id,codename

Response:

[
 {
 "id": 1,
 "codename": "daredevil",
 "_links": {
 "self": {
 "href": "http://yii-examples.localhost.dev/superheroes/1"
 }
 }
 },
 {
 "id": 2,
 "codename": "thor",
 "_links": {
 "self": {
 "href": "http://yii-examples.localhost.dev/superheroes/2"
 }
 }
 },
 {
 "id": 3,
 "codename": "iron man",
 "_links": {
 "self": {
 "href": "http://yii-examples.localhost.dev/superheroes/3"
 }
 }
 },
 {
 "id": 4,
 "codename": "professor x",
 "_links": {
 "self": {
 "href": "http://yii-examples.localhost.dev/superheroes/4"
 }
 }
 },
 {
 "id": 5,
 "codename": "wolverine",
 "_links": {
 "self": {
 "href": "http://yii-examples.localhost.dev/superheroes/5"
 }
 }
 },
 {
 "id": 6,
 "codename": "guardian",
 "_links": {
 "self": {
 "href": "http://yii-examples.localhost.dev/superheroes/6"
 }
 }
 }
]

A more detailed and sorted example

GET /superheroes?expand=superteam&sort=-codename&page=3

response:

[
 {
 "id": 5,
 "team_id": 2,
 "codename": "wolverine",
 "powers": "healing factor, adamantium skeleton",
 "origin": "mutant",
 "superteam": {
 "id": 2,
 "name": "x-men",
 "_links": {
 "self": {
 "href": "http://yii-examples.localhost.dev/superteams/2"
 }
 }
 },
 "_links": {
 "self": {
 "href": "http://yii-examples.localhost.dev/superheroes/5"
 }
 }
 },
 {
 "id": 2,
 "team_id": 1,
 "codename": "thor",
 "powers": "god of thunder",
 "origin": "divine",
 "superteam": {
 "id": 1,
 "name": "avengers",
 "_links": {
 "self": {
 "href": "http://yii-examples.localhost.dev/superteams/1"
 }
 }
 },
 "_links": {
 "self": {
 "href": "http://yii-examples.localhost.dev/superheroes/2"
 }
 }
 },
 {
 "id": 4,
 "team_id": 2,
 "codename": "professor x",
 "powers": "telepathy",
 "origin": "mutant",
 "superteam": {
 "id": 2,
 "name": "x-men",
 "_links": {
 "self": {
 "href": "http://yii-examples.localhost.dev/superteams/2"
 }
 }
 },
 "_links": {
 "self": {
 "href": "http://yii-examples.localhost.dev/superheroes/4"
 }
 }
 },
 {
 "id": 3,
 "team_id": 1,
 "codename": "iron man",
 "powers": "power armor",
 "origin": "super smart",
 "superteam": {
 "id": 1,
 "name": "avengers",
 "_links": {
 "self": {
 "href": "http://yii-examples.localhost.dev/superteams/1"
 }
 }
 },
 "_links": {
 "self": {
 "href": "http://yii-examples.localhost.dev/superheroes/3"
 }
 }
 },
 {
 "id": 6,
 "team_id": 3,
 "codename": "guardian",
 "powers": "power armor",
 "origin": "super smart",
 "superteam": {
 "id": 3,
 "name": "alpha flight",
 "_links": {
 "self": {
 "href": "http://yii-examples.localhost.dev/superteams/3"
 }
 }
 },
 "_links": {
 "self": {
 "href": "http://yii-examples.localhost.dev/superheroes/6"
 }
 }
 },
 {
 "id": 1,
 "team_id": null,
 "codename": "daredevil",
 "powers": "sonar, danger sense",
 "origin": "accident",
 "superteam": null,
 "_links": {
 "self": {
 "href": "http://yii-examples.localhost.dev/superheroes/1"
 }
 }
 }
]

Github project example.

Here is the project in GitHub form for your to dive into. Happy coding!

Extra reading:

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s