ウコンの力でsetter/getter生やし放題

[turmeric-spice](https://github.com/gong023/turmeric-spice) というphpでオブジェクト作成をサポートするライブラリを作ったので書きます。このライブラリは [MagicSpice](https://github.com/gree/magicSpice) という先達のライブラリにインスパイアされたものです。

# 基本的な使い方

packagist からインストールできます

```
composer require gong023/turmeric-spice
```

以下のようなクラス定義を行います。

```php
use TurmericSpice\ReadableAttributes;

class User
{
    use ReadableAttributes {
        mayHaveAsInt     as public getId;
        mayHaveAsString  as public getName;
        mayHaveAsFloat   as public getBalance;
        mayHaveAsBoolean as public getRestricted;
        mayHaveAsArray   as public getFriendIds;
    }
}
```

このような定義をすると、以下の getter メソッドを使うことができます。**なお、これらの getter メソッドはIDEの補完でちゃんと見ることができます。**

```php
$user = new User([
    'id'         => 1,
    'name'       => 'Taro',
    'balance'    => 100.0,
    'restricted' => false,
    'friend_ids' => [2, 3, 4],
]);

$user->getId();        // 1
$user->getName();      // 'Taro'
$user->getBalance();   // 100.0
$user->getRestricted;  // false
$user->getFriendIds(); // [2, 3, 4]
$user->toArray();      // コンストラクタに渡した時のような配列を返します
```

setter を使いたいときは、`TurmericSpice\ReadWriteAttributes` トレイトを使用します。

```php
use TurmericSpice\ReadWriteAttributes;

class User
{
    use ReadWriteAttributes {
        setValue         as public setId;
        setValue         as public setName;
        mayHaveAsInt     as public getId;
        mustHaveAsString as public getName;
    }
}

$user = new User(['id' => null]);
$user->setId(1);
$user->getId(); // 1
$user->setName('Taro');
$user->getName(); // 'Taro'
```

# may / must DSL

turmeric-spice は `may` と `must` という二種類のDSLを持っています。
どちらも getter メソッド呼び出し時に適切な型へのキャストを行ってくれますが、キーが未定義だった場合や値が null だった場合の挙動が変化します。`may` は何も言わずにキャストし、`must` は `\TurmericSpice\Container\InvalidAttributeException` という例外を投げます。

```php
use TurmericSpice\ReadableAttributes;

class User
{
    use ReadableAttributes {
        mayHaveAsString   as public getName;
        mayHaveAsFloat    as public getBalance;
        mustHaveAsInt     as public getId;
        mustHaveAsArray   as public getFriendIds;
        mustHaveAsBoolean as public getRestricted;
    }
}

$user = new User([
    'name'       => null,
    'balance'    => 100,
    'id'         => null,
    'restricted' => 'false',
]);

$user->getName();       // null が与えられていますがキャストされ '' が返ります。
$user->getBalance();    // int の 100 が与えられていますがキャストされ 100.0 が返ります。
$user->getId();         // null が与えられているため InvalidAttributeException が投げられます。
$user->getFriendIds();  // キーが未定義のため InvalidAttributeException が投げられます。
$user->getRestricted(); // 'false' が与えられていますが false が返ります。
```

# 発展的な使い方

turmeric-spice のトレイトをインポートすると `attributes` というプロパティが付き、それに直接アクセスすることでより多彩な挙動を使えます。

```php
use TurmericSpice\ReadableAttributes;

class User
{
    use ReadableAttributes;

    public function getNameTaro()
    {
        $validate = function ($value) { return $value === 'Taro'; };

        return $this->attributes->mustHave('name')->asString($validate);
    }

    public function getCreated()
    {
        return $this->attributes->mustHave('created')->asInstanceOf('\Datetime');
    }

    public function getUpdated()
    {
        return $this->attributes->mayHave('updated')->asInstanceOf('\Datetime');
    }

    public function getUpdatedRaw()
    {
        return $this->attributes->mayHave('updated')->value();
    }

    public function getRawArray()
    {
        return $this->attributes->getRaw();
    }
}

$user = new User([
    'name'    => 'Jiro',
    'created' => '2015-01-01 00:00:00',
    'updated' => '2015-01-01 00:00:00',
]);

$user->getNameTaro();   // バリデーションに引っかかるので InvalidAttributeException が投げられます。
$user->getCreated();    // \Datetime のインスタンスではないので InvalidAttributeException が投げられます。
$user->getUpdated();    // new \Datetime('2015-01-01 00:00:00'); が返ります
$user->getUpdatedRaw(); // '2015-01-01 00:00:00' が返ります
$user->getRawArray();   // コンストラクタに渡した配列が返ります。toArray とは違い値のキャストなどは行われません。
```

version 0.2.0 以降では `int[]`, `float[]`, `string[]`, `bool[]`, `YourClass[]` も指定できるようになっています

```php
use TurmericSpice\ReadableAttributes;

class User
{
    use ReadableAttributes {
        mayHaveAsIntArray    as public getFriendIds;
        mayHaveStringArray   as public getFriendNames;
        mustHaveAsFloatArray as public getBalanceHistories;
    }

    public function getUpdatedHistories()
    {
        return $this->attributes->mayHave('updated_histories')->asInstanceArray('\\Datetime');
    }
}

$user = new User([
    'friend_ids'        => ['1', '2', '3'],
    'friend_names'      => ['name1', 'name2', null],
    'balance_histories' => [10.0, 20.0, null],
    'updated_histories' => ['2015-01-01 00:00:00', '2016-01-01 00:00:00'],
]);

$user->getFriendIds;          // int にキャストされるので [1, 2, 3] が返ります
$user->getFriendNames;        // null が含まれていますが ['name1', 'name2', ''] が返ります
$user->balanceHistories();    // null が含まれているので exception が投げられます
$user->getUpdatedHistories(); // \Datetime にキャストされるので [new Datetime('2015-01-01 00:00:00'), new Datetime('2016-01-01 00:00:00')] が返ります
```

# 他
混みいった型周りの挙動とかを確かめるには実装みてもらうのがいいと思います。

- https://github.com/gong023/turmeric-spice

ちなみに、MagicSpice は関数呼び出しを極力減らそうという意図が感じられるのに対し、turmeric-spice はその点でそこまでストイックに実装していません。では遅いかというとそんなことはない気がしていて、[一番ボトルネックになりそうなこの辺のキャッシュ](https://github.com/gong023/turmeric-spice/blob/master/src/ReadableAttributes.php#L14) はしているし、MagicSpice では関数呼び出しが少ない代わりに Reflection class が使われていたりするので、まあパフォーマンスはちゃんと測ってみないとなんとも言えないなと思っています。
あと名前の由来はメタプログラミングっぽいところがあるので語呂をとって turmeric、MagiSpice からインスパイアされたものなので spice です。