こんにちは、今日もがんばってるuzullaです。PR TIMESでPHP等と向き合い続け、色々なことを日々やっています。
さて、PHPコードの改善というと新しいバージョンのPHP対応と言われて久しいですが(私の感想)、今日のエントリのオチからいえば「PHP5でもよいプログラムは書ける」です。
ムード
PR TIMESに入った当初「PHPのバージョンが古いからダメだ」というムードがありました(まあ、良くはないのだが)。それに対して私は「なぜ?」となったのを覚えています。
PHP7や8になって増えた機能は非常に多岐にわたり魅力的です。しかし、それがないと良いコードがかけないのかといえば「(私の主観では)否」です。
良いコードと言うとかなり抽象的なのですが、
- 読みやすく、理解しやすい
- 新規追加やメンテがしやすい
- PHPのバージョンアップに追従できる
ということに、一旦はしておきます。
その精神をもちつつ、先日PressKitという機能を開発しました。別の開発系の記事もあるのでぜひこちらもどうぞ。

「PHP5 時代のコードとは…?!」
<?
include("abc.php");
include("def.php");
include("conf.php");
include("db.php");
include("some.php");
include("what.php");
Define("NUM", 100);
class super_calc extends great_calc {
/* * * * コンストラクタ * * * * */
public function super_calc($initial_num){
$this->db = DB::getDb(DSN);
$this->initial_num = $initial_num;
}
/* * * * チェック * * * * */
public add_ok($add_num){
$res = $this->addable($add_num);
if(empty($res)){
var_dump($res);
exit;
}
return "1";
}
}
?>
皆さんの考える「PHP5時代のコード」とはこんな感じでしょうかね、今即興で書いたので、動くとかうごかないとかはしりません。ということでコレを改善していきましょう。
まず、コードをPSR-4準拠にしました。そう、PHPは5.3.3くらいからComposerがつかえますしAutoloaderもありました。しかし、なぜか使っていなかったのです。ということでつかうことにしました。私にとっては非常に自然なことです。普通にnamespaceをバンバン切っていきます。
そして、なぜかPHP5では型がないことにされていました。あらゆるものが mixed|string|int|null です。が、普通に is_string や is_array みたいなのをいれると制御ができます。
さらに、なぜかPHP5では引数にType Hintが書けないということにされていましたが、クラス名やArrayは書けます。勿論それ以上もPhpDocに書けばIDEが知ることができます。
なぜかPHP5ではデータ構造に使えるものは配列かStdClassしかない、という事になっていたので、値クラスをつくります。
続いて、なぜかPHP5では例外はExceptionしかないことにされていたので、PDOExceptionや、必要なら自前のExceptionをつくることにしました。いや、そもそもなぜかPHP5では例外のなかで例外をうまく扱うことができないことにされていたので、使うようにしました。
私にはわからない理由で、なぜかPHP5では定数はグローバルしか存在しえないことになっていたので(まあ、定数で配列が使えない等はあるが)、クラスをつくり、その中に定数を書くことにしました。
すると当然のように、なぜかPHP5.4以降では使えるtraitがないことになっていたので、traitをつかうようにしましたし、static::func()(遅延束縛)がないことにもなっていたので、関数作成をやめ、ネームスペース付きクラスの静的メソッドにしました。
ここまできても、なぜかPHP5では… 長いのでやめます。まあ色々ありますね。
それ!PHP5で!できるよ!
<?php
namespace Super;
require_once(__DIR__."/../../autoloader.php");
use Excepiton;
use InvalidArgumentException;
use Parent\BasicCalcFunctionTrait;
use PDO;
use PDOException;
class SuperCalc extends CalculatorInterface
{
use BasicCalcFunctionTrait;
const PERCENT_NUM = 100;
/** @var int */
private $initialNum;
/** @var PDO */
private $pdo
/**
* @var int $initial_num 初期値
* @var PDO $pdo
*/
public function __construct($initial_num, PDO $pdo){
$this->pdo = $pdo;
if(!is_int($num)){
throw new InvalidArgumentException("must int");
}
$this->initialNum = $initial_num;
}
/**
* 基礎数に、DBのレートを勘案した上で、計算が可能か判定
* @var int $add_num 加算数
* @return bool
* @throw PDOException
*/
public function isAddable($add_num){
try{
return $this->isCalculatableNumberWithRateInDB($this->initial_num);
} catch (InvalidArgumentException $e){
error_log(__FILE__.":".__LINE__." out of bound {$e->getMessage()}");
return false;
}
}
}
するとまあ、たとえばこういうような感じになります。(繰り返しますが即興の概念コードです)
何が変わったのかといえば前述の通りですが、この時点でPhpStormや静的解析ツールが効き始めて6割位は勝ちゲーになります。
冒頭にもどりますが、「それPHP5でできるよ!」なのです。なんかPHP8にして、すごい最新のフレームワークをいれないとなにもできないみたいなことになってますが全然そんなことない。PHP5を十分につかえば全部できる。
「できない感情」も理解できます、既存コードに追加する作業でいいじゃないか、安定している継ぎ足しを続けたい、という素朴な発想からきているのだと思っています。あと、なんとなくコードが増えた気がしてうんざりしますよね。
大丈夫、ちゃんとNameSpaceをつけてPhpDocもかいたメソッド等ならIDEがリネームを一発でやってくれます。そして、すぐさま長大なトランザクションスクリプトの共通処理をらくらく抜き出していくこともできるはずです。どんどんIDEの支援がきいてきます。
そしてテストに戻る
え?そんなリファクタが気軽にできるはずがない?まあ、それで腰が重いのもわからんでもない…でもそれはテストがないからです。頑張って書きましょう。
今回はテストも最初からいれました。UnitTestはPHP5でもできます!上記のように「どこからでもpdoが取り出せるサービスロケータ」みたいな便利な毒薬をやめていれば、E2E的な「コントローラから叩く」「Exitしちゃうからテストできない…」等ではなく、コンストラクタインジェクションしてオーバーヘッドのすくないテストがガンガンかけますね。
<?php
namespace MyCorp\Tests;
use PHPUnit_Framework_TestCase;
class MyCalcTest extends PHPUnit_Framework_TestCase
{
public function testManyManyPattern()
{
$pdo = $this->getTestPDO();
$calc = new SuperCalc(100, $pdo);
$success_test_input_list = [
1,2,200,999,//...
];
foreach($success_input_list as $test_input){
$this->assertTrue($calc->isAddable($test_input), "failed with {$test_input}");
}
$fail_test_input_list = [
0,9999999999,//...
];
foreach($fail_test_input_list as $test_input){
$this->assertFalse($calc->isAddable($test_input), "failed with {$test_input}");
}
}
}
(繰り返しになりますが、概念コードです。実際はPDOだけじゃなくて色々インジェクションされたりするでしょう、アンチロケータ!ノーモアexit or die!)
昔のコードだと、毎回 new super_calc() したりしてDBコネクションを使い回せないなどで(まあこの程度のコードなら許されるかもしれませんが)テスト実行速度に差がでたりします。
(しない場合もありますが、まあこれは概念コードなので)
そしてE2Eも必要ですね、E2Eがあればポチポチテストをせずにリファクタができるようになり、気軽に安全にコードを書き直し続けることができます。
ただし、E2Eテストはこのコードと同じプロセスで動く必要がなかったし、もっとモダンな書き方したいし、OASと整合性があるかをJsonSchemaをあててチェックするライブラリ使いたいし…等と欲求もあるので、PHP5で書かず、PHP8.1 + Symfony/HttpBrowser + PhpUnitで書きました。
(APIなので、ヘッドレスブラウザは不要。なお、ヘッドレスブラウザでのE2Eテストも成長中です)
と、いうことで、モダンになりました。
ということで、PHP5が悪いのではなく、PHP5時代にかかれたコードはPHP4時代の常識にとらわれており(今から見れば)悪い設計をしていただけであり、自制心をもって正しく自分を律するかぎり、PHP5でも良いコードはかけるという話でした。
いや、いうて無理に突っ込めばとおっちゃうのであぶなっかしい、さっさとバージョン上げてType declarationsにきりかえていきたいと思います。
話を戻しますが、直せないのは「わからない」からであり、わからないのは「面倒」であり、面倒なことは「先送り」にされて、「PHPのバージョンアップをするぞ!」という錦によって全部書き直すという極端な解決策になりがちです。
そうじゃないぞ、いまある道具で日々やっていくんだ。
- 読みやすく、理解しやすい ⇒ 静的解析でたどることができ、リファクタできる
- 新規追加やメンテがしやすい ⇒ 型がゆれにくくなり、安心して使える
- PHPのバージョンアップに追従できる ⇒ テストが書きやすい
もうほとんど現代のPHPと大差ありません。本当です、8.1も書いてる人間がいうから本当だったら本当です(つまり筆者の主観です)。
コードの良し悪しとPHPバージョンは関係がない(?)
そりゃまあ、完全に比較すれば古いPHPは SomeClass::Class がつかえないとかで不便ですよ、定数に配列もつかえないし。でも、プリミティブな機能しかないとまともな設計ができないなんてことはない。現代の知識をもって、過去のコードをコピペしなければ、昔の道具でもより良くつかうことができます。
どんどんNameSpaceを切り、型を(自前で)固定し、=== におきかえ、empty() みたいなのを排除し、$num=”0” みたいなコードを消し、配列で構造を作るのをやめる…つまり現代では常識となったようなコードにするだけです、かんたんでしょう?
昔からできるのに、「この時代のPHPだから」と諦めるのはPHPとの戯れがたりません。
そして、あの時代から色々常識は進化しているのは御存知の通りです、それはレガシーコードにも持ち帰ることができるのです。
逆にいえば、PHP8で動作しているアプリでも「おや、PHP5.1.6かな?」というコードも現場によっては見かけます。データ構造がStdClassで、DB操作はトランザクションがなく、E_ALLでなく、例外はExceptionのみ、global変数を多様し、$_GETを配列として直接コードに書く等です。
PHPコードの価値は動くバージョンだけじゃないんだ!!みんなわかってくれ!!
と、いうことで進化しました
ということで、既存との整合性も重視した上で、値オブジェクトをつくり、きちんと型がゆれないコードにし、エラーチェックをし、グローバルオブジェクトを廃し、PDOを適切に扱うようにし、キャッチされない例外を外周でとらえてゴミをださずに適切にエラー画面がだせるようにしたコードがうごきはじめました。
PhpStormのCode Inspectionも、「単語が辞書にないぞ」位しか文句が言えない世界です!めでたいですね。
皆さんもPHPや手元のコードを見てみませんか?ツールやスタックが古いと腐らず、設計に正面から向かい合うことで見えてくる・やれることが沢山あります。お互い頑張っていきましょう!
なお、そうはいうても色々がんばっており、もうちょっとでバージョンが上がる予定です。 #めでたいですね。

最後に、なにか一つだけやるならE2Eテストがおすすめです。E2Eしか勝たん。現場からは以上です。
宣伝
なお、PR TIMESはPHPのカンファレンスであるところの、PHPerKaigi 2022のスポンサーをしております。弊社メンバーも登壇します!ぜひ皆さんご参加ください!!
