In this post I will go through setting up XAMPP and Eclipse to let you work with PHP and one of its unit testing frameworks PHPUnit.

seen from United States
seen from United States

seen from Singapore
seen from Australia
seen from China

seen from United States
seen from Switzerland

seen from Switzerland
seen from United Kingdom
seen from Germany

seen from Malaysia

seen from United Kingdom
seen from Singapore

seen from United Kingdom

seen from Singapore
seen from Malaysia
seen from China

seen from United Kingdom
seen from Switzerland

seen from United States
In this post I will go through setting up XAMPP and Eclipse to let you work with PHP and one of its unit testing frameworks PHPUnit.

Anya is live and ready to show you everything. Watch her strip, dance, and perform exclusive shows just for you. Interact in real-time and make your fantasies come true.
Free to watch • No registration required • HD streaming
「PHPの開発環境を構築する」シリーズの第4回です。今回はXAMPPのPHP環境にPHPUnitをインストールし、またEclipseからPHPUnitのテストを実行できるMakeGoodというプラグインを組み込みます。そして実際にテストを実行してみます。
MakeGood 2.5.0、Stagehand_TestRunner 3.6.2のリリース、Stagehand_TestRunner v4の構想
Piece Projectは先日11月1日にStagehand_TestRunner 3.6.2、11月3日にMakeGood 2.5.0をリリースしました。
MakeGood 2.5.0
テスト結果レイアウトとしてタブまたは水平のいずれかを選択できるようになりました。
Eclipse起動時のデフォルトはタブになっています。これはワークスペースの設定で変更することができます。
詳細およびその他の変更についてはリリースノートをご覧ください。
Stagehand_TestRunner 3.6.2
継続的テスト実行時の不具合を修正しました。
Stagehand_TestRunner v4の構想
現在Stagehand_TestRunnerのメジャーバージョンアップを計画中です。新たにいくつかのテスティングフレームワークのサポートを追加し、現行のPHPUnit以外のサポートを廃止するつもりです。
Stagehand_TestRunner v4についてご意見ご要望がありましたら本記事へのコメントやGitHubのIssuesで是非お聞かせください。また、Pull Requestもお待ちしております。
Stagehand_FSM - 有限状態マシン、そしてドメイン特化言語
先日、Piece FrameworkのプロダクトStagehand_FSMのバージョン2.0.0を2条項BSDライセンスでリリースしました。Stagehand_FSMのリリースは2008年7月27日のバージョン1.10.0以来5年ぶりとなります。
Stagehand_FSMは有限状態マシン(FSM: Finite State Machine)の定義・実行を行うためのPHPコンポーネントです。PHP 5.3.2以降で動作します。本記事ではStagehand_FSMがどのようなものなのか簡単に説明します。
有限状態マシンの例:有料ゲート
以下のようなお金を入れると通過できるゲートを考えます。
Stagehand_FSMを使った実装は以下のようになります。
<?php use Stagehand\FSM\StateMachine\StateMachineBuilder; $stateMachineBuilder = new StateMachineBuilder(); $stateMachineBuilder->addState('locked'); $stateMachineBuilder->addState('unlocked'); $stateMachineBuilder->setStartState('locked'); $stateMachineBuilder->addTransition('locked', 'insertCoin', 'unlocked'); $stateMachineBuilder->addTransition('unlocked', 'pass', 'locked'); $stateMachine = $stateMachineBuilder->getStateMachine(); $stateMachine->start(); echo $stateMachine->getCurrentState()->getStateID() . PHP_EOL; // "locked" $stateMachine->triggerEvent('insertCoin'); echo $stateMachine->getCurrentState()->getStateID() . PHP_EOL; // "unlocked" $stateMachine->triggerEvent('pass'); echo $stateMachine->getCurrentState()->getStateID() . PHP_EOL; // "locked"
このように簡単な状態管理であれば有限状態マシン(以下ステートマシン)を使う必要はありません。しかし、状態の数が増えるに従ってプログラムコードは複雑になり、その意図性が低下することは避けられなくなります。ステートマシンの利用は、そのような場合に極めて有効なアプローチとなります。
ドメイン特化言語の基盤としてのStagehand_FSM
Stagehand_FSMの基本的な使い方は、前述の例のように直接的にステートマシンを組み立てて実行することですが、応用としてドメイン特化言語(DSL: Domain Specific Language)の基盤として使うこともできます。現在開発中のPiece_Flow v2のように画面遷移に特化した言語を作ることもできますし、業務フローを定義・実行するワークフローエンジンを作ることもできます。言語の表現形式としてはテキストはもちろんのこと、ツリーや、UMLのステートマシン図(状態遷移図)のようにグラフィカルなものを検討することもできます。
おわりに
Stagehand_FSMは複雑な状態管理コードの置き換えに有用なコンポーネントです。また、ドメイン特化言語の基盤としてさまざまな形で応用することができるでしょう。興味を持たれた方は是非使ってみてください。
Info PHPメンターズでは、PHPソフトウェアにおけるドメイン特化言語の開発について、プロトタイプ作成や技術サポート、メンタリング等を承ります。ドメイン特化言語の開発についてサポートが必要な場合は[email protected]までお問い合わせください。
参考
有限状態マシン - Stagehand_FSM - Piece Framework
Pieceの中のSymfony #4: Configコンポーネント
Symfony Advent Calendar JP 2012 - Day 6
今回はStagehand_TestRunner、Piece_Flow等、Piece Frameworkのいくつかのプロダクトで使われているConfigコンポーネントについて解説します。
Symfony Configコンポーネント
Configコンポーネントはソフトウェアの可変部分を表現する言語を定義し、処理するためのフレームワークです。Symfonyフレームワークにおいてはバンドルおよびその背後にあるコンポーネントの可変部分をユーザーが構成するために使われています。
Configコンポーネントが取り扱うのは一般的に設定や構成と呼ばれるものです。
設定はDSLである
ドメイン固有言語 (DSL) は特定用途向けの言語です。ドメイン固有言語は、システムファミリの具体的なメンバを「発注」するのに使い、ゆえにジェネレーティブプログラミングにおいて重要な役割を果たします。
— ジェネレーティブプログラミング (IT Architects’Archive CLASSIC MODER)
設定はソフトウェアの可変部分(可変点)に対する具体的な値であり、最終的にオブジェクトのような実装コンポーネントの構成に変換されます。別の言い方をすると、設定によって具体的なソフトウェアが構成されます。
設定はドメイン固有の言語の体裁を持っているため、ドメイン特化言語(DSL: Domain Specific Language)であるといえます。
パーサージェネレーター?
設定言語が存在する以上そのパーサーは必須となりますが、PHPの世界ではこれまでは手書きのもので済ませてきた事例が大部分を占めるのではないでしょうか。手軽に使えるパーサージェネレーターが存在しないことが大きな理由かもしれませんが、それだけでしょうか?
Symfonyフレームワークでは設定の記述にINI、PHP、XML、YAML、その他任意のフォーマットを使うことができます。通常のパーサージェネレーターが単一のテキストフォーマットに対する文法を定義することでパーサーを生成するものだとしたら、フォーマット毎に文法定義が必要となり現実的ではありません。
抽象構文を定義する
まず、抽象構文を定義します。これは抽象形の「スキーマ」です。
次に「エディタ」を定義し、抽象形を投影を通じて操作可能にします。
それから「ジェネレータ」を定義します。これは抽象形をどのように実行可能形に変換するかを定義しています。実際には、ジェネレータはDSLのセマンティクスを定義します。
— Martin Fowler's Bliki in Japanese - LanguageWorkbench 新しいDSLの定義
Configコンポーネントでは特定のフォーマットについてではなく、設定の抽象形(抽象構文木)について文法を定義します。これによって単一の文法で複数のフォーマットをサポートすることが可能になります。
DSLの観点から見れば、Configコンポーネントはグラマー言語による設定言語の文法定義を核とするDSL構築フレームワークといえるでしょう。
Configコンポーネントを単体で利用する
Configコンポーネントを単体で利用するのはそれほど難しくはありません。まずは設定言語定義と設定の処理の関係を図でみてみましょう。
グラマー言語による設定言語の定義
Configコンポーネントを利用する場合の主な活動はグラマー言語によって設定の抽象構文を定義することです。以下は現在開発中のPiece_Flow v2におけるページフロー定義の文法を記述したものです。
Piece\Flow\PageFlow\Definition17Configuration:
<?php ... namespace Piece\Flow\PageFlow; ... use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; ... class Definition17Configuration implements ConfigurationInterface { public function getConfigTreeBuilder() { $treeBuilder = new TreeBuilder(); $treeBuilder->root('definition17') ->children() ->scalarNode('firstState')->isRequired()->cannotBeEmpty()->end() ->arrayNode('viewState') ->isRequired() ->requiresAtLeastOneElement() ->prototype('array') ->children() ->scalarNode('name')->isRequired()->cannotBeEmpty()->end() ->scalarNode('view')->isRequired()->cannotBeEmpty()->end() ->arrayNode('transition') ->prototype('array') ->children() ->scalarNode('event')->isRequired()->cannotBeEmpty()->end() ->scalarNode('nextState')->isRequired()->cannotBeEmpty()->end() ->arrayNode('action') ->children() ->scalarNode('class')->defaultNull()->cannotBeEmpty()->end() ->scalarNode('method')->isRequired()->cannotBeEmpty()->end() ->end() ->end() ->arrayNode('guard') ->children() ->scalarNode('class')->defaultNull()->cannotBeEmpty()->end() ->scalarNode('method')->isRequired()->cannotBeEmpty()->end() ->end() ->end() ->end() ->end() ->end() ->arrayNode('entry') ->children() ->scalarNode('class')->defaultNull()->cannotBeEmpty()->end() ->scalarNode('method')->isRequired()->cannotBeEmpty()->end() ->end() ->end() ->arrayNode('exit') ->children() ->scalarNode('class')->defaultNull()->cannotBeEmpty()->end() ->scalarNode('method')->isRequired()->cannotBeEmpty()->end() ->end() ->end() ->arrayNode('activity') ->children() ->scalarNode('class')->defaultNull()->cannotBeEmpty()->end() ->scalarNode('method')->isRequired()->cannotBeEmpty()->end() ->end() ->end() ->end() ->end() ->end() ->arrayNode('actionState') ->prototype('array') ->children() ->scalarNode('name')->isRequired()->cannotBeEmpty()->end() ->arrayNode('transition') ->isRequired() ->requiresAtLeastOneElement() ->prototype('array') ->children() ->scalarNode('event')->isRequired()->cannotBeEmpty()->end() ->scalarNode('nextState')->isRequired()->cannotBeEmpty()->end() ->arrayNode('action') ->children() ->scalarNode('class')->defaultNull()->cannotBeEmpty()->end() ->scalarNode('method')->isRequired()->cannotBeEmpty()->end() ->end() ->end() ->arrayNode('guard') ->children() ->scalarNode('class')->defaultNull()->cannotBeEmpty()->end() ->scalarNode('method')->isRequired()->cannotBeEmpty()->end() ->end() ->end() ->end() ->end() ->end() ->arrayNode('entry') ->children() ->scalarNode('class')->defaultNull()->cannotBeEmpty()->end() ->scalarNode('method')->isRequired()->cannotBeEmpty()->end() ->end() ->end() ->arrayNode('exit') ->children() ->scalarNode('class')->defaultNull()->cannotBeEmpty()->end() ->scalarNode('method')->isRequired()->cannotBeEmpty()->end() ->end() ->end() ->arrayNode('activity') ->children() ->scalarNode('class')->defaultNull()->cannotBeEmpty()->end() ->scalarNode('method')->isRequired()->cannotBeEmpty()->end() ->end() ->end() ->end() ->end() ->end() ->arrayNode('lastState') ->addDefaultsIfNotSet() ->children() ->scalarNode('name')->isRequired()->cannotBeEmpty()->end() ->scalarNode('view')->isRequired()->cannotBeEmpty()->end() ->arrayNode('entry') ->children() ->scalarNode('class')->defaultNull()->cannotBeEmpty()->end() ->scalarNode('method')->isRequired()->cannotBeEmpty()->end() ->end() ->end() ->arrayNode('exit') ->children() ->scalarNode('class')->defaultNull()->cannotBeEmpty()->end() ->scalarNode('method')->isRequired()->cannotBeEmpty()->end() ->end() ->end() ->arrayNode('activity') ->children() ->scalarNode('class')->defaultNull()->cannotBeEmpty()->end() ->scalarNode('method')->isRequired()->cannotBeEmpty()->end() ->end() ->end() ->end() ->end() ->arrayNode('initial') ->children() ->scalarNode('class')->defaultNull()->cannotBeEmpty()->end() ->scalarNode('method')->isRequired()->cannotBeEmpty()->end() ->end() ->end() ->arrayNode('final') ->children() ->scalarNode('class')->defaultNull()->cannotBeEmpty()->end() ->scalarNode('method')->isRequired()->cannotBeEmpty()->end() ->end() ->end() ->end() ; return $treeBuilder; } ...
この設定言語に対応する設定例は以下のようになります。
Warning この例にはルートノードdefinition17の記述がありませんが、通常はルートノードを記述する必要があります。
firstState: Input lastState: name: Finish view: Finish activity: method: onFinish viewState: - name: Input view: Input activity: method: onInput transition: - event: next nextState: Validation - name: Confirmation view: Confirmation activity: method: onConfirmation transition: - event: next nextState: Registration - event: prev nextState: Input actionState: - name: Validation activity: method: onValidation transition: - event: invalid nextState: Input - event: valid nextState: Confirmation - name: Registration activity: method: onRegistration transition: - event: done nextState: Finish
DSLはそのソフトウェアのユーザーのための高レベルなAPIであり、ドメインの言語を使ってユーザーの要求に適合した抽象度で書けることが望まれます。
設定の読み込み、結合、バリデーション、デフォルト値入力
設定の読み込みは任意の方法で行います。基本的にはYamlコンポーネント(YAML)やDependency Injectionコンポーネント(INI、PHP、XML、YAML)が提供するものを使うと良いでしょう。前述のConfigurationInterfaceオブジェクトと読み込まれた設定をSymfony\Component\Config\Definition\Processor::processConfiguration()メソッドに渡すことで、複数の設定の結合、バリデーション、デフォルト値入力が行われ、最終的な設定が返されます。
Piece\Flow\PageFlow\PageFlowGenerator:
<?php ... namespace Piece\Flow\PageFlow; ... use Symfony\Component\Config\Definition\Processor; use Symfony\Component\Yaml\Yaml; ... class PageFlowGenerator { ... protected function readDefinition() { $processor = new Processor(); return $processor->processConfiguration( new Definition17Configuration(), array('definition17' => Yaml::parse($this->pageFlowRegistry->getFileName($this->pageFlow->getID()))) ); } ...
設定の利用
最終的な設定は抽象構文木を表現する配列になっているため、簡単に利用することができます。以下のコードでは、前述のreadDefinition()メソッドの呼び出しによって得られた設定にアクセスし、その値を使ってPiece\Flow\PageFlow\PageFlowオブジェクトを構成しています。
Piece\Flow\PageFlow\PageFlowGenerator:
<?php ... namespace Piece\Flow\PageFlow; ... class PageFlowGenerator { ... public function generate() { $definition = $this->readDefinition(); if (State::isProtectedState($definition['firstState'])) { throw new ProtectedStateException("The state [ {$definition['firstState']} ] cannot be used in flow definitions."); } $this->fsmBuilder->setStartState($definition['firstState']); if (!empty($definition['lastState'])) { if (State::isProtectedState($definition['lastState']['name'])) { throw new ProtectedStateException("The state [ {$definition['lastState']['name']} ] cannot be used in flow definitions."); } $this->fsmBuilder->addTransition($definition['lastState']['name'], Event::EVENT_END, State::STATE_FINAL); $this->configureViewState($definition['lastState']); $this->pageFlow->addEndState($definition['lastState']['name']); $this->pageFlow->addView($definition['lastState']['name'], $definition['lastState']['view']); } $this->configureViewStates($definition['viewState']); $this->configureActionStates($definition['actionState']); if (!empty($definition['initial'])) { $this->fsmBuilder->setExitAction(State::STATE_INITIAL, $this->wrapAction($definition['initial'])); } if (!empty($definition['final'])) { $this->fsmBuilder->setEntryAction(State::STATE_FINAL, $this->wrapAction($definition['final'])); } $this->pageFlow->setFSM($this->fsmBuilder->getFSM()); return $this->pageFlow; } ...
ConfigコンポーネントをSymfonyバンドルで利用する
ConfigコンポーネントをSymfonyバンドルで利用するのはとても簡単です。FooBarBundle\DependencyInjection\Configurationクラスで設定言語を定義し、FooBarBundle\DependencyInjection\FooBarExtensionクラスで設定の結合、バリデーション、デフォルト値自動入力を行い、最終的な設定をDIコンテナのパラメーターやサービス定義に変換します。以下のコードはSymfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtensionクラスの例です。
Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension:
<?php ... namespace Symfony\Bundle\FrameworkBundle\DependencyInjection; ... class FrameworkExtension extends Extension { ... public function load(array $configs, ContainerBuilder $container) { $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('web.xml'); $loader->load('services.xml'); ... $configuration = $this->getConfiguration($configs, $container); $config = $this->processConfiguration($configuration, $configs); $container->setParameter('kernel.secret', $config['secret']); ...
おわりに
ジェネレーティブプログラミングを標榜する私にとって、ConfigコンポーネントはSymfonyコンポーネントの中でもとりわけ重要な位置付けにあります。今後も様々な場面で積極的に使うことになるでしょう。
参考
Config (current) - Symfony
Martin Fowler's Bliki in Japanese - LanguageWorkbench

Anya is live and ready to show you everything. Watch her strip, dance, and perform exclusive shows just for you. Interact in real-time and make your fantasies come true.
Free to watch • No registration required • HD streaming
MakeGood Tip #1: Pyrusによるユーザー固有のPEAR環境の構築とMakeGoodからの利用
MakeGoodでテスティングフレームワークを使う場合の選択肢として、プロジェクトの外部に配置されているパッケージを使う方法と、内部に配置されているパッケージを使う方法があります。今日ではComposerを使うことで、開発時のみに必要とされるパッケージでも簡単にプロジェクトの内部に配置することができるようになりました。
必要なパッケージの多くをプロジェクトの内部に配置することは、アプリケーションの動作を保証する上で有用なプラクティスといえるでしょう。しかし、Eclipse PDTからの利用を考慮すると、多くのプロジェクトで使われることが予想されるテスティングフレームワークやモッキングフレームワーク等について、単一の環境のものを使うことによるパフォーマンス上のメリットは小さくはありません。また、通常のフレームワークやライブラリと比較すると、テストコードの動作に与える影響は極めて小さいため、外部のものを使うデメリットもほとんどないといっていいでしょう。
今回の記事では、PEARパッケージ管理システムPyrusを使用してプロジェクトの外部にユーザー固有のPEAR環境を構築し、それらをMakeGoodから利用する手順について解説します。
Pyrusによるユーザー固有のPEAR環境の構築
まず今回作成するPEAR環境のディレクトリ構造を以下に示します。インストールするPEARパッケージはPHPunitとPhakeとします。
PEAR環境のディレクトリ構造
├── bin │ └── pyrus.phar └── vendor ├── phake │ └── php │ ├── Phake └── phpunit └── php ├── PHPUnit
vendorディレクトリにはPHPUnitとPhake、それぞれ個別のPEARパッケージを配置します。binディレクトリには本環境で使用するPyrusのPharアーカイブを配置します。
PEAR環境のパス
PEAR環境のパスはどこでも構いませんが、ユーザーが書き込み権限を持つディレクトリを用意しましょう。本記事で使用するパスは~/site-phpとします。
Pyrusのインストール
PyrusのPharアーカイブpyrus.pharをPEAR2のWebサイトからダウンロードし、~/site-php/binディレクトリに配置します。
Pyrusの設定
PEARパッケージのインストールの前にPyrusを設定します。まず以下のようにPyrusを初期化します。
$ php ~/site-php/bin/pyrus.phar Pyrus version 2.0.0a4 SHA-1: 72271D92C3AA1FA96DF9606CD538868544609A52 Pyrus: No user configuration file detected It appears you have not used Pyrus before, welcome! Initialize install? Please choose: yes no [yes] : Great. We will store your configuration in: /home/iteman/.pear/pearconfig.xml Where would you like to install packages by default? [/home/iteman] : /home/iteman/site-php You have chosen: /home/iteman/site-php Thank you, enjoy using Pyrus Documentation is at http://pear.php.net Using PEAR installation found at /home/iteman/site-php ...
続いていくつかのパラメーターの値を設定します。
$ php ~/site-php/bin/pyrus.phar set auto_discover 1 $ php ~/site-php/bin/pyrus.phar set cache_dir ~/site-php/tmp/pyrus/cache $ php ~/site-php/bin/pyrus.phar set temp_dir ~/site-php/tmp/pyrus/temp $ php ~/site-php/bin/pyrus.phar set download_dir ~/site-php/tmp/pyrus/downloads
PEARパッケージのインストール
次にPHPUnitとPhakeのPEARパッケージをインストールします。基本となるコマンドはmypearとinstallです。mypearコマンドで対象パッケージの配置先ディレクトリを決定し、installコマンドでインストールを実行します。
PHPUnitのインストール
最初にPHPUnitをインストールします。基本と異なるのはパラメーターbin_dirを設定している箇所です。ここではbin_dirにPHPUnitの配置先ディレクトリを設定しています。
$ php ~/site-php/bin/pyrus.phar mypear ~/site-php/vendor/phpunit $ php ~/site-php/bin/pyrus.phar set bin_dir ~/site-php/vendor/phpunit/bin $ php ~/site-php/bin/pyrus.phar install pear.phpunit.de/phpunit Pyrus version 2.0.0a4 SHA-1: 72271D92C3AA1FA96DF9606CD538868544609A52 Using PEAR installation found at /home/iteman/site-php/vendor/phpunit Sorry, the channel "pear.phpunit.de" is unknown. Discovery of channel pear.phpunit.de successful Sorry, pear.phpunit.de/phpunit references an unknown channel pear.symfony-project.com for pear.symfony-project.com/YAML Discovery of channel pear.symfony-project.com successful Downloading pear.phpunit.de/PHPUnit ... Installed pear.phpunit.de/PHPUnit-3.6.12 Installed pear.phpunit.de/File_Iterator-1.3.1 Installed pear.phpunit.de/Text_Template-1.1.1 Installed pear.phpunit.de/PHP_CodeCoverage-1.1.3 Installed pear.phpunit.de/PHP_Timer-1.0.2 Installed pear.phpunit.de/PHPUnit_MockObject-1.1.1 Installed pear.symfony-project.com/YAML-1.0.6 Installed pear.phpunit.de/PHP_TokenStream-1.1.3 Optional dependencies that will not be installed, use --optionaldeps: pear.phpunit.de/PHP_Invoker depended on by pear.phpunit.de/PHPUnit
Phakeのインストール
次にPhakeをインストールします。
$ php ~/site-php/bin/pyrus.phar mypear ~/site-php/vendor/phake $ php ~/site-php/bin/pyrus.phar install pear.digitalsandwich.com/phake Pyrus version 2.0.0a4 SHA-1: 72271D92C3AA1FA96DF9606CD538868544609A52 Using PEAR installation found at /home/iteman/site-php/vendor/phake Sorry, the channel "pear.digitalsandwich.com" is unknown. Discovery of channel pear.digitalsandwich.com successful Downloading pear.digitalsandwich.com/Phake ... Installed pear.digitalsandwich.com/Phake-1.0.2
これでユーザー固有のPEAR環境の構築は完了です。
MakeGoodからの利用
さて、この段階ではMakeGoodからPEAR環境を利用することができないため、MakeGoodビューには以下のようにPHPUnit_Framework_TestCaseクラスが利用できません。修正...というメッセージが表示されます。
プロジェクトからPEAR環境を利用するには、ユーザーライブラリの定義とプロジェクトへのユーザーライブラリの追加という2つの作業が必要になります。
ユーザーライブラリの定義
ユーザーライブラリは、ユーザー環境のライブラリをプロジェクトから任意の名前によって参照するための仕組みです。ユーザー固有のライブラリパスをプロジェクトから隠蔽することができるため、PHPプロジェクトを複数人で共有する場合に重宝する機能です。
ここではさきほど準備したPHPUnitをユーザーライブラリとして定義します。まず、メニューバーからWindow -> Preferences...を選択し、続いてPHP -> PHP Librariesを選択します。次にNew...ボタンをクリックし、ライブラリの名前を入力後OKボタンをクリックします。
次に先程定義したユーザーライブラリを選択しAdd External folder...ボタンをクリックします。次にフォルダー選択ダイアログからPHPUnitが配置されているフォルダー/home/iteman/site-php/vendor/phpunit/phpを選択します。
同様の手順でPhakeをユーザーライブラリとして定義します。
プロジェクトへのユーザーライブラリの追加
プロジェクトからユーザーライブラリを参照するためには、プロジェクトにユーザーライブラリを追加する必要があります。まず、プロジェクトのプロパティを開きPHP Include PathのLibrariesタブを選択し、続いてAdd Library...ボタンをクリックします。次にUser Libraryを選択しNext >ボタンをクリックします。
次に先程定義したユーザーライブラリにチェックを入れてからFinishボタンをクリックします。
問題なければテストの実行を待っています...というメッセージがMakeGoodビューに表示されます。
参考
PEAR2_Pyrus - PEAR2
概要 - MakeGood - Piece Framework
Pieceの中のSymfony #3: Dependency Injectionコンポーネント
Piece FrameworkのプロダクトのひとつStagehand_TestRunnerは、CLIでユニットテストを実行するための継続的テストランナーです。Stagehand_TestRunner v3の実装には多くのSymfonyコンポーネントが使われています。今回はStagehand_TestRunnerを動作させる上で重要な役割を果たしているDependency Injectionコンポーネントについて具体的な使い方を解説します。
Symfony Dependency Injectionコンポーネント
Dependency InjectionコンポーネントはSymfonyにおけるDIコンテナの実装であり、WebアプリケーションフレームワークとしてのSymfonyの基盤となるプロダクトです。サービスの依存性とその注入方法の定義、サービスの作成・取得、サービスへの依存性の注入、といったDIコンテナの基本機能に加えて、タグによる拡張ポイント・拡張の定義、Configコンポーネントとの統合、コンパイラによるカスタマイズ可能な最適化プロセス等の特徴を持ちます。
構成の知識とDIコンテナ
可変性を持つアーキテクチャの設計では,必ずしも UML のこうした機能を使う必要はないが,何らかの設計上の意図を明確化する表現法を利用して,資産管理を考えることは重要である.
...
このようにして可変性を分離できれば,可変部分をカプセル化し変更の影響を局所化し,あるいは,実行環境の外に定義して,可変点での選択肢を柔軟に取ることが可能となる.たとえば,オブジェクト指向におけるインターフェイスと実装の分離の原則を使った,ストラテジーパターンによる実装や,DI(Dependency Injection)による実行時の実装部分の選択がこの例である.
— 萩原正義 ユニシス技報 93号 Software Factories,現状と未来 - マイクロソフト社の次世代開発基盤技術(PDF)
DIコンテナを使用するメリットとしては、依存性の反転によるコンポーネントの疎結合化の促進、テスタビリティの向上等がよく知られています。しかし、PHPのような動的な言語では静的な言語と比べてこのようなメリットが少ないため、これまで何度となく不要論が唱えられてきました。
しかし、ソフトウェアを構成するための実装コンポーネントの組み合わせに関する知識(構成の知識)を記述する言語としてDIコンテナを位置付けるとどうでしょうか。可変性を含む構成の知識を資産と捉え、サービスの定義(とその可変部分によって構成されたDSL)によって設計上の意図を明確化し、コンパイル時や実行時に可変部分を選択しながらソフトウェアを構成する、そのためのエンジンとしてDIコンテナを使うのです。
このようにDIコンテナを使うことは、実装にともなうソフトウェアの設計情報の喪失をより少なくする手段として多くの言語で有用ではないかと私は考えています。
Stagehand_TestRunnerではまさにそのような意図をもってDependency Injectionコンポーネントを使用しています。
Dependency Injectionコンポーネントを単体で使用する
では具体的な使い方をみていきます。まずは全体像を知るためにDependency Injectionコンポーネントの構造をみてみましょう。以下の図はDependency Injectionコンポーネントを使うにあたって登場するクラスの関連を表したものです。
赤はDependency Injectionコンポーネントが提供するクラスおよびインターフェイスで、緑はユーザーのDependency Injectionコンポーネント関連クラス、青はユーザーのDIコンテナ管理対象クラスを表しています。最終的にFooサービスに依存するクライアントは、DIコンテナによる依存性の注入によって暗黙的に、あるいはファクトリーを使って明示的にFooサービスを取得することになります。
DIコンテナのコンパイル
ContainerBuilderはその名前の通りアプリケーション固有のDIコンテナを組み立てるためのクラスであり、compile()メソッドによって自身が保持するサービス定義とパラメーターをコンパイルします。コンパイルの実態はDIコンテナの最適化です。
PhpDumperクラスはコンパイル済みのContainerBuilderオブジェクトをContainerを継承したクラスのソースコードに変換します。Symfonyアプリケーションでは起動直後に実行されるブートストラップファイルapp/booststrap.php.cacheにこのコンパイルプロセスが組み込まれているため開発者が意識する必要はありませんが、単体で使う場合はコンパイルプロセスを自前で準備する必要があります。Stagehand_TestRunnerにおけるコンパイルプロセスはtestrunner compileコマンドに組み込まれています。このコマンドによって実行されることになるメソッドから順にみていきましょう。
Stagehand\TestRunner\CLI\TestRunnerApplication\Command\CompileCommand:
<?php ... namespace Stagehand\TestRunner\CLI\TestRunnerApplication\Command; ... use Stagehand\TestRunner\DependencyInjection\Compiler; ... class CompileCommand extends Command { ... protected function execute(InputInterface $input, OutputInterface $output) { $compiler = new Compiler(); $compiler->compile(); return 0; } ...
Compilerはコンパイルプロセスを実装したStagehand_TestRunnerのクラスです。Compiler::compile()はコンパイルプロセスそのものを表現するメソッドです。
Stagehand\TestRunner\DependencyInjection\Compiler:
<?php ... namespace Stagehand\TestRunner\DependencyInjection; ... class Compiler { ... public function compile() { // (1) ContainerBuilderオブジェクトの作成 $containerBuilder = new UnfreezableContainerBuilder(); // (2) エクステンションの登録 foreach (ExtensionRepository::findAll() as $extension) { $containerBuilder->registerExtension($extension); } // (3) エクステンションの設定のロード foreach ($containerBuilder->getExtensions() as $extension) { /* @var $extension \Symfony\Component\DependencyInjection\Extension\ExtensionInterface */ $containerBuilder->loadFromExtension($extension->getAlias(), array()); } // (4) 最適化戦略のカスタマイズ $containerBuilder->getCompilerPassConfig()->setOptimizationPasses( array_filter( $containerBuilder->getCompilerPassConfig()->getOptimizationPasses(), function (CompilerPassInterface $compilerPass) { return !($compilerPass instanceof ResolveParameterPlaceHoldersPass); } )); // (5) コンパイルとダンプ $compiler = new \Stagehand\ComponentFactory\Compiler( $containerBuilder, self::COMPILED_CONTAINER_CLASS, self::COMPILED_CONTAINER_NAMESPACE ); file_put_contents( __DIR__ . '/' . self::COMPILED_CONTAINER_CLASS . '.php', $compiler->compile() ); } ...
このプロセスで重要なのは、ContainerBuilderオブジェクトに対する3つの操作、すなわちエクステンションの登録(2)とその設定のロード(3)、コンパイルとダンプ(5)です。
(3)で空の配列を渡していますが、これはStagehand_TestRunnerの設計に起因するのものです。通常は設定ファイル等から読み込んだ生の設定を渡すことになるでしょう。
(2)で登録したエクステンションは(5)でContainerBuilder::compile()メソッドを経由して呼び出されます。
Stagehand\ComponentFactory\Compiler:
<?php ... namespace Stagehand\ComponentFactory; ... class Compiler { ... public function compile($evaluatable = false) { $this->containerBuilder->compile(); $phpDumper = new PhpDumper($this->containerBuilder); $containerClassSource = $phpDumper->dump(array('class' => $this->class)); if (!is_null($this->namespace)) { $containerClassSource = preg_replace( '/^<\?php/', '<?php' . PHP_EOL . 'namespace ' . $this->namespace . ';' . PHP_EOL, $containerClassSource ); } if ($evaluatable) { $containerClassSource = preg_replace('/^<\?php/', '', $containerClassSource); } return $containerClassSource; } ...
Stagehand_TestRunnerのエクステンションは解説に不向きなため、ここでは別のプロダクトPiece_Questetraのエクステンションをみてみましょう。
Piece\Questetra\DependencyInjection\PieceQuestetraExtension:
<?php ... namespace Piece\Questetra\DependencyInjection; ... use Symfony\Component\HttpKernel\DependencyInjection\Extension; ... class PieceQuestetraExtension extends Extension { public function load(array $configs, ContainerBuilder $container) { $config = $this->processConfiguration(new Configuration(), $configs); $loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); $loader->load('services.yml'); $container->setParameter('piece_questetra.context_root', $config['context_root']); $container->setParameter('piece_questetra.user_id', $config['authentication']['user_id']); $container->setParameter('piece_questetra.password', $config['authentication']['password']); } ...
エクステンションのload()メソッドではエクステンションのサービス定義をロードし、受け取った設定をサービス定義やパラメーター等に変換します。
ソフトウェアの可変性の観点からみれば、ここは構成の知識の固定部分(サービス定義)と可変部分(設定)がパラメーターを介して結合し、実装コンポーネントの構成が行われる場所と考えることができます。
最適化戦略のカスタマイズ
DIコンテナの最適化戦略はContainerBuilder::getCompilerPassConfig()メソッドで返されるPassConfigオブジェクトで表現されています。戦略の一つ一つはコンパイラパスと呼ばれるCompilerPassInterfaceインターフェイスのインスタンスです。それらはContainerBuilder::compile()メソッドの呼び出しによって実行され、DIコンテナに適用されます。ユーザーは自由にコンパイラパスの追加・削除・変更を行うことができます。
Stagehand_TestRunnerでは独自のContainerBuilderオブジェクトを使い(1)、パラメーターをインライン化するResolveParameterPlaceHoldersPassオブジェクトを最適化パスから削除することで(4)、コマンドラインパラメーターをDIコンテナのパラメーターに変換できる余地を作っています。
DIコンテナの使用
コンパイルが終われば、アプリケーション固有のDIコンテナをインスタンス化して使うことができます。Stagehand_TestRunnerの場合、PluginCommand::execute()メソッドでDIコンテナのインスタンス化とtest_runnerサービスの作成を行っています。
Stagehand\TestRunner\CLI\TestRunnerApplication\Command\PluginCommand:
<?php ... namespace Stagehand\TestRunner\CLI\TestRunnerApplication\Command; ... abstract class PluginCommand extends Command { ... protected function execute(InputInterface $input, OutputInterface $output) { if (!class_exists(Compiler::COMPILED_CONTAINER_NAMESPACE . '\\' . Compiler::COMPILED_CONTAINER_CLASS)) { $output->writeln( 'Please run the following command before running the ' . $this->getName() . ' command:' . PHP_EOL . PHP_EOL . ' testrunner compile' ); return 1; } // アプリケーション固有のDIコンテナの作成 $container = $this->createContainer(); ApplicationContext::getInstance()->getComponentFactory()->setContainer($container); ApplicationContext::getInstance()->setPlugin($this->getPlugin()); ApplicationContext::getInstance()->setComponent('input', $input); ApplicationContext::getInstance()->setComponent('output', $output); $transformation = $this->createTransformation($container); $this->transformToConfiguration($input, $output, $transformation); $transformation->transformToContainerParameters(); // test_runnerサービスの作成とrun()メソッドの実行 $this->createTestRunner()->run(); return 0; } ... protected function createContainer() { return new Container(); } ... protected function createTestRunner() { return ApplicationContext::getInstance()->createComponent('test_runner'); } ...
Stagehand_TestRunnerのプロダクションコードでサービス名を埋めこんでいるのはこの一箇所だけです。サービス名をどのようにコードの外に追いやっているのか、興味のある方は以下のクラスやサービス定義ファイルを覗いてみてください。
Stagehand\TestRunner\Core\ApplicationContext
Stagehand\TestRunner\Core\Plugin\PluginAwareFactory
Stagehand\ComponentFactory\ComponentAwareFactory
Stagehand\ComponentFactory\ComponentFactory
src/Stagehand/TestRunner/Resources/config/general.yml
おわりに
これまでみてきたようにDependency Injectionコンポーネントは単独で使うには少々手間がかかります。また、依存性とその注入方法の定義が一体となっているため、JSR-330相当の機能を持つDIコンテナ(例えばRay.DiやDingが挙げられます。)と比べると関心の分離という点で遅れをとっているのは確かです。
しかし、Dependency Injectionコンポーネントには、Symfonyアプリケーションとの親和性の高さは当然のことながら、タグによる拡張ポイント・拡張の定義、Configコンポーネントとの統合、コンパイラによるカスタマイズ可能な最適化プロセス等、他にはない特徴があります。これらはジェネレーティブプログラミングの世界を標榜する私にとって大きな魅力となっています。
参考
Dependency Injection (current) - Symfony
CLI のための継続的テストランナー v3 - Stagehand_TestRunner - Piece Framework
Software Factories,現状と未来 - マイクロソフト社の次世代開発基盤技術(PDF)
多言語パラダイムを前提とした設計手法(PDF)
Pieceの中のSymfony #1: Finderコンポーネント
Piece FrameworkのプロダクトのひとつStagehand_TestRunnerは、CLIでユニットテストを実行するための継続的テストランナーです。Stagehand_TestRunner v3の実装には多くのSymfonyコンポーネントが使われています。コンポーネントはソフトウェアにおける部品を指す用語です。SymfonyコンポーネントはPEARパッケージと同様単体で使えるライブラリパッケージであり、多くのものは簡単に導入することができます。
Symfony Finderコンポーネント
Finderコンポーネントはファイルとディレクトリを検索するためのパッケージであり、簡潔かつ強力な検索式や、流れるようなインターフェース(Fluent Interface) によるDSLを使った検索式の組み立てなどが特徴です。Stagehand_TestRunnerではテストファイルの収集や、ディレクトリの監視などの実装に使われています。
テストファイルの収集はStagehand_TestRunnerの重要な機能のひとつであり、中核となるプロセスは抽象クラスStagehand\TestRunner\Collector\Collectorのcollectメソッドに記述されています。
Stagehand\TestRunner\Collector\Collector:
<?php ... namespace Stagehand\TestRunner\Collector; use Symfony\Component\Finder\Finder; ... abstract class Collector { ... public function collect() { $self = $this; $fileSystem = new FileSystem(); $environment = $this->environment; $this->testTargetRepository->walkOnResources(function ($resource, $index, TestTargetRepository $testTargetRepository) use ($self, $fileSystem, $environment) { $absoluteTargetPath = $fileSystem->getAbsolutePath($resource, $environment->getWorkingDirectoryAtStartup()); if (!file_exists($absoluteTargetPath)) { throw new \UnexpectedValueException(sprintf('The directory or file [ %s ] is not found', $absoluteTargetPath)); } if (is_dir($absoluteTargetPath)) { $files = Finder::create() ->files() ->in($absoluteTargetPath) ->depth($self->isRecursive() ? '>= 0' : '== 0') ->sortByName(); foreach ($files as $file) { call_user_func(array($self, 'collectTestCasesFromFile'), $file->getPathname()); } } else { call_user_func(array($self, 'collectTestCasesFromFile'), $absoluteTargetPath); } }); return $this->suite; } ...
テストファイルの収集において、Finderコンポーネントは以下のような条件に合致するファイルを検索するために使われています。
ファイルであること
指定されたディレクトリの絶対パス以下を検索対象とすること
指定されたディレクトリ以下のすべてのディレクトリ、または指定されたディレクトリのみを検索対象とすること
$files = Finder::create() ->files() // (1) ->in($absoluteTargetPath) // (2) ->depth($self->isRecursive() ? '>= 0' : '== 0') // (3) ->sortByName(); // 見つかった要素を名前順にソートする
検索式DSLは検索式を表現するメソッドが常に$thisオブジェクトを返すことで実現されています。
さて、検索式が確定したら次は検索の実行です。検索の実行はFinder::getIterator()メソッドで行われます。FinderクラスはIteratorAggregateインターフェイスを実装していますので、foreachの配列式としてFinderオブジェクトを与えるとgetIterator()メソッドが呼び出されます。
foreach ($files as $file) { call_user_func(array($self, 'collectTestCasesFromFile'), $file->getPathname()); }
Finderコンポーネントにはここに挙げた機能に加えて、更新日時やファイルサイズ、カスタムフィルタによるフィルタリング機能もあります。Finderコンポーネントはファイルやディレクトリを検索する場合に大いに役立つパッケージです。ユーザーが直接操作するクラスはFinderひとつだけなので導入も非常に簡単です。
参考
The Finder Component (current) - Symfony
CLI のための継続的テストランナー v3 - Stagehand_TestRunner - Piece Framework
Martin Fowler's Bliki in Japanese - 流れるようなインタフェース