20190327のPHPに関する記事は12件です。

PHP8でJITが使えるようになる

JITのRFCが2019/03/21に投票開始されました。
締切は2019/03/28ですが、2019/03/27時点で賛成48反対2でほぼ導入確定です。

JITとは

JIT is 何?

PHPは現在は、アクセスが来るたびにソースコードを全部読み取って、opcodeに変換して、順番に逐次実行して、実行が終了したら全てのコードを破棄するというインタプリタ型のプログラミング言語で、処理速度は遅いです。
遅いと言っても、やってる内容からすれば異常なまでに早いんですけどね。

opcodeはCPUやOSなどの実行環境によらず同一のコードが生成されます。
逐次実行するときはさらに実行環境ごとのネイティブコードに変換して実行されます。
OPcacheは、この変換後のopcodeをメモリに保存しておいて、次のリクエストでも使い回すという仕組みです。

JITはもう一段階進んだもので、リクエストが来たらソースコードを読んでopcodeにするまでは同じですが、その後一気にネイティブコードにまで変換してしまいます。
もう一度同じ処理が呼ばれたときはネイティブコードを直接実行することで、処理速度が非常に速くなります。
また、このネイティブコードをメモリに保存して使い回すこともできるようになります。

ただし、ネイティブコードはCPUが直接実行するコードなので、CPUの種類や世代によって異なるものとなります。
実行環境によって異なるネイティブコードを作らないといけないので、大規模な改修が必要で、コードも複雑になってたいへんです。
どのくらい大規模かというと、900コミット5万行の追加という目眩のする量です。

要するにどういうこと?

OPcacheのすごいやつ。

JIT RFC

Introduction

元々PHP7でJITを実装しようと企んでいて、2011年からZendが(ほとんどはDmitryが)色々と試していたんだけど、諸々の理由で結局PHP7に入ることはありませんでした。
理由はというと、当時の方法ではたいして早くならなかったこと、そのわりに複雑だったこと、そしてJIT以外にも多くのパフォーマンス向上技術の投入があったことです。

実際PHP7は、処理時間がPHP5の半分以下になるという驚異的な高速化がなされています。
当時はそれらの高速化に注力したため、JITの導入は見送られました。

The Case for JIT Today

現在のPHPにおけるJITの導入には、以下のようなメリットが見込まれます。

まず、JIT以外の最適化戦略による高速化は、そろそろ限界に達しつつあります。
つまり、JITを使わないかぎり、これ以上の高速化は見込めません。

次に、JITを導入することによって、Web以外のCPUを多用するような処理をPHPで書く、という選択肢が有力なものになります。

最後に、C言語ではなく、もしくはC言語のかわりに、PHPで組み込み関数を開発することが可能になります。
現在のPHPでそのような戦略を採るには大きな壁となっている、パフォーマンス劣化という問題の影響をほとんど受けなくなります。
さらにPHPベースで開発すれば、C言語ベースでの開発では往々にして発生するメモリ管理、オーバーフローといった問題を、言語レベルで安全にしてくれます。

Proposal

PHP8でJITを提供します。

PHP JITはOPcacheの一部として、しかしほぼ独立したものとして実装されます。
PHPのコンパイル時に有効無効を設定します。
有効にした場合、PHPファイルのネイティブコードがOPCacheの共有メモリに保存されるようになります。

ネイティブコードの生成にはDynAsmを使用します。
これはLuaJITプロジェクトで開発された、非常に軽量で高度なツールです。
しかし同時に、ターゲットのアセンブラ言語に対する高レベルの知識を要求します。
かつてLLVMを試してみたものの、コード生成速度は100倍遅く使い物になりませんでした。
DynAsmはPOSIXとWindows上でのx86とx86_64、およびARMをサポートしています。
従って、現在のPHPがサポートしている一般的なプラットフォーム全てに対応できるはずです。
努力すれば。

あと、ここで一部の内部実装について触れてるのですがよくわかりませんでした。
additional IR formに対応してないとか、opcacheオプティマイザのSSA静的解析フレームワークがネイティブコードを生成してるよとか、解析後long型になったらメモリじゃなくてCPUレジスタに直接登録するよとか、PHP JITのレジスタ割り付けアルゴリズムはすごいよとか、なんかそんなことが書いてあったりするようなないような。 
 ※詳細は@sj-i氏のコメント参照

パフォーマンス

以下の関数のベンチマークが公開されています。

function iterate($x,$y){
    $cr = $y-0.5;
    $ci = $x;
    $zr = 0.0;
    $zi = 0.0;
    $i = 0;
    while (true) {
        $i++;
        $temp = $zr * $zi;
        $zr2 = $zr * $zr;
        $zi2 = $zi * $zi;
        $zr = $zr2 - $zi2 + $cr;
        $zi = $temp + $temp + $ci;
        if ($zi2 + $zr2 > BAILOUT)
            return $i;
        if ($i > MAX_ITERATIONS)
            return 0;
    }
}

実行結果は以下のとおり。

環境 実行時間
PHP7-JIT (JIT=on) 0.011
gcc -O2 (4.9.2) 0.013
LuaJIT-2.0.3 (JIT=on) 0.014
gcc -O0 (4.9.2) 0.022
HHVM-3.5.0 (JIT=on) 0.030
Java-1.8.0 (JIT=on) 0.059
LuaJIT-2.0.3 (JIT=off) 0.073
Java-1.8.0 (JIT=off) 0.251
PHP-7 0.281
squirrel-3.0.4 0.335
Lua-5.2.2 0.339
PHP-5.6 0.379
PHP-5.5 0.383
PHP-5.4 0.406
ruby-2.1.5 0.684
PHP-5.3 0.855
HHVM-3.5.0 (JIT=off) 0.978
PHP-5.2 1.096
python-2.7.8 1.128
PHP-5.1 1.217
perl-5.18.4 2.083
PHP-4.4 4.209
PHP-5.0 4.434

バージョンは不明ですが素のPHP7の20倍以上、HHVMの3倍、そしてgccやLuaJITより速い。
さすがに何か間違ってるんじゃないか?と目を疑いたくなる結果です。

なお、このベンチは4年前のものです。
RFCによるとJITなしのPHP7.4では0.046秒ということでした。
PHP7.4では0.046秒で、PHP7では0.281秒って、既にこの時点でおかしい気がするぞ?
単に実行環境の違いでしょうか。

あと、よく見ると、PHPだけob_start/ob_end_flushを使って出力を抑制してるのが気になりますね。
PHP同士での比較には影響ありませんが、他言語との比較はフェアでないと思われます。

ちなみにGoで並列処理したら0.002秒だったそうです。はえー。

実際に出力されたネイティブコード
JIT$Mandelbrot::iterate: ; (/home/dmitry/php/bench/b.php)
    sub $0x10, %esp
    cmp $0x1, 0x1c(%esi)
    jb .L14
    jmp .L1
.ENTRY1:
    sub $0x10, %esp
.L1:
    cmp $0x2, 0x1c(%esi)
    jb .L15
    mov $0xec3800f0, %edi
    jmp .L2
.ENTRY2:
    sub $0x10, %esp
.L2:
    cmp $0x5, 0x48(%esi)
    jnz .L16
    vmovsd 0x40(%esi), %xmm1
    vsubsd 0xec380068, %xmm1, %xmm1
.L3:
    mov 0x30(%esi), %eax
    mov 0x34(%esi), %edx
    mov %eax, 0x60(%esi)
    mov %edx, 0x64(%esi)
    mov 0x38(%esi), %edx
    mov %edx, 0x68(%esi)
    test $0x1, %dh
    jz .L4
    add $0x1, (%eax)
.L4:
    vxorps %xmm2, %xmm2, %xmm2
    vxorps %xmm3, %xmm3, %xmm3
    xor %edx, %edx
.L5:
    cmp $0x0, EG(vm_interrupt)
    jnz .L18
    add $0x1, %edx
    vmulsd %xmm3, %xmm2, %xmm4
    vmulsd %xmm2, %xmm2, %xmm5
    vmulsd %xmm3, %xmm3, %xmm6
    vsubsd %xmm6, %xmm5, %xmm7
    vaddsd %xmm7, %xmm1, %xmm2
    vaddsd %xmm4, %xmm4, %xmm4
    cmp $0x5, 0x68(%esi)
    jnz .L19
    vaddsd 0x60(%esi), %xmm4, %xmm3
.L6:
    vaddsd %xmm5, %xmm6, %xmm6
    vucomisd 0xec3800a8, %xmm6
    jp .L13
    jbe .L13
    mov 0x8(%esi), %ecx
    test %ecx, %ecx
    jz .L7
    mov %edx, (%ecx)
    mov $0x4, 0x8(%ecx)
.L7:
    test $0x1, 0x39(%esi)
    jnz .L21
.L8:
    test $0x1, 0x49(%esi)
    jnz .L23
.L9:
    test $0x1, 0x69(%esi)
    jnz .L25
.L10:
    movzx 0x1a(%esi), %ecx
    test $0x496, %ecx
    jnz JIT$$leave_function
    mov 0x20(%esi), %eax
    mov %eax, EG(current_execute_data)
    test $0x40, %ecx
    jz .L12
    mov 0x10(%esi), %eax
    sub $0x1, (%eax)
    jnz .L11
    mov %eax, %ecx
    call zend_objects_store_del
    jmp .L12
.L11:
    mov 0x4(%eax), %ecx
    and $0xfffffc10, %ecx
    cmp $0x10, %ecx
    jnz .L12
    mov %eax, %ecx
    call gc_possible_root
.L12:
    mov %esi, EG(vm_stack_top)
    mov 0x20(%esi), %esi
    cmp $0x0, EG(exception)
    mov (%esi), %edi
    jnz JIT$$leave_throw
    add $0x1c, %edi
    add $0x10, %esp
    jmp (%edi)
.L13:
    cmp $0x3e8, %edx
    jle .L5
    mov 0x8(%esi), %ecx
    test %ecx, %ecx
    jz .L7
    mov $0x0, (%ecx)
    mov $0x4, 0x8(%ecx)
    jmp .L7
.L14:
    mov %edi, (%esi)
    mov %esi, %ecx
    call zend_missing_arg_error
    jmp JIT$$exception_handler
