はじめに
PHPerの皆様、おはようございます。こんにちは。こんばんは。
本記事はPHP Advent Calendar 2016の12日目のエントリです。
個人的にはQiitaでは特定の問題の解決策を書くのが好きなのですが、たまには少し広いトピックで今日は**「PHP7で新規サービスつくってみた」**的なテーマで書いてみたいと思います。
なので、技術的な深さがないかもしれませんが、一人でも多くPHPerの方がPHP7の導入に重い腰?を上げてもらったり、プライベートでサービスをつくるのって愉しそう!と思ったりしていただければ幸いです。
そして、アレコレ書いてたら凄く長くなってしまった。ごめんなさい。
何をつくったのか
今年の10月にプライベートで「Kocoromo」というAndroidアプリをリリースしました。
KocoromoではバックエンドをPHP7で実装したのでその時やったこと、やらなかったこと、気をつけたことをつらつらと書き連ねてみたいと思います。
ちなみに、サービスの宣伝ではないのでここでは詳細は書きませんが、バックエンドはよくあるRESTのWebAPIです。
技術スタック
まずは簡単に技術スタックを紹介させていただきます。
インフラ
- Sqale
- GMOペパボさんが提供しているPaaSです。
- heroku的な感じなんですが、デプロイが超楽チンなので個人で何かつくる時はオススメ。
データストア
- MySQL 5.5系
- Memcached
- まだMySQLだけで十分なのでほとんど使ってない。
Webアプリケーション
- Nginx 1.11系
- PHP 7.0系
- CakePHP 2.8系
- 本当は3系使いたかったんだけど、Sqaleとの相性が悪いのかLib周りでエラーが出てしまい修正するのに時間がかかりそうだったので(誰か助けてクレメンス)、PHP7に対応してる2.8系にした。
WebAPI(認証)
- 先述の通りよくあるREST APIです。
- 認証はクライアント認証とユーザー認証の2種類があります。
- クライアント認証は、誰でもAPIを叩けると困るので、事前に
client id
とclient key
を発行しておいて、クライアント側でリクエスト時にそれを拡張ヘッダーにセットしてもらっています。(全APIで必須) - ユーザー認証は、新規登録(orログイン)時のレスポンスで
user_id
とaccess_token
を返すので、ユーザー認証が必要なAPIはそれを拡張ヘッダーにセットしてもらっています。(ユーザーを特定する必要があるAPIでのみ必須)
- クライアント認証は、誰でもAPIを叩けると困るので、事前に
PHP7の新機能で利用した機能
先日7.1がリリースされた今、PHP7.0の新機能を語るのは若干周回遅れ感がありますが、次にPHP7の新機能で利用した機能を実際のコードをチラ見せしながら紹介したいと思います。
Null合体演算子
個人的に一番好きな新機能。isset地獄から解放されます。
こいつのせいで5系のコードを書くのがすごく面倒に感じるようになってしまった。
一覧系APIでよくあるクエリパラメーターのデフォルト値を設定したい時はこんな感じで使ってます。
public function setDefaultListQuery()
{
$this->controller->request->query['limit'] = $this->controller->request->query['limit'] ?? self::DEFAULT_LIMIT;
$this->controller->request->query['offset'] = $this->controller->request->query['offset'] ?? self::DEFAULT_OFFSET;
$this->controller->request->query['sort_order'] = $this->controller->request->query['sort_order'] ?? self::DEFAULT_SORT_ORDER;
$this->controller->request->query['sort_key'] = $this->controller->request->query['sort_key'] ?? self::DEFAULT_SORT_KEY;
}
Viewでもよく使う。(開発中のWeb版で利用)
<?= h($user['age']) ?? '未設定'; ?>
スカラー型宣言 / 戻り値の型宣言
僕はゆるふわで書いても動くPHPが好きですが、それでもプロダクションレベルでは型くらい安全に扱いたいので、関数の引数と戻り値にタイプヒンティングというか型宣言がきちんと出来るようになったのはとってもありがたいです。
使い方については僭越ながら以前僕が書いた記事をみていただければと思いますが、
実際のコードではこんな感じで使っています。(サービスにおけるユーザーIDの存在チェックの処理)
public function isExistUsername(string $kocoromoId, int $userId) :bool
{
$conditions = array('kocoromo_id' => $kocoromoId, 'del_flag' => 0);
return $this->usersTable->hasAny($conditions);
}
あと、コードから型が分かるのでPHPDocumentor等のドキュメントのメンテコストが軽減出来るのも(・∀・)イイネ!!
使わなかった機能
PHP7では上記以外にも新機能がたくさんあって、代表的なところでは 宇宙船演算子
や 無名クラス
があると思うのですが、このあたりは特に使いどころがなかったので、使っていません。と言うか Null合体演算子
や 型宣言
も使わなくたってPHP7のコードは書けます。
これはつまりPHP5の知識があれば、PHP7のコードを書けるということとほぼ等価なので、新規で何かをつくる場合、PHP7を採用しない理由は2016年においてないんじゃないかなーと僕は思っています。(5からの移行はまだ色々問題があると思いますが)
使っていて特に地雷を踏んだこともないですし、パフォーマンスも良好ですしお寿司。
あー書くの辛くなってきた。お寿司食べたい。
リポジトリパターン
PHP7というかCake寄りの話ですが、せっかくなのでモデル周りで工夫したことを出血大サービスということで書きたいと思います。(需要があるのかは謎)
オレオレ仕様なので、マサカリが怖いのですが、個人的にControllerからModel(ここでいうModelはCakeデフォルトのActiveRecordライクなModel)を呼ぶのが好きではない(Controllerがグチャグチャになる)ので、中間クラス(リポジトリクラスと呼んでいるがこれが正しいネーミングかは怪しい)をつくって、Controllerからは直接ActiveRecordを呼び出さないようにしています。
実際のコードをみてもらったほうが早いと思うので、具体的な話をします。
Kocoromoではユーザー情報を管理するのに、マスターとなる users
テーブルと 先述の認証で利用するトークン情報だけを管理する user_tokens
テーブルがあります。
そして、新規ユーザーを登録する際、この両テーブルにデータを登録する必要があるのですが、ナイーブなCakeの実装だとController側でこんな風に書くと思います。
public function create()
{
// 省略
// $userdata をつくるのになんやかんや
$userResult = $this->User->save($userData);
// $userResult をもとになんやかんや
$tokenResult = $this->UserToken->save($userResult);
// 省略
}
だいぶ端折っていますが、コメントに書いたなんやかんやの処理が多くなって、Controllerがどんどん肥大化していくのが何となく想像出来ると思います。
そこで僕はリポジトリクラスを使って、少なくともControllerはシンプルになるよう努めています。
まず、こんな感じで UserRepository
クラスをつくります。
App::uses('AppModel', 'Model');
App::uses('User', 'Model');
App::uses('UserToken', 'Model');
class UserRepository extends AppModel
{
public $name = "UserRepository";
public $useTable = false;
private $usersTable;
private $userTokensTable;
public function __construct()
{
$this->usersTable = new User();
$this->userTokensTable = new UserToken();
}
public function register(array $data) :array
{
$userResult = $this->createUser($data);
$userId = (int)$userResult['User']['id'];
$tokenResult = $this->createToken($userId);
$user['id'] = $userId;
$user['access_token'] = $tokenResult['UserToken']['access_token'];
return $user;
}
private function createUser(array $data) :array
{
// 省略
}
private function createToken(int $userId) :array
{
// 省略
}
}
こうすると UsersController
クラスはこんな感じになります。
public function create()
{
// 省略
$user = $this->UserRepository->register($request);
// 省略
}
なんやかんやがなくなって、だいぶスッキリしました。
※だいぶ端折って書いてしまったので、この件の詳細については(需要があれば)別の機会に書きたいと思います。
その他工夫した点
最後に細かいTipsを紹介させていただきます。
日時情報
パフォーマンスの観点からdateやdatetime型で管理するような情報はunixtimestampとしてint型で保存しています。
また、グローバル展開を(一応)意識しているので、DBにはUTCで保存して、クライアントにレスポンスを返すタイミングで適当なタイムゾーンに変換しています。
クエリーメソッド
Railsとかだとバッドノウハウだと思うのですが、複雑なクエリになる時は、query()
メソッドを使って、直SQLでやっています。
Cakeだとその方がパフォーマンスいいことの方が多いですしね。。
Sqale
Kocoromoではユーザーのアイコンである画像ファイルをWebアプリケーションと同じサーバーで管理しています。(ゆくゆくはS3とかを使う予定ですが今は規模が小さいので)
でこれは、Sqaleのデプロイの楽さとのトレードオフなのですが、デプロイをすると最新のソースをrsync するので、ふつうにやるとGitで管理していないユーザーアイコンが消えてしまいます。
これを回避するためにKocoromoでは postinstall
を使っています。postinstallのスクリプトはこんな感じ。
#!/bin/sh
ln -s /home/sqale/uploads/icon /home/sqale/current/public/app/webroot/icon
実際には /home/sqale/uploads/icon
配下に画像をアップロードして、webroot
配下にシンボリックを貼ることでこの問題を回避しています。
とは言え、Sqaleは本当に最高なんでみんな使ったほうがいいですよ!(2回目)
まとめ
長々と書いて疲労コンパイルですが、プライベートでサービスつくるのたのしいのでオススメです。
0→1でつくれるし、しがらみもないし、好きな技術使えるし、スピード感あるし、回り回って仕事にも良い影響があると思います。言いたいことはそれだけ。
ではよいお年を!来年もよろしくお願いします!
明日は @sota1235 さんが 日付バリデーション頑張る話でも
を書いてくれるそうです。お楽しみに!