Creating a RESTful web service with Yii 2

It's important to remember that a REST web service is by definition a stateless service, this will imply some requirements in the way we will test things and deal with the information we need to POST or GET.

The big step forward that Yii made with version 2 can be seen in the built-in REST classes that provide an immediate solution once provided by third-party implementations.

This means we'll have to introduce several changes to what we've achieved so far; the REST part of the application will be developed as a separate module, which will give us the ability to extend it and contain its logic. Because of this, the routes will be rearranged appropriately as well.

Before seeing what the Yii REST functionality is capable of doing, we'll need to first have a quick look at modules in Yii, which we will use to develop our API to be tested.

Writing modular code in Yii

If you've never used modules since you've started working with Yii, well, I think it's time to do so. Right now, modules are really easy and straightforward to use, and they will help you keep your code architecturally well organized and separated from the other components of your application.

Modules are self-contained software units that contain models, views and controllers, and other software components, and the end user will be able to access the controller once it is installed in the main application. For this reason, modules are considered mini-applications, only difference being that they cannot live on their own. As an example, a forum or an administrative area can be developed as modules.

Modules can also be composed of submodules; a forum might have an admin submodule that contains all the logic and interfaces to moderate and customize the main forum module.

Modules can be quite complex in their structure; I would always strongly suggest an architectural analysis before deciding to keep everything under the same module, in the same way as you need to question your choices if you were to keep all the code in the same controller. Always try to keep in mind that you should be able to understand your code in one year's time.

Creating a module with Gii

Developing the REST interface using Yii modules is the easiest way to achieve versioning of the API. This way, we can easily switch and make an improved version of the API while still continuing to support the old version with minimal maintenance, until full deprecation.

So we will start with the creation of the module, using the web interface to the code generator called Gii. In case you skipped a few pages, the configuration for that is available in Chapter 4, Isolated Component Testing with PHPUnit, where you saw how to create a model with it.

Now, we will see how to create a module and what this will mean in terms of generated code.

So, head over to the Gii application, which in my case is http://basic.yii2.sandbox/gii and log in, if you are configured to do so and click on the Module Generator button.

The only two fields we have to fill in are these:

  • Module Class: This represents the main name-spaced class name of the module, which will be set to appmodulesv1Module.
  • Module ID: This will be (automatically) set to v1.

Have a look at the following screenshot:

Creating a module with Gii

Module generator page within the Gii code generation tool

You can avoid creating the view by deselecting the related checkbox, as we're not going to need one. We're going to make more changes to what has been generated.

Click on the Generate button, once ready.

Note

If your application will end up being more complex than what we have here, you still have a few options.

You can simply adjust the routes for the module, as explained in the documentation at http://www.yiiframework.com/doc-2.0/guide-runtime-routing.html#adding-rules-dynamically.

Otherwise, you can create a module within a module (for example, a container module called api which will contain the various versions as modules such as v1, v2, and so on). Just remember to namespace it correctly when creating it. This is usually the solution I'd recommend from the code organization point of view.

The next step is to configure the module in order to be able to use it, and then we will see how to transform it into a REST module.

Using modules in Yii 2

Now that we have our basic code for our module ready, we need to see how we can use it.

Ideally, the created module can be used straight away without much hassle, which is quite helpful in an environment where you want to be able to create reusable and, of course, modular code.

The only step that's really needed is instructing Yii that there is a new module, and in return, it will take care of auto-loading and calling our module controller at the right time.

So let's head over to our configuration file located in /config/web.php and add the following code:

// /config/web.php

$config = [
    // ...
    'modules' => [
        'v1' => [
            'class' => 'appmodulesv1Module',
        ],
    ],
    // ...
];

With this, you're ready to go. In order to convert the newly created module to act as a REST controller, it requires some additional changes, which we will explore immediately.

Converting our controller to be a REST controller

This much anticipated feature of Yii 2 lets you create a REST interface in a clear and easy way.

The REST controller we will inherit from will deal with our models without much configuration needed and even if there was, it's quite straightforward to do and keep in mind.

Our first step is to create UserController which will be dealt with the User model.

Let's start by defining the namespace and the basic classes we're going to use in our new controller:

// /modules/v1/controllers/UserController.php

namespace appmodulesv1controllers;

use appmodelsUser;
use yii
estActiveController;

As we can clearly see, we're going to use the User model and on top of it the REST ActiveController. This controller is where the magic happens, and we're going to illustrate what it is all about in a moment.

Now, let's implement the actual class:

// /modules/v1/controllers/UserController.php

class UserController extends ActiveController
{
    public $modelClass = 'appmodelsUser';
}

The only thing needed at this point is just the definition of the model class that the REST controller is going to manage and that's it.

yii estActiveController is the controller that will deal with Active Records models, such as our User model. If you were to manage custom classes (non active records) that do not connect to a database or do connect to a custom data source (for instance, an online service), you can use the class that ActiveController is inheriting from, which is yii estController.

