OTOBANK Engineering Blog

オトバンクはコンテンツが大好きなエンジニアを募集しています!

BEAR.Sundayをコードリーディングしたのでメモ程度にアウトプットする

お久しぶりです。 @kalibora です。

題名が若干のルー大柴感ありますが、以前から気にはなっていたものの手を出していなかった BEAR.Sunday を最近触りまして、

理解を深めるためにもコードを多少読んだので、ここにそのメモを藪からスティックに垂れ流したいと思います。

誰かのお役に立てれば幸いです。

(多分に間違えている可能性があるので、その際はマサカリを投げてください。)

前提条件

以下のバージョンのコードを読みました。

$ composer show | grep 'bear/\(app-meta\|package\|resource\|sunday\)'
bear/app-meta                      1.2.4              BEAR.Sunday application meta information
bear/package                       1.7.0              BEAR.Sunday framework package
bear/resource                      1.9.0              Hypermedia framework for object as a service
bear/sunday                        1.2.1              A resource-oriented application framework

また、下記のように bear/skeleton を使ってプロジェクトを開始しています。

$ composer create-project bear/skeleton MyVendor.MyPackage

github.com

読み進める前提としては、BEAR.Sunday の チュートリアル をやって、なんとなく理解していた方が分かりやすいかと思います。

3つのエントリポイント

何はともあれ開始地点であるエントリポイントを見てみます。 webからのアクセス用と、CLI用とで3つありました。

<?php
// public/index.php

$context = PHP_SAPI === 'cli-server' ? 'hal-app' : 'prod-hal-app';
require dirname(__DIR__) . '/bootstrap/bootstrap.php';
<?php 
// bootstrap/web.php

$context = PHP_SAPI === 'cli' ? 'cli-hal-app' : 'hal-app';
require __DIR__ . '/bootstrap.php';
<?php
// bootstrap/api.php

$context = PHP_SAPI === 'cli' ? 'cli-hal-api-app' : 'hal-api-app';
require __DIR__ . '/bootstrap.php';

それぞれの違いは単純にコンテキストを変えているだけ。 そして bootstrap.php を呼んでいるのみ。

bootstrap.phpでは何をしているのか?

<?php
// bootstrap/bootstrap.php

use BEAR\Package\Bootstrap;
use BEAR\Resource\ResourceObject;

require dirname(__DIR__) . '/autoload.php';

/* @global string $context */
$app = (new Bootstrap)->getApp('MyVendor\MyPackage', $context, dirname(__DIR__));
$request = $app->router->match($GLOBALS, $_SERVER);

try {
    $page = $app->resource->{$request->method}->uri($request->path)($request->query);
    /* @var $page ResourceObject */
    $page->transfer($app->responder, $_SERVER);
    exit(0);
} catch (\Exception $e) {
    $app->error->handle($e, $request)->transfer();
    exit(1);
}

このように非常に短いスクリプトで全体の流れを記述しているのみ。

次節からは $app, $request, $page, $page->transfer(), $app->error->handle() について深掘りしていく。

$app とは?

<?php
$app = (new Bootstrap)->getApp('MyVendor\MyPackage', $context, dirname(__DIR__));

上記の $app とは何者なのか?

<?php
// `BEAR\Package\Bootstrap` より抜粋
use BEAR\Sunday\Extension\Application\AppInterface;

final class Bootstrap
{
    public function getApp(string $name, string $contexts, string $appDir = null) : AbstractApp
    {
        return $this->newApp(new AppMeta($name, $contexts, $appDir), $contexts);
    }

    public function newApp(AbstractAppMeta $appMeta, string $contexts, Cache $cache = null) : AbstractApp
    {
        // snip: $contexts が prod や stage だったらキャッシュから取得する処理

        $app = (new AppInjector($appMeta->name, $contexts))->getInstance(AppInterface::class);

        // snip: キャッシュに保存する処理

        return $app;
    }
}

BEAR\Sunday\Extension\Application\AppInterface をDI(Dependency Injection. 後述の AppInjector がその役割を担っている)で解決したインスタンスが $app となる。

