19
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

doctrineアノテーションとPHP8アトリビュート

Last updated at Posted at 2020-12-17

Hello

こんにちは。@koriymです。

この記事はBEAR.Sunday Advent Calendar 2020の18日目というか1日目というか最初の記事です。1つか2つでもカレンダーとして残れば良いかと、今年も今頃になって作成しました。 昨日公開したばかりのツールについて書きます。

PHP8 アトリビュート

PHP8でアトリビュートがサポートされました。以下はphp.netからの引用です。

アトリビュートの概要

アトリビュートを使うと、 コンピューターが解析できる構造化されたメタデータの情報を、 コードの宣言時に埋め込むことができます。 ... アトリビュートで定義されたメタデータは、 実行時に リフレクションAPI を使って調べることが出来ます。 よって、アトリビュートは、 コードに直接埋め込むことが出来る、 設定のための言語とみなすことができます。
アトリビュートを使うと、機能の抽象的な実装と、アプリケーションでの具体的な利用を分離できます。 この点でアトリビュートは、インターフェイスとその実装と比較できます。 インターフェイスとその実装はコードに関する情報ですが、 アトリビュートはコードの追加情報と設定に注釈を付けるものです。

BEAR.Sundayの開発はPHP5.4まで遡りますが、その時からAOPやDIをアプリケーションとフレームワーク全体に対して制約をもたらすものとし、その実現にコードにメタ情報をもたらすアノテーションを利用してきました。

Doctrineアノテーション

PHP8でAttributesがサポートされる前もPHPのライブラリの多くで、アノテーションがそれぞれの実装で実現されていました。多くの人に馴染みがあるのがphpunit@test@dataProvierアノテーションではないでしょうか。

それぞれが実装を持つことも少なくありませんでしたが、BEAR.Sundayもそうであったようにdoctrine/annotationを利用していたものも少なくありませんでした。

このようなカスタムアノテーションを作成して

/**
 * @Annotation
 */
final class MyAnnotation
{
    public $myProperty;
}

メソッドやクラスにphpdocで/** @MyAnnotation */とアノテートする事ができます。

パフォーマンス

phpdocを利用し、リフレクションで文字列の解析を行うのでコストはそれなりにかかり、通常はプロダクションではCacheAnnotatioReaderを利用します。

BEAR.Sundayでは一歩進んだ解決法を採用しました。すなわち設計時からプログラム実行のコンパイルタイムランタイムの区分を明確にし、プロダクションではアノテーションの読み込みそのものがほとんど存在しないようにしました。コンパイルタイムではアノテーションを利用したDI/AOPの束縛にしたがってコードをジェネレートし、ランタイムではそれを実行するだけです。

Ray.Aop

Ray.Aopではアノテーションで束縛した結果に基付いてAOPのPHPコードを作成し、ランタイムではそのコードが実行されるだけなのでプロダクションではアノテーションの読み込みは行われません。

<?php

declare (strict_types=1);
namespace {
    use Ray\Aop\WeavedInterface;
    use Ray\Aop\ReflectiveMethodInvocation as Invocation;
    /** doc comment of FakeMock */
    class FakeFoo_2030086604 extends \FakeGlobalNamespaced implements WeavedInterface
    {
        public $bind;
        public $bindings = [];
        public $methodAnnotations = 'a:0:{}';
        public $classAnnotations = 'a:0:{}';
        private $isAspect = true;

        public function doSomething(int $a)
        {
            if (!$this->isAspect) {
                $this->isAspect = true;
                return call_user_func_array([$this, 'parent::' . __FUNCTION__], func_get_args());
            }
            $this->isAspect = false;
            $result = (new Invocation($this, __FUNCTION__, func_get_args(), $this->bindings[__FUNCTION__]))->proceed();
            $this->isAspect = true;
            return $result;
        }
    }
}

コード生成のついでに、アノテーションも埋め込んでしまっていて、もしインターセプターがアノテーションをランタイムが読む必要がある時でも通常のアノテーショリーダーが起動することがありません。

Ray.Di

Ray.Diでも同様でインスタンスを生成するコードそのものをジェネレートしているのでプロダクション実行時にアノテーションが読まれることはありません。

<?php

namespace Ray\Di\Compiler;

$instance = new \BEAR\QueryRepository\HttpCache($prototype('BEAR\\QueryRepository\\ResourceStorageInterface-'));
$is_singleton = false;
return $instance;

問題点

IDEの十分なサポートもあり、言語機能のように十分機能してきたdoctrine/annotationですが問題がなかったわけではありません。

  • 開発時は遅い
  • クラスとメソッドにしか記述できず、引数にはアノテートできない。
  • 視認性にやや難あり

PHP8のアトリビュートではこれらの問題が解決します。

Koriym.Attribute

PHP8に新しくフルスクラッチでライブラリもアプリケーションも開発するなら問題はありませんが、旧来のdoctrineアノテーションリーダーを利用したコードでもPHP8アトリビュートでも読めれば既存のコードが活かせ、フォワードコンパチブルなコードも記述できます。

Doctrineの人達が作るのではと待っていたのですが、どうも出そうにありません

そこでdoctrine/annotationリーダーのインターフェイスを持ちながら、PHP8アトリビュートも読めるKoriym.Attributesを開発しました。

このリーダーを使ったRay.Aop、Ray.DiのマスターブランチではPHP8アトリビュートを利用することができます。

