(日本語訳) Migrating Garoon codebase to PHP 7

(※注 この記事は2017年8月18日に公開したMigrating Garoon codebase to PHP 7の日本語訳です。)

Garoon開発チームは 一年前にGaroonのソースコードのPHP 7移行作業を開始しました。 Garoonはソースコードも多かったり、もともとPHP 4で書いていたものをPHP 5に移行していたりするので、 移行作業は大変でしたが、移行することで大きな改善が行えました。 メリットの一つは性能改善です。GaroonをPHP 7 で動かしたときのベンチマークは、PHP 5.6と比べると33%改善しました。

そしてまた、php.netによると、PHP 5.6のアクティブサポートは2017年1月19日に終了しています。 ソース: http://php.net/supported-versions.php

この記事では、Garoon開発チームの遭遇した現象と、どうやってPHP 7に移行したかをお伝えします。

互換性のない変更を検出するためのツール

PHP 5から PHP 7に移行するとき、ソースコード全体から"下位互換性のない変更"を見つける必要がありました。私たちのコードがPHP 7で正しく動作することを確認するためです。 ソースコードを調べてPHP 7で動かすために修正が必要なコードを指摘してくれるツールがいくつかあります。その中にPhanとLintがあります

Phan

PhanはPHP用の静的解析ツールです。MITライセンスで公開されています。

環境の準備

PHP 7がインストールされた CentOSマシンを用意します。それから、以下のコマンドでcomposerをインストールします:

curl \-sS https://getcomposer.org/installer \| php \-\- \--install-dir=/usr/local/bin \--filename=composer

Phanとphp-ast拡張のインストール

まず、php-astのソースコードをcloneします:

git clone https://github.com/nikic/php-ast.git

次に、php-ast拡張をビルドして、phpに追加します。以下のコマンドを順番に実行してください。

cd php-ast
phpize
./configure
make install
echo 'extension=ast.so' > /etc/php.ini

それから、Phanをインストールして以下のコマンドで実行してください:

git clone https://github.com/etsy/phan.git
cd phan
composer install
./test
ln \-s /phan/phan /usr/local/bin/phan

Phanを実行して下位互換性のない変更を見つける

私たちは以下の内容のようなbashファイル(例: testPhan.sh)を作成しました:

COMMAND="phan \--backward-compatibility-checks \--ignore-undeclared \--quick"
find /repo/source \-type f \-regex '.*\.\(php\)' \-print0 \| xargs \-0 \-n1 \-P 1 \-I % bash \-c "$COMMAND % "

ファイル中の"/repo/source"をチェック対象のソースコードのパスに変更してください。それから実行します:

sh testPhan.sh

Lint

LintはPHPのコマンドラインオプションです。PHPの文法チェックに使われます。

前述のCentOSのマシンで以下のコマンドを実行し、PHPのソースコードをLintでチェックしました:

find . -name "*.php" -print0 | xargs -0 -n1 -P8 php -l

E_STRICT通知の深刻度の変更

E_STRICT通知の深刻度がPHP 7で変更されています。 すべてのE_STRICT通知がほかのレベルに移動しています。

状況 新しいレベル / 挙動
Indexing by a resource E_NOTICE
Abstract static methods Notice removed, triggers no error
"Redefining" a constructor Notice removed, triggers no error
Signature mismatch during inheritance E_WARNING
Same (compatible) property in two used traits Notice removed, triggers no error
Accessing static property non-statically E_NOTICE
Only variables should be assigned by reference E_NOTICE
Only variables should be passed by reference E_NOTICE
Calling non-static methods statically E_DEPRECATED

私たちのコードでもこれらのいくつかは影響がありました。

Signature mismatch during inheritance

"Signature mismatch during inheritance"通知は以下のようなケースで通常発生します: 一つ目のケースは、関数/メソッドのパラメータの数が継承時に異なる場合です。例:

<?php
class Foo{
    function method($a){

    }
}
class Bar extends Foo{
    function method(){

    }
}

上記の例の出力結果はこちらです:

Warning: Declaration of Bar::method() should be compatible with Foo::method($a) in /in/YoDHq on line 11

この例の実行結果はこちらで確認できます。

二つ目のケースは、関数/メソッドのパラメータの型が継承時に異なる場合です。例:

<?php
class Foo{
    function method($a){

    }
}
class Bar extends Foo{
    function method(array $a){

    }
}

上記の例の出力結果はこちらです:

Warning: Declaration of Bar::method(array $a) should be compatible with Foo::method($a) in /in/fSIbS on line 11

この例の実行結果はこちらで確認できます。

