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

前回 [WebAPIの設計から実装まで〜設計編〜](http://qiita.com/takeokunnn/items/5442bb266fee6ee07799)に引き続き、今回はWebAPIの具体的な実装についての解説をしていく。

`GitHub`のレポジトリ→[chat_app](https://github.com/takeokunn/chat_app)

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

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

## 注意

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

参考
[コーディング規則PSRを学ぶ](http://rei-farms.jp/php/cording_psr.html)
[PHPコーディング規約まとめ](http://qiita.com/hshimo/items/04be1f432240c58300f4)

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

## DB構造

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

![](https://qiita-image-store.s3.amazonaws.com/0/125325/7493c8d1-742a-e2ea-0b15-5e4928f1f77d.png)

## 今回説明する内容

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

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

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

## Routeについての解説

```php: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-auth`は`json web token`であり、

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

である。

引用→[JSON Web Token の効用](http://qiita.com/kaiinui/items/21ec7cc8a1130a1a103a)

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

`Client`は`/api/v1/auth/login`に`email`と`password`とともに`Post`したら、`Route`は`AuthController`に`email`と`password`を投げる。

### Routeは最小限にする

前回も述べたように

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

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

### 気をつけた点

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


## Controllerについての解説

```php: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](https://github.com/takeokunn/chat_app/blob/master/app/Http/Controllers/Auth/AuthController.php)

### Controllerの役割

`Controller`は`Route`から受けた`Request`をうまいこと処理して`Client`に`Response`する

### コード解説

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

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

### 気をつけた点
- `Controller`でほとんどすべてのエラー処理をして、即`Client`に`response`を返すようにした
- `if`文のネストをなるべくしないようにした

## Serviceについての解説

```php: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](https://github.com/takeokunn/chat_app/blob/master/app/Services/AuthService.php)

### Serviceの役割

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

### コード解説

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

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

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

## ResponseするJsonの解説

```php: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'
        ];
    }
}

```

```php: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....
}
```

`php`の`trait`に関して→[PHP MANUAL](http://php.net/manual/ja/language.oop5.traits.php)

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

## 最後に

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