.L15:
    mov %edi, (%esi)
    mov %esi, %ecx
    call zend_missing_arg_error
    jmp JIT$$exception_handler
.L16:
    cmp $0x4, 0x48(%esi)
    jnz .L17
    vcvtsi2sd 0x40(%esi), %xmm1, %xmm1
    vsubsd 0xec380068, %xmm1, %xmm1
    jmp .L3
.L17:
    mov %edi, (%esi)
    lea 0x50(%esi), %ecx
    lea 0x40(%esi), %edx
    sub $0xc, %esp
    push $0xec380068
    call sub_function
    add $0xc, %esp
    cmp $0x0, EG(exception)
    jnz JIT$$exception_handler
    vmovsd 0x50(%esi), %xmm1
    jmp .L3
.L18:
    mov $0xec38017c, %edi
    jmp JIT$$interrupt_handler
.L19:
    cmp $0x4, 0x68(%esi)
    jnz .L20
    vcvtsi2sd 0x60(%esi), %xmm3, %xmm3
    vaddsd %xmm4, %xmm3, %xmm3
    jmp .L6
.L20:
    mov $0xec380240, (%esi)
    lea 0x80(%esi), %ecx
    vmovsd %xmm4, 0xe0(%esi)
    mov $0x5, 0xe8(%esi)
    lea 0xe0(%esi), %edx
    sub $0xc, %esp
    lea 0x60(%esi), %eax
    push %eax
    call add_function
    add $0xc, %esp
    cmp $0x0, EG(exception)
    jnz JIT$$exception_handler
    vmovsd 0x80(%esi), %xmm3
    jmp .L6
.L21:
    mov 0x30(%esi), %ecx
    sub $0x1, (%ecx)
    jnz .L22
    mov $0x1, 0x38(%esi)
    mov $0xec3802b0, (%esi)
    call rc_dtor_func
    jmp .L8
.L22:
    mov 0x4(%ecx), %eax
    and $0xfffffc10, %eax
    cmp $0x10, %eax
    jnz .L8
    call gc_possible_root
    jmp .L8
.L23:
    mov 0x40(%esi), %ecx
    sub $0x1, (%ecx)
    jnz .L24
    mov $0x1, 0x48(%esi)
    mov $0xec3802b0, (%esi)
    call rc_dtor_func
    jmp .L9
.L24:
    mov 0x4(%ecx), %eax
    and $0xfffffc10, %eax
    cmp $0x10, %eax
    jnz .L9
    call gc_possible_root
    jmp .L9
.L25:
    mov 0x60(%esi), %ecx
    sub $0x1, (%ecx)
    jnz .L26
    mov $0x1, 0x68(%esi)
    mov $0xec3802b0, (%esi)
    call rc_dtor_func
    jmp .L10
.L26:
    mov 0x4(%ecx), %eax
    and $0xfffffc10, %eax
    cmp $0x10, %eax
    jnz .L10
    call gc_possible_root
    jmp .L10

後方互換性

互換性の壊れる変更はありません。

その他の影響

エクステンション

Xdebugのようなデバッガ、XHProf、Blackfire、Tidewaysといったプロファイラに影響が発生します。

Opcache

JITはOpcacheの一機能として実装されます。

追加される定数

追加される定数はありません。

デバッグ

JITのデバッグはとっても大変だよ!がんばれ!

php.ini

php.iniに複数の項目が追加されます。

opcache.jit_buffer_size

ネイティブコードのために予約するメモリサイズ。バイト単位で、K・Mの表記に対応。
デフォルトは0で、JIT無効という意味。

opcache.jit

JITの制御オプション。順番にCRTOを表し、デフォルトは"1205"。
おそらく"1235"に変更した方がいいかもしれない。

C

CPU最適化レベル、範囲は0-1。
0は使用しない、1はAVX命令セットを有効にする。

R

レジスタ割り当て、範囲は0-2。
0はレジスタ割り当てを使用しない、1はローカルレジスタ割り付け、2はグローバルレジスタ割り付け。

T

JITを起動するタイミング、範囲は0-5。
0は最初のスクリプト起動時に全機能を有効にする。
1は最初の処理実行時にJITを有効にする。
2は最初のリクエストでプロファイルを行い、2回目のリクエストでコンパイルする。
3はオンザフライでプロファイル、コンパイルを行う。
4は@jitってコメントが書いてある関数をコンパイルする。

O

最適化レベル、範囲は0-5。
0はJITを使わない、5が最も高度な最適化を行う。

opcache.jit_debug

JITデバッグ制御オプション。
デフォルトは0。
それぞれビット指定することで各種デバッグ情報を出力できるみたいですが、具体的に何が何なのかはよくわかりませんでした。
SSA formとかperf.mapとかJIt-ed codeとか出せるらしい。

パフォーマンス

bench.phpが0.320秒から0.140秒になりました。
CPUを多用する処理については、劇的な高速化が見込めます。
またNikitaによると、PHP-Parserが1.3倍速くなりました。

しかしながら、WordPressのようなWebアプリについては、さほど恩恵は見込めません。
315req/秒から326req/秒になった程度のようです。
このような現実的アプリについても高速化の改善を行う、追加の取り組みを実施予定です。

今後の展望

関数のプロファイリング後に最適化されたコードを生成することで、JITの改善を行う予定です。
またプリローディングやFFIとのより深い統合を行うことができるでしょう。
CではなくPHPで書かれた組み込み関数を提供する方法の標準化も見込めます。

投票

2019/03/21に投票開始、2019/03/28に投票終了。
可決には投票者の2/3+1の賛成が必要です。

PHP7.4

PHP7.4には入りませんでした。
ブランチもできていたのですが、7.4への導入は賛成18反対34で却下されました。
7.4はただでさえ新機能盛り盛りで大変ですからね。
このうえさらに5万行の追加とか、さすがに厳しいでしょう。

外部リンク

プルリクエスト / JITブランチ

コミット数900、追加5万行を超える非常に大規模なプルリクです。
こんなマージ作業、自分じゃ絶対やりたくない。

PHP7.4ブランチ

PHP7.4用のJITブランチ。
こちらが使われることはなさそうです。

DynASM / 非公式DynASMドキュメント

JITで使われるライブラリです。

[RFC] [VOTE] JIT

みんな『7.4は無理、8だけにすべき』というかんじです。
これだけ大規模な改修にもかかわらず、スレッドが意外と伸びてないのは、もはや対象バージョン以外語ることがないくらい予定調和だからでしょうか。

感想

普通にWebアプリを作っているかぎりにおいては、さしたる影響はないようです。
JITが真価を表すのは、バッチなどバックエンド処理においてでしょう。

一昔前であれば『PHPでバッチ?正気か!?』というイメージでしたが、今後はもはや下手な言語で書くよりPHPのほうが速い、までありそうですね。

なお、opcacheやそもそもJITについての理解があやふやなので、間違っている部分が多々あると思われます。
きっと誰かがプルリクしてくれるはず。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Phalcon HelloWorld

概要

MacでPHP 7.3のインストール 〜 curl localhostで適当プロジェクトにアクセスするまで

ver

  • Phalcon v3.4.2
  • PHP v7.3

Install PHP

brew install php@7.3
brew services start php # php-fpmが開始される
php -v # 7.3が表示されるはず

Install composer

brew install composer

Phalcon インストールする必要があるもの

  • phalcon.so
    • phalcon用のPHP Extension。Phalcon本体はCで動いている
  • phalcon-devtools
    • phalcon コマンドのために必要

Install phalcon.so

brew install php72-phalcon
ls /usr/local/Cellar/php72-phalcon/3.4.2/phalcon.so # 存在するはず
echo "extension=phalcon.so" > /usr/local/etc/php/7.3/conf.d/phalcon.ini
brew services restart php

Install phalcon-devtools

今回はcomposer経由でインストールする

mkdir ~/phalcon
cd ~/phalcon
vim composer.json # 中身は下記
composer install
ls ~/phalcon/vendor/phalcon/devtools/phalcon.php # 存在確認
echo "alias phalcon ~/phalcon/vendor/phalcon/devtools/phalcon.php" >> ~/.zshrc # zshを使っているという前提で・・
source ~/.zshrc
phalcon # 疎通テスト
composer.json
{
    "require": {
        "phalcon/devtools": "dev-master"
    }
}

Phalconでプロジェクト作成

mkdir /tmp/test # 適当にディレクトリ作成
cd /tmp/test
phalcon project mypj

nginxの設定

vim /usr/local/nginx/conf/nginx.conf # 中身は下記
nginx -s reload
nginx.conf
...略

    server {
        listen       10000;
        server_name  localhost;

        location ~ \.php$ {
            root   /tmp/test/mypj/public;

            fastcgi_split_path_info ^(.+\.php)(/.+)$;
            fastcgi_pass localhost:9000;
            fastcgi_index index.php;
            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_param PATH_INFO $fastcgi_path_info;
        }
    }

(...実際すごい適当)

アクセステスト

curl localhost:10000/index.php
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【CraftCMS】管理画面のソート機能拡張

タイトルと投稿日以外でソートしたい

サンプルソースに準じてみたけど動かなかったので色々検証してついに実現。(朝4時半)

Modules.php
namespace modules;

use craft\elements\Entry;
use craft\events\RegisterElementSortOptionsEvent;
use yii\base\Event;

class Module extends \yii\base\Module
{
    public function init()
    {
        parent::init();

        //↓ここに書いてあることまんまだけどどこに組み込めばいいか分からず...
        // https://github.com/craftcms/cms/issues/2818
        // https://docs.craftcms.com/v3/extend/updating-plugins.html#plugin-hooks
        Event::on(Entry::class, Entry::EVENT_REGISTER_SORT_OPTIONS, function (RegisterElementSortOptionsEvent $event) {
            $event->sortOptions[] = [
                'orderBy' => 'field_XXXXXX',
                'label' => "セレクタで表示したいラベル",
                'attribute' => 'field:XXX',
            ];

            $event->sortOptions[] = [
                'orderBy' => 'field_teriyaki',
                'label' => "TERIYAKI",
                'attribute' => 'field:54',
            ];
        });
    }
}

要素解説

key 役割
orderBy ソートのキーにしたいfieldを指定。
「field_」 + ハンドル名の形式で記述
label ソートセレクタに表示する名称。何でもOK。
Craft::t()で翻訳も可。
attribute 対象ハンドルのfield_id。
「field:」 + id の形式で記述。