私たちはPhanを使ってコードをチェックし、PhanSignatureMismatch や PhanSignatureMismatchInternal として検知された問題を修正しました。

このタイプの問題の修正方法は、変更対象の基底クラスやサブクラスの中のロジックに依存します。

Only variables should be assigned by reference

例:

<?php
error_reporting(E_ALL);
function foo(){
    return 'string';
}
$temp =& foo();

上記の例の出力結果は以下のようになります:

Notice: Only variables should be assigned by reference in /in/vXUIZ on line 6

この例の実行結果はこちらで確認できます。

上の例では、'&'演算子が"Only variables should be assigned by reference"というE_NOTICEを引き起こしています。PHP 7の環境でコードがコンパイルされた場合は、エラーが検出されてしまうので、以下のように'&'演算子を削除して修正する必要があります:

<?php
error_reporting(E_ALL);
function foo(){
    return 'string';
}
$temp = foo();

しかし、私たちのコードには'&'演算子がたくさん使われており、すべて修正するには時間がかかりそうなことがわかりました。 この問題を解決するため、CentOS上にあるツールを導入しました。 まずはじめに、E_NOTICEメッセージをphp_error.logに出力するため、以下の手順を行います:

1. 以下のスクリプトをwebアプリケーションに追加して、実行します:

set_error_handler(function($errno, $errstr, $errfile, $errline){
  file_put_contents('/tmp/php_error.log', implode(',', [$errno, $errstr, $errfile, $errline]) . PHP_EOL, FILE_APPEND);
});

2. "php_error.log"は以下のようになっているはずです:

2048,Only variables should be assigned by reference,/var/www/project/code/xxx1.php,12
2048,Only variables should be assigned by reference,/var/www/project/code/xxx2.php,121
2048,Only variables should be assigned by reference,/var/www/project/code/xxx3.php,26
2048,Only variables should be assigned by reference,/var/www/project/code/xxx.php,194

このツールは以下の手順を実行します:

  1. tmpフォルダの中からsystem-private-から始まるフォルダの最新のものを見つけます。
  2. tmpフォルダ中のphp_error.logから"assigned by reference"という文字列をgrepで検索します。そして、ファイル名と行番号を一時的な結果として出力します。
  3. 2.の結果をもとに、"=&"、"= &"、"= & "を"="に置換します。
#!/bin/bash

systemd_private_name=$(ls -t1F /tmp/ | grep systemd-private-* | head -n1)
php_error_path="/tmp/${systemd_private_name}tmp/php_error.log"

if [ ! -f $php_error_path ]; then
    echo "File is not found"
    exit;
fi


grep "assigned by reference"  $php_error_path  | sort | uniq -c | while IFS=',' read -ra line ; do
    sed -ri ${line[3]}'s/(\$.+)(\s+=\s*&\s*)(.+())/\1 = \3/g'  ${line[2]}
done

Only variables should be passed by reference

PHP 7では、関数のパラメータを参照渡しにした場合、エラーが発生します。 この問題はPHPのフレームワークやユーザ定義メソッドを使用するときに発生します。

この問題を解決するため、私たちは関数の値を保存するための変数を宣言し、 直接呼ばれていた箇所にこれを渡しました。以下がその例です:

例: explodeで作られた配列をarray_popに渡すと、Strict Standards警告が発生する:

<?php
   $fruit = array_pop(explode(",", "Orange,Apple,strawberry"));
   echo $fruit;

修正後のコード:

<?php
   $fruits = explode(",", "Orange,Apple,strawberry");
   $fruit = array_pop($fruits);
   echo $fruit;

この例で注目していただきたいのは、explode関数用に変数を割り当ててからその変数の参照をarray_popに渡すことで、Strict Standards警告を回避している点です。

PHP フレームワークのメソッド

例:

<?php
   $file_name = "abc.txt";
   $file_extension = end(explode('.', $file_name));

上記の例の出力結果はこちらです:

Notice: Only variables should be passed by reference in /in/Cv65t on line 3

この例の実行結果はこちらで確認できます。

修正後のコード:

<?php
   $tmp = explode('.', $file_name);
   $file_extension = end($tmp);

私たちはPhanを使ってコードをチェックし、PhanTypeNonVarPassByRef として検知された問題を修正しました。 しかし、Phanが調べるメソッドはPHPフレームワークのものだけです。 ユーザ定義関数は PhanTypeNonVarPassByRef として検知されません。以下の例をご覧ください。

ユーザ定義メソッド

例:

<?php
function getArray() {
    return [1, 2, 3];
}

function squareArray(array &$a) {
    foreach ($a as &$v) {
        $v **= 2;
    }
}

