10年戦士のレガシーPHPを改善するためにやってきたこと

ドワンゴで、ニコニコ動画とプレミアム課金の開発をしている @yoshikyoto です。

最近では、総合TOPのリニューアルや、↓の記事で書かれているプレミアム課金の移行をやっていました。

* [ニコニコで12年運用した決済システムを移行する上で必要だったこと](
https://qiita.com/chiyoppy/items/2571e8e3ae675cc7a991)

# レガシーなPHPを改善してきた知見

ニコニコは古くからあるサービスで、レガシーなコードも多く存在しています。

僕は新卒でニコニコ動画に配属され、5年間ずっと PHP を触ってきました。その中で、多くの新機能の開発、古いコードのリファクタリング、いろいろやってきました。その知見をまとめます。

# Composer, Laravel （フレームワーク）の導入

PHP を使っていて Composer を導入していないシステムは珍しい思いますが、10年もののシステムには、Composer が導入されていない場合もあります。導入されていないなら導入します。（Composer は JavaScript でいうところの npm 、 Ruby でいうところの Bundler です）

10年もののPHPには、フレームワークは導入されていません。URLのエンドポイントごとに PHP ファイルが存在しています。フレームワークを導入しましょう。基本的には Laravel を導入するのが良いです。

* https://readouble.com/laravel/7.x/ja/

レガシーなプロダクトに Laravel を導入する記事も以前に書きました。

* https://www.utakata.work/entry/laravel/for-legacy


Slim Framework を導入する場合もありました。 DI、DB、Redis、Log といったライブラリが導入済みで、ルーティングだけなんとかしたい場合は Slim Framework でもよいです。

![仙台の作並温泉のとある宿の本棚](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29024/7e593cf8-9049-8b8d-f870-172661d06b4b.png "仙台の作並温泉のとある宿の本棚")



# ユニットテスト（PHPUnit）

> 私にとって、レガシーコードとは、単にテストの無いコードです

『レガシーコード改善ガイド』にはこう書いてあります。

コードに変更を加えづらくなる一番の原因は、テストが無いことです。ユニットテストがあれば、コード修正時にある程度の動作が保証できます。コードが綺麗でも、テストがなければ、コードの修正がしづらくなるのです。

ユニットテスト導入時には同時に、以下の2点を考えると良いです。

* CIの導入
  * CIを導入しないと、いつの間にかテストが落ちている事に気づかない
* IDE から phpunit コマンドを実行できるようにする
  * CIでしかテストを実行できないと、開発効率が良くないので、当然手元のPCでもテストを実行できる必要があるのですが、IDE の設定をすることで、開発効率が格段によくなる

## CI の導入

特に説明不要かと思います。僕のチームでは Jenkins を利用していますが、CircleCI を使っているチームもあります。

`Jenkinsfile` という、CI の実行内容を定義できる仕組みがあるので（CircleCI でいう `.circleci/config.yml` みたいなもん）、これを使って phpunit コマンドを実行し、結果を表示させます。

* https://www.jenkins.io/doc/book/pipeline/jenkinsfile/

```groovy
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 におまかせできるのも良い点です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29024/e8603b30-9a0d-bc31-c68a-cedc889b781c.png)

これを出すためには

* PHP Interpreter の設定
* composer.json の設定
* Test Frameworks の設定

が必要です。

**PHP Interpreter の設定** は、 Preferences > Languages & Frameworks > PHP の「CLI Interpreter」から設定できます。PCにインストールされているPHPのバージョンから選択できるので、適切なバージョンをインストールしてください。Docker上のPHPを設定するとかもできるはずです。

![スクリーンショット 2020-12-04 16.11.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29024/096a4891-f3b1-7398-2714-a149ccd8b725.png)

**composer.json の設定** は、Languages & Frameworks > PHP > Composer から設定できます。

**Test Frameworks** の設定は、 Languages & Frameworks > PHP > Test Frameworks から設定できます。composer で PHPUnit を入れていれば、「User composer autoloader」を選択し、`autoload.php` のバージョンを指定すれば OK です。うまく設定できていれば PHPUnit のバージョンが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29024/73c19628-9f87-5f4c-b7bd-35912b08b9db.png)

## モック

PHPUnit にはモックの仕組みがありますが、コードとして読みづらいので、 Mockery というライブラリを使うのがおすすめです。

* https://readouble.com/mockery/1.0/ja/index.html

# コーディング規約

括弧や演算子の前後のスペースの有無がぐちゃぐちゃなコードを見た経験はありますか？インデントにタブとスペースが混ざっていると、とても芸術的な見た目になります。しかし、コードにはそのような芸術性より、レビューしやすさが必要です。

PHP には PSR-12 というコーディング規約があるので、これをもとにコードを書きます。

コーディング規約についても PHPUnit と同じ理由で、CI と IDE の設定を両方設定しておくと良いです。

設定の方法については、僕が過去のブログで書いているので参考にしてください。

* [PSR-2 に変わって新しく制定された PSR-12 のチェックを PHP_CodeSniffer で行う](https://www.utakata.work/entry/php/phpcs-psr12-intellij-jenkins)


# ログ

ログは、基本的に1つのファイルにすべて出力するのが良いかなと思います（ファイル数を増やしすぎないのが良いかなと思います）。ログファイルが増えると、そのたびに監視対象のファイルを追加する必要があります。また、もう出さなくなったログのファイルが残り、空のログファイルが無限にログローテートされていたりします。

ログを ElasticSearch などに突っ込めば、検索性については問題ありません。むしろ、複数ファイルを横断して検索する必要もなくなり便利です。

PHP の標準規約である PSR-3 （ https://www.php-fig.org/psr/psr-3/ ）では、ログレベルは9段階が用意されています。Laravel で利用している Monolog はこれに従っていますが、全部を利用する必要は無いです。

ただ、最低でも、3段階くらいには分けておいたほうが良いです。

* 即時対応が必要なログ（アラートが上がって電話がかかってくるタイプのやつ）
* 翌営業日の対応でも問題ないが、検知しておく必要があるログ（Slackとかメールで流れるやつ）
* ユーザーや関連システムからの問い合わせがあった場合のみ参照するログ（プッシュ通知の必要は無い）

ログには、

* ログの日時（Laravel だと自動で付与される）
* どのサーバーで出たか
* どのエンドポイント（URL）で出たか
* アクセスしたユーザーのID
* スタックトレースや、スクリプトファイル名
* 必要に応じてその他の情報

などを出します。

ログには、「何のログなのか・対応が必要なログなのか」「対応が必要な場合どのような対応をすれば良いのか」も出しておきましょう。エラーっぽいログなのに、必要な対応が書かれていないと、どうしたら良いのかわかりません。

![仙台駅の近くにあるマンホール](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29024/c475cc7b-1a9f-12c1-3604-bd8de6822dd5.jpeg "仙台駅の近くにあるマンホール")

# コードの設計について

ニコニコ動画のコードは、以前はレイヤードアーキテクチャでしたが、最近は「依存性の逆転」をかなり意識したコードになっています（レイヤードアーキテクチャより前は、スパゲッティアーキテクチャでした）。

一方で、プレミアム課金については、まだほとんどがスパゲッティアーキテクチャです。まずはレイヤードアーキテクチャを目指してコードをリファクタリングして行こうと思っています。

どういう設計を目指すかについては、現状のコードとメンバーのスキルを考えてやっていくことが重要です。プレミアム課金システムのことを考えると

* とりあえずテストを拡充していくことが重要だと思うので、シンプルでわかりやすいレイヤードアーキテクチャでよい
    * テストが充実すれば、レイヤードアーキテクチャ→クリーンアーキテクチャの変更もしやすくなる
* メンバーのレベル感も考える
    * プレミアム課金システムに詳しいメンバーが少ないので、適切な設計をしづらい（配属されて浅い人が多い）
        * 教育コストとかもかかる
    * レイヤードアーキテクチャとクリーンアーキテクチャを両方つかって初めて、それぞれの良さがわかる。
        * この経験がメンバーの成長にもつながる

といった感じです。

# PHPDoc

PHPDoc で、クラスの Doc を書くことは重要だと僕は思っています。

```php
/**
 * 何をするクラスなのか、何をしないクラスなのか書くことで、
 * 適切なメソッドを適切なクラスに実装できるようになる。
 * @see 仕様ページへのリンクなど
 */
class Xxxxx
{
    ...
}
```

また、`@param` や `@return` についても、PHP の型宣言より詳細な情報を書け、IDE の補完による恩恵も受けられるので、書いておいたほうがいいです。

```php
/**
 * @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 を使うほうが良いです。

* http://docs.guzzlephp.org/en/stable/quickstart.html

Guzzle は PSR-18 のインタフェースに従って実装されています。

* https://www.php-fig.org/psr/psr-18/

PSR-18 で重要なのが、エラーハンドリングの部分です。400エラーや、500エラーが返ってきた時には例外を投げるようになっており、Guzzle のエラーは以下のような構造になっています。

* \RuntimeException
    * TransferException -- HTTPリクエストに関する例外
        * ConnectException -- 接続がtimeoutした場合などの例外
        * TooManyRedirectsException -- リダイレクトループの場合の例外
        * BadResponseException -- 4XXまたは5XXエラー
            * ClientException -- 4XXエラー
            * ServerException -- 5XXエラー

エラーについて理解し、どのようなエラーの時にどのようなハンドリングをすればいいのか、注意しましょう。

# まとめ

![仙台市作並のあたりにあるニッカウヰスキー宮城峡蒸溜所](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29024/c76b5418-97f9-29a0-8319-3fc0d95f812a.png "仙台市作並のあたりにあるニッカウヰスキー宮城峡蒸溜所")


先日、ウィスキーの蒸留所に行ってきたのですが、できたばかりのウィスキーの色は、12年後のウィスキーの色と全然違いました。

このように、ものは10年経つと全然変わります。先日 PHP 8 がリリースされたり、去年には PSR-12 が発表されたり、PHP も進歩しているので、我々も、常に新しい技術を取り入れて、改善を続けています。

レガシーコードばっかりいじっていて大変そうに思えますが、このレベルのレガシーコード改善は、ほぼ新規アプリケーションの開発のようなものです。ニコニコ動画のサーバーの規模は大きく、その中でどのようにコードを組み上げて行くか考えるのは非常に面白いです。

こういうリファクタリングのための工数を取ってもらえるのは、ドワンゴの非常に良い文化ですね。