zend-expressiveに見るPSR-7時代のPHPフレームワーク

Zendによる新しいマイクロフレームワーク、zend-expressiveのバージョン1.0.0rc2がリリースされました。

今日は、このPSR-7対応のzend-expressiveをインストールしてみます。

zend-expressiveのインストール

最も簡単なインストール方法である、スケルトンプロジェクトをComposerでインストールする方法でインストールします。

$ composer create-project -s rc zendframework/zend-expressive-skeleton ze

ルータの選択

まず、以下のようにルータをどれにするか聞かれます。

Installing zendframework/zend-expressive-skeleton (1.0.0rc2)
  - Installing zendframework/zend-expressive-skeleton (1.0.0rc2)
    Loading from cache

Created project in ze
> App\Composer\OptionalPackages::install
Setup data and cache dir
Setting up optional packages

  Which router you want to use?
  [1] Aura.Router
  [2] FastRoute
  [3] Zend Router
  Make your selection or type a composer package name and version (FastRoute): 

デフォルトはFastRouteですが、Aura.RouterやZend Routerも選べます。

とりあえず、デフォルトのまま [return] キーを押して進みます。

  Make your selection or type a composer package name and version (FastRoute): 
  - Adding package zendframework/zend-expressive-fastroute (^0.2)
  - Copying /config/autoload/routes.global.php

zendframework/zend-expressive-fastrouteが追加されました。

DIコンテナの選択

続いてDIコンテナをどれにするか聞かれます。

  Which container you want to use for dependency injection?
  [1] Aura.Di
  [2] Pimple-interop
  [3] Zend ServiceManager
  Make your selection or type a composer package name and version (Zend ServiceManager): 

デフォルトはZend ServiceManagerですが、Aura.DiとPimple-interopが選べます。

デフォルトのまま [return] キーを押して進みます。

  Make your selection or type a composer package name and version (Zend ServiceManager): 
  - Adding package zendframework/zend-servicemanager (^2.5)
  - Adding package ocramius/proxy-manager (^1.0)
  - Copying /config/container.php

zendframework/zend-servicemanagerなどが追加されました。

テンプレートエンジンの選択

次はテンプレートエンジンの選択です。

  Which template engine you want to use?
  [1] Plates
  [2] Twig
  [3] Zend View installs Zend ServiceManager
  [n] None of the above
  Make your selection or type a composer package name and version (n): 

デフォルトはなしですが、Plates、Twig、Zend Viewが選択できます。

今回はデフォルトのなしにします。

エラーハンドラの選択

開発時に使用するエラーハンドラの選択です。

  Which error handler do you want to use during development?
  [1] Whoops
  [n] None of the above
  Make your selection or type a composer package name and version (Whoops): 

Whoopsしか選択肢はないので、Whoopsにしておきます。

  Make your selection or type a composer package name and version (Whoops): 
  - Adding package filp/whoops (^1.1)
  - Copying /config/autoload/errorhandler.local.php

filp/whoopsが追加されました。

そして、パッケージのインストールが開始されます。

インストールされたパッケージは以下でした。

$ composer show -i
composer/composer                       1.0.0-alpha10     
container-interop/container-interop     1.1.0             
doctrine/instantiator                   1.0.5             
filp/whoops                             1.1.7             
justinrainbow/json-schema               1.5.0             
nikic/fast-route                        v0.6.0            
ocramius/proxy-manager                  1.0.2             
phpdocumentor/reflection-docblock       2.0.4             
phpspec/prophecy                        v1.5.0            
phpunit/php-code-coverage               2.2.4             
phpunit/php-file-iterator               1.4.1             
phpunit/php-text-template               1.2.1             
phpunit/php-timer                       1.0.7             
phpunit/php-token-stream                1.4.8             
phpunit/phpunit                         4.8.15            
phpunit/phpunit-mock-objects            2.3.8             
psr/http-message                        1.0               
roave/security-advisories               dev-master 43ac5a3
sebastian/comparator                    1.2.0             
sebastian/diff                          1.3.0             
sebastian/environment                   1.3.2             
sebastian/exporter                      1.2.1             
sebastian/global-state                  1.1.1             
sebastian/recursion-context             1.0.1             
sebastian/version                       1.0.6             
seld/jsonlint                           1.3.1             
squizlabs/php_codesniffer               2.3.4             
symfony/console                         v2.7.5            
symfony/finder                          v2.7.5            
symfony/process                         v2.7.5            
symfony/yaml                            v2.7.5            
zendframework/zend-code                 2.5.2             
zendframework/zend-diactoros            1.1.4             
zendframework/zend-escaper              2.5.1             
zendframework/zend-eventmanager         2.5.2             
zendframework/zend-expressive           1.0.0rc2          
zendframework/zend-expressive-fastroute 0.2.0             
zendframework/zend-hydrator             1.0.0             
zendframework/zend-servicemanager       2.6.0             
zendframework/zend-stdlib               2.7.4             
zendframework/zend-stratigility         1.1.2   

このようにzend-expressiveでは、ルータ、DIコンテナ、テンプレートエンジン、開発時のエラーハンドラが最初から選択可能になっています。

このフレームワークを使っているからルータはフレームワークに付属しているものしか使えないなどというバカバカしいことはありません。

Webサーバの起動

PHPビルトインWebサーバを起動してみましょう。

$ php -S 127.0.0.1:8080 -t public/

http://localhost:8080/にブラウザでアクセスすると、以下のJSONを見ることができます。

{"welcome":"Congratulations! You have installed the zend-expressive skeleton application.","docsUrl":"zend-expressive.readthedocs.org"}

さて、この値はどこから来ているのでしょう?

ルーティング

ルーティングの設定は、以下にありました。

config/autoload/routes.global.php

<?php

return [
    'dependencies' => [
        'invokables' => [
            Zend\Expressive\Router\RouterInterface::class => Zend\Expressive\Router\FastRouteRouter::class,
        ],
    ],

    'routes' => [
        [
            'name' => 'home',
            'path' => '/',
            'middleware' => App\Action\HomePageAction::class,
            'allowed_methods' => ['GET'],
        ],
        [
            'name' => 'api.ping',
            'path' => '/api/ping',
            'middleware' => App\Action\PingAction::class,
            'allowed_methods' => ['GET'],
        ],
    ],
];

キーroutesの最初の配列

        [
            'name' => 'home',
            'path' => '/',
            'middleware' => App\Action\HomePageAction::class,
            'allowed_methods' => ['GET'],
        ],

がさきほどのJSON出力のルートです。クラス名がApp\Action\HomePageActionであることがわかります。

アクション

App\Action\HomePageActionクラスは以下にあります。

src/Action/HomePageAction.php

<?php

namespace App\Action;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Expressive\Router;
use Zend\Expressive\Template;

class HomePageAction
{
    private $router;

    private $template;

    public function __construct(Router\RouterInterface $router, Template\TemplateRendererInterface $template = null)
    {
        $this->router   = $router;
        $this->template = $template;
    }

    public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next = null)
    {
        $data = [];

        if ($this->router instanceof Router\AuraRouter) {
            $data['routerName'] = 'Aura.Router';
            $data['routerDocs'] = 'http://auraphp.com/packages/Aura.Router/';
        } elseif ($this->router instanceof Router\FastRouteRouter) {
            $data['routerName'] = 'FastRoute';
            $data['routerDocs'] = 'https://github.com/nikic/FastRoute';
        } elseif ($this->router instanceof Router\ZendRouter) {
            $data['routerName'] = 'Zend Router';
            $data['routerDocs'] = 'http://framework.zend.com/manual/current/en/modules/zend.mvc.routing.html';
        }

        if ($this->template instanceof Template\PlatesRenderer) {
            $data['templateName'] = 'Plates';
            $data['templateDocs'] = 'http://platesphp.com/';
        } elseif ($this->template instanceof Template\TwigRenderer) {
            $data['templateName'] = 'Twig';
            $data['templateDocs'] = 'http://twig.sensiolabs.org/documentation';
        } elseif ($this->template instanceof Template\ZendViewRenderer) {
            $data['templateName'] = 'Zend View';
            $data['templateDocs'] = 'http://framework.zend.com/manual/current/en/modules/zend.view.quick-start.html';
        }

        if (!$this->template) {
            return new JsonResponse([
                'welcome' => 'Congratulations! You have installed the zend-expressive skeleton application.',
                'docsUrl' => 'zend-expressive.readthedocs.org',
            ]);
        }

        return new HtmlResponse($this->template->render('app::home-page', $data));
    }
}

__invoke()メソッド内になんかごちゃごちゃif文がありますが、今はテンプレートエンジンがないので、結局以下のif文だけが実行されていることがわかります。

        if (!$this->template) {
            return new JsonResponse([
                'welcome' => 'Congratulations! You have installed the zend-expressive skeleton application.',
                'docsUrl' => 'zend-expressive.readthedocs.org',
            ]);
        }

テンプレートエンジンをインストールした方がページの見栄えがよかったみたいですね。

まとめ

PHPはPSR-7時代に突入しました。フレームワークの抽象化、コンポーネント化はさらに進みます。すでにルータやDIコンテナさえもこのように組み替え可能です。

参考

Tags: php, psr7

CodeIgniter初心者の方に知って欲しいCodeIgniterでのMVCについて

MVCとは?

『CodeIgniter徹底入門』によると?

『CodeIgniter徹底入門』(P.76)によると、コントローラ、モデル、ビューは以下のように説明されています。

  • Controller(コントローラ)は、入力データに従って適切なデータを呼び出したり、ModelやViewの連携など、アプリケーション全体の制御を行なう
  • Model(モデル)は、データベースへのアクセスやデータの修正/加工などの処理を行なう
  • View(ビュー)は、処理結果の表示など、画面表示を担当する

なんとなくわかったようなわからないような説明です。

実際には書籍にはもっといろいろな説明があるので読めば理解が進むと思いますが、コントローラとモデルの区別は実はそんなに簡単ではありません。

CodeIgniter User Guideによると?

公式ドキュメントではどのように説明されているでしょうか。

  • The Model represents your data structures. Typically your model classes will contain functions that help you retrieve, insert, and update information in your database.
  • The View is the information that is being presented to a user. A View will normally be a web page, but in CodeIgniter, a view can also be a page fragment like a header or footer. It can also be an RSS page, or any other type of “page”.
  • The Controller serves as an intermediary between the Model, the View, and any other resources needed to process the HTTP request and generate a web page.

モデルはデータの構造を表し、典型的にはデータベースの情報を処理する。

ビューはユーザに表示される情報で、通常はWebページまたはヘッダやフッタなどのページの断片。

コントローラはモデルとビューやその他のリソースの仲介者。

やはり、コントローラとモデルの区別はそんなに簡単ではありません。

何故CodeIgniterはMVC初心者に使いやすいのか?

CodeIgniterではURIに関連付けられたクラス(コントローラ)が実行されます。そのコントローラさえあれば、その中のメソッドが実行されて結果としてページが出力されます。

CodeIgniterではモデルはオプションとされており、従わないとコードが動作しないフレームワークの規約もほとんどありません。例えば、コントローラとモデルやビューファイルが規約により紐づいており、自動的に実行されるようなことは一切ありません。

なので、MVCでない既存のコードがあれば、それをコントローラに移せばだいたい動作します。つまづくことが少ないのです。

そして、HTMLをビューに分離し、データベース関連のロジックをモデルに分離すると、MVC以前のコードより見通しがよくなります。

ただし、ここでめでたしめでたしとはなりません。

何が問題か?

ファットコントローラになりやすいことです。

ファットコントローラとは長すぎるコントローラのことでアンチパターンです。何行だとファットかという基準があるわけではないですが、100行を大きく超えるようなコントローラはファットコントローラでしょう。

例えば、モデルをデータベースアクセスに限定してしまうと、残りのロジックはコントローラに記述されることになり、必然的にコントローラが長くなります。

どうすれば?

コントローラを薄くすることです。

そもそもMVCでもっとも重要なパーツはモデルです。いわゆるビジネスロジックやドメインモデルと言われるコードはモデルに記述します。

CodeIgniterでは、MVC以外に「ライブラリ」と「ヘルパー」が存在します。ここで、ヘルパーは関数を定義するものです。クラスを定義するには、ライブラリを使います。

つまり、CodeIgniterではビジネスロジックやドメインモデルを

  • application/modelsフォルダに配置する
  • application/librariesフォルダに配置する

という方法が考えられます。

ライブラリについては、CodeIgniterの標準のライブラリなどから、プロジェクトを超えて共有されるべきものというイメージを持つ人がいるかも知れませんが、それにとらわれる必要はありません。

フレームワークを超えて

また、Composerのオートローダを使いドメインモデルをもっと自由に配置するという方法も考えられます。

なお、CodeIgniter 3.0でComposerのオートローダを使うには、$config['composer_autoload']を設定する必要があります(Auto-loading Resources 参照)。

例えば、application/modelsに以下のようにドメインモデルを配置するとすると、

application/
└─ models/
     ├── Entity/
     ├── Repository/
     └── Service/

composer.jsonに以下のようにオートロードの設定を追加します。ここでは、名前空間をApp\Modelとしています。

composer.json

    "autoload": {
        "psr-4": {
            "App\\Model\\": "application/models/"
        }
    }

Composerのオートローダを更新します。

$ composer dump-autoload

モデルファイルは以下のようになり、

application/models/Repository/ProjectRepository.php

<?php

namespace App\Model\Repository;

use CI_Model;
use App\Model\Entity\Project;

class ProjectRepository extends CI_Model
{
    public function __construct()
    {
        parent::__construct();
        $this->load->database();
    }

    ...
}

コントローラは以下のようになります。

application/controllers/Project.php

<?php

use App\Model\Repository\ProjectRepository;

class Project extends MY_Controller
{
    public $repository;

    public function __construct()
    {
        parent::__construct();
        $this->repository = new ProjectRepository();
    }

    ...
}

もはや、CodeIgniterの$this->load->model()メソッドを使う必要はありません。というか使えません。しかし、フレームワーク固有のメソッドに固執する必要はありません。

上記では、application/modelsとしましたが、PSR-4に従う限り、application/domainでもapplication/fooでもどこにでも好きな場所に配置可能です。

ファットコントローラ撲滅運動

こんなスライドがありました。ご参考まで。

参考

Tags: codeigniter, mvc

PHP7でバックトレースが少し変わっていた

PHP7への移行時にここでハマる人はほとんどいないと思いますが、バックトレースの出力がPHP7では少々変更されていることがあります。

これは、PHP7では内部が色々と変更されているため、その影響です。

関数の引数値

1つは、PHP 7.0 UPGRADE NOTESに載っている「関数の引数値」です。

<?php

function foo($x)
{
    $x = 42;

    debug_print_backtrace();
}

foo("string");

この実行結果は、PHP7では以下のようになります。引数は関数内で変更された後の42になっています。

#0  foo(42) called at [/vagrant/debug_backtrace/param_value.php:10]

PHP 5.6までは、以下のように引数はstringになっていました。

#0  foo(string) called at [/vagrant/debug_backtrace/param_value.php:10]

ちょっと直感に反しますが、PHP7ではそうなっているということです。

__callStatic()

__callStatic()の場合も出力に変更があります。

<?php

class Proxy
{
    public static function __callStatic($function, array $arguments)
    {
        if (self::checkCalledMethod($function))
        {
        }
    }

    public static function checkCalledMethod($function)
    {
        debug_print_backtrace();
    }
}

class Test
{
    public function run()
    {
        Proxy::bar();
    }
}

$test = new Test();
$test->run();

PHP 5.6までは、以下のようになっていました。

#0  Proxy::checkCalledMethod(bar) called at [/vagrant/debug_backtrace/callstatic.php:7]
#1  Proxy::__callStatic(bar, Array ())
#2  Proxy::bar() called at [/vagrant/debug_backtrace/callstatic.php:22]
#3  Test->run() called at [/vagrant/debug_backtrace/callstatic.php:27]

PHP7では最適化により、Proxy::bar()の行がなくなり以下のようになっています。

#0  Proxy::checkCalledMethod(bar) called at [/vagrant/debug_backtrace/callstatic.php:7]
#1  Proxy::__callStatic(bar, Array ()) called at [/vagrant/debug_backtrace/callstatic.php:22]
#2  Test->run() called at [/vagrant/debug_backtrace/callstatic.php:27]

他にもあるかも知れませんが。

当然ですが、debug_backtrace()関数の結果も同じように変更されています。

関連

Tags: php, php7