Lumen with Aspect

Lumen使ってますか?

APIなどのセッションや、テンプレートを必要としないアプリケーションには、
Laravelのマイクロフレームワーク版でもあるLumenを選択する方も多いのではないでしょうか。

このフレームワークはLaravelと同様にファサードを使うかどうかの選択が可能で、
もちろん一切使わず、Packagistなどで公開されているライブラリをデータベースに利用したりと
変更することも容易です。(現在でしたら、組み替えのしやすさではzend-expressiveの方が軍配があがるでしょう。)
ファサードを利用しない場合の利用方法は過去のエントリを参考にしてください。

ytake.hateblo.jp

ちょっとの工夫

手軽なアプリケーションでもトランザクションや、キャッシュ、ログなどいろんなものを記述することが多いでしょう。
そんな場合に、拙作のLaravel向けのパッケージではありますが、
アスペクトを取り入れることができます。

github.com

install

そのままです

    "require": {
        "php": ">=5.5.9",
        "laravel/lumen-framework": "5.2.*",
        "vlucas/phpdotenv": "~2.2",
        "ytake/laravel-fluent-logger": "~1.0",
        "ytake/laravel-aspect": "~1.0"
    },

install後に、vendor/ytake/laravel-aspect/src/config/ytake-laravel-aop.phpファイルを
プロジェクト配下にconfigディレクトリを作成して設置します。
Lumenのconfigファイル設置と同じです。

register ServiceProvider

パッケージのサービスプロバイダそのままでは、Laravelを前提としているので動きません。
Lumen用に継承して下記の通りに変更します。

<?php

namespace App\Providers;

use Ytake\LaravelAspect\AspectManager;
use Ytake\LaravelAspect\AnnotationManager;
use Ytake\LaravelAspect\AspectServiceProvider as Aspect;


/**
 * Class AspectServiceProvider
 */
class AspectServiceProvider extends Aspect
{
    /**
     * @return void
     */
    public function boot()
    {

    }

    /**
     * {@inheritdoc}
     */
    public function register()
    {
        /**
         * for package configure
         */
        $this->app->configure('ytake-laravel-aop');
        $this->app->singleton('aspect.annotation.reader', function ($app) {
            return (new AnnotationManager($app))->getReader();
        });
        $this->app->singleton('aspect.manager', function ($app) {
            // register annotation
            return new AspectManager($app);
        });
    }
}

次にbootstrap/app.phpにこのサービスプロバイダと、パッケージのartisanコマンドのサービスプロバイダを追加します。

<?php
// 省略
$app->register(App\Providers\AppServiceProvider::class);
$app->register(App\Providers\AspectServiceProvider::class);
$app->register(Ytake\LaravelAspect\ConsoleServiceProvider::class);

$app->group(['namespace' => 'App\Http\Controllers'], function ($app) {
    require __DIR__.'/../app/Http/routes.php';
});

return $app;

php artisanコマンドでパッケージのコマンドが追加されているはずです。
php artisan ytake:aspect-module-publishでパッケージがサポートしているアスペクトのモジュールを設置できますが、
Lumenでは動かないためプロジェクト内で継承して利用します。
ここでは例としてCacheを作成する @Cacheable を取り上げましょう。
利用するキャッシュは通常のファイルを指定しておきましょう。

@Cacheable利用手引き

app配下にModulesディレクトリを作成します(なんでも良いです)。
以下の通りにCacheableModuleクラスを作成します。

<?php

namespace App\Modules;

/**
 * Class CacheableModule
 */
class CacheableModule extends \Ytake\LaravelAspect\Modules\CacheableModule
{
    /** @var string[] */
    protected $classes = [
        
    ];
}

次にこのアスペクトを利用するクラスを用意してみましょう。

App\Repository\CustomerRepositoryInterface

インターフェースと具象クラスをバインドする場合でも正しく動きますので、
これらのクラスを作成していきましょう

<?php

namespace App\Repository;

/**
 * Interface CustomerRepositoryInterface
 */
interface CustomerRepositoryInterface
{
    /**
     * @return string[]
     */
    public function getAll();
}

App\Repository\CustomerRepository

このクラスのgetAllメソッドの戻り値をキャッシュするように、@Cacheableアノテーションを記述します。
このアノテーションではキャッシュ名、タグ、キャッシュキーに使う引数や保存時間を指定できます。
キャッシュ名を指定しなかった場合は、メソッド名をキャッシュ名として利用します。

<?php

namespace App\Repository;

use Ytake\LaravelAspect\Annotation\Cacheable;

/**
 * Class CustomerRepository
 */
