Aura for PHP をさわってみる - vol1. DI -

2013-12-23

Aura はどういうフレームワークか?については、こちらにすでに書きました。 今日は、サンプルコードを書いてみて、DIの初歩的なところを理解するところまでです。

DIについて、予備知識

下記を読む。

すみません。正直、議論の詳細はちゃんと読めてないです。 だんなが、「ファウラーは難しいよ、最初に考えすぎて何も完成しないのは良くない」って言ってるので、今日は先に進みます。

これから作ろうとしているサンプルコードの仕様

海上輸送の予約アプリケーション

  • 貨物船には積載量(キャパシティ)がある
  • [基本機能] 輸送詳細ページ:船に載せる貨物のCRUD
  • [基本機能] 輸送詳細ページ:貨物の追加時に、船のキャパシティと付きあわせて、輸送予約の可否を判定

なのだけど、今日は単に一覧を表示するところまでです。

作ったサンプルコード置き場はこちら!

https://github.com/kumamidori/HelloAura

パッケージ構成について

Auraではすべてのコードは「パッケージ」として分類されます。「Library First, Framework Second」とうたわれているように、 機能 = 独立パッケージなわけです。

http://auraphp.com/manuals/v1/ja/package-organization/

まずは初期スケルトン作成

以下、プロジェクトパス{$PROJECT_PATH} = ~/HelloAura とした場合。

 1# まずは下準備...
 2
 3# プロジェクトスケルトン作成
 4% composer create-project aura/system ~/HelloAura
 5% cd ~/HelloAura
 6
 7# ビルトインサーバ起動でHelloWorld が出ることを確認
 8% php package/Aura.Framework/cli/server
 9
10# テストが通ることを確認
11% cd tests
12% phpunit

パッケージを作る

  • 自分が作るアプリケーションコードも、なんでも「パッケージ」にすべきなの?

公式のデモコードを見ると、パッケージ化されていない「include」というフォルダがプロジェクトの直下にあるから、 別に全部パッケージ化しなくても良いのかも・・・。

→ でも、パッケージで作る方が、詰まることなくマニュアル通りに進めそうなので、今回はそうする!

  • ベンダー名: Kumamidori
  • アプリケーション名:Transportation(輸送アプリケーションなので)

で作る。

1% cd ~/HelloAura
2% mkdir -p package/Kumamidori.Transportation/src/Kumamidori/Transportation/Web/Home/views
3% mkdir package/Kumamidori.Transportation/config

作ったパッケージをロード

HelloAura/config/_packagesKumamidori.Transportationの一行を追記。

パッケージの初期設定ファイル

HelloAura/package/Kumamidori.Transportation/config/default.php

に、アプリケーションコードのパッケージに対するオートローダを追加、ルーティング指定も書いた。

 1<?php
 2// add the package to the autoloader
 3$loader->add('Kumamidori\Transportation\\', dirname(__DIR__) . DIRECTORY_SEPARATOR . 'src');
 4
 5// add a route to the page and action
 6$di->get('router_map')->add('home', '/', [
 7    'values' => [
 8        'controller' => 'home',
 9        'action' => 'index',
10    ],
11]);
12
13// map the 'greet' controller value to a page controller class
14$di->params['Aura\Framework\Web\Controller\Factory']['map']['home'] = 'Kumamidori\Transportation\Web\Home\HomePage';

これでビルトインサーバを起動すれば、とりあえずホームが表示される。OK。

  • 疑問:この、↑ $loader$di は、どこから来たの?

