PHP で湯婆婆を実装してみる (OOP)

## はじめに

様々な言語で湯婆婆を実装するのが流行っているようなので PHP で実装しました。

作った後で[既に PHP で実装している人がいる](https://qiita.com/hinatao3o/items/52b8d457c650c3e00ea4)ことに気が付きましたが、
なるべくオブジェクト指向っぽく表現した版という名目でここに供養します。

- 元ネタ: [Javaで湯婆婆を実装してみる \- Qiita](https://qiita.com/Nemesis/items/c7192a7c510788d2cba2)
- まとめ: [【毎日自動更新】湯婆婆 LGTMランキング！ \- Qiita](https://qiita.com/torifukukaiou/items/c8361231cdc56e493245)

## 実行環境

- PHP 7.4

## 登場するクラス

- メインクラス: Main (プログラムの入り口)
- 契約書クラス: Contract
- 湯婆婆クラス: Yubaba
- あなたクラス: You (作中で言うところの千尋のこと)

## 実装 (一部抜粋)

### メインクラス

プログラムの入り口です。

```php
class Main
{
    public static function main(): void
    {
        // 白紙の契約書を持たせた状態で「湯婆婆」のインスタンスを生成する
        $yubaba = new Yubaba(Contract::createBlankContract());

        // 契約を開始する (戻り値は湯婆婆の持つ白紙の契約書)
        $contract = $yubaba->startNewContract();

        // 「あなた」のインスタンスを生成する
        $inputName = static::_getStdinInteractively();
        $you = new You($inputName);

        // 「湯婆婆」から渡された契約書に「あなた」が署名する
        $you->signContract($contract);

        // 強制的に名前を奪う
        $yubaba->forciblyRename($you);
    }

    // 対話型シェルで標準入力から何らかの文字列を得る
    // 元ネタを踏襲し入力値のバリデーションは行わない
    private static function _getStdinInteractively(): string
    {
        $fp = fopen('php://stdin', 'rb');
        return trim(fgets($fp));
    }
```


### 契約書クラス

データクラスです。署名を行うことができます。

```php
class Contract
{
    private string $_signatureName;

    // 白紙の契約書を作成する
    public static function createBlankContract(): self
    {
        return new self('');
    }

    // インスタンスは非公開 (同クラスの静的メソッドを介した生成のみを許可する)
    private function __construct(string $_signatureName)
    {
        $this->_signatureName = $_signatureName;
    }

    // 契約書に署名する
    public function sign(string $name): void
    {
        $this->_signatureName = $name;
    }
}
```

### 湯婆婆クラス

喋ります。

```php
class Yubaba
{
    // 湯婆婆の持つ契約書
    private Contract $_contract;

    public function __construct(Contract $contract)
    {
        $this->_contract = $contract;
    }

    // 契約を開始する (記入のため契約書を返す)
    public function startNewContract(): Contract
    {
        echo '契約書だよ。そこに名前を書きな。' . PHP_EOL;
        return $this->_contract;
    }

    // 強制的に改名させる
    public function forciblyRename(You $you): void
    {
        $name = $you->getName();
        echo sprintf('フン。%sというのかい。贅沢な名だねぇ。', $name);

        $newName = static::_pickOneCharacter($name);
        echo sprintf('今からお前の名前は%1$sだ。いいかい、%1$sだよ。分かったら返事をするんだ、%1$s!!', $newName);
        $you->changeName($newName);
    }

    // 入力文字列からランダムに一文字を選択する
    private static function _pickOneCharacter(string $str): string
    {
        /** @noinspection PhpUnhandledExceptionInspection */
        $randomIndex = random_int(0, mb_strlen($str) - 1);
        return mb_substr($str, $randomIndex, 1);
    }
}
```

## あなたクラス

作中で言うところの千尋を表すデータクラスです。名前のみを持ちます。
ゲッター、セッター、コンストラクターなどの機械的なコードは省略します。

```php
class You
{
    private string $_name;

    public function changeName(string $name): void
    {
        $this->_name = $name;
    }

    // 契約書を受け取り署名する
    public function signContract(Contract $contract): void
    {
        $contract->sign($this->_name);
    }
}
```

## 実行例

```
$ make run
契約書だよ。そこに名前を書きな。
山田太郎
フン。山田太郎というのかい。贅沢な名だねぇ。
今からお前の名前は太だ。いいかい、太だよ。分かったら返事をするんだ、太!!
```

## 名前を何も入力せずに実行すると...

湯婆婆がクラッシュします。（元ネタを再現）

```
$ make run
契約書だよ。そこに名前を書きな。
 
フン。というのかい。贅沢な名だねぇ。
Fatal error: Uncaught Error: Minimum value must be less than or equal to the maximum value in /app/src/Yubaba.php:40
Stack trace:
#0 /app/src/Yubaba.php(40): random_int(0, -1)
```

## ソースコード

GitHub に上げました。

- [ymm1x/yubaba\_php](https://github.com/ymm1x/yubaba_php)