LoginSignup
2
3

More than 5 years have passed since last update.

【PHPカンファレンスメモ】PHP7で堅牢なコードを書く - 例外処理、表明プログラミング、契約による設計(第1部:防御的プログラミング)

Last updated at Posted at 2017-08-10

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);

$paramsnull
$paramsのキー名や数の不一致
$paramsの値が文字列に変換不能
・(ここで)データベース接続エラー

(4)

$return $stmt->fetchAll(PDO::FETCH_CLASS, Bug::class);

Bug::classが未定義
・(ここで)データベース接続エラー

■対策

1.誤った使い方による例外の処理

$paramsnull -> 誤った使い方による例外
$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になって色々制約を入れられるようになって来た
・制約を入れることで予防出来る
・予防することでコードの肥大化を防ぎつつ誤った使い方やバグを減らせる

以上。
続きは別途書くかも。

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3