Do You PHP はてブロ

Do You PHPはてなからはてブロに移動しました

Request for Comments: Class Metadataを和訳してみた

ざっくりですが。PHPでここまでやるか?というのは置いといて、面白そうなネタではあります。ホントにやるのかなぁ。。。?
訳の対象は、2010/08/24付けのVersion 1.0です。間違いがあれば、指摘してください。

導入

多くの言語が、現在メタデータ情報をサポートしている。このRFCは、多くのアプリケーションに利益があるこの強力なツールがPHPでどう実装されるかについてのアイデアを示す。

なぜクラスメタデータが必要なのか?

一般的に、フレームワークは正しく動作するためにメタデータ情報を利用している。それらは様々な目的のために使われる。

  • phpUnit:テストケース用の機能を提供する。たとえば、@dataProviderはテストデータのイテレーションのため、@expectedExceptionは例外をキャッチするため、など
  • phpDoc:APIを生成するための有益な情報を提供する。たとえば、@author, @param, @returnなど。
  • Doctrine:ORマッピング用。たとえば、@Entity, @OneToOne, @Idなど。
  • Zend Framework Server classes:XML-RPCSOAPなどで自動マッピングに使われる。
  • その他:思い浮かぶのは、バリデーションや機能的な振る舞いのインジェクション(Traitsをうまく利用できるかも)など。また、あるフレームワークは、何らかの形でうまく利用できるかも知れない。

そのため、アノテーションのサポートを実装することで、何らかのメタマッピングインジェクションは簡単に実現できるだろう。

一般的な誤解

メタデータマッピングは、一般的に、幅広く利用されない機能なのでその実装は役に立たない、と言われているが、先に指摘したように、多くのユースケースがある。

提案

最初に決定するものは、アノテーションをカテゴライズするためのトークンだろう。

メタマッピングを利用する場合、速度を上げるため、より少ない文字を使用するのが好まれる。
PHPアノテーションは、次のEBNFに単純化できる。