後述するが、結局の所これは MyVendor\MyPackage\Module\App のインスタンスである。

AppInjector

AppInjector::getInstance() メソッドでは、指定したinterfaceに束縛されたインスタンスを、依存解決済みで返してくれる。
(最初の1回目の場合は、依存解決したものをすべてフラットなPHPファイルとしてダンプする。これをコンパイル処理と呼んでいるみたい)

では、膨大なクラス同士の依存関係はどのように定義されているのか?

<?php
// `BEAR\Package\AppInjector`から抜粋
use Ray\Di\AbstractModule;

final class AppInjector implements InjectorInterface
{
    /**
     * Return configured module
     */
    private function newModule(AbstractAppMeta $appMeta, string $contexts) : AbstractModule
    {
        $contextsArray = array_reverse(explode('-', $contexts));
        $module = null;
        foreach ($contextsArray as $context) {
            $class = $appMeta->name . '\Module\\' . ucwords($context) . 'Module';
            if (! class_exists($class)) {
                $class = 'BEAR\Package\Context\\' . ucwords($context) . 'Module';
            }
            if (! is_a($class, AbstractModule::class, true)) {
                throw new InvalidContextException($class);
            }
            /* @var $module AbstractModule */
            $module = new $class($module);
        }
        $module->install(new ResourceObjectModule($appMeta));
        $module->override(new AppMetaModule($appMeta));

        return $module;
    }
}

このように Ray\Di\AbstractModule を継承したモジュールがその役割を担っている。

MyVendor\MyPackage\Module\{Context}Module もしくは BEAR\Package\Context\{Context}Module$contexts の逆順で読み込まれる。

例えば、 $contexts = cli-hal-api-app であれば、

  • MyVendor\MyPackage\Module\AppModule
  • BEAR\Package\Context\ApiModule
  • BEAR\Package\Context\HalModule
  • BEAR\Package\Context\CliModule

の順番で読み込まれる。しかし優先順位はその逆である。($module = new $class($module); だと、引数で渡されたモジュールの方が優先度が低いので)

その後に ResourceObjectModule を install し、 AppMetaModule を override しているので、最終的な優先順位は

  1. BEAR\Package\AppMetaModule
  2. BEAR\Package\Context\CliModule
  3. BEAR\Package\Context\HalModule
  4. BEAR\Package\Context\ApiModule
  5. MyVendor\MyPackage\Module\AppModule
  6. BEAR\Package\Provide\Resource\ResourceObjectModule

となる。

モジュールが依存関係(AOPの設定もしているけど)を定義していると述べたが、具体的にはどのように行っているのか?

AppMetaModule を例に取ると、

<?php
// `BEAR\Package\AppMetaModule`から抜粋
    protected function configure()
    {
        // (snip)
        $this->bind(AppInterface::class)->to($this->appMeta->name . '\Module\App');
        // (snip)
    }

このように AppInterface$this->appMeta->name . '\Module\App' に束縛(bind)している事がわかる。

この設定から、先に述べたように最終的に $appMyVendor\MyPackage\Module\App のインスタンスとなるのだ!(ドヤァ)

$requestとは?

<?php
$request = $app->router->match($GLOBALS, $_SERVER);

上記の $request とは何者なのか?

前節で $appMyVendor\MyPackage\Module\App のインスタンスであると述べた。

また、これは BEAR\Sunday\Extension\Application\AbstractApp を継承している。

そしてデフォルトでは AbstractApp::$router : RouterInterrfaceBEAR\Package\Provide\Router\WebRouter に束縛されている。

この設定は下記のモジュールを見ると分かる。

  • BEAR\Package\PackageModule
    • BEAR\Package\Provide\Router\WebRouterModule

そういうわけで、実態である WebRouter を見ていく。

// `BEAR\Package\Provide\Router\WebRouter` から抜粋
<?php
    public function match(array $globals, array $server)
    {
        $request = new RouterMatch;
        list($request->method, $request->query) = $this->httpMethodParams->get($server, $globals['_GET'], $globals['_POST']);
        $request->path = $this->schemeHost . parse_url($server['REQUEST_URI'], PHP_URL_PATH);

        return $request;
    }