これを CRAFT_PATH/modules/Module.phpへ組み込み
(今回は他にモジュール使ってないのでそのまま上書き)

さらにCRAFT_PATH/config/app.phpを↓のようにする

config/app.php
return [
    'modules' => [
        'control-panel' => \modules\Module::class,
    ],
    'bootstrap' => ['control-panel'],
];

おわり

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

twig templateで、外部のcssやjs、htmlやテキスト文書の内容を、テンプレ側で読み込む

twigには別のテンプレートファイルを埋め込む仕組みが用意されており、{% include('template path name') %} のように書けば、外部テンプレートの内容を埋め込むことができます。

が、取り込めるのは PHP側で指定したtwigテンプレートファイルの基準ディレクトリ配下のファイルに限られるようです。

今回は、.twig内に、テンプレートファイル基準ディレクトリの外側にあるファイルの内容を埋め込むのに、僕がとった方法の紹介です。

さて、twigには、filter という仕組みがあり、これを使うと、phpから渡ってきた変数やべた書きした文字列を twig template内で加工することができます。

標準でも、以下のような様々なフィルターが用意されています。

sample.twig
{# 配列の長さを出力する #}
{{valArray|length}}

{# html escapeせずに値をそのまま出力する #}
{{htmlSource|raw}}

{# PHPのnl2br関数と同等 #}
{{"今日は\n良い天気ですね\n"|nl2br}}

さらに、独自フィルターも簡単に追加できるようになっています。

filter の本来想定された使い方からは逸脱してしまうような気もしますが、今回は、.twig ファイル内に書かれたファイルパス名をファイルの内容に置き換えるようなフィルターを書いてみました。

// 自作フィルターを集めた class
// global function でもいいけど、classにまとめといたほうが煩雑にならないと思う
class TwigCustomFilter {
    // $fileをファイルのパス名とみなし、ファイルの中身を返却する
    public function twig_embed($file) {
        if (!empty($file) && 
            file_exists($file) && 
            is_file($file)) {
            return file_get_contents($file);
        } else {
            return '';
        }
    }
}


$loader = new Twig_Loader_Filesystem($templateDir);
$twig = new Twig_Environment($loader, ['debug' => false]);

// フィルターの追加
$twigCustomFilter = new TwigCustomFilter();

// embed というフィルターが .twigに書かれていたら  $twigCustomFilter->twig_embed($file) を呼び出す
$filter = new Twig_SimpleFilter('embed', [$twigCustomFilter, 'twig_embed']);
$twig->addFilter($filter);

$template = $twig->loadTemplate($templateFileName);
$template->display(['documentRootPath' => $_SERVER['DOCUMENT_ROOT']);
example.twig
<script>
{# スクリプトの内容を(htmlエスケープせずに)そのまま埋め込む #}
{{documentRootPath~'/assets/js/common.js'|embed|raw}}
</script>

{# テキストファイルの内容を htmlエスケープして埋め込む #}
{{documentRootPath~'/resource/license.txt'|embed}}

補足
twigの文字列連結演算子は、PHPの書き方とは異なっています。
PHPでは 「 . 」ですが、 twigの場合「 ~ 」になります。

フィルターはパイプ同様に数珠つなぎにすることができます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LaravelからSlackのメッセージを改行させる

文字列をダブルクォートで囲って改行文字を使うと改行されます。

<?php

use Illuminate\Notifications\Messages\SlackMessage;

class CustomSlackMessage extends SlackMessage
{
    public function __construct() {
        $this->content = "1行目\n"
            . "2行目\n"
            . "3行目";
    }
}

ちなみにシングルクォートだと改行されません。

ソースはgithubのコメント https://github.com/cleentfaar/slack/issues/21 から

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

php-master-changes 2019-03-26

今日は interbase のバグ修正、extract() で SEGV が出る場合がある問題の修正、FTP ストリームへの再帰的な mkdir() が正しく動作しなかった問題の修正、ドキュメントの更新、FFI のパーサ再生成、組み込み Web サーバの Date ヘッダフォーマットが間違っていた問題の修正、PCRE で CLI でのキャッシュの扱い修正、内部 API zend_error_at の追加があった!

2019-03-26

nikic: Fixed bug #72175

nikic: Fixed bug #77793

vtemian: Fix bug #77680: Correctly implement recursive mkdir on FTP stream

derickr: Update README.RELEASE_PROCESS

dstogov: Regenerate parser

kelunik: Fix #77794: Incorrect Date header format in built-in server

petk: [ci skip] Update NEWS

petk: [ci skip] Join contributing and patches docs

nikic: Make PCRE cache per-request on CLI

Hywan: Fix typos in the documentation

nikic: Add zend_error_at API that accepts a filename and lineno

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HomebrewでPHP7.2をインストールする

Homebrewのインストール

コマンドラインを開いて下記を実行する。

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

実行できなければ、下記の公式ページを開いてコマンドを確認してください。
https://brew.sh/index_ja

PHP7.2をインストール

$ brew search php72

==> Formulae
php@7.2

パッケージ名がわかったので、インストールを実行

$ brew install php@7.2

インストールが終わったらパスを通します

$ echo 'export PATH="/usr/local/opt/php@7.2/bin:$PATH"' >> ~/.bash_profile
$ echo 'export PATH="/usr/local/opt/php@7.2/sbin:$PATH"' >> ~/.bash_profile
$ source ~/.bash_profile
$ php -v

PHP 7.2.16 (cli) (built: Mar 22 2019 08:49:28) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.2.16, Copyright (c) 1999-2018, by Zend Technologies

以上になります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初期構築からSymfony4を動かすまでのメモ

はじめに

個人的にメモしてたやつだけど、もし使う人が居たらってことで構築メモを落としておく。
だけどいまはLalabel(スペルあってる?)が凄いから、こんなの出してもってのもある(汗)

導入と初期設定

導入環境はWin10のWSL。WSLじゃなくてままのUbuntuでも。
Apache2とphpはXAMPPじゃなくて、個別にモジュール追加でインストール。

Apache2 and PHP install and Composer Setup

サーバー環境はUbuntuなのでaptで追加していく。
導入後にphp.iniも忘れずにしておく。

sudo apt install curl zip git vim apache2 php7.2 mysql-server php7.2-cli php7.2-curl php7.2-intl php7.2-mbstring php7.2-xml php7.2-mysql libapache2-mod-php7.2

php -r "readfile('https://getcomposer.org/installer');" | php

sudo mv composer.phar /usr/local/bin/composer


composer高速化

Composerのパッケージダウンロードを並列化するプラグインPrestissimoを導入する。依存関係のパッケージが並列にダウンロードされるので、SymfonyやLaravelなどパッケージ数の多いフレームワークをComposerで利用する時には相当な時間短縮になる。

もうこれ入れないとたまに入れ直しでイライラする。必需品。

composer global require hirak/prestissimo

Apache2 Setup

Apacheの設定と導入したPHPのモジュール有効化からRootディレクトリの設定。そして有効化の再起動をしていく。

apache2ctl -M
sudo /usr/sbin/a2enmod rewrite
sudo vim /etc/apache2/apache2.conf
sudo vim /etc/apache2/sites-available/000-default.conf
sudo vim /etc/apache2/sites-enabled/000-default.conf
sudo service apache2 restart

Git Setup

一応でコンソールにて済ませておく。上から
1. Githubの登録ID
2. 同じく今度はメールアドレス
3. カラーパターンの設定
4. git checkoutの短縮
5. git commitの短縮
6. git statusの短縮
7. git branchの短縮
8. 勝手に改行コードを変えない為の設定

git config --global user.name "GithubID"
git config --global user.email "GithubMail"
git config --global color.ui auto
git config --global alias.co checkout
git config --global alias.ci commit
git config --global alias.st status
git config --global alias.br branch
git config --global alias.hist 'log --pretty=format:\"%h %ad | %s%d [%an]\" --graph --date=short'


インストール(v4)

昔はsymfony newとかで入れてた気がしたけど、今はcomposerでらくちん導入。

composer create-project symfony/skeleton project-name

Symphonyで使える便利なサーバの追加

昔はデフォで入ってたはずだけどv4から消えて、軽くなった感じ。確かに中身を見るとだいぶすっきりした印象がある。そしてインストールはいつものcomposer require serverでらくちん。

※サーバー起動(bin/console server:run)したが、この時点ではデフォルトルートがないよと怒られた。何もないから当然。

作成を便利に作成するやつ

CakePHPで言うならbakeがsymfonyにもあるらしい。あるのとないのじゃかなり変わってくるから、かなり助かる(触ってたのはv2だから...)。

composer require maker

コントローラー作成の助け

正式名称はannotationの省略形。

SensioFrameworkExtraBundleをインストールします。SymfonyMakerBundleは、コントローラーを作成するとき、ルーティング定義にアノテーションを使います。アノテーションによるルーティングの制御は、SensioFrameworkExtraBundleが提供する機能です。

うん、よくわからないけど便利にしてくれるみたい。

composer require annot

デバッグツール

お馴染みの大好きデバッグツールで、これがないと開発がつらい。そしてテストもできるイケメン。

composer require debug-pack

いつも使ってるテンプレートエンジン追加(v2)

色々使って、結局落ち着くTwig。Laravel bladeもかなり良いと聞くけど(Laravelの使用率と検索率を見ながら)、なんとなく自分には合わなかった。地味にSmartyも好きだったりする。

composer require twig


作って動かす

コントローラーの作成とビューの作成。

コントローラー作成

コマンドでらくちん。

(ROOT)/bin/console make:controller DefaultController

動かしてみる

前はココで色々と変更が必要だったけどmakeする前にtwigが入ってると自動で認識して、設定してくれるようになったみたい。便利。

(ROOT)/bin/console server:run


メモツールつくる

動きはなんとなく分かったから、メモツールを作ってみる。

とりあえずDemoを拡張して、更に感覚を

自分が入れたのはsymfony/skeletonで、名前の通り最低限のもの(そりゃ軽い)。なのでsymfony/symfony-demoでFull-Verを使って弄る。

composer create-project symfony/skeleton-demo symfony-demo

一応起動確認

php bin/console server:runでlocalhost:8000へ。すると日本語化されたDemoが。コレはありがたい...。

早速弄る

日本語になっているのは、どうやら言語ファイルがあるらしく、そこからロードして出してる様子。それにしてもSymfonyProfilerが有能過ぎて惚れる...。

Controller

Controllerはココに入ってた。

(ROOT)/src/controller

Lang

翻訳するには下記ファイルに書き足すことで翻訳追加が出来る。これによりグローバル化がかなり簡単であり、更にはSymfonyProfilerで抜けている翻訳があれば警告でお知らせをしてくれる親切機能。

(ROOT)/translations/messages.ja.xlf

DataBase

どうやらDemoはSQLiteを使ってデータを保存している。だけどそのやり取りをしているであろうModelが何処なのか、この時点では見当たらない。

とりあえず設定箇所はsymfonyのROOTに.envがあるのでこれで指定する。
.env.dist
DATABASE_URL=sqlite:///%kernel.project_dir%/var/data/blog.sqlite

設定後にphp bin/console doctrine:database:createで.env.distの内容を読み取って作成してくれる。この辺はLaravelとほぼ同じ。

Databaseを作る

php bin/console make:entity (DBNAME)にてsrc/Entity/(DBNAME).phpが出来るので、これを弄る。

でModelはどこよ

どうやらControllerでもDBの操作が可能みたいだけどskeleton-demoはsrc/Repository/にてDBの操作をしていた。

ん、と思ってControllerに戻るとuse App\Repository\PostRepository;の記述が、コレで呼んでたのか...。それを踏まえて読むとpublic function index(int $page, string $_format, PostRepository $posts): Responseと確かにPostRepositoryを呼んでる記述。なにこれ初見でこんなん気付けないって...。

じゃあViewは?

Controllerにて普通に呼び出されてた。ちなみに下記の$_formatはURLをそのままファイル名で呼んでいるので、そのための記述。

return $this->render('blog/index.'.$_format.'.twig', ['posts' => $latestPosts]);


まとめ

Slim3やCodeigniter3をメインとして使っていたが、慣れれば結構Symfonyも扱いやすいのではと、調べてみるとそう思った。あと割と色々と勉強で様々のフレームワークを触れてたので、意外とどうにかなった(CakePHP3 FuelPHP Laravel Lumen(Laravelのマイクロ版))。暇があればゴリッと何か作ってもいいかもと思いました(・ω・)
ただ、やはりSymfonyはYahooが採用しただけあって結構お硬いフレームワークなので学習コストが高そうとも思いました...。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

bladeのcomponent化による再利用

最近 blade を使ったhtmlのコンポーネント化について調べたのでまとめます。
Laravel公式のbladeテンプレートのセクションにも記載があり、実用できそうでした。

サンプル

コンポーネントを/resources/views/components/attribute.blade.phpから呼び出すサンプルです。

resources/views/sample.blade.php

<div class="attribute-wrapper">
    @component('components.attribute',
                ['attributeName'=>__("氏名"),
                    'required'=>true,
                    'attributeInfo'=> __("名前を入力してください")])
    @endcomponent
</div>

/resources/views/components/attribute.blade.php

<div class="attribute-name">
    <span>{{ $attributeName }}</span>
    <div class="d-flex justify-content-between position-relative">
        @if($required)
        <div class="required ">{{ __('必須') }}</div>
        @endif
        <i class="fas fa-info-circle attribute-info-mark"></i>
        <p class="attribute-info">{{ $attributeInfo }}</p>

    </div>
</div>

cssとhtmlの記述をbladeで完結させたい

コンポーネント化をすすめていく中で、cssとhtmlの記述をbladeで完結させたいと思いつきました。
調べたところ@ push ディレクティブと、@ stackディレクティブを使うと
pushしたテンプレートをstackで取り出すことができるようです。

スタック
Bladeはさらに、他のビューやレイアウトでレンダできるように、名前付きのスタックへ内容を退避できます。子ビューで必要なJavaScriptを指定する場合に、便利です。

@push('scripts')
    <script src="/example.js"></script>
@endpush

必要なだけ何回もスタックをプッシュできます。スタックした内容をレンダするには、@stackディレクティブにスタック名を指定してください。

<head>
    <!-- Headの内容 -->

    @stack('scripts')
</head>

laravel5.8公式ドキュメント(https://readouble.com/laravel/5.8/ja/blade.html)

コンポーネントのcssが複数回呼び出される

stackに登録したcssを呼び出すと、コンポーネントが複数回呼び出されている場合、複数の同じcssが登録されてしまいました。
解決方法として、Bladeファサードを使い、同一コンポーネントからの同じ要素のpushを防ぐ独自のディレクティブ pushonceを作成することで解決する方法を記載します。

1. pushonceディレクティブの登録

App\Providers\AppServiceProvider

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //
        \Blade::directive('pushonce', function ($expression) {
            $var = '$__env->{"__pushonce_" . md5(__FILE__ . ":" . __LINE__)}';

            return "<?php if(!isset({$var})): {$var} = true; \$__env->startPush({$expression}); ?>";
        });

        \Blade::directive('endpushonce', function ($expression) {
            return '<?php $__env->stopPush(); endif; ?>';
        });
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}

2. component毎にcssをpushする

resources\views\components\operations.blade.php

div class="operations-container status-bar">
    {{ $slot }}
</div>

@pushonce('css')
    <style>
        .operations-container{
            display: flex;
            /* justify-content: space-evenly; */
            flex-flow: row wrap;
            align-content: space-around;
        }
    </style>
@endpushonce

pushonceの呼び出し方は、pushと全く同じです。

3. stackで css を呼び出す。

resources\views\layouts\app.blade.php

    <body>
       <main id="app">
            <div class="main-container">
                @yield('content')  
            </div>
        </main>
    </body>
    @stack('css') 

stackは通常の通りの呼び出しです。
これでコンポーネントのcssの重複呼び出しはなくなりました。

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP のフィルタ型 FILTER_SANITIZE_STRING、FILTER_SANITIZE_SPECIAL_CHARS、FILTER_SANITIZE_FULL_SPECIAL_CHARS の違い

PHP で値を検証したいとき、filter 関数(filter_var や filter_input 関数)を使用すると簡単に値を検証できます。この記事は、filter 系関数に指定するフィルタ型についてのメモです。

FILTER_SANITIZE_STRING、FILTER_SANITIZE_SPECIAL_CHARS、FILTER_SANITIZE_FULL_SPECIAL_CHARS の違い

覚えてしまえば簡単だとは思いますが、私は覚えが悪いのでメモです(アウトプットすると覚えられる気がする)。

以下のコードの出力結果は…

<?php
$var = '<script>alert("XSS");</script>';

var_dump(filter_var($var, FILTER_SANITIZE_STRING));
var_dump(filter_var($var, FILTER_SANITIZE_SPECIAL_CHARS));
var_dump(filter_var($var, FILTER_SANITIZE_FULL_SPECIAL_CHARS));

このようになります。

string(21) "alert(&#34;XSS&#34;);"
string(54) "&#60;script&#62;alert(&#34;XSS&#34;);&#60;/script&#62;"
string(52) "&lt;script&gt;alert(&quot;XSS&quot;);&lt;/script&gt;"

結果から分かるように、違いは以下のようになります。

ID 説明
FILTER_SANITIZE_STRING タグを取り除く。オプションで、 特殊文字を取り除いたりエンコードしたりする
FILTER_SANITIZE_SPECIAL_CHARS '"<>& および ASCII 値が 32 未満の文字を HTML エスケープする。オプションで、 特殊文字を取り除いたりエンコードしたりする
FILTER_SANITIZE_FULL_SPECIAL_CHARS htmlspecialchars() に ENT_QUOTES を指定してコールするのと同じ

より詳しい仕様などは PHP マニュアルの「フィルタの型」をご覧ください。

終わり

私のような雑魚い頭的には「FILTER_SANITIZE_FULL_SPECIAL_CHARS を指定しておけばいい」という感じですかね(笑)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelでクリーンアーキテクチャ(続編)

この記事は次の記事の続編に近い内容となっています。
もし可能であればこちらの記事を読み進める前に、次の記事をご覧いただくとより内容がわかりやすいでしょう。

Laravelで実践クリーンアーキテクチャ: https://qiita.com/nrslib/items/aa49d10dd2bcb3110f22

はじめに

皆さんこんな図をご存知でしょうか。
clean.jpg
The Clean Architecture: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

これはクリーンアーキテクチャというアイデアを表した図です。
同心円が特徴的な図ですね。
この同心円は、最も重要なビジネスロジックを中心に見据えることで外界の変化から防衛、対応していこうというコンセプトを表しています。
より具体的にいえば、ビジネスロジックは特定の技術に依存すべきでないということです。
つまり、ソフトウェアレベルで疎結合を達成しようとしています。

ビジネスに比べて UI やデータベース、フレームワークなどは移り変わりやすいものです。
そういった変化に対して、最も重要なビジネスロジックは本来影響を受けるべきではありません。
反対にビジネスロジックの変化は UI やデータベース、フレームワークに正しく影響させるべきです。

同心円の図は依存の方向を中心に向け、内側のレイヤーの変更は外側のレイヤーに伝播し、外側のレイヤーの変更は内側のレイヤー影響しないようにするというアイデアを表しています。

コンセプト自体は明快なクリーンアーキテクチャですが、図だけではとても抽象的に思えますね。
しかし、実はこの図、かなり詳細な実装まで落とし込むことができるのです。

それについては以下記事にて解説を行いました。

Laravelで実践クリーンアーキテクチャ: https://qiita.com/nrslib/items/aa49d10dd2bcb3110f22

では、この記事は何なのかというと、「Laravelで実践クリーンアーキテクチャ(以降、以前の記事と称します)」で妥協した部分も実装に落とし込んでみよう、という内容です。

コード

以前の記事に加筆して作ったサンプルコードです。
https://github.com/nrslib/StrictLaraClean

妥協点

冒頭でお話したとおり、以前の記事では妥協していた部分があります。
image.png
この図の Presenter と UseCaseOutputPort の部分ですね。

classes.jpg
こちらの図であれば Presenter と OutputBoundary です。

具体的なコードを確認してみましょう。
次のコードは MVC フレームワークのコントローラです。

class UserController extends BaseController
{
    public function index(UserGetListUseCaseInterface $interactor)
    {
        $request = new UserGetListRequest(1, 10);
        $response = $interactor->handle($request);

        $users = array_map(
            function ($x) {
                return new UserViewModel($x->id, $x->name);
            },
            $response->users
        );
        $viewModel = new UserIndexViewModel($users);

        return view('user.index', compact('viewModel'));
    }

    public function create(UserCreateUseCaseInterface $interactor, Request $request)
    {
        $name = $request->input('name');
        $request = new UserCreateRequest($name);
        $response = $interactor->handle($request);

        $viewModel = new UserCreateViewModel($response->getCreatedUserId(), $name);
        return view('user.create', compact('viewModel'));
    }
}

いずれのアクションもリクエストからデータを処理してレスポンスを戻すという一般的な処理です。
しかし、この処理がすでにクリーンアーキテクチャの図からはかけ離れているのです。

このコードを図に表すと次のようになります。
image.png
オリジナルの図(次の図)と比べるとだいぶ形がちがいますね。
image.png
元々の図では Controller が UseCaseInputPort (コードでは UserCreateUseCaseInterface) を呼び出して、その後の処理は UseCaseInteractor に流れ、UseCaseOutputPort を経て Presenter へ処理が流れるようになっています。

もしもこの図を再現した場合、コードは次のようになるでしょう。

class UserController extends BaseController
{
    public function index(UserGetListUseCaseInterface $interactor)
    {
        $request = new UserGetListRequest(1, 10);
        $interactor->handle($request);
    }

    public function create(UserCreateUseCaseInterface $interactor, Request $request)
    {
        $name = $request->input('name');
        $request = new UserCreateRequest($name);
        $interactor->handle($request);
    }
}

戻り値がなくなってしまいました。
これは通常の MVC フレームワークのコードとは大分異なるコードです。
だいぶ違和感を感じるのではないでしょうか。

通常 MVC フレームワークにおいて、このようなコードを書いてもうまくは動きません。
ですので以前の記事では妥協をすることにしたのですが(妥協してもクリーンな状態は保たれますし)、今回は敢えて妥協しないという方向で進んでいきます。

調査

というわけで妥協しないで魔改造実装するためにまずは調査をしていきましょう。

まずは最終形となる理想的なコードを確認します。
コントローラは次のコードを目指します。

class UserController extends BaseController
{
    public function index(UserGetListUseCaseInterface $interactor)
    {
        $request = new UserGetListRequest(1, 10);
        $interactor->handle($request);
    }

    public function create(UserCreateUseCaseInterface $interactor, Request $request)
    {
        $name = $request->input('name');
        $request = new UserCreateRequest($name);
        $interactor->handle($request);
    }
}

続いて UseCase のコードは次のように結果を Presenter に伝えるようにします。

class UserCreateInteractor implements UserCreateUseCaseInterface
{
    /**
     * @var UserRepositoryInterface
     */
    private $userRepository;
    /**
     * @var UserCreatePresenter
     */
    private $presenter;

    /**
     * UserCreateInteractor constructor.
     * @param UserRepositoryInterface $userRepository
     * @param UserCreatePresenter $presenter
     */
    public function __construct(UserRepositoryInterface $userRepository, UserCreatePresenter $presenter)
    {
        $this->userRepository = $userRepository;
        $this->presenter = $presenter;
    }

    /**
     * @param UserCreateRequest $request
     * @return void
     */
    public function handle(UserCreateRequest $request)
    {
        $userId = new UserId(uniqid());
        $userName = $request->getName();
        $createdUser = new User($userId, $userName);
        $this->userRepository->save($createdUser);

        $response = new UserCreateResponse($userId->getValue(), $userName);
        $this->presenter->output($response);
    }
}

そして Presenter はどのような実装になるかわかりませんが UseCase の結果を表示用に整形し、どうにかして表示する仕組みへの通知を行うことを目指します。

class UserCreatePresenter {
    public function output(UserCreateResponse $outputData)
    {
        $viewModel = new UserCreateViewModel($outputData->getCreatedUserId(), $outputData->getUserName());
        // ここでどうにかして表示できるように通知する
    }
}

この流れを整理すると次の順序で処理が行われます。

  1. Controller が InputPort (UserCreateUseCaseInterface) を呼び出す
  2. InputPort の実装である UserCreateInteractor に処理が移譲される
  3. UserCreateInteractor は Presenter (UserCreateUseCasePresenter) に結果を伝える
  4. Presenter の実装である UseCreatePresenter に処理が移譲される

これであれば次の図の再現になっているのではないでしょうか。
image.png
それでは調査していきましょう。

なにはともあれ、まずは Controller が戻り値を返却しなくてもデータを表示できるか確認します。
エントリポイントの public\index.php を確認してみると次のコードがあります。

/*
 * 省略
 */
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

$response->send();

$kernel->terminate($request, $response);

ここを見る限りだと$kernel->handleがレスポンスを戻り値として返却していて、そのレスポンスのsendメソッドを呼ぶことで処理がうまくいきそうです。
であれば $response にあたるものを戻り値以外の方法で手に入れれば何とかなりそうな気がしてきました。

次は$kernelがどうやってレスポンスを生成しているか探してみます。
$kernelはコードを見てわかるとおり Illuminate\Contracts\Http\Kernel から生成されています。
この Kernel は interface ですので、実装しているクラスを検索してみましょう。
すると Illuminate\Foundation\Http\Kernel が見つかり、更にそれを継承した App\Http\Kernel というクラスも見つかりました。

class Kernel extends HttpKernel
{
    /**
     * The application's global HTTP middleware stack.
     *
     * These middleware are run during every request to your application.
     *
     * @var array
     */
    protected $middleware = [
        \App\Http\Middleware\CheckForMaintenanceMode::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
        \App\Http\Middleware\TrustProxies::class,
    ];

    /**
     * The application's route middleware groups.
     *
     * @var array
     */
    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            // \Illuminate\Session\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

        'api' => [
            'throttle:60,1',
            'bindings',
        ],
    ];

    /**
     * The application's route middleware.
     *
     * These middleware may be assigned to groups or used individually.
     *
     * @var array
     */
    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
    ];

    /**
     * The priority-sorted list of middleware.
     *
     * This forces non-global middleware to always be in the given order.
     *
     * @var array
     */
    protected $middlewarePriority = [
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\Authenticate::class,
        \Illuminate\Session\Middleware\AuthenticateSession::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
        \Illuminate\Auth\Middleware\Authorize::class,
    ];
}

Middleware の戻り値がどんなものか気になります。
ためしに独自の Middleware を作ってみて var_dump してみます。
Middleware は次のコマンドでスケルトンを生成できます。

$ php artisan make:middleware CleanArchitectureMiddleware

できあがったスケルトンに var_dump を差し込みましょう。

class CleanArchitectureMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request $request
     * @param  \Closure $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $response = $next($request);
        var_dump($response);
        return $response;
    }
}

そして忘れずに Kernel に登録もしておきます。

class Kernel extends HttpKernel
{
    /**
     * The application's global HTTP middleware stack.
     *
     * These middleware are run during every request to your application.
     *
     * @var array
     */
    protected $middleware = [
        \App\Http\Middleware\CheckForMaintenanceMode::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
        \App\Http\Middleware\TrustProxies::class,
        CleanArchitectureMiddleware::class // ← ここに追加
    ];

この状態でコードを実行すると Illuminate\Http\Response というものがやり取りされていて、その中に Http のコンテントや Http ステータスコードなどが存在しているのがわかります。

image.png

ためしにコントローラの戻り値をなくしてみるとどうなるでしょうか。

class UserController extends BaseController
{
    public function index(UserGetListUseCaseInterface $interactor)
    {
        $request = new UserGetListRequest(1, 10);
        $response = $interactor->handle($request);

        $users = array_map(
            function ($x) {
                return new UserViewModel($x->id, $x->name);
            },
            $response->users
        );
        $viewModel = new UserIndexViewModel($users);

//        return view('user.index', compact('viewModel')); // ← 試しにコメントアウト
    }

すると先ほど存在していたコンテントやステータスコードが現れなくなりました。
image.png
コントローラの戻り値がここに利用されているのは確定のようです。

リクエストとレスポンスの流れがわかったところで、今度はコントローラの戻り値の具体的な生成方法を確認してみましょう。
コントローラの戻り値はview()という関数の戻り値ですので、view()の実装を探してみると helpers.php のコードが見つかります。

helpers.php
if (! function_exists('view')) {
    /**
     * Get the evaluated view contents for the given view.
     *
     * @param  string  $view
     * @param  array   $data
     * @param  array   $mergeData
     * @return \Illuminate\View\View|\Illuminate\Contracts\View\Factory
     */
    function view($view = null, $data = [], $mergeData = [])
    {
        $factory = app(ViewFactory::class);

        if (func_num_args() === 0) {
            return $factory;
        }

        return $factory->make($view, $data, $mergeData);
    }
}

view関数はどこでも呼ぶことができそうなので、Presenter で呼びだすことができます。
また具体的な処理を眺めた感じもビューをうまく生成してくれそうな感じにみえます。

実装

というわけでいざ実装をしてみましょう。

Middleware

先ほど作成した Middleware に view 関数の結果を格納するフィールドを用意しておきます。

class CleanArchitectureMiddleware
{
    public static $view; // view 関数の結果を格納するフィールド

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request $request
     * @param  \Closure $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $response = $next($request);
        return $response;
    }
}

正直な話この格納場所はどこでもよかったのですが、ここがわかりやすそうなのでそのまま使います。

index.php

次にエントリポイントを魔改造改修します。
具体的には Middleware に用意した view 関数の結果を利用するようにします。

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$kernelResponse = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

$view = \App\Http\Middleware\CleanArchitectureMiddleware::$view; // 格納された view 関数の結果を取り出して
$response = $view !== null                                       // データが存在してたら
    ? new \Symfony\Component\HttpFoundation\Response($view)      // そのデータをレスポンスに
    : $kernelResponse;                                           // さもなければ通常のレスポンスを利用
$response->send();

$kernel->terminate($request, $response);

これで戻り値を返却する以外の方法でレスポンスをつくることができそうです。

これでコア部分の魔改造ができたので次節からは次の図を準備していきます。
image.png
classes.jpg

UseCaseOutputPort (OutputBoundary)

image.png
UseCaseOutputPort (OutputBoundary) は<I>というマークがついているとおり interface です。
UseCaseInteractor が呼び出します。
図にもあるとおり OutputData と呼ばれる表示出力用のデータ構造体が引き渡されます。

/**
 * Interface UserCreatePresenterInterface
 * @package packages\UseCase\User\Create
 */
interface UserCreatePresenterInterface
{
    /**
     * @param UserCreateResponse $outputData
     * @return void
     */
    public function output(UserCreateResponse $outputData);
}

これが interface で用意されているのは後述の Presenter という表示出力のバリエーションを増やせるようにするためです。

Presenter

image.png
Presenter は UseCaseOutputPort (OutputBoundary) を実装し、UseCaseInteractor が生成した OutputData をそれぞれのビュー用に変換する作業を行います。

class UserCreatePresenter implements UserCreatePresenterInterface
{
    public function output(UserCreateResponse $outputData)
    {
        $viewModel = new UserCreateViewModel($outputData->getCreatedUserId(), $outputData->getUserName());
        CleanArchitectureMiddleware::$view = view('user.create', compact('viewModel'));
    }
}

元となる OutputData が同じであってもビューが異なれば表示用のデータは異なるということですね。
具体例としては、たとえばテスト用にどのようなデータが得られたかを確認する場合は次のような Presenter を実装することで結果を取得するオブジェクトを用意することができます。

class TestUserCreatePresenter implements UserCreatePresenterInterface {
    public $id;
    public $name;

    /**
     * @param UserCreateResponse $outputData
     * @return void
     */
    public function output(UserCreateResponse $outputData)
    {
        $this->id = $outputData->getCreatedUserId();
        $this->name = $outputData->getUserName();
    }
}

Middleware をいちいち通す必要がなくなるので気軽に処理結果を得ることができます。

$presenter = new TestUserCreatePresenter();
$repository = new InMemoryUserRepository();
$interactor = new UserCreateInteractor($repository, $presenter);
var_dump($presenter->id);

UseCaseInteractor

image.png
UseCaseInteractor はもはや戻り値を戻さず、UseCaseOutputPort (OutputBoundary) に OutputData を伝えるだけになります。

class UserCreateInteractor implements UserCreateUseCaseInterface
{
    /**
     * @var UserRepositoryInterface
     */
    private $userRepository;
    /**
     * @var UserCreatePresenter
     */
    private $presenter;

    /**
     * UserCreateInteractor constructor.
     * @param UserRepositoryInterface $userRepository
     * @param UserCreatePresenterInterface $presenter
     */
    public function __construct(UserRepositoryInterface $userRepository, UserCreatePresenterInterface $presenter)
    {
        $this->userRepository = $userRepository;
        $this->presenter = $presenter;
    }

    /**
     * @param UserCreateRequest $request
     * @return void
     */
    public function handle(UserCreateRequest $request)
    {
        $userId = new UserId(uniqid());
        $userName = $request->getName();
        $createdUser = new User($userId, $userName);
        $this->userRepository->save($createdUser);

        $response = new UserCreateResponse($userId->getValue(), $userName);
        $this->presenter->output($response);
    }
}

Controller

image.png
Controller は入力情報を UseCaseInputPort (InputBoundary) が要求するデータ (InputData) に適合させることに集中することになります。

class UserController extends BaseController
{
    public function index(UserGetListUseCaseInterface $interactor)
    {
        $request = new UserGetListRequest(1, 10);
        $interactor->handle($request);
    }

    public function create(UserCreateUseCaseInterface $interactor, Request $request)
    {
        $name = $request->input('name');
        $request = new UserCreateRequest($name);
        $interactor->handle($request);
    }
}

戻り値がなくなり、一般的な MVC フレームワークのコントローラとはかけ離れたものになりました。
これでクリーンアーキテクチャの図を完全に再現した Web アプリケーションの形が完成です。

処理の流れ

ためしにユーザを生成するときの処理の流れを追ってみましょう。

まず入力データはUserControllerに引き渡され、UserCreateUseCaseInterface(UseCaseInputPort, InputBoundary) の処理が呼び出されます。

class UserController extends BaseController
{
    public function create(UserCreateUseCaseInterface $interactor, Request $request)
    {
        $name = $request->input('name');
        $request = new UserCreateRequest($name);
        $interactor->handle($request);
    }
}

UserCreateUseCaseInterfaceの処理が呼び出されるとその実装クラスUserCreateInteractor(UseCaseInteractor) に処理が移譲されます。
このオブジェクトが処理した結果はUserCreatePresenterInterface(UseCaseOutputPort, OutputBoundary) に通知されます。

class UserCreateInteractor implements UserCreateUseCaseInterface
{
    private $userRepository;
    private $presenter;

    public function __construct(UserRepositoryInterface $userRepository, UserCreatePresenterInterface $presenter)
    {
        $this->userRepository = $userRepository;
        $this->presenter = $presenter;
    }

    public function handle(UserCreateRequest $request)
    {
        $userId = new UserId(uniqid());
        $userName = $request->getName();
        $createdUser = new User($userId, $userName);
        $this->userRepository->save($createdUser);

        $response = new UserCreateResponse($userId->getValue(), $userName);
        $this->presenter->output($response);
    }
}

UserCreatePresenterInterfaceを実装するUserCreatePresenter(Presenter) に処理が移譲され、UserCreatePresenterは表示用にデータを整形し、「どうにかして」表示できるように通知を行います。

class UserCreatePresenter implements UserCreatePresenterInterface
{
    public function output(UserCreateResponse $outputData)
    {
        $viewModel = new UserCreateViewModel($outputData->getCreatedUserId(), $outputData->getUserName());
        CleanArchitectureMiddleware::$view = view('user.create', compact('viewModel'));
    }
}

Flow of control (処理の流れ)と照らし合わせると一致することがわかるでしょうか。
image.png

で、なにが嬉しいの?

今回のコードは MVC フレームワークの常識から逸脱したコードになっています。
正直な気持ちとして「これが最高だからこれで作ろうな!」と手放しに推せるコードではないです。

それでもここまでコードを書いて、なぜ Presenter のようなまだるっこしいものを採用しているのかを見出すことができた気がしたので、それを書いてみます。

GUI のプログラムのパターンに古典的 MVC (Web の MVC とは異なるので注意)というものがあります。
図にすると次のイメージになります。
image.png
古典的 MVC ではユーザの操作は Controller に伝えられ、Controller はその入力から Model の処理を呼び出します。Model の処理結果は View に通知され、View がそれを描画することでユーザに伝えられます。
このパターンにおいて、着目されるのは責務を分けることになりがちです。
しかし、それ以外にも大事な要素があって、それは処理の方向だったりします。

なにかをするとき、臨機応変な対応が求められるのと決まりきった手順が決まっているのでは後者の方が簡単ですよね。
プログラムの処理の流れも場合によってはあっちへこっちへいくようなプログラムよりも、常に一方向であった方が理解しやすいものです。

つまり、古典的 MVC は複雑になりがちな GUI アプリケーションにおいて、責務を分散し、処理方向を一方向にすることで理解しやすいソフトウェアを作ろうというパターンだったりします。

さて、ここで改めてクリーンアーキテクチャの Flow of Control を確認してみます。
image.png
Presenter を用意することで戻り値を用意する必要がなくなりました。
結果として処理の方向は一方向に固定されます。
処理の流れが一方向ということは理解しやすいはずです。

そう考えると、戻り値を戻す従来の形よりも、今回実装した構成の方がシンプルなソフトウェアではないでしょうか。

処理を一方向に固定することに意義がある。そういった意向がここにあるのではないのかと感じた次第でした。

まとめ

正直な話、今回のコードを実装しながら思っていたのは「物議を醸しだしそうなコードだな」でした。
もちろんこれこそが正しいコードである、とは考えておりません。
封印することも考えたのですが、アイデアとしては面白く感じたため、今回の記事を投稿するに至りました。

この記事でお話したとおりクリーンアーキテクチャの構成を MVC フレームワークにあてはめてみるとかなり違和感を感じます。
MVC フレームワークの大前提を覆すようなコードになっていると言っても過言ではないと思います。

しかし Controller をゲームのコントローラとして見立ててみると案外素直に受け入れられる可能性もなくはないと感じました。

ゲームのコントローラはボタンを押した結果をユーザに伝えたりはしません。(振動機能などはありますが)
押した結果をユーザに伝えるのは View であるモニタの役目です。
こう考えると Controller は戻り値など返さず、入力された情報だけに集中する方が自然ではないのでしょうか。

クリーンアーキテクチャの目標のひとつは特定の技術からの独立です。
それがアプリケーションの防衛に繋がるからです。

特定の技術に依存した形でソフトウェアを開発することはある種の危険性をはらみます。

もしもフレームワークが廃れ、別のフレームワークに乗せ換えることになったらどうなるか。
データ永続化装置がアーキテクチャレベルで変更することになったらどうすればよいのか。

こういったリスクから距離を取るための手法として、クリーンアーキテクチャはよい選択肢になるのではないでしょうか。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelでクリーンアーキテクチャ

この記事は次の記事の続編に近い内容となっています。
もし可能であればこちらの記事を読み進める前に、次の記事をご覧いただくとより内容がわかりやすいでしょう。

Laravelで実践クリーンアーキテクチャ: https://qiita.com/nrslib/items/aa49d10dd2bcb3110f22

はじめに

皆さんこんな図をご存知でしょうか。
clean.jpg
The Clean Architecture: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

これはクリーンアーキテクチャというアイデアを表した図です。
同心円が特徴的な図ですね。
この同心円は、最も重要なビジネスロジックを中心に見据えることで外界の変化から防衛、対応していこうというコンセプトを表しています。
より具体的にいえば、ビジネスロジックは特定の技術に依存すべきでないということです。
つまり、ソフトウェアレベルで疎結合を達成しようとしています。

ビジネスに比べて UI やデータベース、フレームワークなどは移り変わりやすいものです。
そういった変化に対して、最も重要なビジネスロジックは本来影響を受けるべきではありません。
反対にビジネスロジックの変化は UI やデータベース、フレームワークに正しく影響させるべきです。

同心円の図は依存の方向を中心に向け、内側のレイヤーの変更は外側のレイヤーに伝播し、外側のレイヤーの変更は内側のレイヤー影響しないようにするというアイデアを表しています。

コンセプト自体は明快なクリーンアーキテクチャですが、図だけではとても抽象的に思えますね。
しかし、実はこの図、かなり詳細な実装まで落とし込むことができるのです。

それについては以下記事にて解説を行いました。

Laravelで実践クリーンアーキテクチャ: https://qiita.com/nrslib/items/aa49d10dd2bcb3110f22

では、この記事は何なのかというと、「Laravelで実践クリーンアーキテクチャ(以降、以前の記事と称します)」で妥協した部分も実装に落とし込んでみよう、という内容です。

コード

以前の記事に加筆して作ったサンプルコードです。
https://github.com/nrslib/StrictLaraClean

妥協点

冒頭でお話したとおり、以前の記事では妥協していた部分があります。
image.png
この図の Presenter と UseCaseOutputPort の部分ですね。

classes.jpg
こちらの図であれば Presenter と OutputBoundary です。

具体的なコードを確認してみましょう。
次のコードは MVC フレームワークのコントローラです。

class UserController extends BaseController
{
    public function index(UserGetListUseCaseInterface $interactor)
    {
        $request = new UserGetListRequest(1, 10);
        $response = $interactor->handle($request);

        $users = array_map(
            function ($x) {
                return new UserViewModel($x->id, $x->name);
            },
            $response->users
        );
        $viewModel = new UserIndexViewModel($users);

        return view('user.index', compact('viewModel'));
    }

    public function create(UserCreateUseCaseInterface $interactor, Request $request)
    {
        $name = $request->input('name');
        $request = new UserCreateRequest($name);
        $response = $interactor->handle($request);

        $viewModel = new UserCreateViewModel($response->getCreatedUserId(), $name);
        return view('user.create', compact('viewModel'));
    }
}

いずれのアクションもリクエストからデータを処理してレスポンスを戻すという一般的な処理です。
しかし、この処理がすでにクリーンアーキテクチャの図からはかけ離れているのです。

このコードを図に表すと次のようになります。
image.png
オリジナルの図(次の図)と比べるとだいぶ形がちがいますね。
image.png
元々の図では Controller が UseCaseInputPort (コードでは UserCreateUseCaseInterface) を呼び出して、その後の処理は UseCaseInteractor に流れ、UseCaseOutputPort を経て Presenter へ処理が流れるようになっています。

もしもこの図を再現した場合、コードは次のようになるでしょう。

class UserController extends BaseController
{
    public function index(UserGetListUseCaseInterface $interactor)
    {
        $request = new UserGetListRequest(1, 10);
        $interactor->handle($request);
    }

    public function create(UserCreateUseCaseInterface $interactor, Request $request)
    {
        $name = $request->input('name');
        $request = new UserCreateRequest($name);
        $interactor->handle($request);
    }
}

戻り値がなくなってしまいました。
これは通常の MVC フレームワークのコードとは大分異なるコードです。
だいぶ違和感を感じるのではないでしょうか。

通常 MVC フレームワークにおいて、このようなコードを書いてもうまくは動きません。
ですので以前の記事では妥協をすることにしたのですが(妥協してもクリーンな状態は保たれますし)、今回は敢えて妥協しないという方向で進んでいきます。

調査

というわけで妥協しないで魔改造実装するためにまずは調査をしていきましょう。

まずは最終形となる理想的なコードを確認します。
コントローラは次のコードを目指します。

class UserController extends BaseController
{
    public function index(UserGetListUseCaseInterface $interactor)
    {
        $request = new UserGetListRequest(1, 10);
        $interactor->handle($request);
    }

    public function create(UserCreateUseCaseInterface $interactor, Request $request)
    {
        $name = $request->input('name');
        $request = new UserCreateRequest($name);
        $interactor->handle($request);
    }
}

続いて UseCase のコードは次のように結果を Presenter に伝えるようにします。

class UserCreateInteractor implements UserCreateUseCaseInterface
{
    /**
     * @var UserRepositoryInterface
     */
    private $userRepository;
    /**
     * @var UserCreatePresenter
     */
    private $presenter;

    /**
     * UserCreateInteractor constructor.
     * @param UserRepositoryInterface $userRepository
     * @param UserCreatePresenter $presenter
     */
    public function __construct(UserRepositoryInterface $userRepository, UserCreatePresenter $presenter)
    {
        $this->userRepository = $userRepository;
        $this->presenter = $presenter;
    }

    /**
     * @param UserCreateRequest $request
     * @return void
     */
    public function handle(UserCreateRequest $request)
    {
        $userId = new UserId(uniqid());
        $userName = $request->getName();
        $createdUser = new User($userId, $userName);
        $this->userRepository->save($createdUser);

        $response = new UserCreateResponse($userId->getValue(), $userName);
        $this->presenter->output($response);
    }
}

そして Presenter はどのような実装になるかわかりませんが UseCase の結果を表示用に整形し、どうにかして表示する仕組みへの通知を行うことを目指します。

class UserCreatePresenter {
    public function output(UserCreateResponse $outputData)
    {
        $viewModel = new UserCreateViewModel($outputData->getCreatedUserId(), $outputData->getUserName());
        // ここでどうにかして表示できるように通知する
    }
}

この流れを整理すると次の順序で処理が行われます。

  1. Controller が InputPort (UserCreateUseCaseInterface) を呼び出す
  2. InputPort の実装である UserCreateInteractor に処理が移譲される
  3. UserCreateInteractor は Presenter (UserCreateUseCasePresenter) に結果を伝える
  4. Presenter の実装である UseCreatePresenter に処理が移譲される

これであれば次の図の再現になっているのではないでしょうか。
image.png
それでは調査していきましょう。

なにはともあれ、まずは Controller が戻り値を返却しなくてもデータを表示できるか確認します。
エントリポイントの public\index.php を確認してみると次のコードがあります。

/*
 * 省略
 */
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

$response->send();

$kernel->terminate($request, $response);

ここを見る限りだと$kernel->handleがレスポンスを戻り値として返却していて、そのレスポンスのsendメソッドを呼ぶことで処理がうまくいきそうです。
であれば $response にあたるものを戻り値以外の方法で手に入れれば何とかなりそうな気がしてきました。

次は$kernelがどうやってレスポンスを生成しているか探してみます。
$kernelはコードを見てわかるとおり Illuminate\Contracts\Http\Kernel から生成されています。
この Kernel は interface ですので、実装しているクラスを検索してみましょう。
すると Illuminate\Foundation\Http\Kernel が見つかり、更にそれを継承した App\Http\Kernel というクラスも見つかりました。

class Kernel extends HttpKernel
{
    /**
     * The application's global HTTP middleware stack.
     *
     * These middleware are run during every request to your application.
     *
     * @var array
     */
    protected $middleware = [
        \App\Http\Middleware\CheckForMaintenanceMode::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
        \App\Http\Middleware\TrustProxies::class,
    ];

    /**
     * The application's route middleware groups.
     *
     * @var array
     */
    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            // \Illuminate\Session\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

        'api' => [
            'throttle:60,1',
            'bindings',
        ],
    ];

    /**
     * The application's route middleware.
     *
     * These middleware may be assigned to groups or used individually.
     *
     * @var array
     */
    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
    ];

    /**
     * The priority-sorted list of middleware.
     *
     * This forces non-global middleware to always be in the given order.
     *
     * @var array
     */
    protected $middlewarePriority = [
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\Authenticate::class,
        \Illuminate\Session\Middleware\AuthenticateSession::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
        \Illuminate\Auth\Middleware\Authorize::class,
    ];
}

Middleware の戻り値がどんなものか気になります。
ためしに独自の Middleware を作ってみて var_dump してみます。
Middleware は次のコマンドでスケルトンを生成できます。

$ php artisan make:middleware CleanArchitectureMiddleware

できあがったスケルトンに var_dump を差し込みましょう。

class CleanArchitectureMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request $request
     * @param  \Closure $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $response = $next($request);
        var_dump($response);
        return $response;
    }
}