/package/Aura.Framework/src/Aura/Framework/Bootstrap/Factory.php に該当コードがあった。

 1    public function prep($mode = null, $silent_loader = false)
 2    {
 3        // ・・・略
 4        // create the autoloader
 5        $system = new System($this->root);
 6        // ・・・略
 7        $loader = new Loader;
 8        $loader->prep($system);
 9        if ($silent_loader) {
10            $loader->setMode($loader::MODE_SILENT);
11        }
12        $loader->register();
13
14        // create the DI container
15        $di = new Container(new Forge(new Config));
16
17        // ・・・略
18
19        if (! $mode) {
20            $file = $system->getConfigPath('_mode');
21            if (is_readable($file)) {
22                $mode = trim(file_get_contents($file));
23            } else {
24                $mode = 'default';  // <※ここでconfig/default が出てくる
25            }
26        }
27
28        $read = function ($file) use ($di, $system, $loader) {
29            require $file;
30        };
31
32        $cache = $this->readCacheConfig($system, $read, $mode);
33        if (! $cache) {
34            $this->readPackageConfig($system, $read, $mode);
35        }
36
37        $this->readSystemConfig($system, $read, 'default');
38        if ($mode != 'default') {
39            $this->readSystemConfig($system, $read, $mode);
40        }
41
42        $di->lock();
43
44        return $di;
45    }
  • 疑問:$di で使えるメソッドは何なの?

HelloAura/package/Aura.Di/src/Aura/Di/Container.php に定義があった

(その1) メソッド定義

 1    /**
 2     * Sets a service object by name.
 3     */
 4    public function set($key, $val);
 5
 6    /**
 7     * Gets a service object by key, lazy-loading it as needed.
 8     */
 9    public function get($key);
10
11    /**
12     * Returns a Lazy that gets a service.
13     *
14     * @param string $key The service name; it does not need to exist yet.
15     * @return Lazy A lazy-load object that gets the named service.
16     */
17    public function lazyGet($key);
18
19    /**
20     * Returns a new instance of the specified class, optionally
21     * with additional override parameters.
22     */
23    public function newInstance($class, array $params = [], array $setters = []);
24
25    /**
26     * Returns a Lazy that creates a new instance.
27     */
28    public function lazyNew($class, array $params = [], array $setters = []);
29
30    /**
31     * Returns a Factory that creates an object over and over again (as vs
32     * creating it one time like the lazyNew() or newInstance() methods).
33     */
34    public function newFactory($class, array $params = [], array $setters = [])
35    {
36        return new Factory($this->forge, $class, $params, $setters);
37    }

(その2) マジックメソッドでパラメータとセッター定義

1    public function __get($key)
2    {
3        // ・・・略
4        if ($key == 'params' || $key == 'setter') {
5            return $this->$key;
6        }
7        // ・・・略
8    }

DB接続に、DIの「コンストラクトインジェクション」を使う話

Aura DI -ディペンデンシーインジェクション を読む。

  • サンプルコードのクラス設計:
Kumamidori\Transportation\
  \Database: DBクラス(DB接続そのもの)
  \Model\AbstractRecord: DBアクセスするモデル系のスーパークラス(DB接続を持っていて利用する)

にしてみた。DB名は「transportation」。

サービスの設定

  • データベース接続を返すサービスの定義

Kumamidori\Transportation\Database

 1<?php
 2// ・・・略
 3class Database
 4{
 5    private $connection;
 6
 7    public function __construct($hostname, $dbname, $username, $password)
 8    {
 9        $connection_factory = new ConnectionFactory;
10        $dsn = sprintf('mysql:host=%s;dbname=%s', $hostname, $dbname);
11        $this->connection = $connection_factory->newInstance(
12            'mysql',
13            $dsn,
14            $username,
15            $password
16        );
17    }
18
19    public function getConnection()
20    {
21        return $this->connection;
22    }
23}
  • データベース接続サービスの利用側
 1<?php
 2// ・・・略
 3abstract class AbstractRecord extends AbstractModel
 4{
 5    protected $connection;
 6
 7    public function __construct(Database $db)
 8    {
 9        $this->connection = $db->getConnection();
10    }
11}
  • サービスの設定

HelloAura/package/Kumamidori.Transportation/config/default.php

 1// http://auraphp.com/manuals/v1/ja/di/
 2// 方法 5: lazyNew() メソッドのコール(クロージャを使って新しいインスタンスを返す、と同様)
 3// この1行だけで、この params がそのまま、Database クラスの __construct の引数になる。引数の順番は関係無いところがGoodですね。
 4$di->params['Kumamidori\Transportation\Database'] = [
 5    'hostname' => 'localhost',
 6    'dbname' => 'transportation',
 7    'username' => 'root',
 8    'password' => '',
 9];