BEAR\Package\Provide\Router\HttpMethodParams も併せて読むと分かるが、

$_GET$_POSTphp://input などの差異をうまく吸収して、 クエリ文字列だろうがPOSTされたボディであろうが、それらを気にせず、 $request->query に入れてくれている。

また、 _methodHTTP_X_HTTP_METHOD_OVERRIDE を見てよしなに $request->method に入れている。

例として、 コンテキストが api で、 GET /path/to?key=value をリクエストすると、 下記のような値が入る。

  • $request->method: get
  • $request->query:['key'=> 'value', 'hoge' => 'fuga']
  • $request->path: app://self/path/to

HTTPリクエストから、BEARで扱う形式への変換、マッピングというのがこの処理の肝なのではないだろうか。

$pageとは?

<?php
$page = $app->resource->{$request->method}->uri($request->path)($request->query);

上記の $page とは何者なのか?

と、その前に分かりやすくするために、 GET /path/to?key=value というリクエストが来たことにしておく。

するとこうなる。

<?php
$page = $app->resource->get->uri('app://self/path/to')(['key'=> 'value', 'hoge' => 'fuga']);

デフォルトでは AbstractApp::$resource : ResourceInterfaceBEAR\Resource\Resource に束縛されている。

この設定は下記のモジュールを見ると分かる。

  • BEAR\Package\PackageModule
    • BEAR\Sunday\Module\SundayModule
      • BEAR\Sunday\Module\Resource\ResourceModule
        • BEAR\Resource\Module\ResourceClientModule

では実態である Resource を見ていく。

<?php
// `BEAR\Resource\Resource` から抜粋
    // (snip)
    public function __get($name)
    {
        $this->method = $name;

        return $this;
    }
    // (snip)
    public function uri($uri)
    {
        if (is_string($uri)) {
            $uri = new Uri($uri);
        }
        $uri->method = $this->method;
        $resourceObject = $this->newInstance($uri);
        $resourceObject->uri = $uri;
        $this->request = new Request(
            $this->invoker,
            $resourceObject,
            $uri->method,
            $uri->query,
            [],
            $this->linker
        );
        $this->method = 'get';

        return $this->request;
    }
    // (snip)
    public function newInstance($uri)
    {
        return $this->factory->newInstance($uri);
    }

oops. まずは factory が何か知らないといけない。 詳細は割愛するが、 BEAR\Resource\Factory がそれだ。

というわけでさらに BEAR\Resource\Factory を読むと、

<?php
// `BEAR\Resource\Factory` から抜粋
    // (snip)
    /**
     * Resource adapter biding config
     *
     * @var SchemeCollectionInterface
     */
    private $scheme;
    // (snip)
    public function newInstance($uri)
    {
        if (! $uri instanceof Uri) {
            $uri = new Uri($uri);
        }
        $adapter = $this->scheme->getAdapter($uri);

        return $adapter->get($uri);
    }
    // (snip)

というように、今度は SchemeCollectionInterface にたどり着く。

これは BEAR\Resource\Module\SchemeCollectionProvider によってインスタンスが提供されている。

<?php
// `BEAR\Resource\Module\SchemeCollectionProvider` から抜粋
    // (snip)
    public function get()
    {
        $schemeCollection = new SchemeCollection;
        $pageAdapter = new AppAdapter($this->injector, $this->appName);
        $appAdapter = new AppAdapter($this->injector, $this->appName);
        $schemeCollection->scheme('page')->host('self')->toAdapter($pageAdapter);
        $schemeCollection->scheme('app')->host('self')->toAdapter($appAdapter);

        return $schemeCollection;
    }
    // (snip)

とまぁこんな感じで、 BEAR\Resource\AppAdapter を見ればいいんだなってことが何となく分かる。

<?php
// `BEAR\Resource\AppAdapter`から抜粋
    // (snip)
    public function get(AbstractUri $uri)
    {
        if (substr($uri->path, -1) === '/') {
            $uri->path .= 'index';
        }
        $path = str_replace('-', '', ucwords($uri->path, '/-'));
        $class = sprintf('%s%s\Resource\%s', $this->namespace, $this->path, str_replace('/', '\\', ucwords($uri->scheme) . $path));

        try {
            $instance = $this->injector->getInstance($class);
        } catch (Unbound $e) {
            throw $this->getNotFound($uri, $e, $class);
        }

        return $instance;
    }
    // (snip)

はい、ここは結構重要だと思ったところ。

ここで、 Uri('app://self/path/to') を元にしたクラス名に変換され、

それを DI から取得している事がわかる。

この場合は、 MyVendor\MyPackage\Resource\App\Path\To になるので、このインスタンスが取得される。

ちなみに、これらのリソースオブジェクトをDIに束縛しているのは、BEAR\Package\Provide\Resource\ResourceObjectModule みたい。

さぁ、ここまででやっと、BEAR\Resource\Resource::uri($uri) 内の処理である

<?php
$resourceObject = $this->newInstance($uri);

が何をしているか分かった。

URIに基づくリソースオブジェクトクラスのインスタンスをDIから取得したわけだ。

しかしまだ実行はしていない。

で、これを BEAR\Resource\Request に詰め込んで生成して返している。

この BEAR\Resource\Request には実行すべきリソースオブジェクトと BEAR\Resource\Invoker という、リソースオブジェクトを実行するインスタンスを持っているため、 あとはこれを実行すればよい。

実行するメソッドは親クラスである BEAR\Resource\AbstractRequest にある。

<?php
// `BEAR\Resource\AbstractRequest`から抜粋
    // (snip)
    public function __invoke(array $query = null)
    {
        if ($query !== null) {
            $this->query = array_merge($this->query, $query);
        }
        if ($this->links) {
            return $this->linker->invoke($this);
        }

        return $this->invoker->invoke($this);
    }
    // (snip)

引数にクエリパラメーターを取り、Invokerを用いて実行している。

<?php
// `BEAR\Resource\Invoker`から抜粋
    // (snip)
    public function invoke(AbstractRequest $request)
    {
        $onMethod = 'on' . ucfirst($request->method);
        if (method_exists($request->resourceObject, $onMethod) !== true) {
            return $this->invokeOptions($request->resourceObject, $request, $onMethod);
        }
        if ($request->resourceObject->uri instanceof AbstractUri) {
            $request->resourceObject->uri->query = $request->query;
            $request->resourceObject->uri->method = $request->method;
        }
        $params = $this->params->getParameters([$request->resourceObject, $onMethod], $request->query);
        $result = call_user_func_array([$request->resourceObject, $onMethod], $params);

        return $this->postRequest($request, $result);
    }
    // (snip)

なんじゃかんじゃあるが、結局のところ重要なのは下記だけ。

<?php
call_user_func_array([$request->resourceObject, $onMethod], $params)

リソースオブジェクトのonGetなどのメソッドは与えられた引数を元に、 自分自身の内部状態を変えて、自分自身(リソースオブジェクト)を返すことになっているので、 最終的にリソースオブジェクトが返る。

ここまでを振り返ってみると、

<?php
$page = $app->resource->get->uri('app://self/path/to')(['key'=> 'value', 'hoge' => 'fuga']);

これは

<?php
$resource = $app->resource; // BEAR\Resource\Resource
$resource = $resource->get; // BEAR\Resource\Resource
$request = $resource->uri('app://self/path/to'); // BEAR\Resource\Request
$page = $request('key'=> 'value', 'hoge' => 'fuga']); // BEAR\Resource\ResourceObject

このように理解することが出来る。

$page->transfer() は何をしている?

力尽きたのでまた今度・・。

$app->error->handle() は何をしている?

力尽きたのでまた今度・・。

今回のまとめ

今回読んだ範囲での自分の理解をまとめてみます。

