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

PHPカンファレンスの動画で、
「[phpconfuk2017] PHP7で堅牢なコードを書く - 例外処理、表明プログラミング、契約による設計」という動画があって勉強になったのでメモ。

<動画URL>
https://www.youtube.com/watch?v=54jHDHvcYAo
<スライドURL>
https://speakerdeck.com/twada/php-conference-2016?slide=25

ここでは、その一部の型制約による例外の予防、責務の再配置あたりを記載します。
時間があれば続きの部分も記載しようと思います。

###■エラーが発生しやすいコード

```php
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)

```php
$pdo = new PDO($CONF['dsn'],$CONF['usr'],$CONF['passwd'],[PDO::ATTR_EMULATE_PREPARES => false]);
```
・データベース接続確立失敗
・```'usr'```、```'passwd'```が変更された場合

(2)

```php
$stmt = $pdo->prepare($sql);
```
・テーブル名やカラム名が誰かに変更された場合
・(ここで)データベース接続エラー

(3)

```php
$stmt->execute($params);
```
・```$params```が```null```
・```$params```のキー名や数の不一致
・```$params```の値が文字列に変換不能
・(ここで)データベース接続エラー

(4)

```php
$return $stmt->fetchAll(PDO::FETCH_CLASS, Bug::class);
```
・```Bug::class```が未定義
・(ここで)データベース接続エラー


###■対策

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

・```$params```が```null``` -> **誤った使い方による例外**
・```$params```のキー名や数の不一致 -> **誤った使い方による例外**
・```$params```の値が文字列に変換不能 -> **誤った使い方による例外**

まずは、この**誤った使い方による例外**に対応する

よくある対策は様々な条件で値のチェックを行うというアプローチ

```php
if(is_null($params)){
    throw new InvalidArgumentException('params should not be null');
}
if(is_array($params)){
    throu new InvalidArgumentException('params should be an array');
}
・・・
・・・
```

　->これだと大量のチェック項目が必要だったり、チェックすべき項目が変わるとコード修正が大量に発生する場合がある


そうではなく、そもそも誤った使い方が出来ないようにする、というアプローチを取る。

**①型宣言**
引数に取る値の型を制限して、型チェック等を減らず

```php
   // 引数の型を制限
   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型

```php
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

これにより、以下のように書けて値チェックや型チェックを無くした上で、誤った使い方が出来ないインターフェースに出来る。


```php

   // $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生成と設定の責務を外部に出して、コンストラクタで受け取る
(責務の再配置)

```php

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

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