CakePHP3のx-or-y()メソッドシリーズ

この記事は [CakePHP Advent Calendar 2018 \- Qiita](https://qiita.com/advent-calendar/2018/cakephp) の12本目です。  
昨日は @junichirojp さんの[CakePHP3系の便利なTips](https://coodip.com/articles/3604)でした。

私も「便利なTips」系から1本、記事にまとめてみたいと思います。

## この記事は何
CakePHP3には、「X or Y」形式の命名をとるメソッドがいくつかあると思います。  
これらは「Xという処理に失敗したらYをする」というメソッドです。  

フレームワークを使って開発していく際のやり方として、「より表現力のある処理を用いる」という物があるかと思います。  
この「X or Y」というやり方も、「短いコードで、生じ得る結果をより明瞭にする」ために役立つ場面もありそうです。  

この記事では、CakePHP3.7までに実装している「X or Y」なメソッドを紹介してみたいと思います。

※末尾に、 [3.7.0](https://github.com/cakephp/cakephp/tree/3.7.0)時点でのコメントとインターフェイスを載せておきます。

## `orFail()` パターン
特に多いのは、 `xOrFail()` というメソッドです。  
これらは、「元々実装されている`x()`というメソッドの、処理失敗時の挙動を変える = 例外をthrowするという形になるようにラップしたもの」となります。

t_wadaさんの有名な講演「[PHP7 で堅牢なコードを書く \- 例外処理、表明プログラミング、契約による設計](https://speakerdeck.com/twada/php-conference-2016)」でも触れられていますが、「適切なタイミングでコケさせてあげる」ことは重要です。そして、うまく例外と付き合っていくことでような実装はそれを支援します。

これらのメソッドを武器の1つとして、`null` `false` の代わりに、例外を投げてあげるのはどうでしょうか。

### ORM関連

#### 1. `QueryTrait::firstOrFail()`
##### 概要
selectクエリ(`Query`インスタンス経由)を実行し、1件目を返す。<br>該当が0件だった場合は、RecordNotFoundExceptionを投げる

##### 利用例
「まだcloseしておらず、かつ誰にもassignされていないTaskがあれば、古い順に1個割り当てる」のような要件を考えてみます。

```php
$Tickets = TableRegistry::getTableLocator()->get('Tickets');
$ticket = $Articles->find()
    ->where([
        'assignee IS' => NULL,
        'is_closed' => false
    ])
    ->orderAsc('created')
    ->firstOrFail();
$Tickets->save($ticket->set('asignee', $this->Auth->user('id'));
```
これで「古いものを1つとってくる」処理が完成しました。

##### 関連
Tableクラスの `get()` メソッドは有名だと思います。  
これも内部的には `firstOrFail()` を用いてます。  

```php
$me = $UsersTable->get($userId);
```

利用方法の違いとしては

* `get()` はPrimaryKeyのみでレコードを引っ張ってくる際に利用する
* `firstOrFail()` はQueryに対して生えているメソッドなので、どんな複雑な条件でも利用ができる

ということが言えます。

#### 2. `Table::saveOrFail()`
##### 概要
Entityを引数にとって保存(永続化)する。  
保存に失敗したときに PersistenceFailedExceptionを投げる

##### 補足
「保存に失敗したとき」とは、という事ですが、これは基本的にバリデーション(validation / application rule)に違反した時のことを指しているものとして考えて良いと思います。  
より下のレイヤー(ex: DB接続失敗など)は、PDOExceptionなど、それぞれの責務から例外が投げられてきます。  
また、PersistenceFaileExceptionは `getEntity()` メソッドを備えているので、catchした後に「だめだったEntity」へとアクセスすることができます。

#### 3. `Table::deleteOrFail()`
##### 概要
Entityを引数にとって削除する。  
削除に失敗したときに PersistenceFailedExceptionを投げる

##### 補足
バリデーション(ルール)違反が生じて処理失敗となった場合に例外が発生するのは、 `saveOrFail()` と同じです。  
それに加えて、`$entity->isNew()`がtrueだったとき = 「保存する前のentityを利用しようとした時」にも、処理失敗となります。

### Configure
Model(Table, Query)以外にも `orFail` を提供しているクラスがあります。  
それが `Cake\Core\Configure` です。  
「設定を読み込めなかったなら、処理を継続するべきではない」という意図を、より濃くコードに反映することができるのではないでしょうか。

#### 4. `readOrFail()`
##### 概要
渡されたkeyに設定されている値を読み込む。
値を読み込めなかったら `\RuntimeException` を投げる。

##### 利用例
bootstrapフェーズ(bootstrap.phpやapp.php)にて、「環境変数から値を読み込む」際の処理をする場合を例に上げます。  

```php
// app.php
return [
    'Slack' => [
        'token' => env('SLACK_BOT_TOKEN'),
        ],
    ],
];
```
これだけだと、「環境変数の設定漏れ」を確実に防ぐ事ができません。(`env()`がnullを返す)
しかし、この設定内容を `readOrFail()` でのアクセスに切り替えることで、問題の切り分けを少し楽にできます。

```php
// SlackService: Slackへのコミュニケーションを担うクラスだと思ってください・・
$client = new SlackClient(['token' => Configure::readOrFail('Slack.token')]);
```

##### 補足
「読み込めなかった」と判定されるのは、「keyが設定されていなかった」場合と「nullが設定されていた」場合です。

#### 5. `consumeOrFail()`
##### 概要
渡されたkeyに設定されている値を読み込み、Cofigureから削除する。
値を読み込めなかったら `\RuntimeException` を投げる。

##### 補足
`read()` と違い、再設定が不要であると断定できるものは `consume()` / `consumeOrFail()` での読み込みに変えておけば、不用意に「書き換えられた値を読み込んでしまう」という恐れがなくなるので、安全性が高まります。

## その他
`orFail()` は、「失敗をわかりやすくする」ための処置でした。  
他方で、「Xが駄目だったら(よしなに)Yをする」という実装も、一部に見られます。  

これは、既存機能の高機能化でもあり、ともすれば「1つの呼び出しでやっていることが複雑すぎないか」という懸念も生まれます。  
もちろん、「自分の書く範囲でのコードがシンプルになる[^1]」というメリットは魅力です。

プロコンや保守性・見通しを考えた上で適用していけたらベターです。

[^1]: 意図が読み取りやすくなる・ネストが浅くなる・「失敗することもあるが、投げ捨てる(=考慮漏れではなく意図的)」ことを他のコーダーに伝えられる

#### 6. `ORM\Table::findOrCreate()`
##### 概要
第1引数で渡した条件でレコードを探し、該当する最初の1件目を返す。  
存在しなかったら、新たに第1引数をセットしたentityを作成・保存し、返却する。

##### 補足
Qiita外になりますが、以前にこのような記事を書きました。
[findOrCreate\(\)時にvalidationを行う](https://cake.nichiyoubi.land/posts/24-find-or-create-validation/)


## source
以下に、「実際にソースコード中に同記述されているか」を紹介します。

### Datasource\QuerTrait
* 1. [firstOrFail()](https://github.com/cakephp/cakephp/blob/3.7.0/src/Datasource/QueryTrait.php#L474-L493)

### ORM\Table
* 2. [saveOrFail()](https://github.com/cakephp/cakephp/blob/3.7.0/src/ORM/Table.php#L1938-L1956)
* 3. [deleteOrFail()](https://github.com/cakephp/cakephp/blob/3.7.0/src/ORM/Table.php#L2302-L2320)
* 3. [findOrCreate()](https://github.com/cakephp/cakephp/blob/3.7.0/src/ORM/Table.php#L1653-L1702)

### Core\Configure
* 4. [readOrFail()](https://github.com/cakephp/cakephp/blob/3.7.0/src/Core/Configure.php#L145-L171)
* 5. [consumeOrFail()](https://github.com/cakephp/cakephp/blob/3.7.0/src/Core/Configure.php#L191-L211)


---
Advent Calendarも、いよいよ折り返しですね！  
明日は @okinaka さんの記事です :smile: :smile: