samdark / yii2-cookbook

Yii 2.0 Community Cookbook
1.45k stars 296 forks source link

REST API Guide #165

Open CyberPunkCodes opened 7 years ago

CyberPunkCodes commented 7 years ago

One of the issues I have just ran into, is using Yii2 to create a REST API. The official docs lack in this area (and many others). It takes into account, assumptions, that you know where things go. While I have used Yii2 for a while, it still would be nice to get the full story, instead of bits and pieces and having to find the rest. The docs say "Implement xxx", but doesn't tell you how, so we have to fill in the gaps. My experience with Yii helps me find these answers quickly, but beginners will get stuck on these things for days.

Your cookbook is helpful, and shows great examples of use-cases and a "how-to" for specific things. Which is why I think it would be great if you could add a full walk through on setting up a REST API, without the gaps of assuming everyone knows what they are doing. Assuming that they know nothing about Yii :)


While I just got a QueryParamAuth finally working, it was a pain. After following various guides (including official Yii docs), it leaves out a lot of necessary info. I will start with what I have so far.

I copied the "frontend" app, renamed it to "api", and removed unnecessary things like the "views" folder.

Here is a tree view:

[api]
- [config]
- - bootstrap.php
- - main-local.php
- - main.php
- - params-local.php
- - params.php
- [modules]
- - [v1]
- - - [components]
- - - -ApiController.php - (extends \yii\rest\ActiveController)
- - - [controllers]
- - - - UserController.php - (extends ApiController)
- - - [models]
- - - - User.php - (sets tableName, primaryKey, rules)
- - - Module.php

api/config/main.php

<?php
$params = array_merge(
    require(__DIR__ . '/../../common/config/params.php'),
    require(__DIR__ . '/../../common/config/params-local.php'),
    require(__DIR__ . '/params.php'),
    require(__DIR__ . '/params-local.php')
);

return [
    'id' => 'app-api'
    'name' => 'My Yii2 API',
    'basePath' => dirname(__DIR__),
    'controllerNamespace' => 'api\controllers',
    'bootstrap' => ['log'],
    'modules' => [
        'v1' => [
            'basePath' => '@app/modules/v1',
            'class' => 'api\modules\v1\Module'
        ]
    ],
    'components' => [
        'user' => [
            'identityClass' => 'common\models\User',
            'enableSession' => false,
            'loginUrl' => null,
            'enableAutoLogin' => false,
        ],
        'request' => [
            'class' => '\yii\web\Request',
            'enableCookieValidation' => false,
            'parsers' => [
                'application/json' => 'yii\web\JsonParser',
            ]
        ],
        'log' => [
            'traceLevel' => YII_DEBUG ? 3 : 0,
            'targets' => [
                [
                    'class' => 'yii\log\FileTarget',
                    'levels' => ['error', 'warning'],
                ],
            ],
        ],
        'urlManager' => [
            'enablePrettyUrl' => true,
            'enableStrictParsing' => true,
            'showScriptName' => false,
            'rules' => [
                [
                    'class' => 'yii\rest\UrlRule',
                    'controller' => 'v1/user',
                ]
            ],
        ],
    ],
    'params' => $params,
];

api/modules/v1/components/ApiController.php

<?php
namespace api\modules\v1\components;

use yii\filters\auth\QueryParamAuth;
use yii\filters\Cors;

/**
 * API Base Controller
 * All controllers within API app must extend this controller!
 */
class ApiController extends \yii\rest\ActiveController
{

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

        // add CORS filter
        $behaviors['corsFilter'] = [
            'class' => Cors::className(),
        ];

        // add QueryParamAuth for authentication
        $behaviors['authenticator'] = [
            'class' => QueryParamAuth::className(),
        ];

        // avoid authentication on CORS-pre-flight requests (HTTP OPTIONS method)
        $behaviors['authenticator']['except'] = ['options'];

        return $behaviors;
    }

}

api/modules/v1/controllers/UserController.php

<?php
namespace api\modules\v1\controllers;

/**
 * User Controller
 */
class UserController extends \api\modules\v1\components\ApiController
{
    public $modelClass = 'api\modules\v1\models\User';
}

api/modules/v1/models/User.php

<?php
namespace api\modules\v1\models;

use \yii\db\ActiveRecord;

/**
 * User Model
 */
class User extends ActiveRecord
{
    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return 'user';
    }

    /**
     * @inheritdoc
     */
    public static function primaryKey()
    {
        return ['id'];
    }

    /**
     * Define rules for validation
     */
    public function rules()
    {
        return [
            [['username', 'email', 'password_hash'], 'required']
        ];
    }
}

api/modules/v1/Module.php

<?php
namespace api\modules\v1;

class Module extends \yii\base\Module
{
    public $controllerNamespace = 'api\modules\v1\controllers';

    public function init()
    {
        parent::init();
    }
}

Then lastly, outside of the API app, in "common" directory, modify /common/models/User.php to handle the access_token

/**
 * Finds an identity by the given token.
 *
 * @param string $token the token to be looked for
 * @return IdentityInterface|null the identity object that matches the given token.
 */
public static function findIdentityByAccessToken($token, $type = null)
{
    return static::findOne(['access_token' => $token, 'status' => self::STATUS_ACTIVE]);
}

Now, it still doesn't work. You have to manually put in an access_token in order for it to work. I grabbed it from another guide:

Replace with these 2 functions:

public function up()
{
    $this->addColumn('{{%user}}', 'access_token', $this->string()->unique()->after('auth_key'));
}

public function down()
{
    $this->dropColumn('{{%user}}', 'access_token');
}

Now you can call it using PostMan: http://api.mydomain.dev/v1/users?access-token=4p9mj82PTl1BWSya7bfpU_Nm8u07hkcB

NOTE: I have mapped api.mydomain.dev as a VHOST to /path/to/yii2site/api/web


Now what? There is no guidance on how to set the access_token for the users, or how to revoke it. I assume this would be done on login. I can do this, but others may not be able to! You can create a login process for your API, but also could set it during normal frontend login, and use the API for filling the GridView and DetailView.

What about revoking the token? If you look at the findByPasswordResetToken() function, it splits the token and checks the time stamp (second half after the underscore). The findIdentityByAccessToken() (As shown in Yii2 docs) doesn't show any verification on the token itself. You don't want the token to live forever unchecked!


I think there needs to be a full step-by-step guide on how to properly setup a REST API, and go further by showing how to login, validate the token, handle invalid token (ie: session expired), logout (removing the token). Maybe split into a few parts to handle the different types: Query, BasicAuth, Custom, etc.

mrFleshka commented 7 years ago

@WadeShuler I think you can create PR with your vision about that, and samdark helps with implementing this information to cookbook.

samdark commented 7 years ago

Yes, that would be good.

buttflattery commented 5 years ago

any updates on this issue when will we be having the proper and complete guide for the REST API

samdark commented 5 years ago

When someone contributes. I'm too busy with Yii 3.0 currently.