10
11// default params for the AbstractRecord class
12// この1行だけで、この params がそのまま、AbstractRecord クラスの __construct の引数になる。
13$di->params['Kumamidori\Transportation\Model\AbstractRecord'] = [
14    'db' => $di->lazyGet('database'),
15];
16
17// define the database service
18$di->set('database', $di->lazyNew('Kumamidori\Transportation\Database'));
  • 疑問:こういう設定を書く「順番」は関係あるの? → 無い。設定ファイルのどこかに書いてあれば正しく解決される。

  • 疑問:ここで言う「サービス」って何?オブジェクトなの?処理なの?

  • → DIコンテナで取得、設定できるモノ。 Aura だと、$di->get()/ $di->set() で、任意のキー名を使ってアクセスされる。 $di->set('database', $di->lazyNew('Kumamidori\Transportation\Database')); データベースサービス、具体的には、「コンテナがDatabaseオブジェクトをlazyNewする」というサービスを設定したら、 $di->lazyGet('database') でそれを取得することができるようになる。 言い換えるとなんだろう・・・「組み立てにより生成されるインスタンスが提供する動的な機能のこと」を指すのかな?・・・自信なし。

モデルオブジェクトのファクトリに、DI「セッターインジェクション」を使う

気力が尽きてきたので省きます。コードだけ抜粋。クロージャってPHP実務で使った記憶ほとんど無いけど、便利そう。

  • ファクトリの定義
 1<?php
 2// 略
 3class ModelFactory
 4{
 5    // a map of model names to factory closures
 6    protected $map = [];
 7
 8    public function __construct($map = [])
 9    {
10        $this->map = $map;
11    }
12
13    public function newInstance($model_name)
14    {
15        $factory = $this->map[$model_name];
16        $model = $factory();
17        return $model;
18    }
19}
  • ファクトリを利用する(セッターインジェクトされる)側
 1<?php
 2//・・・略
 3use Kumamidori\Transportation\Model\ModelFactory;
 4
 5class ApplicationPage extends AbstractPage
 6{
 7    protected $model_factory;
 8
 9    public function setModelFactory(ModelFactory $model_factory)
10    {
11        $this->model_factory = $model_factory;
12    }
13}
  • 設定
 1// default params for the model factory
 2$di->params['Kumamidori\Transportation\Model\ModelFactory'] = [
 3    'map' => [
 4        'transportation' => $di->newFactory('Kumamidori\Transportation\Model\Transportation\TransportationRecord'),
 5    ]
 6];
 7// define the model factory service
 8$di->set('model_factory', $di->lazyNew('Kumamidori\Transportation\Model\ModelFactory'));
 9// after construction, the Forge will call ApplicationPage::setModelFactory()
10// and inject the 'model_factory' service object
11$di->setter['Kumamidori\Transportation\Web\ApplicationPage']['setModelFactory'] = $di->lazyGet('model_factory');

コントローラ

HelloAura/src/Kumamidori/Transportation/Web/Home/HomePage.php

 1<?php
 2//略
 3class HomePage extends ApplicationPage
 4{
 5    public function actionIndex()
 6    {
 7        $transportation = $this->model_factory->newInstance('transportation');  //<※自分でnewしないでファクトリで取得
 8        $total = $transportation->getTotal();
 9        $cargoList = $transportation->getCargoList();
10
11        $this->data = [
12            'total' => $total,
13            'cargo_list' => $cargoList
14        ];
15        $this->view = 'index';
16    }
17}

今日はここまで

DIコンテナでできること、よく出てくる基本用語については、これでちょっとイメージがついてきた気がします。 設計としての特質については分かっていないので、勉強していきたいなと。

分かっていないこと、素朴な疑問をメモ。

  • LL(動的型付け言語)において、PHPだけが(違う?)DI/ServiceLocator主流になったのはなぜ?
  • → モックテストができるようになり、Trait がついても、DIが良いとされたのはなぜ?
  • Rubyの人たちはオープンクラスがあるからそうならなかったの?<全然わかってなくて書いてます

関連情報リンクメモ

追記

アドバイスを頂いたので貼らせて頂きます(ありがとうございます)。