class CustomerRepository implements CustomerRepositoryInterface
{
    /**
     * @var string[]
     */
    private $customers = [
        [
            'name' => 'laravel'
        ],
        [
            'name' => 'lumen'
        ],
    ];

    /**
     * @Cacheable(cacheName="customers",lifetime=20)
     * @return string[]
     */
    public function getAll()
    {
        return $this->customers;
    }
}

このクラスを利用するサービスクラスを用意して、インターフェースと具象クラスを束縛します。
サービスクラスなどを利用しない方は、
コントローラなどの任意のクラスにインターフェースをタイプヒンティングしましょう

App\Services\CustomerService

<?php

namespace App\Services;

use App\Repository\CustomerRepositoryInterface;

/**
 * Class CustomerService
 */
class CustomerService
{
    /** @var CustomerRepositoryInterface  */
    protected $customerRepository;

    /**
     * CustomerService constructor.
     *
     * @param CustomerRepositoryInterface $customerRepository
     */
    public function __construct(CustomerRepositoryInterface $customerRepository)
    {
        $this->customerRepository = $customerRepository;
    }

    /**
     * @return array|\string[]
     */
    public function getAllCustomers() : array
    {
        return $this->customerRepository->getAll();
    }
}

App\Providers\AppServiceProvider

インターフェースと利用する具象クラスを記述します

<?php

namespace App\Providers;

use App\Repository\CustomerRepository;
use App\Repository\CustomerRepositoryInterface;
use Illuminate\Support\ServiceProvider;

/**
 * Class AppServiceProvider
 */
class AppServiceProvider extends ServiceProvider
{
    /**
     * {@inheritdoc}
     */
    public function register()
    {
        $this->app->bind(CustomerRepositoryInterface::class, CustomerRepository::class);
    }
}

キャッシュなし動作

この状態でコントローラからクラスを利用します。

<?php

namespace App\Http\Controllers;

use App\Services\CustomerService;

/**
 * Class CacheController
 */
class CacheController extends Controller
{
    /** @var \App\Services\CustomerService  */
    protected $customer;

    /**
     * CacheController constructor.
     *
     * @param \App\Services\CustomerService $customer
     */
    public function __construct(CustomerService $customer)
    {
        $this->customer = $customer;
    }

    /**
     * @return \string[]
     */
    public function index()
    {
        return $this->customer->getAllCustomers();
    }
}

routes.phpに任意のURIを追加してアクセスしてもキャッシュは作成されません。
これから実装したクラスには一切手を加えず、アスペクトが作用するように動作を拡張します。

App\Modules\CacheableModule

作成したCacheableModuleにアスペクトを利用するクラスを記述します。

<?php

namespace App\Modules;

use App\Repository\CustomerRepository;

/**
 * Class CacheableModule
 */
class CacheableModule extends \Ytake\LaravelAspect\Modules\CacheableModule
{
    /** @var string[] */
    protected $classes = [
        CustomerRepository::class,
    ];
}

次に先に作成したAspectServiceProviderにこのモジュールクラスを登録します。

<?php

namespace App\Providers;

use App\Modules\CacheableModule;
use Ytake\LaravelAspect\AspectManager;
use Ytake\LaravelAspect\AnnotationManager;
use Ytake\LaravelAspect\AspectServiceProvider as Aspect;

/**
 * Class AspectServiceProvider
 */
class AspectServiceProvider extends Aspect
{
    /**
     * @return void
     */
    public function boot()
    {
        /** @var \Ytake\LaravelAspect\AspectManager $aspect */
        $aspect = $this->app['aspect.manager'];
        $aspect->register(CacheableModule::class);
        $aspect->dispatch();
    }

    /**
     * {@inheritdoc}
     */
    public function register()
    {
        /**
         * for package configure
         */
        $this->app->configure('ytake-laravel-aop');
        $this->app->singleton('aspect.annotation.reader', function ($app) {
            return (new AnnotationManager($app))->getReader();
        });
        $this->app->singleton('aspect.manager', function ($app) {
            // register annotation
            return new AspectManager($app);
        });
    }
}

コンテナに登路したaspect.managerサービスにアクセスして、アスペクトカーネルクラスに登録します。
登録が完了したら再度URIにアクセスしてみましょう。

storage/framework/cache配下にキャッシュが作成されているのが確認できると思います。
このLaravel-Aspectは様々な処理に対して横断的に処理を追加することが簡単に行えます。
ライブラリではデフォルトでCacheの追加、削除が可能なアスペクト、アノテーション
データベースのトランザクションや、ロギングといったものを用意していますので、
これらを利用してアプリケーション開発に役立ててみてください。