前回 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/にまとめた。
最後に
試行錯誤して学んだ知識なので間違いがあるかもしれないし、さらに良い設計方法もあるかもしれません。
コメントで批判していただけると嬉しいです。
