PHP7->PHP8に、PHPUnit5->9にバージョンアップした話


## 概要
PHP7系から8系にバージョンアップをしました。
それはもう大変な、大型対応でした:angel_tone2:

* PHP7.2->8.0
* PHPUnit5.7->9.5
* 対象は約20万行、400クラス

フレームワークはPhalcon。

まだまだこういったバージョンアップ対応はいろんな場所で行われると思うので、
* やり方
* 主な修正点

を書き記しておきます。


## やり方
全UnitTestが通るようにコードを整備:fire:
1. まず、全クラスUnitTestがあることを確認。ない場合は作成
2. PHP7.2/PHPUnit5.7環境のJenkinsでUT実行し、エラーを1つずつ拾って修正
3. PHP8.0/PHPUnit9.5環境のJenkinsでUT実行し、エラーを1つずつ拾って修正
4. PHP8.0/PHPUnit9.5環境のJenkinsでUT全部通ったら完了！

とにかく骨が折れましたが、チームメンバー全員で協力し、なんとかやり遂げました。
* 普段の業務をしながら並行で進める。約1年くらいの期間で実施
* UT実行はJenkinsさんにお任せ。人間はエラー内容調査と修正に注力
* エラーの種類ごとに担当を決め、同じ種類のエラーは同じ人が修正
* 調べてもよく分からない場合は早めにアラートをあげてチーム内で協力して調査


# 主に修正した点
修正箇所は沢山ありましたが、中でも修正件数が多かったものをピックアップします。

## PHP7->8
修正点のほとんどは、下位互換性のない変更点でした。

https://www.php.net/manual/ja/migration80.incompatible.php


### 三項演算子をネストする場合、括弧が必要になった

```php
// エラーになる
$page = ($pageTemp < 1) ? 1 : ($pageTemp > $pageEnd) ? $pageEnd : $pageTemp;
// エラーにならない
$page = ($pageTemp < 1) ? 1 : (($pageTemp > $pageEnd) ? $pageEnd : $pageTemp);
```

PHP8以降、三項演算子でネストしている部分は括弧で囲ってあげないとエラーになるようになりました。


### 厳密でない比較での数値比較の挙動変更

この変更、今更かい！という声も多かったです。

| 比較 | PHP8以前 | PHP8以降 |
|:-:|:-:|:-:|
| 0 == "foo" | true | false |
| 0 == "" | true | false |
| 42 == "42foo" | true | false |

```php
// 全角文字は数値でない文字列
$count = '５';

// PHP8以前ではtrue、PHP8以降ではfalse
$count == 0;
```

「そんな実装するなよ」というのは一旦置いておいて、
厳密でない比較で文字列が「0」として扱われることを利用している箇所の修正が必要でした。
画面からの入力値チェックとかで実際にありました。

### PDO::inTransactionで実際のトランザクションの状態を見るようになった

PHP8以前の`inTransaction()`は、`beginTransaction()`でトランザクションが開始されたかどうかのみを判定しており、それ以外の方法で開始されたトランザクションは検出できなかったとのこと。

しかしPHP8以降では、実際のトランザクションの状態を見るようになりました。

> PDO::inTransaction() は、 PDO が管理しているおおよその情報ではなく、 実際のトランザクションの状態を報告するようになりました。 クエリが "暗黙のコミット" に依存していた場合、 PDO::inTransaction() は後に false を返します。 なぜなら、トランザクションが既にアクティブではないからです。

MySQLなど一部のDBでは、トランザクションの途中で`DROP TABLE`や`CREATE TABLE`といったDDL文があると、そのDDL文が暗黙的にコミットされるという挙動があります。（="暗黙のコミット"）

これに当てはまる場合、PHP8以前と以後では`inTransaction()`の結果が変わります。


### 一部NOTICE, WARNINGのエラーレベル変更

一部のNOTICEがWARNINGに、
一部のWARNINGが例外（TypeError）に変更となりました。

[PHP: 下位互換性のない変更](https://www.php.net/manual/ja/migration80.incompatible.php)より抜粋

>  "A non well-formed numeric value encountered" という E_NOTICE が発生する文字列 は、"A non-numeric value encountered" という E_WARNING が発生するようになっています。 そして、"A non-numeric value encountered" という E_WARNING が発生していた全ての文字列は、 TypeError が発生するようになりました。

> この E_WARNING から TypeError への変更は、 不正な文字列オフセットの場合に発生する "Illegal string offset 'string'" という E_WARNING にも影響します。

WARNINGだから後で直せばいいや〜と放置していた場合、動かなくなります:cry:

### 継承でLSP原則に違反するとエラーになる

「LSP原則」＝「リスコフの置換原則」。
「派生型（サブクラス）はその基底型（スーパークラス）と置換可能でなければならない」
という原則です。

下記はPHP8以前ではWARNINGですが、PHP8以降ではエラーとなります。
```php
class ServiceA {
    public function methodX(array $a) {
        // 処理
    }
}

class ServiceB extends ServiceA {
    public function methodX(int $a) {
        // 処理
    }
}
```

## PHPUnit5->9

### `@expectedException`アノテーションが廃止に

```php
/**
 * これが使えなくなる
 * @expectedException
 */
public function testHoge()
{
    // 代わりにexpectedExceptionメソッドを使う
    $this->expectedException(Exception::class);
}
```

### `at()`が非推奨に

```php
public function testHoge()
{
    $mock->createMock(XXXService::class);
    // XXXServiceのhoge()が2回呼び出されることを検証
    // 非推奨のwarningが出るようになる
    $mock->expects($this->at(2))
        ->method('hoge')
        ->willRetuen('result');
}
```

ここでは、`exactly()`や`once()`を使うことでwarningを回避しました。

### 「test」で始まる名前のメソッドではアサーションしないとエラーになる
```php
/**
 * @dataProvider testProvideHoge
 */
public function testHoge($params)
{
    $result = $target->hoge($params);
    $this->assertEquals(10, $result);
}

// データプロバイダでも、「test」から始まるとテストメソッドとして認識されてしまうので改名が必要
public function testProvideHoge()
{
    return [
        'Case1 => ...
    ];
}
```