変数 インターフェイスとクラス 私の理解
$app BEAR\Sunday\Extension\Application\AppInterface
実態は MyVendor\MyPackage\Module\App
$contexts に応じ、DIが依存解決したオブジェクトグラフを持つアプリケーション
$request BEAR\Sunday\Extension\Router\RouterMatch HTTPリクエストをBEARリソースへのメソッド,URI,パラメータに変換したもの
$page BEAR\Resource\ResourceObject BEARリソースのメソッドとURIから、それに該当するリソースオブジェクトを取得し、それを実行した結果

$app が面白いですね。全部そこにまとまっているっていうのが。

さてさて、それではまた。

SymfonyのService Containerについて(後編)

こんにちは。@mrtry_です。
最近ずっとサンダルで出社していたら、日焼けでサンダルの紐の跡ができました。

さて、今回は前回に引き続き、DIについて書こうと思います。 前回でDIとはなにか、という話をしたので、今回はSymfonyでDIする際に利用するService Containerについて書きたいと思います

以下、今回の記事の目次になります。

  • Service Contaier とは
  • DIするまでの手順
  • サービスを作る
  • サービスの設定をする
  • コントローラでサービスを呼び出してみる

Service Container とは

Service Containerとは、Symfonyで提供されているDIコンテナです。 Symfonyでは、ビジネスロジックをまとめたクラスをサービスと呼んでいます。 Service Containerは、サービスをインスタンス化するときに、必要とする定数や他のサービスのオブジェクトを注入したりと、依存関係を解決する役割を担ってくれます。 前回の記事で言うところの「インジェクタ」の役割にあたります。

今回は、実際に簡単なサービスを作ってみて、Service Containerを利用して依存する定数やサービスを注入していきたいと思います。

DIするまでの手順

実際にサービスを作成し、依存する他のサービスを注入してみたいと思います。 以下のような手順で進めていきたいと思います。

  • サービスを作る
  • サービスの設定をする
    • ローダーの作成
    • 設定ファイルの作成
    • コマンドラインからの確認

サービスを作る

はじめに、元となるビジネスロジックをまとめたクラス、サービスを作ろうと思います。

まず、サービスのソースコードを配置するディレクトリを作っておきます。

$ mkdir src/AppBundle/Service

次に、サービスクラスを作ります。

今回は、GreetingServiceというサービスを作成しました。 sayGreeting()すると、時間帯にあった挨拶を返してくれるサービスです。 このサービスには、TimeSlotInterfaceを継承したクラスと、VariousGreetingsInterfaceを継承したクラスが注入されています。 時間帯の判定をTimeSlotInterfaceを継承したクラス、時間帯に依ったあいさつをVariousGreetingsInterfaceを継承したクラスにまかせるようになっています。

ちなみに、今回の例では、DIをコンストラクタで依存オブジェクトを受け取るConstruct Injectionで行っていますが、Setter InjectionProperty Injectionも行うことができます。

src/AppBundle/Service/GreetingService.php

<?php

namespace AppBundle\Service;

use AppBundle\Service\VariousGreetingsInterface;
use AppBundle\Service\TimeSlotInterface;

class GreetingService
{
    private $variousGreetings;
    private $timeSlot;

    public function __construct(TimeSlotInterface $timeslot, VariousGreetingsInterface $variousGreetings)
    {
        $this->timeSlot = $timeSlot;
        $this->variousGreetings = $variousGreetings;
    }

    public function sayGreeting()
    {
        $greeting = '';

        switch ($this->timeSlot->getCurrentTimeSlot()) {
            case TimeSlotInterface::MORNING:
                $greeting = $this->variousGreetings->sayMorningGreeting();
                break;

            case TimeSlotInterface::NOON:
                $greeting = $this->variousGreetings->sayNoonGreeting();
                break;

            case TimeSlotInterface::NIGHT:
                $greeting = $this->variousGreetings->sayNightGreeting();
                break;
        }

        return $greeting;
    }
}

続いて、注入しているクラスのインターフェースと実際のクラスを見ていきます。

