Issei.M's Techlog

Web/iOS エンジニアの僕が技術関連のメモ等をつらつらと。主に Symfony について書いています。

Plug 'n Play with Puli

この記事はSymfony Advent Calendar 2015 13日目の記事です。前回は@77webさんのSymfonyのFormをテストするとき、どんなテストクラスを書くべきか?でした。

今日はSymfonyのコアメンバーであり、Form, Validator, OptionsResolverなどの主要コンポーネントのメンテナであるBarnhard Schussek (a.k.a @webmozart)さんが手掛ける、次世代のユニバーサルパッケージシステム "Puli" のお話とSymfonyでの導入例を紹介したいと思います。

Plug 'n Play

Composerの登場により、昨今のPHPの開発現場でサードパーティ製のパッケージを使う事は当たり前となりました。PSRの発足とComposerのオートローダーの機構により、利用者は労する事なくパッケージクラスの利用に集中できるようになったのです。 しかし、PHPクラスの他に、Twigテンプレートや言語カタログ、WebアセットなどのリソースはComposerの恩恵を受ける事ができず、これらを含むパッケージを利用する場合は手動でプロジェクトへの統合(通常、ロケータへパスを通す作業)が必要です。また非常に大きなパッケージではクラスが抽象化されており、実際に利用する前に多数の設定を書く必要があります。

この問題は通常、大手のフレームワークに大抵備わっているモジュールシステム(Symfonyで言うとBundle)の上で配布する事で解決します。現にTwigやDoctrineと言った主要なパッケージは各フレームワーク用に統合するためのモジュールが作られています:

f:id:issei_m:20151213004547p:plain

しかし、当然ながらこれらのモジュールはそのフレームワーク用に特化しており、それぞれ一定の規約がある為、別のフレームワークを使用したプロジェクトに導入するのは困難です。

これを解決する為に考えられたのがPuliです。Puliは、あらゆるPHPプロジェクト上でのPlug 'n Play、すなわち、導入してすぐ利用可能なパッケージの構築を可能にする事を目標としています:

公式ドキュメントによると、読みは "poo-lee" だそうです。カタカナにするとプーリーでしょうか。また、語源は犬のPuli)から来ているらしいです。

f:id:issei_m:20151213004510p:plain

次章ではPuliがどのようにPlug 'n Playを実現しようとしているか、またプロジェクトでどのように活用していくかをSymfonyを使ってその一部分をご紹介します。

Let's get started

Installation

※Puliは現在BETAバージョンとして公開されています。実際の製品で使う場合は注意して下さい。また今回使用したPuliのバージョンは1.0.0-beta9でした。

今回はSymfony 2.8を使います。本当は3でやりたかったのですが、Puliの対応が不十分な為やむなく2系を使う事にしました。

$ symfony new puli-symfony 2.8

幸いPuliはSymfonyのBundleを提供している為、導入は簡単です。 プロジェクトのcomposer.jsonminimum-stability と以下の3つパッケージを定義し、インストールします。

...
"require": {
   ...
   "puli/symfony-bundle": "~1.0",
   "puli/twig-extension": "~1.0",
   "puli/cli": "~1.0"
},
"minimum-stability": "beta"
...
$ composer update puli/symfony-bundle puli/twig-extension puli/cli

オートローダーの生成後、なにやら見慣れない文字列が表示されますが、これはPuliのComposer Pluginによる物で、後述するパスマッピングのログです。

Writing lock file
Generating autoload files
Generating PULI_FACTORY_CLASS constant
Registering Puli\GeneratedPuliFactory with the class-map autoloader
Setting "bootstrap-file" to "vendor/autoload.php"
Looking for updated Puli packages
Installing paragonie/random_compat (vendor/paragonie/random_compat) in prod
Installing symfony/polyfill-php70 (vendor/symfony/polyfill-php70) in prod
Installing symfony/polyfill-util (vendor/symfony/polyfill-util) in prod
...
Running "puli build"

また、プロジェクトルートには puli.json が生成されます。これがパッケージ設定ファイルとなる訳です。インストールしたパッケージの全ての設定と共にマージされ、ビルドされます。ビルド結果は .puli ディレクトリ内に生成されます。この為、.puliディレクトリは .gitignore に追加した方が良いでしょう。

最後にKernelにBundleを登録してインストール完了です:

<?php

public function registerBundles()
{
    $bundles = array(
        // ...
        new Puli\SymfonyBundle\PuliBundle(),
        // ...
    );
    
    // ...
}

Using Resources

