PHPカンファレンスの動画で、
「[phpconfuk2017] PHP7で堅牢なコードを書く - 例外処理、表明プログラミング、契約による設計」という動画があって勉強になったのでメモ。
<動画URL>
https://www.youtube.com/watch?v=54jHDHvcYAo
<スライドURL>
https://speakerdeck.com/twada/php-conference-2016?slide=25
ここでは、その一部の型制約による例外の予防、責務の再配置あたりを記載します。
時間があれば続きの部分も記載しようと思います。
■エラーが発生しやすいコード
class BugRepository
{
public static function findAll($params)
{
global $CONF;
$pdo = new PDO($CONF['dsn'],$CONF['usr'],$CONF['passwd'],[PDO::ATTR_EMULATE_PREPARES => false]);
$sql = 'SELECT bug_id,summary,date_reported FROM Bugs WHERE assigned_id = :assignedTo AND status = :status';
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$return $stmt->fetchAll(PDO::FETCH_CLASS, Bug::class);
}
}
print_r(BugRepository::findAll([
'assignedTo' => '12',
'status' => 'OPEN',
]));
■エラーが発生する可能性がある箇所
(1)
$pdo = new PDO($CONF['dsn'],$CONF['usr'],$CONF['passwd'],[PDO::ATTR_EMULATE_PREPARES => false]);
・データベース接続確立失敗
・'usr'
、'passwd'
が変更された場合
(2)
$stmt = $pdo->prepare($sql);
・テーブル名やカラム名が誰かに変更された場合
・(ここで)データベース接続エラー
(3)
$stmt->execute($params);
・$params
がnull
・$params
のキー名や数の不一致
・$params
の値が文字列に変換不能
・(ここで)データベース接続エラー
(4)
$return $stmt->fetchAll(PDO::FETCH_CLASS, Bug::class);
・Bug::class
が未定義
・(ここで)データベース接続エラー
■対策
1.誤った使い方による例外の処理
・$params
がnull
-> 誤った使い方による例外
・$params
のキー名や数の不一致 -> 誤った使い方による例外
・$params
の値が文字列に変換不能 -> 誤った使い方による例外
まずは、この誤った使い方による例外に対応する
よくある対策は様々な条件で値のチェックを行うというアプローチ
if(is_null($params)){
throw new InvalidArgumentException('params should not be null');
}
if(is_array($params)){
throu new InvalidArgumentException('params should be an array');
}
・・・
・・・
->これだと大量のチェック項目が必要だったり、チェックすべき項目が変わるとコード修正が大量に発生する場合がある
そうではなく、そもそも誤った使い方が出来ないようにする、というアプローチを取る。
①型宣言
引数に取る値の型を制限して、型チェック等を減らず
// 引数の型を制限
public static function findAll(int $assignedTo,string $status)
{
// $status内の値だけチェック
if(!in_array($status,['OPEN','NEW','FIXED'],ture)){
throw new InvalidArgumentException('params['status'] should be in OPEN,NEW,FIXED');
}
global $CONF;
$pdo = new PDO($CONF['dsn'],$CONF['usr'],$CONF['passwd'],[PDO::ATTR_EMULATE_PREPARES => false]);
$sql = 'SELECT bug_id,summary,date_reported FROM Bugs WHERE assigned_id = :assignedTo AND status = :status';
$stmt = $pdo->prepare($sql);
// $assignedToと$statusをそれぞれ指定した型でバインド
$stmt->bindValue(':assignedTo',$assignedTo,PDO::PARAM_INT);
$stmt->bindValue(':status',$status,PDO::PARAM_STR);
$stmt->execute();
$return $stmt->fetchAll(PDO::FETCH_CLASS, Bug::class);
}
->これにより間違いにくいインターフェースを設計
②値の制限により値のチェックを減らす
問題領域の知識を活かして、領域に特化した型を作る
OPEN、NEW、FIXEDしか取れないStatus型
final class Status extends Enum
{
const OPEN = 'OPEN';
const NEW = 'NEW';
const FIXED = 'FIXED';
}
$status = new Status(Status::OPEN);
$status = new Status('OPEN');
// "InvalidArgumentException:value [HOGE] is not defined"
$status = new Status('HOGE');
(参照)
http://qiita.com/Hiraku/items/71e385b56dcaa37629fe
これにより、以下のように書けて値チェックや型チェックを無くした上で、誤った使い方が出来ないインターフェースに出来る。
// $statusの型をStatus型に制限することで値の中身も制限できる
public static function findAll(int $assignedTo,Status $status)
{
global $CONF;
$pdo = new PDO($CONF['dsn'],$CONF['usr'],$CONF['passwd'],[PDO::ATTR_EMULATE_PREPARES => false]);
$sql = 'SELECT bug_id,summary,date_reported FROM Bugs WHERE assigned_id = :assignedTo AND status = :status';
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':assignedTo',$assignedTo,PDO::PARAM_INT);
// Statusクラスのvalue()メソッドで値を取得
$stmt->bindValue(':status',$status->value(),PDO::PARAM_STR);
$stmt->execute();
$return $stmt->fetchAll(PDO::FETCH_CLASS, Bug::class);
}
2.知りすぎ、責務の多すぎ
・データベース接続確立失敗 -> 責務の多すぎ
・'usr'
、'passwd'
が変更された場合 -> 知りすぎ
PDO生成と設定の責務を外部に出して、コンストラクタで受け取る
(責務の再配置)
// クラス
class BugRepository
{
private $pdo;
public function __construct($pdo)
{
$this->pdo = $pdo;
}
// 以下省略
}
// 設定者(DIコンテナ等)
$pdo = new PDO($CONF['dsn']),$CONF['usr'],$CONF['passwd'],[PDO::ATTR_EMULATE_PREPARES => false]);
$repo = new BugRepository($pdo);
// 使用者
print_r($repo->findAll(12,new Status(Status::OPEN)));
■ここまでのまとめ
・PHPは緩めの言語
・緩めの言語に対してチェックを入れようとするとコードが肥大化する
・PHP7になって色々制約を入れられるようになって来た
・制約を入れることで予防出来る
・予防することでコードの肥大化を防ぎつつ誤った使い方やバグを減らせる
以上。
続きは別途書くかも。