src/AppBundle/Service/TimeSlotInterface.php

TimeSlotInterfaceは以下のようなコードになっています。 時間帯を表す定数と、現在の時間帯を表す値を返すメソッドgetCurrentTimeSlot()が定義されています。

<?php

namespace AppBundle\Service;

interface TimeSlotInterface
{
    const MORNING = 0;
    const NOON = 1;
    const NIGHT = 2;

    /**
     * 現在の時間帯を表す値を返す
     *
     * @return int
     */
    public function getCurrentTimeSlot();
}

src/AppBundle/Service/TimeSlotService.php

TimeSlotInterfaceを継承し、TimeSlotServiceを作成しました。 GreetingService に注入する実際のクラスはこれになります。 以下のようなコードになっています。

<?php

namespace AppBundle\Service;

use AppBundle\Service\TimeSlotInterface;

class TimeSlotService implements TimeSlotInterface
{
    public function getCurrentTimeSlot()
    {
        $now = new \DateTime('now');

        $morningOfTheDay = clone $now;
        $morningOfTheDay->setTime(6, 0);

        $noonOfTheDay = clone $now;
        $noonOfTheDay->setTime(11, 0);

        $nightOfTheDay = clone $now;
        $nightOfTheDay->setTime(18, 0);

        if ($morningOfTheDay <= $now && $now < $noonOfTheDay) {
            return TimeSlotInterface::MORNING;
        } else if ($noonOfTheDay <= $now && $now < $nightOfTheDay) {
            return TimeSlotInterface::NOON;
        } else {
            return TimeSlotInterface::NIGHT;
        }
    }
}

src/AppBundle/Service/VariousGreetingsInterface.php

VariousGreetingsInterfaceは以下のようなコードになっています。 朝昼夜、それぞれにあたるあいさつを返すメソッドが定義されています。

<?php

namespace AppBundle\Service;

interface VariousGreetingsInterface
{
    /**
     * 朝のあいさつを返す
     *
     * @return string
     */
    public function sayMorningGreeting();

    /**
     * 昼のあいさつを返す
     *
     * @return string
     */
    public function sayNoonGreeting();

    /**
     * 夜のあいさつを返す
     *
     * @return string
     */
    public function sayNightGreeting();
}

src/AppBundle/Service/JapaneseGreetingService.php

VariousGreetingsInterfaceを継承し、JapaneseGreetingServiceを作成しました。 GreetingServiceに注入する実際のクラスはこれになります。 以下のようなコードになっています。

<?php

namespace AppBundle\Service;

use AppBundle\Service\VariousGreetingsInterface;

class JapaneseGreetingService implements VariousGreetingsInterface
{
    public function sayMorningGreeting()
    {
        return 'おはようございます';
    }

    public function sayNoonGreeting()
    {
        return 'こんにちわ';
    }

    public function sayNightGreeting()
    {
        return 'こんばんわ';
    }
}

DIすることで得られた利点

このように実装したことで、以下の利点があります。

結合度の低下

依存するクラスをInterfaceで実装を統一しているため、結合度が低くなっています。 今回の例で言うと、多言語対応が必要となった時に嬉しいです。 VariousGreetingsInterfaceを継承したクラスを新しく作成し、GreetingServiceに注入すれば、それだけで別言語でのあいさつを返すサービスを実装することができます。 英語のあいさつを返すサービスにしたければ、VariousGreetingsInterfaceを継承したEnglishGreetingServiceを作成し、注入するだけで対応できます。

※ なお、今回の実装での多言語化はDIの例で挙げているだけで、通常は以下のような手法を使います。

テストのしやすさ

スタブやモックを用いたテストが状態になっています。 今回の例で言うと、時間帯ごとに意図したあいさつが返ってくるかを確認したい時に嬉しいです。 TimeSlotInterfaceを継承したクラスを作成し、getCurrentTimeSlot()で確認したい時間帯の定数を返すことで、時間帯ごとに意図したあいさつが返ってくるかを確認することができます。

サービスの設定をする