BEAR.Sundayでアトリビュート

BEAR.SundayのResourceObjectはこのようになります。

#[Cacheable(expirySecond: 60)]
class Recent extends ResourceObject
{
    public function __construct(#[AuthKey] string $authKey)
    {
    }

    #[Inject]
    public function setRenderer(#[Json] RenderInterface $render): ResourceObject
    {
    }

    #[Link(rel: "tag", crawl: "meta", href: "app://self/article/{id}")]
    #[Embed(rel: "user", src: "app://self/users/{id}")]
    #[HttpCache(isPrivate: true, maxAge: 60)]
    public function onGet(int $id, #[Assisted] $pdo): static
    {
    }

    #[LoginUserOnly]
    #[Loggable]
    #[Transactional]
    #[Notify('post')]
    public function onPost(string $body): static

}

"コードが仕様を明らかにするべき"に沿ったものになっていると思います。

移行シナリオ

PHP8の移行にはいくつかのシナリオを考えてみます。

開発はPHP8でプロダクションはPHP7.x

プロダクションはまだPHP7でも、アノテーションは後述するアトリビュートとアノテーションが共用できる方式で記述し将来のPHP8移行に備えます。PHPコードはdocotrineアノテーション記法と共にPHP8アトリビュートでも記述します。

PHP8アトリビュートは#始まりでPHP7にとってはコメントとなり無視されますが、PHP8開発環境では高速な実行が可能です。またPHP7での動作はCIで保証します。

これはPHP7/PHP8双方で実行できるライブラリを開発する開発者にも必要な方法です。

プロダクションでもPHP8

rectorphp/rector を使って、docotrineアノテーションのコードを以下のようにPHP8アトリビュートに変更可能なようです。

-use Doctrine\Common\Annotations\Annotation\Target;
+use Attribute;

-/**
- * @Annotation
- * @Target({"PROPERTY", "ANNOTATION"})
- */
+#[Attribute(Attribute::TARGET_PROPERTY)]
 final class PHPConstraint extends Constraint
 {
 }

docotrineアノテーションはアノテーションのネストができますが、PHP8アトリビュートはできない事に注意が必要です。1

共用アノテーション

上記のアノテーションのようにコンストラクタの無いdoctrineアノテーションを、PHP8アトリビュートで使えるとようにするためにはコンストラクタを作成する必要があります。

/**
 * @Annotation
 */
+#[Attribute]
final class MyAnnotation
{
    public $myProperty;
+    public function __construct($value)
+    {
+        $this->myProperty = $value['value'] ?? $value
+    }
}

doctrineアノテーションは全てのプロパティが1つの連想配列として渡されますが、PHP8では引数で渡されるのでそれを考慮してコンストラクタを記述します。

上記のアノテーションはdoctirnアノテーションの場合は以下のように記述しますが

/**
 * @MyAnnotation('foo')
 */

PHP8アトリビューとの場合には

#[MyAnnotation('foo')]

になります。

以下のようなプロパティを指定する場合には

#[HttpCache(isPrivate: true, MaxAge:60)]

このようなコードになります。doctrineアノテーションでは最初の$valueしか使われないので他の引数にはデフォルト値を設定する必要があります。

/**
 * @Annotation
 */
#[Attribute]
final class MyAnnotation
{
    public $isPrivate;
    public $maxAge;
+    public function __construct(array $value = [], bool $isPrivate = false, $maxAge = 0)
+    {
+        $this->isPrivate = $value['isPravate'] ?? $isPrivate;
+        $this->maxAge = $value['maxAge'] ?? $maxAge;
+    }
}

PHP8+専用であればこのようにシンプルです。

#[Attribute]
final class HttpCache
{
    public function __construct(
        public bool $isPrivate,
        public int $maxAge
    ){}
}

従来記法が煩雑だった@Namedアノテーションも引数に直接記述できます。

    #[Inject]
    public function setFeet(
        #[Named('right')] Foot $rightFoot,
        #[Named('left')] Foot $leftFoot
    ): void
    {

カスタムアノテーションを用意すればさらに可読性が良くなります。2

    #[Inject]
    public function setFeet(
        #[Right] Foot $rightFoot,
        #[Left] Foot $leftFoot
    ): void
    {

まとめ

マニュアルで「進化可能なメンテナンス性の良いコードが長期的に利用できることを重視します。」との価値観表明をしているBEAR.Sundayは、継続性を維持しながら共に新しい技術にも対応します。

PHP5.4の頃からアノテーションをコア技術として取り入れてきたBEAR.Sundayが、PHP8のアトリビュートをサポートするのは自然な事ですが3、同時に特別な感慨もあります。PHPのネイティブサポートを待つのに多くの年月が必要でした。

明日

明日はProject 8で一緒にBEAR.Sundayを使っている@yuki777さんの 「BEAR.Sundayの出力(echoとかprint)を追ってみる(仮)」です。お楽しみに!

リンク

  1. BEAR.Sundayフレームワーク側ではアノテーションのネストは行ってないので問題にならないでしょう。

  2. ちなみにこれはDIの「ロボットの足問題」として知られるものをサンプルにしています。https://github.com/google/guice/wiki/FrequentlyAskedQuestions#how-do-i-build-two-similar-but-slightly-different-trees-of-objects

  3. 名前付き引数もそうです!

19
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?