The beauty of ActiveController is that it provides already implemented actions that are available immediately, which are:

  • index, which is accessed via GET or HEAD and returns the list of the models and their (database-bound) attributes
  • view, which is accessed via GET and HEAD and returns the details of a single model
  • create, which can be accessed only via POST and lets you create a new model
  • update, which is accessed via PUT or PATCH and does what it says on the tin
  • delete, which is used to delete a model and can be invoked using DELETE
  • OPTIONS, which, lastly, you can invoke to see all the allowed HTTP methods

In the actions that you'll be able to implement yourself, you will be dealing with the raw models, which are rendered by default in XML and JSON (depending on the Accept header that was sent along with the request).

We know we'll need to modify the list of exposed endpoints, and we'll see how to do it in a moment.

Before getting there, there are a few other bits that need to be addressed first, in particular the access credentials, as we don't want anybody to access our endpoints without being authenticated.

Adding the access check and security layer

You might already have asked yourself how to prevent non-authenticated users from using certain endpoints of your application. For instance, we might want to give a client access to the user endpoint only if it's authenticated and authorized.

The authorization and authentication happen at two different phases.

Authorization is done at controller level by simply overriding the checkAccess() method and performing the right checks, which might involve establishing if the user has been authenticated and if he/she is active, in case this flag exists in the user model.

In our case, we can simply add the following method to our controller:

// /modules/v1/controllers/UserController.php

public function checkAccess($action, $model = null, $params = [])
{
    if (Yii::$app->user->isGuest) {
        throw new UnauthorizedHttpException;
    }
}

This means that if the user is a guest, we raise a 401 response.

Yii will automatically call the method on each request as we can see in the actions() method in its parent class, which is yii estActiveController:

class ActiveController extends Controller
{
    // ...

    public function actions()
    {
        return [
            'index' => [
                'class' => 'yii
estIndexAction',
                'modelClass' => $this->modelClass,
                'checkAccess' => [$this, 'checkAccess'],
            ],
            // ...
        ];
    }
    
    // ...
}

Instead, the authentication is done in a completely different way and varies depending on the implementation and level of security you want to implement in the application.

As far as it goes, in case you haven't touched the argument in depth, you have different possibilities, which are:

  • HTTP Basic Auth: This is basically the same that you would have by using htpasswd and configuring Apache accordingly and is the simplest one available, but needs the username and password to be sent in a header with every request. This requires the communication to work over HTTPS for obvious reasons.
  • Query parameter: Here, the client is already possessing an access token, which will be sent to the server as a query parameter as https://server.com/users?access-token=xxxxxxx, which is quite handy if you don't have the ability to send additional tokens with the request.


There are some other ways that use a combination of different techniques and/or asymmetric and symmetric encryption or different types of handshakes to authenticate a client. One of the most well-known, although potentially complex, is OAuth 2, which has different implementations as it's considered more of a framework than a well-defined protocol. Most of the well-known social websites such as Google, Twitter, Facebook, and so on implement it. Its Wikipedia page, available at http://en.wikipedia.org/wiki/OAuth, provides some good links and references to help you explore it further.

As encryption and authentication protocols are outside the scope of this book, I've decided to use the simplest solution, which will anyway give us enough hints on where to put our hands, should we want to implement something more robust or complex.

Building the authentication layer

