Help us understand the problem. What is going on with this article?

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

ドワンゴで、ニコニコ動画とプレミアム課金の開発をしている @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 におまかせできるのも良い点です。

image.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

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

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

image.png

モック

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エラー

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

まとめ

仙台市作並のあたりにあるニッカウヰスキー宮城峡蒸溜所

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

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

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

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away