LoginSignup
112

More than 5 years have passed since last update.

WebAPIの設計から実装まで〜実装編〜

Posted at

前回 WebAPIの設計から実装まで〜設計編〜に引き続き、今回はWebAPIの具体的な実装についての解説をしていく。

GitHubのレポジトリ→chat_app

PHPFrameworkLaravelを使っているが、他のFramework(e.g. CakePHP)でも大差ないと思う。

Laravelの使い方や仕様についてはいずれ書く予定。

注意

コーディング規約については、PSRを参考に自分が見やすいように書いている。

参考
コーディング規則PSRを学ぶ
PHPコーディング規約まとめ

PHP7.0の機能を使っているので、文法がPHP5.xとは違います。あらかじめご了承ください。

DB構造

前回も載せたが再度貼っておく

今回説明する内容

全部を説明するのは大変なので、今回はログイン時の一連の処理を見ていこうと思う。

ファイル分けをかなり細かくしているので、あっち行ったりこっち行ったりするがご理解ください。

ログイン時に必要なのはEmailPasswordだと仮定する。

Routeについての解説

app/http/route.php
Route::group(['prefix' => '/api/v1'], function()
{
    Route::post('auth/login',    'Auth\AuthController@login');
    Route::post('auth/register', 'Auth\AuthController@register');

    Route::group(['middleware' => 'jwt.auth'], function()
    {
        Route::get('auth/me',     'Auth\AuthController@me');
        Route::get('auth/logout', 'Auth\AuthController@logout');

        Route::get('profiles', 'ProfileController@index');
        Route::get('profiles/{userId}', 'ProfileController@show');
        Route::put('profiles/{userId}', 'ProfileController@update');
        Route::get('profiles/{userId}/articles', 'ArticleController@getUserArticles');
        Route::get('profiles/{userId}/comments', 'CommentController@getUserComments');

        Route::get   ('articles', 'ArticleController@index');
        Route::post  ('articles', 'ArticleController@create');
        Route::get   ('articles/{articleId}', 'ArticleController@show');
        Route::put   ('articles/{articleId}', 'ArticleController@update');
        Route::delete('articles/{articleId}', 'ArticleController@delete');

        Route::get   ('articles/{articleId}/comments', 'CommentController@index');
        Route::post  ('articles/{articleId}/comments', 'CommentController@create');
        Route::get   ('articles/{articleId}/comments/{commentId}', 'CommentController@show');
        Route::put   ('articles/{articleId}/comments/{commentId}', 'CommentController@update');
        Route::delete('articles/{articleId}/comments/{commentId}', 'CommentController@delete');

        Route::get('tags', 'TagController@index');
        Route::get('tags/{tagId}', 'TagController@show');

        Route::post('singlechatrooms', 'SinglechatroomController@create');
        Route::get ('singlechatrooms/{singlechatroomId}', 'SinglechatroomMessageController@index');
        Route::post('singlechatrooms/{singlechatroomId}', 'SinglechatroomMessageController@create');

        Route::get ('groupchatrooms', 'GroupchatroomController@index');
        Route::post('groupchatrooms', 'GroupchatroomController@create');
        Route::get ('groupchatrooms/{groupchatroomId}', 'GroupchatroomMessageController@index');
        Route::post('groupchatrooms/{groupchatroomId}', 'GroupchatroomMessageController@create');
    });
});

上コードのようにRoutingした。

jwt-authjson web tokenであり、

JSON Web Token とは、ざっくりいって署名の出来る JSON を含んだ URL Safe なトークンです。

である。

引用→JSON Web Token の効用

RouteはURLを解析してControllerにDispatchする

Client/api/v1/auth/loginemailpasswordとともにPostしたら、RouteAuthControlleremailpasswordを投げる。

Routeは最小限にする

前回も述べたように

RouteはClientから見るとWebAPIの玄関口であり、ここの間口を必要最低限にすることによりセキュリティ性を高めることができる。

なので公開する際のRouteは最小限にするべきだ。

気をつけた点

  • endpointを名詞複数形にし、動作をMethodでわかるようにした
  • /api/v1というprefixをつけることによって、WebAPIをversion管理できるようにした。
  • また、公開用と非公開用のURLを切り分けるようにprefixをつけた

Controllerについての解説

app/Http/Controllers/Auth/AuthController.php

<?php

namespace App\Http\Controllers\Auth;

use Illuminate\Http\Request;

use Auth;
use App\Http\Controllers\Controller;

class AuthController extends Controller
{
    use \App\Json\AuthJson, \App\Json\CommonJson;