ビルドされたPuliパッケージのリソースは全てResourceRepositoryに集約されます。 SymfonyBundleでは、RepositoryをSymfonyとTwigのロケーターに統合しており、 Bundleのpuli.jsonにはいくつかのSymfony Bundleのリソースへのマッピングが定義されている為、すぐに試す事ができます。試しに app/config/routing_dev.yml のWebProfilerの定義を以下の様に修正してみます:

_wdt:
    resource: "/symfony/web-profiler-bundle/config/routing/wdt.xml"
    prefix:   /_wdt

画面をリロードしても問題なくプロファイラーが表示されると思います。次に、プロジェクトのリソースへのマッピングを定義してみましょう。puli.json に次の行を加えます:

"path-mappings": {
    "/app": "app/Resources",
    "/config": "app/config"
}

※キー名はパッケージ毎にユニークになるような名前を付ける必要があります。基本的にはPSR−0の名前空間に準拠した命名規則に従えば問題無さそうです。

Puliをリビルドします:

$ bin/puli build

音沙汰、無し!wですがきちんとビルドされてます。試しにDefaultControllerのindexActionを次のように書き換えます:

<?php

public function indexAction(Request $request)
{
    // replace this example code with whatever you need
    return $this->render('/app/views/default/index.html.twig', array(
        'base_dir' => realpath($this->container->getParameter('kernel.root_dir').'/..'),
    ));
}

同様に config.yml のrouting部分を次のように置き換えます。

framework:
    # ...
    router:
        resource: "/config/routing.yml"
    # ...

画面をリロードしてもエラーなく画面が表示されるはずです。このように、本来Symfonyしか解釈できない @WebProfilerBundle/~~default/index.html.twig%kernel.root%/~~ のような定義をPuliを使う事で正規化する事が可能となります。

Providing Package

最後にパッケージの配布方法を紹介します。 ドキュメントによれば、以下の様なディレクトリ構成がベストプラクティス(強制ではない)のようです:

my-package/
    src/
        ... PHP files ...
    res/
        ... non-PHP files ...
    puli.json

この構成に則ってテストパッケージを作成しました。 内容は見てもらうとわかりますが、ただHello World!と表示するTwigを1つ作成し、リソースのマッピングを定義しているだけとなります。

このパッケージを試しにインストールしてみます。composer.jsonに次の行を加えてupdateして下さい:

"require": {
    ...
    "issei-m/puli-test": "dev-master#9d46daa9bc84fac0515052d906542edcbb5c0092"
},
"repositories": [
    {
        "type": "vcs",
        "url": "https://github.com/issei-m/puli-test"
    }
]

※Packagistに登録していないのでリポジトリを直接指定しています。

インストール後、デフォルトのテンプレートのbodyブロックの先頭に次の行を追加します:

{# app/Resources/views/default/index.html.twig #}

{% block body %}
    {% include '/issei-m/puli-test/views/print_hello_world.twig' %}

    {# ... #}

画面にHello World!と表示されれば成功です。

f:id:issei_m:20151213004618p:plain

Resource Discovery

Puliには、Discoveryと言う概念が存在します。PuliパッケージのリソースがRepositoryに集約されると言う話は前章でしました。 Discoveryは、集約したリソースの集合をグルーピングします。これにより、そのリソースが特定のライブラリの為に存在すると言う意味付けをする事ができます。最終的に一つのグループがデファクトスタンダード化すれば、各パッケージはそのグループに所属するようリソースを定義づける事で、いかなるフレームワーク上でも特定のライブラリのリソースを最小限のコード量で集約する事が可能となります。

実装例については、Symfonyの話から大きく逸脱してしまう為、詳しくはドキュメントをご覧ください。

最後に

今回のPuliのまとめ:

  • PuliはPHPパッケージの、フレームワーク間のインテグレーションコードを最小限に留める事を目的としたユニバーサルパッケージシステム
  • 各PuliパッケージのリソースファイルはResourceRepositoryに集約され、アクセス手段の正規化を図る
  • リソースはグルーピングする事ができ、Discoveryによってグループ別に取り出す事ができる

本当は他にもPlug 'n Playを実現する機能が多数あるのですが、今回はSymfonyのAdvent Calendarなので多くは語りません。興味のある方は是非ご自身で体験してみて下さい! また、Puliについては僕自身も最近注目した物で、解釈を誤っている可能性が大いにあります。もし誤りがあればご指摘頂けると嬉しいです! Puli自身、開発途上なので皆さんどんどんお試し頂いて、その発展に貢献しましょう!パッケージマネージャは広く普及してその真価を発揮すると思いますので。今ならPRを送ればすぐマージされるかもしれませんよ(笑)

なお、今回作業したプロジェクトはGitHub公開してあります。

明日は@tai2さんのDoctorineのBest Practices翻訳です!乞うご期待!