As Yii uses sessions by default, which will violate the stateless constraints of a RESTful server according to the fielding dissertation (http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm#sec_5_1_3), we will want to disable the session in the module's init() method:

// /modules/v1/Module.php

public function init()
{
    parent::init();
    // custom initialization code goes here
    // disable the user session
    Yii::$app->user->enableSession = false;
}

In Yii the actual authentication is then done via the available authenticator behavior.

Yii provides four different authenticators which are:

  • HttpBasicAuth: This is used for HTTP Basic Auth, which we will use here
  • QueryParamAuth: This is used for query parameter authentication
  • HttpBearerAuth: This is used for OAuth and similar methods
  • CompositeAuth: This is a way to use multiple cascading authentication methods

Again open our UserController and let's define the one we want to use:

// /modules/v1/controllers/UserController.php

public function behaviors()
{
    $behaviors = parent::behaviors();

    $behaviors['authenticator'] = [
        'class' => HttpBasicAuth::className(),
    ];

    return $behaviors;
}

If you were to run the tests against this implementation, you will have problems making them pass; the default implementation will use findIdentityByAccessToken() and use the $username part of the header as an access token. So, there's no real password check.

HTTP Basic Auth defines that, together with your request, you will also have to send an Authorization header containing 'Basic '.base64($username.':'.$password);.

As explained in the documentation of the HttpBasicAuth class at https://github.com/yiisoft/yii2/blob/master/framework/filters/auth/HttpBasicAuth.php#L55, you need to override the $auth attribute in order to perform the password authentication in the way that you want.

As you saw, findIdentityByAccessToken() is not a method we're going to need, and we have the unit tests that clearly state that. The best way to address this is by adding our authenticator method straight in the definition of the behavior in the following way:

// modules/v1/controllers/UserController.php    

public function behaviors()
{
    $behaviors = parent::behaviors();

    $behaviors['authenticator'] = [
        'class' => HttpBasicAuth::className(),
        'auth' => function ($username, $password) {
                /** @var User $user */
                $user = User::findByUsername($username);
                if ($user && $user->validatePassword($password)) {
                    return $user;
                }
            }
    ];

    return $behaviors;
}

As explained in the documentation, the auth attribute should be a function that expects $username and $password as actual parameters, and returns the user identity if the authentication is verified.

With this last method, implementation of our authentication and authorization scheme should be complete.

Modifying the existing actions

Now that we've restricted access to any other user, we need to re-implement the view and update actions, in order to allow only the currently logged in user to view just his/her details and allow him to update only the password. If you have already started implementing the actions, this won't be enough as the parent class, yii estController, already implements all the default actions, so we need to redefine their configuration, which happens to be set within the actions() method:

public function actions()
{
    $actions = parent::actions();
    unset($actions['view'], $actions['update']);
    return $actions;
}

Once we unset the two actions, our own overridden methods will be picked up automatically without much else to do:

public function actionView($id)
{
    if ($id == Yii::$app->user->getId()) {
        return User::findOne($id);
    }
    throw new ForbiddenHttpException;
}

The view action just adds a check on the ID of the user and returns a 403 error, while the update action can be something along the lines of the following code:

public function actionUpdate($id)
{
    if (! Yii::$app->request->isPut) {
        return new HttpRequestMethodException();
    }

    /** @var User $user */
    $user = User::findIdentity($id);

    if (Yii::$app->request->post('password') !== null) {
        $user->setPassword(Yii::$app->request->post('password'));
    }

    return $user->save();
}

In the update, we only allow changing of the password, after which we return the value of the save method. We could have returned a more comprehensive status, but for our cause, this is good enough.

Note

We won't actually need to add the check if the request is not a PUT, as the current internal implementation restricts it by default. We'll see in Chapter 8, Analyzing Testing Information, how this will be fixed, using the coverage report information.

Adding a new endpoint with parameters

With all we have done, if we try to run the tests on UserAPICept, we will see that it will fail immediately at the first sendGET('user/search')command.

Implementing the new actionSearch() method won't be a problem, and it can be implemented in the following way:

public function actionSearch($username)
{
    /** @var User $user */
    $user = User::findByUsername($username);
    if ($user && $user->id === Yii::$app->user->getId()) {
        return $user;
    }
    throw new ForbiddenHttpException;
}

What is important to note is how we will customize the routes to add this new action in a "compliant" way.

Switch to the configuration file located at config/web.php and let's start by adding the search action to the list of allowed methods:

'only' => ['view', 'update', 'search', 'options']

The UrlRule class that is used to create routes exposes some variables that you can configure, either to extend or entirely re-define the patterns and the structure of the tokens. The first are extraPatterns and patterns respectively. Tokens can be used in the patterns and represent the parameters passed to the action.

In Yii terminology, a pattern is a tuple composed of allowed HTTP method(s), the actual structure of the resource to identify, and the corresponding action to be called. The following is an example of this:

'GET search/{username}' => 'search'

A token is one or more parameters that can be as complex as a regular expression. In the preceding example, {username} is a token and can be defined as shown in the following code:

'{username}' => '<username:\w+>'

Our final list of rules will end up looking like the following code:

// config/web.php

'rules' => [
    [
        'class' => 'yii
estUrlRule',
        'controller' => 'v1/user',
        'tokens' => [
            '{id}' => '<id:\d[\d,]*>',
            '{username}' => '<username:\w+>'
        ],
        'extraPatterns' => [
            'GET search/{username}' => 'search',
        ],
        'only' => ['view', 'update', 'search', 'options']
    ],
    '/' => 'site/index',
    '<action:w+>' => 'site/<action>',
    '<controller:w+>/<id:d+>' => '<controller>/view',
    '<controller:w+>/<action:w+>/<id:d+>' => '<controller>/<action>',
    '<controller:w+>/<action:w+>' => '<controller>/<action>',
]

The first thing to note is that we had to re-define all the tokens rather than adding them as we are doing with extraPatterns.

In the list of rules, we have defined the REST interface rules before any other as rules are read top to bottom, and the first one that is found matching will be captured. This means that specific rules must stay at the top, while generic catch-all rules are at the bottom.

The preceding configuration will be fed to urlManager, as explained in the official guide at http://www.yiiframework.com/doc-2.0/guide-runtime-routing.html#using-pretty-urls:

'urlManager' => [
    'enablePrettyUrl' => true,
    'showScriptName' => false,
    'enableStrictParsing' => true,
    'rules' => [ ... ]
]

Now we can check that the tests are passing using the following command:

../vendor/bin/codecept run functional UserAPICept.php
Codeception PHP Testing Framework v2.0.8
Powered by PHPUnit 4.5-ge3692ba by Sebastian Bergmann and contributors.

Functional Tests (1) ------------------------------------------------
Trying to test the user REST API (UserAPICept)                     Ok
---------------------------------------------------------------------


Time: 9.8 seconds, Memory: 14.50Mb

OK (1 test, 18 assertions)
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset