この記事は株式会社アイスタイルアドベントカレンダーの10日目の記事です。
PSR-7対応のzend-expressive、注目している方やすでに利用している方も多いとおもいます。
PSR-7対応のマイクロフレームワークとしては、本エントリで扱うzend-expressiveを除いて、
現在以下のようなものがあります。
slim3
radarphp/Radar.Project
sparkphp/project
zend-expressiveはルータやDIコンテナを選択することができ、
開発者の開発スタイルや要件に合わせて自由にライブラリを組み込むことができるマイクロフレームワークで、
主にHTTPのリクエスト、レスポンスを主軸に設計されています。
zend-expressiveのスケルトンを利用する場合は、
composerのcreate-projectでインストールできます。
いろんな方がブログなどで説明されていますのでここでは割愛します
$ composer create-project -s rc zendframework/zend-expressive-skeleton expressive
スケルトンを使わずに、expressiveを利用して独自のフレームワークを構築する場合は、
zendframework/zend-expressive単体で利用することもできます。
スケルトンを使ってアプリケーションの処理の流れを追いながら、
少しだけライブラリを入れ替えてみましょう。
ここではfastRoute, serviceManagerを選択しています
エントリポイント public/index.php
public/index.phpでHTTPリクエストを受け取り、アプリケーションが構築されます。
/** @var \Interop\Container\ContainerInterface $container */
$container = require 'config/container.php';
/** @var \Zend\Expressive\Application $app */
$app = $container->get('Zend\Expressive\Application');
$app->run();
最初にコンテナの設定が読み込まれています。
config/container.php
use Zend\ServiceManager\Config;
use Zend\ServiceManager\ServiceManager;
// Load configuration
$config = require __DIR__ . '/config.php';
// Build container
$container = new ServiceManager(new Config($config['dependencies']));
// Inject config
$container->setService('config', $config);
return $container;
設定ファイルを読み込みます。
アプリケーションの実行に必要な設定情報やクラスの依存関係などがここで読み込まれます。
config.phpではconfig/autoload配下の設定ファイルを対象に設定値を読み込んでいます。
foreach (Glob::glob('config/autoload/{{,*.}global,{,*.}local}.php', Glob::GLOB_BRACE) as $file) {
$config = ArrayUtils::merge($config, include $file);
}
Zend\ServiceManager\ServiceManagerのインスタンスを生成し、
読み込んだ設定をconfigというサービス名でServiceManagerに登録します。
ServiceManagerインスタンス生成時にZend\ServiceManager\Configクラスの
configureServiceManagerメソッドが実行されます。
ここでは設定ファイルのそれぞれの値をコンテナが管理するための処理が行われます。
Zend\ServiceManager\Config
例えば設定値の配列に'factories'キーがあれば以下の処理が実行されます。
foreach ($this->getFactories() as $name => $factory) {
$serviceManager->setFactory($name, $factory);
}
config/autoload/dependencies.global.phpを例とすると、
'factories' => [
Application::class => ApplicationFactory::class,
// 省略
],
下記のように登録されます。
#canonicalNames: array:1 [▼
"Zend\Expressive\Application" => "zendexpressiveapplication"
]
#allowOverride: false
#invokableClasses: []
#factories: array:1 [▼
"zendexpressiveapplication" => "Zend\Expressive\Container\ApplicationFactory"
]
最後にエントリポイントでServiceManagerインスタンスを返却します。
このZend\ServiceManager\ServiceManagerクラスはInterop\Container\ContainerInterfaceを実装したクラスで、
expressiveで利用するコンテナは、
このInterop\Container\ContainerInterfaceを実装したコンテナライブラリであれば動作するようになっています。
そのため、phpdocにはServiceManagerではなくインターフェースが記述されています。
/** @var \Interop\Container\ContainerInterface $container */
$container = require 'config/container.php';
アプリケーションインスタンスの取得
index.phpにある下記の行でzend-expressiveのアプリケーションインスタンスを取得します。
$app = $container->get('Zend\Expressive\Application');
ここではInterop\Container\ContainerInterfaceを実装した
Zend\ServiceManager\ServiceManagerクラスのgetメソッドが利用されています。
getメソッドはコンテナへの登録方法によってインスタンス生成の処理が異なりますが、
ファクトリのZend\Expressive\Container\ApplicationFactory
クラスが
Zend\Expressive\Applicationクラスのインスタンスを生成して返却します。
public function __invoke(ContainerInterface $container)
{
$router = $container->has(RouterInterface::class)
? $container->get(RouterInterface::class)
: new FastRouteRouter();
$finalHandler = $container->has('Zend\Expressive\FinalHandler')
? $container->get('Zend\Expressive\FinalHandler')
: null;
$emitter = $container->has(EmitterInterface::class)
? $container->get(EmitterInterface::class)
: null;
$app = new Application($router, $container, $finalHandler, $emitter);
$this->injectPreMiddleware($app, $container);
$this->injectRoutes($app, $container);
$this->injectPostMiddleware($app, $container);
return $app;
}
アプリケーションの実行
エントリポイントのrunメソッドがアプリケーションを実行します。
public function run(ServerRequestInterface $request = null, ResponseInterface $response = null)
{
$request = $request ?: ServerRequestFactory::fromGlobals();
$response = $response ?: new Response();
$response = $this($request, $response);
$emitter = $this->getEmitter();
$emitter->emit($response);
}
このZend\Expressive\ApplicationクラスはZend\Stratigility\MiddlewarePipeクラスを拡張したもので、
フレームワーク全体がミドルウェアによって実行されるように作られています。
コンテナを変更
処理の流れをみると
Interop\Container\ContainerInterfaceを実装しているコンテナライブラリであれば、
変更できることがわかると思います。
早速 league/container に差し替えてみます。
config/container.phpを下記のように変更します。
ファクトリクラスでは __invoke()
メソッドが定義されていますので、
ここでは__invokeを実行してインスタンスを返却するようにしています。
それ以外は単純なインターフェースと具象クラスのバインドです。
$config = require __DIR__ . '/config.php';
$container = new League\Container\Container;
$container->delegate(
new League\Container\ReflectionContainer
);
$dependencies = $config['dependencies'];
foreach($dependencies['invokables'] as $abstract => $invoke) {
$container->add($abstract, $invoke);
}
foreach($dependencies['factories'] as $class => $factory) {
$container->add($class, function () use ($factory, $container) {
$factory = (new $factory);
return $factory($container);
});
}
$container->add('config', $config);
return $container;
これでアプリケーション実行の準備が整いました。
実際にコンテナを入れ替えたアプリケーションを実行してみましょう。
config/autoload/routes.global.phpに下記のルートを追記します。
'routes' => [
[
'name' => 'home',
'path' => '/',
'middleware' => App\Action\SimpleAction::class,
'allowed_methods' => ['GET'],
],
],
つぎにapp\Action\SimpleActionクラスを作成します。
<?php
declare(strict_types = 1);
namespace App\Action;
use App\Domain\MessageService;
use Psr\Http\Message\ResponseInterface;
use Zend\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ServerRequestInterface;
/**
* Class SimpleAction
*/
class SimpleAction
{
/**
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @param callable|null $next
* @return ResponseInterface
*/
public function __invoke(
ServerRequestInterface $request,
ResponseInterface $response,
callable $next = null
) : ResponseInterface
{
return new JsonResponse(['message' => 'zend-expressive']);
}
}
ブラウザなどからこのルートにアクセスしてみましょう。
jsonでレスポンスが返却されます。
最後に、このクラスで簡単なコンストラクタインジェクションを利用してみます。
<?php
declare(strict_types = 1);
namespace App\Service;
/**
* Class MessageService
*/
class MessageService
{
/**
* @return string
*/
public function get() : string
{
return 'zend-expressive';
}
}
SimpleActionクラスを少しだけ変更します。
class SimpleAction
{
/** @var MessageService */
protected $service;
/**
* SimpleAction constructor.
*
* @param MessageService $service
*/
public function __construct(MessageService $service)
{
$this->service = $service;
}
/**
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @param callable|null $next
* @return ResponseInterface
*/
public function __invoke(
ServerRequestInterface $request,
ResponseInterface $response,
callable $next = null
) : ResponseInterface
{
return new JsonResponse(['message' => $this->service->get()]);
}
}
簡単にコンストラクタインジェクションが利用できました。
PHPはコンポーネント化が進み、
フレームワークやライブラリを組み合わせて高品質なアプリケーション開発が行えるようになっています。
最初にあげたフレームワークやこのzend-expressiveのコードを読んで、
次世代フレームワークを学ぶのも非常に有益なのではないかと思います。