spindle/types

Basic types for PHP object system

Installs: 54

Dependents: 0

Stars: 3

Watches: 1

Language: PHP

1.0.3 2014-08-24 15:37 UTC

README

Build Status Scrutinizer Code Quality Code Coverage Latest Stable Version Total Downloads Latest Unstable Version License

PHPにより強い型付けを提供する基底クラス群です。

$ composer require 'spindle/types:*'

基本の型

Spindle\Types\Enum

Enumを継承すると列挙型になります。インスタンスは、クラスに定義したconstのいずれかの値であることを型から保証することができます。

<?php

final class Suit extends Spindle\Types\Enum
{
    const SPADE   = 'spade'
       ,  CLUB    = 'club'
       ,  HEART   = 'heart'
       ,  DIAMOND = 'diamond'
}

$spade = new Suit(Suit::SPADE);
$spade = Suit::SPADE(); //syntax sugar

echo $spade, PHP_EOL;
echo $spade->valueOf(), PHP_EOL;

function doSomething(Suit $suit)
{
    //$suitは必ず4種類のうちのどれかである
}

Spindle\Types\TypedObject

TypedObjectを継承すると、プロパティの型を固定化したクラスを作ることができます。複雑なデータをより確実に扱うことができます。Domain Driven Designにおける"Entity"や"ValueObject"の実装に使えます。

型はschema()というstaticメソッドで定義します。 schemaは配列を返す必要があり、その配列はプロパティ名 => 型, デフォルト値,を繰り返したものになります。デフォルト値は省略でき、その場合はnullがセットされます。

<?php
class User extends Spindle\Types\TypedObject
{
    static function schema()
    {
        return array(
            'firstName' => self::STR,
            'lastName'  => self::STR,
            'age'       => self::INT,
            'birthday'  => 'DateTime', new DateTime('1990-01-01'),
        );
    }

    function checkErrors()
    {
        $errors = array();
        if ($this->age < 0) {
            $errors['age'] = 'ageは0以上である必要があります';
        }

        return $errors;
    }
}

$taro = new User;
$taro->firstName = 'Taro';
$taro->lastName = 'Tanaka';
$taro->age = 20;

//$taro->age = '20'; とすると、InvalidArgumentExceptionが発生して停止する

型として指定できる値には以下のものがあります。

  • self::BOOL (真偽値)
  • self::INT (整数)
  • self::DBL (浮動小数点数)
  • self::STR (文字列)
  • self::ARR (配列)
  • self::OBJ (オブジェクト)
  • self::RES (リソース)
  • self::CALL (コールバック関数)
  • self::MIX (型を指定なし)
  • className クラス名/インターフェース名。完全修飾名で指定します。クラス名の場合、instanceofで判定を行います。

__get()__set()をfinalで固定化してしまうため、TypedObjectを継承するとクラスが持つ能力を一部奪うことになります。全てのクラスをTypedObjectから派生させて作るような設計は推奨しません。

TypedObjectはforeachに対応しています。(IteratorAggregate) TypedObjectはcount()関数で要素数を数えることができます。(Countable)

TypedObjectはcheckErrors()というメソッドを実装することを推奨します。これは、型だけではチェックしきれないバリデーションを行うためのメソッドです。 デフォルト実装ではすべてのプロパティがnot nullであることをチェックします。

TypedObject::$preventExtensions

TypedObjectはデフォルト状態ではschema()で定義されていないプロパティへの代入・参照を拒否します。これはプロパティのtypoを発見しやすくする効果がありますが、不便に感じることもあるでしょう。

TypedObject::$preventExtensionsをfalseにすると、未定義のプロパティを拒否せず、自動で拡張するようになります。(デフォルトはtrue)

なお、拡張したプロパティは自動的にmixed(型検査しない)として扱われます。

<?php
use Spindle\Types;

class MyObj extends Types\TypedObject
{
    static function schema()
    {
        return array(
            'a' => self::INT,
            'b' => self::BOOL,
        );
    }
}

$obj = new MyObj;

Types\TypedObject::$preventExtensions = false;
$obj->c = 'str'; //エラーは起きない

Types\TypedObject::$preventExtensions = true;
$obj->c = 'str'; //例外発生
TypedObject::$casting

TypedObjectはプロパティに代入時、schemaと型が違えば例外を発生させます。 しかしPHPの標準的な挙動のように、違う型を代入しようとしたら型キャストを行ってほしい場合もあるでしょう。例えばデータベースから取り出した文字列からオブジェクトを復元したい場合などです。