// Generates a warning in PHP 7.
squareArray((getArray()));

上記の例の出力結果はこちらです:

Notice: Only variables should be passed by reference in in /in/HYONN on line 14

この例の実行結果はこちらで確認できます。

プロジェクト内のユーザ定義メソッドに対して調査する際は、notepad++などのエディタで正規表現による検索が利用できます。

function.*\&\s+\$.*\)$

上記の例の出力結果はこちらです:

E:\yourproject\comment_util.php (3 hits)
    Line 30:     function getFollowByUser( & $user, & $article )
    Line 61:     function _setNameRowSet( & $rowset, $inverse = FALSE )
    Line 119:     function _row2object( & $row )

参照メソッドを検索結果から見つけた後は、手動で関数呼び出しを修正します。

Calling non-static methods statically

staticではないメソッドのstatic呼び出しはPHP 7で非推奨になり、将来的には削除される可能性があります。

例:

<?php
class foo {
    function bar() {
        echo 'I am not static!';
    }
}

foo::bar();

上記の例の出力結果はこちらです:

Deprecated: Non-static method foo::bar() should not be called statically in - on line 8
I am not static!

私たちはPhanを使ってコードをチェックし、PhanStaticCallToNonStatic として検知された問題を修正しました。 一番簡単な修正は、staticではない関数をすべてstaticな関数に変更することです。

PHP 4コンストラクタ

PHP 4コンストラクタは、クラス内で定義されているメソッドでクラスと同じ名前のものです。

PHP 7ではPHP 4コンストラクタが定義されていると、E_DEPRECATEDが発生します。 メソッド名がクラス名と一致し、クラスが名前空間内になく、PHP 5のコンストラクタ(__construct)がない場合、E_DEPRECATEDが発生します。

例:

<?php

class Filter {
    function Filter() {

    }
}

new Filter();

出力:

Deprecated: Methods with the same name as their class will not be constructors in a future version of PHP; Filter has a deprecated constructor in /in/IDpi7 on line 3

この例の実行結果はこちらで確認できます。

以下のように、PHP 5のコンストラクタを使用する必要があります:

<?php

class Filter {
    function __construct() {

    }
}

new Filter();

大きなプロジェクトの場合、手動での修正は時間がかかります。PHP-CS-Fixerという便利なツールがあります。これを使うと自動で修正することができます。 GaroonではPHP-CS-Fixerを使って500以上のファイルを修正しました。

このツールでPHP 4コンストラクタを修正する方法を記載します。下記のコードをPHP-CS-Fixer 2.2.1で実行してください。

1. まず、PHP-CS-Fixerの設定ファイルを作成します

.php_cs
<?php

$source_dir = 'c:\repo\projectX\source';
$finder = PhpCsFixer\Finder::create()
    ->in($source_dir)
    ->files()->name('*.csp')->name('*.php')
;

return PhpCsFixer\Config::create()
    ->setRiskyAllowed(true)
    ->setRules(array(
        'no_php4_constructor' => true
    ))
    ->setFinder($finder)
;

注意: Garoonでは、PHPファイルの拡張子として".csp"を使用しているので、 ".csp"を設定ファイルに追加しています。

2. PHP-CS-Fixerを実行します

php php-cs-fixer.phar fix --config=.php_cs --verbose

このツールの詳細はこちらをご参照ください: https://github.com/FriendsOfPHP/PHP-CS-Fixer

内部関数の変更

PHP 7ではいくつかの内部関数が変更されています。substr()関数もその一つです。

例:

<?php
$foo = substr("foo",3);
if($foo !== FALSE){
    echo "PHP 7\n";
    echo gettype($foo); //string
}
else{
    echo "PHP 5\n";
    echo gettype($foo); //boolean
}

上記のコードの$foo変数の型は、PHP 5ではbooleanですが、PHP 7ではstringです

この例の実行結果はこちらで確認できます。

この問題を解決するため、strlen関数で文字長をチェックしました。

<?php
$foo = substr("foo",3);
if(strlen($foo) == 0){
    echo "Run on both PHP 5 and PHP 7\n";
    echo gettype($foo); //boolean on PHP 5 and string on PHP 7
}

この例の実行結果はこちらで確認できます。

まとめ

GaroonではPHP 5.6からPHP 7に移行することでパフォーマンスが向上し、コードも読みやすくなりました: 例えば、社内の性能検証では33%の性能向上が確認できています。 私たちは、移行作業を分けて一つずつ実施していました。これにより、起こりえる問題に対処しながら、短期間で移行作業を完了できました。