次に、サービスに注入する依存オブジェクトを定義したりする設定ファイルを作成していきます。

この記事では、サービスの設定ファイルをyamlで作成します。 作成した設定ファイルの配置場所として、以下の2箇所があります。

  • app/config
    • composerなどで導入したBundleをService Containerに登録したいときはここに置く
  • Bundle内のResouces/config
    • 自身で定義したサービスをService Containerに登録するときは、ここに置く

今回は、自身で定義したサービスをService Containerに登録したいので、Bundle内のResouces/configに配置したいと思います。

ただ、$ app/console generate:bundleでBundleを生成した際は、 設定ファイルを配置するディレクトリや、設定ファイルを読み込むためのローダーが準備されているのですが、プロジェクトを立ち上げたときに生成されるAppBundleでは準備がされていません。 ローダーの準備と設定ファイルの配置場所の作成、設定ファイルの作成をしていきます。

ローダーの作成

まず、ローダーのソースコードを配置するディレクトリを作成します。

$ mkdir src/AppBundle/DependencyInjection

その後、以下のようにローダーを作成します。

src/AppBundle/DependencyInjection/AppExtension.php

<?php

namespace AppBundle\DependencyInjection;

use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;

class AppExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $loader = new YamlFileLoader(
            $container,
            new FileLocator(__DIR__.'/../Resources/config')
        );
        $loader->load('services.yml');
    }

    public function getAlias()
    {
        return 'app';
    }
}

継承しているExtensionというクラスは、サービスコンテナに対して、何かしらの拡張を行うためのクラスです。 YAMLなどの設定ファイルからサービスの定義を読み取ったり、設定に基づいてサービスの構成を行ったりします。

今回は、AppBundle/Resources/configに配置した設定ファイルを読み込むためにYamlFileLoaderを用意し、services.ymlを読み込む処理をしています。

設定ファイルの作成

先程Resources/configに配置したservices.ymlを読み込むようにローダーの準備をしたので、ディレクトリを作成し、設定を配置します。

$ mkdir -p src/AppBundle/Resources/config

src/AppBundle/Resources/config/services.yml

services:
    # サービスID
    app.japanese_greeting_service:
        # 実際のクラス
        class: AppBundle\Service\JapaneseGreetingService

    app.timeslot_service:
        class: AppBundle\Service\TimeSlotService

    app.greeting_service:
        class: AppBundle\Service\GreetingService
        # 注入するサービスIDを渡す (サービスIDの頭には`@`をつける)    
        arguments:
            - @app.timeslot_service
            - @app.japanese_greeting_service

servicesのキー以下では、サービスの定義をすることができます。 任意の名前のサービスIDを設定し、実際のクラス名指定することで、フレームワーク側にサービスを登録することができます。 また、依存する定数や他のクラスを注入することもできます。

今回の例だと、以下のように定義しています。

クラス名 サービスID 注入したもの
JapaneseGreetingService japanese_greeting_service なし
TimeSlotService timeslot_service なし
GreetingService greeting_service japanese_greeting_service, timeslot_service

コマンドラインからの確認

一通りの設定ができたら、$ php app/console debug:containerを叩いてサービスがSymfony側に登録されたことを確認します。 きちんと設定ができていれば、以下のようにサービスIDを確認することができます。

$ app/console debug:container | grep app
  app.greeting_service                                                 AppBundle\Service\GreetingService
  app.japanese_service                                                 AppBundle\Service\Japanese
  app.timeslot_service                                                 AppBundle\Service\TimeSlotService
  doctrine.orm.default_entity_listener_resolver                        Doctrine\ORM\Mapping\DefaultEntityListenerResolver

これで、サービスを利用するまでの準備は一通り終わりました。

コントローラでサービスを呼び出してみる

では、定義したサービスをコントローラで利用してみます。 サービスを利用する際は、$this->get(サービスID)を実行するとサービスのインスタンスが取得できます。

今回の例では、MessageServiceを取得し、getMessage()した内容をJsonで返すコントローラを書きました。

<?php

namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;

class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction(Request $request)
    {
        $greetingService = $this->get('app.greeting_service');
        $message = [
            'message' => $greetingService->sayGreeting(),
        ];

        return new JsonResponse($message);
    }
}

app/console server:startをし、http://127.0.0.1:8000/へアクセスしてみると、時間帯に合ったあいさつが表示されていることを確認できたかと思います。

おわりに

今回は、SymfonyのService Containerを利用してDIする例を紹介しました。 Service Containerは、実際に開発でも高頻度で触る箇所なので、きちんと理解しておきたい所ですね!!

さて、今回をもちまして、「Symfony2入門」の投稿は、一旦終えたいと思います。 いろいろ紹介していない内容は多々あるのですが、全部紹介していたら永遠と続くので今回を区切りにしたいと思います。笑 (最近、私の開発担当がモバイルアプリになって、Symfonyを全然さわっていないというのも、あります。)

今まで書いた記事が、私と同じような初心者の方々の参考になったなら嬉しく思います。 ありがとうございました!

参考

重い腰上げDay(仮)を開催しました

ペンギン村からおはこんばんちは。今年のフジロックは3日通しで参加予定の @kalibora です。久しぶり過ぎてどんなテンションでブログ書けばいいのか忘れました。

さて、今日は技術的な話題というよりも、先日社内で開催した 重い腰上げDay(仮) というイベントについて書きたいと思います。

重い腰上げDay(仮)とは

結論から申しますと、メルカリ の Be Professional Day(BPD) のパクリです。

定義については t-wada 先生の

“Be Professional Day: 1ヶ月に1日、普段の業務を一旦ストップさせて普段手を付けていないことをやる日” 7つの習慣でいうと緊急度が低く重要度が高い象限にフォーカスする日

http://b.hatena.ne.jp/entry/277751164/comment/t-wada

これが一番わかりやすく、しっくり来たので、これを採用してます。

ちなみに7つの習慣は弊社サービスのFeBe↓でも購入できますので、耳で聴きたい方は是非。

www.febe.jp

まー、アレですよ。普段は目の前にやらなきゃいけないことが山積みであるじゃないですか。 で、それはそれで必要なんだけどもでもちょっと待って、それやって仕事した気になってるけど、それで本当にいいんだっけ?そのうち緩やかに死んでいかないんだっけ? みたいなやつを考えてやったりなんだりする日です。

あと名前については言い出しっぺの僕が付けた仮称のままでとりあえず動いてます。

1回目の成果

さぁ、そんなわけで大上段に構えつつ実際は緩やかに1回目を開催したわけですが、 成果としてはこんな感じでした。

  1. 古いシステムで動き続けて使い辛いまま放置されていた管理画面の移植&機能改善
  2. 古くて保守しづらかったり機能がイマイチなライブラリの置換
  3. アプリのビルド周りのアップデート

エンドユーザーに直接的にメリットがあるものもあれば、そうでないものもあり。 成果として上がったかどうかは、一概には判断し辛いと思ってます。

でもそれで良いのです。そういうものこそを進めるための時間なので。

やる意味あんのか?あんのかバカヤロー

いわゆる技術的負債のようなものをどのように返していくか?ということにも近いと思うのですが、 こういう類のものは往々にして後回しにされます。(直近で直接的なメリットが薄いので。)

しかし放置しておくとにっちもさっちもいかなくなる(あとは後任に丸投げ? or 行き着く先はサービス終了?)ので、それをどう処理していくのか?

scrumでやってるなら、バックログ的なものにエンドユーザー目線でのメリットがどうか?ということを加味して同じ枠内で優先度を付けて混ぜてやる方法もあると思いますし、 負債処理班、みたいに専任で人で分けちゃうっていうやり方もあるだろうし。 今回みたいに、期間を設けて対応するというやり方もあるだろうし。

何が言いたいのかよくわからなくなってきましたが、、 ともかく、普段とは別の角度から考える時間っていうのは必要だと思うので、この取り組みは今後も続けて行こうと思っています。