ドワンゴで、ニコニコ動画とプレミアム課金の開発をしている @yoshikyoto です。
最近では、総合TOPのリニューアルや、↓の記事で書かれているプレミアム課金の移行をやっていました。
レガシーなPHPを改善してきた知見
ニコニコは古くからあるサービスで、レガシーなコードも多く存在しています。
僕は新卒でニコニコ動画に配属され、5年間ずっと PHP を触ってきました。その中で、多くの新機能の開発、古いコードのリファクタリング、いろいろやってきました。その知見をまとめます。
Composer, Laravel (フレームワーク)の導入
PHP を使っていて Composer を導入していないシステムは珍しい思いますが、10年もののシステムには、Composer が導入されていない場合もあります。導入されていないなら導入します。(Composer は JavaScript でいうところの npm 、 Ruby でいうところの Bundler です)
10年もののPHPには、フレームワークは導入されていません。URLのエンドポイントごとに PHP ファイルが存在しています。フレームワークを導入しましょう。基本的には Laravel を導入するのが良いです。
レガシーなプロダクトに Laravel を導入する記事も以前に書きました。
Slim Framework を導入する場合もありました。 DI、DB、Redis、Log といったライブラリが導入済みで、ルーティングだけなんとかしたい場合は Slim Framework でもよいです。
ユニットテスト(PHPUnit)
私にとって、レガシーコードとは、単にテストの無いコードです
『レガシーコード改善ガイド』にはこう書いてあります。
コードに変更を加えづらくなる一番の原因は、テストが無いことです。ユニットテストがあれば、コード修正時にある程度の動作が保証できます。コードが綺麗でも、テストがなければ、コードの修正がしづらくなるのです。
ユニットテスト導入時には同時に、以下の2点を考えると良いです。
- CIの導入
- CIを導入しないと、いつの間にかテストが落ちている事に気づかない
- IDE から phpunit コマンドを実行できるようにする
- CIでしかテストを実行できないと、開発効率が良くないので、当然手元のPCでもテストを実行できる必要があるのですが、IDE の設定をすることで、開発効率が格段によくなる
CI の導入
特に説明不要かと思います。僕のチームでは Jenkins を利用していますが、CircleCI を使っているチームもあります。
Jenkinsfile
という、CI の実行内容を定義できる仕組みがあるので(CircleCI でいう .circleci/config.yml
みたいなもん)、これを使って phpunit コマンドを実行し、結果を表示させます。
pipeline {
agent any
// composer などのコマンドを叩くために必要
environment {
PATH = "/usr/local/bin:$PATH"
}
stages {
stage ('setup') {
steps {
checkout scm
// phpunit 等をインストールするために --dev を付ける
sh 'composer install --dev'
// テスト結果を格納しているディレクトリをきれいにする
sh 'rm -rf results'
sh 'mkdir results'
}
}
stage ('phpunit') {
steps {
sh './vendor/bin/phpunit --log-junit results/phpunit_junit.xml'
}
}
}
post {
always {
junit 'results/phpunit_junit.xml'
}
}
}
IDE(IntelliJ / PHPStorm)の設定
僕は開発に IntelliJ を使っているのですが、テスト関数の横のボタンを押すだけで、テストを実行できるようになります。この機能を知った時には衝撃的でした。
手元で毎回すべてのテストを実行するには時間がかかるので、自分が修正した部分のテストだけ実行し、全体のテストは、CIで保証しますが、「自分の実装したテストだけを実行するコマンド」を忘れがちなので、それを IDE におまかせできるのも良い点です。
これを出すためには
- PHP Interpreter の設定
- composer.json の設定
- Test Frameworks の設定
が必要です。
PHP Interpreter の設定 は、 Preferences > Languages & Frameworks > PHP の「CLI Interpreter」から設定できます。PCにインストールされているPHPのバージョンから選択できるので、適切なバージョンをインストールしてください。Docker上のPHPを設定するとかもできるはずです。
composer.json の設定 は、Languages & Frameworks > PHP > Composer から設定できます。
Test Frameworks の設定は、 Languages & Frameworks > PHP > Test Frameworks から設定できます。composer で PHPUnit を入れていれば、「User composer autoloader」を選択し、autoload.php
のバージョンを指定すれば OK です。うまく設定できていれば PHPUnit のバージョンが表示されます。
モック
PHPUnit にはモックの仕組みがありますが、コードとして読みづらいので、 Mockery というライブラリを使うのがおすすめです。
コーディング規約
括弧や演算子の前後のスペースの有無がぐちゃぐちゃなコードを見た経験はありますか?インデントにタブとスペースが混ざっていると、とても芸術的な見た目になります。しかし、コードにはそのような芸術性より、レビューしやすさが必要です。
PHP には PSR-12 というコーディング規約があるので、これをもとにコードを書きます。
コーディング規約についても PHPUnit と同じ理由で、CI と IDE の設定を両方設定しておくと良いです。
設定の方法については、僕が過去のブログで書いているので参考にしてください。
ログ
ログは、基本的に1つのファイルにすべて出力するのが良いかなと思います(ファイル数を増やしすぎないのが良いかなと思います)。ログファイルが増えると、そのたびに監視対象のファイルを追加する必要があります。また、もう出さなくなったログのファイルが残り、空のログファイルが無限にログローテートされていたりします。
ログを ElasticSearch などに突っ込めば、検索性については問題ありません。むしろ、複数ファイルを横断して検索する必要もなくなり便利です。
PHP の標準規約である PSR-3 ( https://www.php-fig.org/psr/psr-3/ )では、ログレベルは9段階が用意されています。Laravel で利用している Monolog はこれに従っていますが、全部を利用する必要は無いです。
ただ、最低でも、3段階くらいには分けておいたほうが良いです。
- 即時対応が必要なログ(アラートが上がって電話がかかってくるタイプのやつ)
- 翌営業日の対応でも問題ないが、検知しておく必要があるログ(Slackとかメールで流れるやつ)
- ユーザーや関連システムからの問い合わせがあった場合のみ参照するログ(プッシュ通知の必要は無い)
ログには、
- ログの日時(Laravel だと自動で付与される)
- どのサーバーで出たか
- どのエンドポイント(URL)で出たか
- アクセスしたユーザーのID
- スタックトレースや、スクリプトファイル名
- 必要に応じてその他の情報
などを出します。
ログには、「何のログなのか・対応が必要なログなのか」「対応が必要な場合どのような対応をすれば良いのか」も出しておきましょう。エラーっぽいログなのに、必要な対応が書かれていないと、どうしたら良いのかわかりません。
コードの設計について
ニコニコ動画のコードは、以前はレイヤードアーキテクチャでしたが、最近は「依存性の逆転」をかなり意識したコードになっています(レイヤードアーキテクチャより前は、スパゲッティアーキテクチャでした)。
一方で、プレミアム課金については、まだほとんどがスパゲッティアーキテクチャです。まずはレイヤードアーキテクチャを目指してコードをリファクタリングして行こうと思っています。
どういう設計を目指すかについては、現状のコードとメンバーのスキルを考えてやっていくことが重要です。プレミアム課金システムのことを考えると
- とりあえずテストを拡充していくことが重要だと思うので、シンプルでわかりやすいレイヤードアーキテクチャでよい
- テストが充実すれば、レイヤードアーキテクチャ→クリーンアーキテクチャの変更もしやすくなる
- メンバーのレベル感も考える
- プレミアム課金システムに詳しいメンバーが少ないので、適切な設計をしづらい(配属されて浅い人が多い)
- 教育コストとかもかかる
- レイヤードアーキテクチャとクリーンアーキテクチャを両方つかって初めて、それぞれの良さがわかる。
- この経験がメンバーの成長にもつながる
- プレミアム課金システムに詳しいメンバーが少ないので、適切な設計をしづらい(配属されて浅い人が多い)
といった感じです。
PHPDoc
PHPDoc で、クラスの Doc を書くことは重要だと僕は思っています。
/**
* 何をするクラスなのか、何をしないクラスなのか書くことで、
* 適切なメソッドを適切なクラスに実装できるようになる。
* @see 仕様ページへのリンクなど
*/
class Xxxxx
{
...
}
また、@param
や @return
についても、PHP の型宣言より詳細な情報を書け、IDE の補完による恩恵も受けられるので、書いておいたほうがいいです。
/**
* @return string[] <- array の中身の型も書ける
*/
public function get(): array;
/**
* @return int|false <- mixed の詳細まで書ける
* @throw RuntimeExeption なんの時のエラーなのかちゃんと説明を書いてね
*/
public function get(): mixed;
GuzzleHttp
レガシーなコードでは API Client の実装に file_get_contents()
や curl
を使っていたと思いますが、 Composer があるなら Guzzle を使うほうが良いです。
Guzzle は PSR-18 のインタフェースに従って実装されています。
PSR-18 で重要なのが、エラーハンドリングの部分です。400エラーや、500エラーが返ってきた時には例外を投げるようになっており、Guzzle のエラーは以下のような構造になっています。
- \RuntimeException
- TransferException -- HTTPリクエストに関する例外
- ConnectException -- 接続がtimeoutした場合などの例外
- TooManyRedirectsException -- リダイレクトループの場合の例外
- BadResponseException -- 4XXまたは5XXエラー
- ClientException -- 4XXエラー
- ServerException -- 5XXエラー
- TransferException -- HTTPリクエストに関する例外
エラーについて理解し、どのようなエラーの時にどのようなハンドリングをすればいいのか、注意しましょう。
まとめ
先日、ウィスキーの蒸留所に行ってきたのですが、できたばかりのウィスキーの色は、12年後のウィスキーの色と全然違いました。
このように、ものは10年経つと全然変わります。先日 PHP 8 がリリースされたり、去年には PSR-12 が発表されたり、PHP も進歩しているので、我々も、常に新しい技術を取り入れて、改善を続けています。
レガシーコードばっかりいじっていて大変そうに思えますが、このレベルのレガシーコード改善は、ほぼ新規アプリケーションの開発のようなものです。ニコニコ動画のサーバーの規模は大きく、その中でどのようにコードを組み上げて行くか考えるのは非常に面白いです。
こういうリファクタリングのための工数を取ってもらえるのは、ドワンゴの非常に良い文化ですね。