そして忘れずに Kernel に登録もしておきます。

class Kernel extends HttpKernel
{
    /**
     * The application's global HTTP middleware stack.
     *
     * These middleware are run during every request to your application.
     *
     * @var array
     */
    protected $middleware = [
        \App\Http\Middleware\CheckForMaintenanceMode::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
        \App\Http\Middleware\TrustProxies::class,
        CleanArchitectureMiddleware::class // ← ここに追加
    ];

この状態でコードを実行すると Illuminate\Http\Response というものがやり取りされていて、その中に Http のコンテントや Http ステータスコードなどが存在しているのがわかります。

image.png

ためしにコントローラの戻り値をなくしてみるとどうなるでしょうか。

class UserController extends BaseController
{
    public function index(UserGetListUseCaseInterface $interactor)
    {
        $request = new UserGetListRequest(1, 10);
        $response = $interactor->handle($request);

        $users = array_map(
            function ($x) {
                return new UserViewModel($x->id, $x->name);
            },
            $response->users
        );
        $viewModel = new UserIndexViewModel($users);

//        return view('user.index', compact('viewModel')); // ← 試しにコメントアウト
    }

すると先ほど存在していたコンテントやステータスコードが現れなくなりました。
image.png
コントローラの戻り値がここに利用されているのは確定のようです。

リクエストとレスポンスの流れがわかったところで、今度はコントローラの戻り値の具体的な生成方法を確認してみましょう。
コントローラの戻り値はview()という関数の戻り値ですので、view()の実装を探してみると helpers.php のコードが見つかります。

helpers.php
if (! function_exists('view')) {
    /**
     * Get the evaluated view contents for the given view.
     *
     * @param  string  $view
     * @param  array   $data
     * @param  array   $mergeData
     * @return \Illuminate\View\View|\Illuminate\Contracts\View\Factory
     */
    function view($view = null, $data = [], $mergeData = [])
    {
        $factory = app(ViewFactory::class);

        if (func_num_args() === 0) {
            return $factory;
        }

        return $factory->make($view, $data, $mergeData);
    }
}

view関数はどこでも呼ぶことができそうなので、Presenter で呼びだすことができます。
また具体的な処理を眺めた感じもビューをうまく生成してくれそうな感じにみえます。

実装

というわけでいざ実装をしてみましょう。

Middleware

先ほど作成した Middleware に view 関数の結果を格納するフィールドを用意しておきます。

class CleanArchitectureMiddleware
{
    public static $view; // view 関数の結果を格納するフィールド

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request $request
     * @param  \Closure $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $response = $next($request);
        return $response;
    }
}

正直な話この格納場所はどこでもよかったのですが、ここがわかりやすそうなのでそのまま使います。

index.php

次にエントリポイントを魔改造改修します。
具体的には Middleware に用意した view 関数の結果を利用するようにします。

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$kernelResponse = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

$view = \App\Http\Middleware\CleanArchitectureMiddleware::$view; // 格納された view 関数の結果を取り出して
$response = $view !== null                                       // データが存在してたら
    ? new \Symfony\Component\HttpFoundation\Response($view)      // そのデータをレスポンスに
    : $kernelResponse;                                           // さもなければ通常のレスポンスを利用
$response->send();

$kernel->terminate($request, $response);

これで戻り値を返却する以外の方法でレスポンスをつくることができそうです。

これでコア部分の魔改造ができたので次節からは次の図を準備していきます。
image.png
classes.jpg

UseCaseOutputPort (OutputBoundary)

image.png
UseCaseOutputPort (OutputBoundary) は<I>というマークがついているとおり interface です。
UseCaseInteractor が呼び出します。
図にもあるとおり OutputData と呼ばれる表示出力用のデータ構造体が引き渡されます。

/**
 * Interface UserCreatePresenterInterface
 * @package packages\UseCase\User\Create
 */
interface UserCreatePresenterInterface
{
    /**
     * @param UserCreateResponse $outputData
     * @return void
     */
    public function output(UserCreateResponse $outputData);
}

これが interface で用意されているのは後述の Presenter という表示出力のバリエーションを増やせるようにするためです。

Presenter

image.png
Presenter は UseCaseOutputPort (OutputBoundary) を実装し、UseCaseInteractor が生成した OutputData をそれぞれのビュー用に変換する作業を行います。

class UserCreatePresenter implements UserCreatePresenterInterface
{
    public function output(UserCreateResponse $outputData)
    {
        $viewModel = new UserCreateViewModel($outputData->getCreatedUserId(), $outputData->getUserName());
        CleanArchitectureMiddleware::$view = view('user.create', compact('viewModel'));
    }
}

元となる OutputData が同じであってもビューが異なれば表示用のデータは異なるということですね。
具体例としては、たとえばテスト用にどのようなデータが得られたかを確認する場合は次のような Presenter を実装することで結果を取得するオブジェクトを用意することができます。

class TestUserCreatePresenter implements UserCreatePresenterInterface {
    public $id;
    public $name;

    /**
     * @param UserCreateResponse $outputData
     * @return void
     */
    public function output(UserCreateResponse $outputData)
    {
        $this->id = $outputData->getCreatedUserId();
        $this->name = $outputData->getUserName();
    }
}

Middleware をいちいち通す必要がなくなるので気軽に処理結果を得ることができます。

$presenter = new TestUserCreatePresenter();
$repository = new InMemoryUserRepository();
$interactor = new UserCreateInteractor($repository, $presenter);
var_dump($presenter->id);

UseCaseInteractor

image.png
UseCaseInteractor はもはや戻り値を戻さず、UseCaseOutputPort (OutputBoundary) に OutputData を伝えるだけになります。

class UserCreateInteractor implements UserCreateUseCaseInterface
{
    /**
     * @var UserRepositoryInterface
     */
    private $userRepository;
    /**
     * @var UserCreatePresenter
     */
    private $presenter;

    /**
     * UserCreateInteractor constructor.
     * @param UserRepositoryInterface $userRepository
     * @param UserCreatePresenterInterface $presenter
     */
    public function __construct(UserRepositoryInterface $userRepository, UserCreatePresenterInterface $presenter)
    {
        $this->userRepository = $userRepository;
        $this->presenter = $presenter;
    }

    /**
     * @param UserCreateRequest $request
     * @return void
     */
    public function handle(UserCreateRequest $request)
    {
        $userId = new UserId(uniqid());
        $userName = $request->getName();
        $createdUser = new User($userId, $userName);
        $this->userRepository->save($createdUser);

        $response = new UserCreateResponse($userId->getValue(), $userName);
        $this->presenter->output($response);
    }
}

Controller

image.png
Controller は入力情報を UseCaseInputPort (InputBoundary) が要求するデータ (InputData) に適合させることに集中することになります。

class UserController extends BaseController
{
    public function index(UserGetListUseCaseInterface $interactor)
    {
        $request = new UserGetListRequest(1, 10);
        $interactor->handle($request);
    }