    private $auth = null;

    public function __construct(
        \App\Services\AuthService $auth
    ) {
        $this->auth = $auth;
    }

    public function login(Request $request)
    {
        $data  = $request->only('email', 'password');

        if(empty($data['email']) || empty($data['password'])) {
            return response()->json($this->invalidArgument(), 400);
        }
        $token = $this->auth->login($data['email'], $data['password']);
        if (empty($token)) {
            return response()->json($this->failureLogin(), 400);
        }

        return response()->json($this->successLogin($token), 200);
    }
}

説明を簡単にするために、必要な部分のみ抜粋した。実際のコード→GitHub

Controllerの役割

ControllerRouteから受けたRequestをうまいこと処理してClientResponseする

コード解説

  1. Routeから受け取った値をparseして$dataに格納する
  2. Requestで受け取った値$dataemail,passwordが空か、もしくはそもそも存在していないか判定する
  3. AuthServicelogin関数に$data['email]$data['password]を渡す
  4. 後に解説するが、AuthServiceではログイン処理が成功したらtokenを、失敗したらnullを返すようにしている
  5. $tokennullだった場合、ステータスコード400番台でClientresponseを返し、成功だった場合ステータスコード200番台でClientresponseを返す

responsejson形式で返すのだが、jsonを別のところで管理することによってスッキリさせられた。(後に解説する)

気をつけた点

  • Controllerでほとんどすべてのエラー処理をして、即Clientresponseを返すようにした
  • if文のネストをなるべくしないようにした

Serviceについての解説

app/Services/AuthService.php
<?php

namespace App\Services;

use JWTAuth;

use App\Model\{User, Profile};

class AuthService
{
    public function registerUser(string $username_, string $email_, string $password_, int $twitterId_ = null, int $facebookId_ = null)
    {
        $user = new User;
        $user->username    = htmlspecialchars($username_);
        $user->email       = htmlspecialchars($email_);
        $user->password    = bcrypt($password_);
        $user->twitter_id  = $twitterId_;
        $user->facebook_id = $facebookId_;
        $user->save();

        $profile = new Profile;
        $profile->user_id = $user->id;
        $profile->save();

        return $user;
    }

    public function login(string $email_, string $password_)
    {
        if ($token = JWTAuth::attempt(['email' => $email_, 'password' => $password_])) {
            return $token;
        }
        return null;
    }
}

一部必要な部分だけ抜粋した。実際のコード→GitHub

Serviceの役割

Controllerから受けた値を使ってDBを操作し、その実行結果をControllerに返す。

コード解説

特に解説することもないが、login関数はログインに成功したら$tokenを、失敗したらnullを返す関数だ。

Laravelはスッキリかけますね。

気をつけた点

  • なるべく簡潔かつ明瞭にかく
  • 型のチェックはきちんとする
  • stringを入れる場合はhtmlspecialcharsでエスケープしてから入れる(XSS対策)

ResponseするJsonの解説

app/Json/AuthJson.php
<?php

declare(strict_types=1);

namespace App\Json;

trait AuthJson
{
    private function successRegistered() : array
    {
        return [
            'status'  => 'created',
            'code'    => 201,
            'message' => 'success registered'
        ];
    }

    private function successLogin(string $token_) : array
    {
        return [
            'status'  => 'success',
            'code'    => 200,
            'token'   => $token_,
            'message' => 'success login'
        ];
    }

    private function failureLogin() : array
    {
        return [
            'status'  => 'failure',
            'code'    => 400,
            'message' => 'failure login'
        ];
    }

    private function successLogout() : array
    {
        return [
            'status'  => 'success',
            'code'    => 200,
            'message' => 'success logout'
        ];
    }

    private function successGetMe($data_) : array
    {
        return [
            'status'  => 'success',
            'code'    => 200,
            'data'    => $data_,
            'message' => 'success get me'
        ];
    }
}

app/Http/Controller/Auth/AuthController.php
class AuthController extends Controller
{
    use \App\Json\AuthJson, \App\Json\CommonJson;

    private $auth = null;

    public function __construct(
        \App\Services\AuthService $auth
    ) {
        $this->auth = $auth;
    }
    // something....
}

phptraitに関して→PHP MANUAL

WebAPIにおいてClientResponseするデータはjson形式なのだが、Controllerに書くと内容を充実させようと思った時に汚くなってしまうので`app/Json/にまとめた。

最後に

試行錯誤して学んだ知識なので間違いがあるかもしれないし、さらに良い設計方法もあるかもしれません。
コメントで批判していただけると嬉しいです。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
112