前回 WebAPIの設計から実装まで〜設計編〜に引き続き、今回はWebAPIの具体的な実装についての解説をしていく。
GitHub
のレポジトリ→chat_app
PHP
のFramework
はLaravel
を使っているが、他のFramework
(e.g. CakePHP
)でも大差ないと思う。
Laravel
の使い方や仕様についてはいずれ書く予定。
注意
コーディング規約については、PSR
を参考に自分が見やすいように書いている。
参考
コーディング規則PSRを学ぶ
PHPコーディング規約まとめ
PHP7.0
の機能を使っているので、文法がPHP5.x
とは違います。あらかじめご了承ください。
DB構造
前回も載せたが再度貼っておく
今回説明する内容
全部を説明するのは大変なので、今回はログイン時の一連の処理を見ていこうと思う。
ファイル分けをかなり細かくしているので、あっち行ったりこっち行ったりするがご理解ください。
ログイン時に必要なのはEmail
とPassword
だと仮定する。
Routeについての解説
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 なトークンです。
である。
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
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の役割
Controller
はRoute
から受けたRequest
をうまいこと処理してClient
にResponse
する
コード解説
-
Route
から受け取った値をparse
して$data
に格納する -
Request
で受け取った値$data
のemail
,password
が空か、もしくはそもそも存在していないか判定する -
AuthService
のlogin
関数に$data['email]
と$data['password
]を渡す - 後に解説するが、
AuthService
ではログイン処理が成功したらtoken
を、失敗したらnull
を返すようにしている -
$token
がnull
だった場合、ステータスコード400番台でClient
にresponse
を返し、成功だった場合ステータスコード200番台でClient
にresponse
を返す
response
はjson
形式で返すのだが、json
を別のところで管理することによってスッキリさせられた。(後に解説する)
気をつけた点
-
Controller
でほとんどすべてのエラー処理をして、即Client
にresponse
を返すようにした -
if
文のネストをなるべくしないようにした
Serviceについての解説
<?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の解説
<?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'
];
}
}
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
WebAPI
においてClient
にResponse
するデータはjson
形式なのだが、Controller
に書くと内容を充実させようと思った時に汚くなってしまうので`app/Json/にまとめた。
最後に
試行錯誤して学んだ知識なので間違いがあるかもしれないし、さらに良い設計方法もあるかもしれません。
コメントで批判していただけると嬉しいです。