【composer】アノテーションでfriendとかpackage-privateとかをエミュレートするライブラリを作った

LaravelPHP

【composer】アノテーションでfriendとかpackage-privateとかをエミュレートするライブラリを作った

  • 業務でcomposerライブラリを作りそうだったので練習がてら
  • まだまだ開発途上だが、とりあえずデモできる段階になった

WandTa/Annotation-Visibility

composer require wand/annotation-visibility
  • ‘19/07/25現在、dev-master

    • composer.jsonに"minimum-stability": "dev"が必要

Objective: 非標準のアクセス制御を使いたい

  • デザインパターン等を学ぶと…

    • 特定のクラスからのみ呼び出し可能なメソッドがほしい
    • 同一パッケージのクラスからのみ呼び出し可能なメソッドがほしい
  • しかしPHPで使用可能なアクセス修飾子は下記の3つのみ

    • public

      • フルオープン
    • protected

      • 派生クラスにのみオープン
    • private

      • 自分のクラスのみアクセス可能
  • 自分のクラス以外にアクセスを許そうとしたら、publicにせざるをえない

    • 「振る舞いに関するデザインパターン」が軒並み残念な感じに

Solution: アノテーションでオレオレアクセス制御子を定義する

Sample 1. Layered Architecture

Domain.php

<?php

namespace App;

use WandTa\Annotations\VisibleTo;

class Domain
{
    /**
     * @VisibleTo("App\Presentation");
     */
    public function someLogic()
    { }
}

Presentation.php

<?php

namespace App;

use WandTa\Container;

class Presentation
{
    public function callDomain()
    {
        $domain = (new Container)->make(Domain::class);
        $domain->someLogic(); // OK
    }
}

Infrastructure.php

<?php

namespace App;

use WandTa\Container;

class Infrastructure
{
    public function callDomain()
    {
        $domain = (new Container)->make(Domain::class);
        $domain->someLogic(); // NG
    }
}

index.php

<?php

namespace App;

// OK
(new Presentation)->callDomain();

// Uncaught WandTa\Exceptions\AccessViolationException:
//   someLogic can't be called by App\Infrastructure
(new Infrastructure)->callDomain();

Sample 2. Visitor Pattern

<?php

declare(strict_types=1);

namespace Tests\Feature\Sample\VisitorPattern;

use WandTa\Annotations\VisibleTo;

class HelloVisitor implements IVisitor
{
    /** @var string */
    private $greet;

    /**
     * @VisibleTo("Tests\Feature\Sample\VisitorPattern\AcceptorA")
     * @param AcceptorA $acceptorA
     */
    public function visitA(AcceptorA $acceptorA)
    {
        $this->greet = 'hello A';
    }

    /**
     * @VisibleTo("Tests\Feature\Sample\VisitorPattern\AcceptorB")
     * @param AcceptorB $acceptorB
     */
    public function visitB(AcceptorB $acceptorB)
    {
        $this->greet = 'hello B';
    }

    /**
     * get greeting message
     * @return string
     */
    public function getGreet(): string
    {
        return $this->greet;
    }
}
  • VisitAメソッドはTests\Feature\Sample\VisitorPattern\AcceptorAクラスからのみ呼び出し可能
  • VisitBメソッドはTests\Feature\Sample\VisitorPattern\AcceptorBクラスからのみ呼び出し可能
  • getGreetメソッドは任意のクラスから呼び出し可能(通常のpublic)

使いどころ

  • 本番環境で実行時にエラーを出したいわけではない。あくまで開発用
  • 静的にエラーが出るのが理想
  • 次点で、CIの自動テストでエラーが出てくれることを目的とした
  • クラスの依存の向きを取り締まることで秩序をもたらす

Laravelでつかう

  • テスト用のサービスプロバイダを作る
php artisan make:Provider TestServiceProvider

app/TestServiceProvider.php

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

use App\Domain\HogeDomain;
use WandTa\Container;

class TestServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        app()->bind(HogeDomain::class, function ($app) {
            return (new Container)->make(
                HogeDomain::class
            );
        });
    }
}
  • テスト環境でのみ上記サービスプロバイダを読み込む

config/app.php

<?php

$config = [
...
    // 本番用サービスプロバイダ
    'providers' => [
        ...
    ],
...
];

// テスト環境でのみTestServiceProviderを読み込む
if (env('APP_ENV') === 'testing') {
    $config['providers'][] = App\Providers\TestServiceProvider::class;
}

return $config;
  • HogeDomainオブジェクト利用するときは、サービスコンテナから取り出すようにする
  • テスト環境でのみ「特定のクラス以外から呼び出されたらエラーを吐く」メソッドを利用可能になる