TypedObject::$castingをtrueにすると、型が違う代入をしようとしても、なるべくキャストを行おうとします。

PDO::FETCH_CLASSとの組み合わせ

DBからSELECTしてきた結果をTypedObjectへ流し込むことができます。PDOの標準機能として、直接クラスをnewして流し込むPDO::FETCH_CLASSというモードがあるので、これを使うとよいでしょう。 通常、PDOから渡ってくるデータはstring型ですので、$castingオプションを有効にしておいてください。

<?php
use Spindle\Types;

class User extends Types\TypedObject
{
    static $casting = true;

    static function schema()
    {
        return array(
            'userId' => self::INT,
            'name' => self::STR,
            'age' => self::INT
        );
    }
}

$pdo = new PDO('sqlite::memory:', null, null, array(
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
));
$pdo->exec('CREATE TABLE User(userId INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, age INTEGER)');
$pdo->exec('INSERT INTO User(name, age) VALUES("taro", 20)');
$pdo->exec('INSERT INTO User(name, age) VALUES("hanako", 21)');

$stmt = $pdo->prepare('SELECT * FROM User');
$stmt->setFetchMode(PDO::FETCH_CLASS|PDO::FETCH_PROPS_LATE, __NAMESPACE__ . '\\RowModel');
$stmt->execute();

foreach ($stmt as $row) {
    self::assertInternalType('integer', $row->userId);
    self::assertInternalType('string',  $row->name);
    self::assertInternalType('integer', $row->age);
}

注意点として、PDO::FETCH_CLASSは通常のオブジェクト初期化と挙動が違い、 先にセッターを実行してから、コンストラクタを起動 します。TypedObjectはコンストラクタでオブジェクトを初期化しているため、この挙動ではうまく動作しません。

PDO::FETCH_CLASSを用いる場合、必ずPDO::FETCH_PROPS_LATEを同時に指定してください。 このオプションを指定すると、コンストラクタが先に起動するようになり、正常に動作します。

TypedObjectの継承

TypedObjectで作られたクラスを継承する場合、親クラスのschemaは自動的には引き継がれません。extendメソッドを使って明示的に拡張する必要があります。

<?php
class Employee extends Spindle\Types\TypedObject
{
    static function schema()
    {
        return array(
            'id' => self::INT, 0,
            'name' => self::STR,
        );
    }
}

class Boss extends Employee
{
    static function schema()
    {
        return self::extend(parent::schema(), array(
            'room' => self::INT,
        ));
    }
}

Spindle\Types\ConstObject

ConstObjectはTypedObjectを変更不可のオブジェクトにするDecoratorです。

<?php
$const = new ConstObject($typedObject);

echo $const->foo; //参照は透過的に可能
//$const->foo = 'moo'; どのプロパティに対しても代入操作は常に例外を発生させる

Spindle\Types\Collection

array()からいくつか制限を追加した配列です。

  • 数値の添え字しか許容しない
  • 順番が保証される
  • 必要に応じて、要素の型も固定できる

Spindle\Types\ConstCollection

Collectionを読み取り専用にするDecoratorです。

Polyfill

PHPは5.4や5.5から使えるようになった標準インターフェースがいくつか存在します。 それらをPHP5.3においても使えるようにする目的で、Polyfillを用意しています。

DateTime implements DateTimeInterfaceなどの状態を保証するため、独自の名前空間上に配置しています。

Spindle\Types\Polyfill\JsonSerializable

JsonSerializableインターフェース(PHP5.4以降)に相当します。

Spindle\Types\Polyfill\DateTimeInterface

DateTimeInterfaceインターフェース(PHP5.5以降)に相当します。

Spindle\Types\Polyfill\DateTime

DateTimeInterfaceをimplementsしたDateTimeです。

Spindle\Types\Polyfill\DateTimeImmutable

DateTimeImmutable(PHP5.5以降)に相当します。状態を変更することができず、modifyやsetTimestampなどのメソッドを作用させると、別のインスタンスを返します。

License

spindle/typesの著作権は放棄するものとします。 利用に際して制限はありませんし、作者への連絡や著作権表示なども必要ありません。 スニペット的にコードをコピーして使っても問題ありません。

ライセンスの原文

CC0-1.0 (No Rights Reserved)