    public function create(UserCreateUseCaseInterface $interactor, Request $request)
    {
        $name = $request->input('name');
        $request = new UserCreateRequest($name);
        $interactor->handle($request);
    }
}

戻り値がなくなり、一般的な MVC フレームワークのコントローラとはかけ離れたものになりました。
これでクリーンアーキテクチャの図を完全に再現した Web アプリケーションの形が完成です。

処理の流れ

ためしにユーザを生成するときの処理の流れを追ってみましょう。

まず入力データはUserControllerに引き渡され、UserCreateUseCaseInterface(UseCaseInputPort, InputBoundary) の処理が呼び出されます。

class UserController extends BaseController
{
    public function create(UserCreateUseCaseInterface $interactor, Request $request)
    {
        $name = $request->input('name');
        $request = new UserCreateRequest($name);
        $interactor->handle($request);
    }
}

UserCreateUseCaseInterfaceの処理が呼び出されるとその実装クラスUserCreateInteractor(UseCaseInteractor) に処理が移譲されます。
このオブジェクトが処理した結果はUserCreatePresenterInterface(UseCaseOutputPort, OutputBoundary) に通知されます。

class UserCreateInteractor implements UserCreateUseCaseInterface
{
    private $userRepository;
    private $presenter;

    public function __construct(UserRepositoryInterface $userRepository, UserCreatePresenterInterface $presenter)
    {
        $this->userRepository = $userRepository;
        $this->presenter = $presenter;
    }