Annotations     ::= Annotation {Annotation}*
Annotation      ::= "[" AnnotationName ["(" [Values] ")"] "]"
AnnotationName  ::=  QualifiedName | SimpleName | AliasedName
QualifiedName   ::= {"\"}* NameSpacePart "\" {NameSpacePart "\"}* SimpleName
AliasedName     ::= Alias ":" SimpleName
NameSpacePart   ::= identifier
SimpleName      ::= identifier
Alias           ::= identifier
Values          ::= Array | Value {"," Value}*
Value           ::= PlainValue | FieldAssignment
PlainValue      ::= integer | string | float | boolean | Array | Annotation
FieldAssignment ::= FieldName "=" PlainValue
FieldName       ::= identifier
Array           ::= "array(" ArrayEntry {"," ArrayEntry}* ")"
ArrayEntry      ::= Value | KeyValuePair
KeyValuePair    ::= Key "=>" PlainValue
Key             ::= string | integer

identifierは[a-zA-Z_][a-zA-Z0-9_]*。アノテーションは開始/終了のトークンで構成される。次はPHPアノテーションの例である。

<?php
[Entity(tableName="users")]
class User
{
    [Column(type="integer")]
    [Id]
    [GeneratedValue(strategy="AUTO")]
    protected $id;

    // ...

    [ManyToMany(targetEntity="Phonenumber")]
    [JoinTable(
        name="users_phonenumbers",
        joinColumns=array(
            [JoinColumn(name="user_id", referencedColumnName="id")]
        ),
        inverseJoinColumns=array(
            [JoinColumn(name="phonenumber_id", referencedColumnName="id", unique=true)]
        )
    )]
    protected $Phonenumbers;
}

このサポートは、新しいクラス ReflectionAnnotation の導入によって行われる。

アノテーションの定義の仕方

アノテーションは、クラス、メソッド、プロパティそして関数として定義される。ReflectionAnnotationは抽象クラスで、アノテーションの定義を受け入れるための実装を行う必要がある。一度このクラスが拡張すれば、そのサブクラスはアノテーションとして使う準備が整う。

class Foo extends \ReflectionAnnotation {}

[Foo(true)]
class Bar { /* ... */ }

ベースクラスを拡張しただけで、一意の値を定義することができる。その値は、publicプロパティ "value" としてアクセスできる。

$reflClass = new \ReflectionClass('Bar');
$reflAnnot = $reflClass->getAnnotation('Foo');

echo $foo->value; // true

アノテーションを拡張するには、他のプロパティを定義する。それらはpublicである必要がある。これをすることで、アノテーションを定義でき、自動的に値をそれらに代入できる。

<?php
namespace App\Annotation;

class Link extends \ReflectionAnnotation {
    public $url;
    public $target;
}

[App\Annotation\Link(url="http://www.php.net", target="_blank")]
class PHPWebsite {
    /* ... */
}

アノテーション情報の扱われ方

アノテーションは、何らかの処理がされる情報が定義された場合のみ有用である。アノテーション情報の扱われ方はいくつかあるが、どう定義されたかによっても異なる。
次の説明は、クラスに対してのみ有効である。
サブクラスにエクスポートされるアノテーションを定義するには、"Inherited" というアノテーションを定義した ReflectionAnnotation のサブクラスが必要になる。次は、継承されたアノテーションの一般的なルールである。
[Inherited] クラス以外の何かに付けられたアノテーションは継承されない。1つ以上のインターフェースを実装しているクラスは、実装しているインターフェースのどのアノテーションも継承しない
たとえば。

<?php
[Inherited]
class Foo extends \ReflectionAnnotation {}

class Bar extends \ReflectionAnnotation {}

[Foo]
[Bar]
class A {}

class B extends A {}

クラスAとBに定義された情報を処理する場合、次の結果を得る。

<?php
$reflClassA = new \ReflectionClass('A');
var_dump($reflClassA->getAnnotations());
/*
array(2) {
  ["Foo"]=>
  object(Foo)#%d (1) {
    ["value"]=>NULL
  },
  ["Bar"]=>
  object(Bar)#%d (1) {
    ["value"]=>NULL
  }
}
*/

$reflClassB = new \ReflectionClass('B');
var_dump($reflClassB->getAnnotations());
/*
array(2) {
  ["Foo"]=>
  object(Foo)#%d (1) {
    ["value"]=>NULL
  }
}
*/

メソッド "getAnnotations()" は、次の3つのうち、1つをサポートする。

他の利用可能なメソッドには、特定のアノテーションを処理する "getAnnotation($name)" がある。これは、マッチしたアノテーションを返す、もしくは、見つからない場合にNULLを返す。
単一コードの要素(プロパティ、クラス、メソッドなど)のレベルでは、常に与えられたアノテーションの単一インスタンスを取得することになる。これは、getAnnotation を同一要素に対して複数回呼び出した場合、常に同じインスタンスを得る、ということを意味する。
これらは、基本的に、リフレクションAPIの拡張メソッドである。生のPHPで書くとの通り。

<?php
abstract class ReflectionAnnotation {
    const INHERITED = 1;
    const DECLARED  = 2;
    const ALL       = 3;

    public $value   = null;

    public function __construct(Reflector $reflector, array $properties = null) {
        if (is_array($properties)) {
            foreach ($properties as $k => $v) {
                $this->$k = $v;
            }
        }
    }
}

class ReflectionFunction {
    // ...

    public function getAnnotations();
    public function getAnnotation($name);
    public function hasAnnotation($name);
}

class ReflectionClass {
    // ...

    public function getAnnotations($type = ReflectionAnnotation::ALL);
    public function getAnnotation($name, $type = ReflectionAnnotation::ALL);
    public function hasAnnotation($name, $type = ReflectionAnnotation::ALL);
}

class ReflectionProperty {
    // ...

    public function getAnnotations();
    public function getAnnotation($name);
    public function hasAnnotation($name);
}

class ReflectionMethod {
    // ...

    public function getAnnotations();
    public function getAnnotation($name);
    public function hasAnnotation($name);
}

後方互換性の喪失

  • 追加される2つのクラス ReflectionAnnotation と Inherited が既存コードに影響するかも
  • 他は特に無し(新しいキーワードはない)

更新履歴

  • 2010-05-26 guilhermeblanco Initial RFC creation.
  • 2010-08-24 guilhermeblanco Updated for a real doable support
  • 2010-08-24 pierrick Add the patch