    public function handle(UserCreateRequest $request)
    {
        $userId = new UserId(uniqid());
        $userName = $request->getName();
        $createdUser = new User($userId, $userName);
        $this->userRepository->save($createdUser);

        $response = new UserCreateResponse($userId->getValue(), $userName);
        $this->presenter->output($response);
    }
}

UserCreatePresenterInterfaceを実装するUserCreatePresenter(Presenter) に処理が移譲され、UserCreatePresenterは表示用にデータを整形し、「どうにかして」表示できるように通知を行います。

class UserCreatePresenter implements UserCreatePresenterInterface
{
    public function output(UserCreateResponse $outputData)
    {
        $viewModel = new UserCreateViewModel($outputData->getCreatedUserId(), $outputData->getUserName());
        CleanArchitectureMiddleware::$view = view('user.create', compact('viewModel'));
    }
}

Flow of control (処理の流れ)と照らし合わせると一致することがわかるでしょうか。
image.png

で、なにが嬉しいの?

今回のコードは MVC フレームワークの常識から逸脱したコードになっています。
正直な気持ちとして「これが最高だからこれで作ろうな!」と手放しに推せるコードではないです。

それでもここまでコードを書いて、なぜ Presenter のようなまだるっこしいものを採用しているのかを見出すことができた気がしたので、それを書いてみます。

GUI のプログラムのパターンに古典的 MVC (Web の MVC とは異なるので注意)というものがあります。
図にすると次のイメージになります。
image.png
古典的 MVC ではユーザの操作は Controller に伝えられ、Controller はその入力から Model の処理を呼び出します。Model の処理結果は View に通知され、View がそれを描画することでユーザに伝えられます。
このパターンにおいて、着目されるのは責務を分けることになりがちです。
しかし、それ以外にも大事な要素があって、それは処理の方向だったりします。

なにかをするとき、臨機応変な対応が求められるのと決まりきった手順が決まっているのでは後者の方が簡単ですよね。
プログラムの処理の流れも場合によってはあっちへこっちへいくようなプログラムよりも、常に一方向であった方が理解しやすいものです。

つまり、古典的 MVC は複雑になりがちな GUI アプリケーションにおいて、責務を分散し、処理方向を一方向にすることで理解しやすいソフトウェアを作ろうというパターンだったりします。

さて、ここで改めてクリーンアーキテクチャの Flow of Control を確認してみます。
image.png
Presenter を用意することで戻り値を用意する必要がなくなりました。
結果として処理の方向は一方向に固定されます。
処理の流れが一方向ということは理解しやすいはずです。

そう考えると、戻り値を戻す従来の形よりも、今回実装した構成の方がシンプルなソフトウェアではないでしょうか。

処理を一方向に固定することに意義がある。そういった意向がここにあるのではないのかと感じた次第でした。

まとめ

正直な話、今回のコードを実装しながら思っていたのは「物議を醸しだしそうなコードだな」でした。
もちろんこれこそが正しいコードである、とは考えておりません。
封印することも考えたのですが、アイデアとしては面白く感じたため、今回の記事を投稿するに至りました。

この記事でお話したとおりクリーンアーキテクチャの構成を MVC フレームワークにあてはめてみるとかなり違和感を感じます。
MVC フレームワークの大前提を覆すようなコードになっていると言っても過言ではないと思います。

しかし Controller をゲームのコントローラとして見立ててみると案外素直に受け入れられる可能性もなくはないと感じました。

ゲームのコントローラはボタンを押した結果をユーザに伝えたりはしません。(振動機能などはありますが)
押した結果をユーザに伝えるのは View であるモニタの役目です。
こう考えると Controller は戻り値など返さず、入力された情報だけに集中する方が自然ではないのでしょうか。

クリーンアーキテクチャの目標のひとつは特定の技術からの独立です。
それがアプリケーションの防衛に繋がるからです。

特定の技術に依存した形でソフトウェアを開発することはある種の危険性をはらみます。

もしもフレームワークが廃れ、別のフレームワークに乗せ換えることになったらどうなるか。
データ永続化装置がアーキテクチャレベルで変更することになったらどうすればよいのか。

こういったリスクから距離を取るための手法として、クリーンアーキテクチャはよい選択肢になるのではないでしょうか。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む