- 投稿日:2019-03-27T20:00:29+09:00
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についての理解があやふやなので、間違っている部分が多々あると思われます。
きっと誰かがプルリクしてくれるはず。
- 投稿日:2019-03-27T19:12:49+09:00
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 composerPhalcon インストールする必要があるもの
- 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 phpInstall 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 mypjnginxの設定
vim /usr/local/nginx/conf/nginx.conf # 中身は下記 nginx -s reloadnginx.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
- 投稿日:2019-03-27T13:20:04+09:00
【CraftCMS】管理画面のソート機能拡張
タイトルと投稿日以外でソートしたい
サンプルソースに準じてみたけど動かなかったので色々検証してついに実現。(朝4時半)
Modules.phpnamespace 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.phpreturn [ 'modules' => [ 'control-panel' => \modules\Module::class, ], 'bootstrap' => ['control-panel'], ];おわり
- 投稿日:2019-03-27T12:03:42+09:00
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の場合「 ~ 」になります。フィルターはパイプ同様に数珠つなぎにすることができます。
- 投稿日:2019-03-27T10:59:47+09:00
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 から
- 投稿日:2019-03-27T09:59:43+09:00
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
- https://github.com/php/php-src/commit/85095dfd0956bc09f5157211948c40cfa2859d27
- [7.2~]
- ext/interbase で、PHP7.x で複数接続が持てない問題の修正
- ext/interbase は長いことメンテナ不在でちょくちょく公式バンドルから外す話が出ている
nikic: Fixed bug #77793
- https://github.com/php/php-src/commit/e97577edde49e1f6e86219091b343f80b3b92e65
- [7.3~]
- extract() で SEGV が出る場合がある問題の修正
- 参照カウントの操作の問題だった
vtemian: Fix bug #77680: Correctly implement recursive mkdir on FTP stream
- https://github.com/php/php-src/commit/ec2ecb7e12b96f8f95af2885d173a0d46c88e190
- [7.2~]
- FTP ストリームへの再帰的な mkdir() が正しく動作しなかった問題の修正
derickr: Update README.RELEASE_PROCESS
- https://github.com/php/php-src/commit/af1be9c33e8208f739a411c8716c249dbbf103dd
- README.RELEASE_PROCESS で、リリースマネージャ向けのチェック項目を修正
dstogov: Regenerate parser
- https://github.com/php/php-src/commit/164b7ec549402eecb18c95156c66974a9ac5ef0a
- [7.4~]
- ext/ffi で、パーサの再生成
kelunik: Fix #77794: Incorrect Date header format in built-in server
- https://github.com/php/php-src/commit/7f9872387e38a05b11ce233b4d142325d742c487
- sapi/cli で、組み込み Web サーバの Date ヘッダフォーマットが間違っていた問題の修正
- "GMT" と出さないとあかんところを "+0000" とか吐いてた
- 実際これで困るケースがあったらしい
petk: [ci skip] Update NEWS
petk: [ci skip] Join contributing and patches docs
- https://github.com/php/php-src/commit/886b2a22e98ae96497a27d581795debaf6407c5c
- [7.4~]
- CONTRIBUTING.md と README.SUBMITTING_PATCH をがっちゃんこした
- ルートディレクトリのファイルを減らす活動の一環かな
nikic: Make PCRE cache per-request on CLI
- https://github.com/php/php-src/commit/a9b01b60d89506b4f661b7108836d217158e83a2
- [7.4~]
- ext/pcre で、CLI では persistent キャッシュではなく per-request キャッシュを使うよう修正
- これだと任意の文字列をキャッシュに使えるらしい、が、よく分かってない
Hywan: Fix typos in the documentation
nikic: Add zend_error_at API that accepts a filename and lineno
- https://github.com/php/php-src/commit/0122f395c7dc5afb9feb3e2dcd11bb90e7433948
- [7.4~]
- zend_error_at API の追加
- opcache の preloading の警告とかに使えるらしい
- 投稿日:2019-03-27T09:26:24+09:00
HomebrewでPHP7.2をインストールする
Homebrewのインストール
コマンドラインを開いて下記を実行する。
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"実行できなければ、下記の公式ページを開いてコマンドを確認してください。
https://brew.sh/index_jaPHP7.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以上になります。
- 投稿日:2019-03-27T08:59:17+09:00
初期構築から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/prestissimoApache2 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-nameSymphonyで使える便利なサーバの追加
昔はデフォで入ってたはずだけど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/controllerLang
翻訳するには下記ファイルに書き足すことで翻訳追加が出来る。これによりグローバル化がかなり簡単であり、更にはSymfonyProfilerで抜けている翻訳があれば警告でお知らせをしてくれる親切機能。
(ROOT)/translations/messages.ja.xlfDataBase
どうやら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が採用しただけあって結構お硬いフレームワークなので学習コストが高そうとも思いました...。
- 投稿日:2019-03-27T01:53:56+09:00
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.phpdiv 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> @endpushoncepushonceの呼び出し方は、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の重複呼び出しはなくなりました。以上です。
- 投稿日:2019-03-27T00:57:22+09:00
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("XSS");" string(54) "<script>alert("XSS");</script>" string(52) "<script>alert("XSS");</script>"結果から分かるように、違いは以下のようになります。
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 を指定しておけばいい」という感じですかね(笑)
- 投稿日:2019-03-27T00:52:58+09:00
Laravelでクリーンアーキテクチャ(続編)
この記事は次の記事の続編に近い内容となっています。
もし可能であればこちらの記事を読み進める前に、次の記事をご覧いただくとより内容がわかりやすいでしょう。Laravelで実践クリーンアーキテクチャ: https://qiita.com/nrslib/items/aa49d10dd2bcb3110f22
はじめに
皆さんこんな図をご存知でしょうか。
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妥協点
冒頭でお話したとおり、以前の記事では妥協していた部分があります。
この図の Presenter と UseCaseOutputPort の部分ですね。
こちらの図であれば 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')); } }いずれのアクションもリクエストからデータを処理してレスポンスを戻すという一般的な処理です。
しかし、この処理がすでにクリーンアーキテクチャの図からはかけ離れているのです。このコードを図に表すと次のようになります。
オリジナルの図(次の図)と比べるとだいぶ形がちがいますね。
元々の図では 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()); // ここでどうにかして表示できるように通知する } }この流れを整理すると次の順序で処理が行われます。
- Controller が InputPort (UserCreateUseCaseInterface) を呼び出す
- InputPort の実装である UserCreateInteractor に処理が移譲される
- UserCreateInteractor は Presenter (UserCreateUseCasePresenter) に結果を伝える
- Presenter の実装である UseCreatePresenter に処理が移譲される
これであれば次の図の再現になっているのではないでしょうか。
それでは調査していきましょう。なにはともあれ、まずは 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 ステータスコードなどが存在しているのがわかります。
ためしにコントローラの戻り値をなくしてみるとどうなるでしょうか。
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')); // ← 試しにコメントアウト }すると先ほど存在していたコンテントやステータスコードが現れなくなりました。
コントローラの戻り値がここに利用されているのは確定のようです。リクエストとレスポンスの流れがわかったところで、今度はコントローラの戻り値の具体的な生成方法を確認してみましょう。
コントローラの戻り値はview()という関数の戻り値ですので、view()の実装を探してみると helpers.php のコードが見つかります。helpers.phpif (! 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);これで戻り値を返却する以外の方法でレスポンスをつくることができそうです。
これでコア部分の魔改造ができたので次節からは次の図を準備していきます。
UseCaseOutputPort (OutputBoundary)
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
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
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
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 (処理の流れ)と照らし合わせると一致することがわかるでしょうか。
で、なにが嬉しいの?
今回のコードは MVC フレームワークの常識から逸脱したコードになっています。
正直な気持ちとして「これが最高だからこれで作ろうな!」と手放しに推せるコードではないです。それでもここまでコードを書いて、なぜ Presenter のようなまだるっこしいものを採用しているのかを見出すことができた気がしたので、それを書いてみます。
GUI のプログラムのパターンに古典的 MVC (Web の MVC とは異なるので注意)というものがあります。
図にすると次のイメージになります。
古典的 MVC ではユーザの操作は Controller に伝えられ、Controller はその入力から Model の処理を呼び出します。Model の処理結果は View に通知され、View がそれを描画することでユーザに伝えられます。
このパターンにおいて、着目されるのは責務を分けることになりがちです。
しかし、それ以外にも大事な要素があって、それは処理の方向だったりします。なにかをするとき、臨機応変な対応が求められるのと決まりきった手順が決まっているのでは後者の方が簡単ですよね。
プログラムの処理の流れも場合によってはあっちへこっちへいくようなプログラムよりも、常に一方向であった方が理解しやすいものです。つまり、古典的 MVC は複雑になりがちな GUI アプリケーションにおいて、責務を分散し、処理方向を一方向にすることで理解しやすいソフトウェアを作ろうというパターンだったりします。
さて、ここで改めてクリーンアーキテクチャの Flow of Control を確認してみます。
Presenter を用意することで戻り値を用意する必要がなくなりました。
結果として処理の方向は一方向に固定されます。
処理の流れが一方向ということは理解しやすいはずです。そう考えると、戻り値を戻す従来の形よりも、今回実装した構成の方がシンプルなソフトウェアではないでしょうか。
処理を一方向に固定することに意義がある。そういった意向がここにあるのではないのかと感じた次第でした。
まとめ
正直な話、今回のコードを実装しながら思っていたのは「物議を醸しだしそうなコードだな」でした。
もちろんこれこそが正しいコードである、とは考えておりません。
封印することも考えたのですが、アイデアとしては面白く感じたため、今回の記事を投稿するに至りました。この記事でお話したとおりクリーンアーキテクチャの構成を MVC フレームワークにあてはめてみるとかなり違和感を感じます。
MVC フレームワークの大前提を覆すようなコードになっていると言っても過言ではないと思います。しかし Controller をゲームのコントローラとして見立ててみると案外素直に受け入れられる可能性もなくはないと感じました。
ゲームのコントローラはボタンを押した結果をユーザに伝えたりはしません。(振動機能などはありますが)
押した結果をユーザに伝えるのは View であるモニタの役目です。
こう考えると Controller は戻り値など返さず、入力された情報だけに集中する方が自然ではないのでしょうか。クリーンアーキテクチャの目標のひとつは特定の技術からの独立です。
それがアプリケーションの防衛に繋がるからです。特定の技術に依存した形でソフトウェアを開発することはある種の危険性をはらみます。
もしもフレームワークが廃れ、別のフレームワークに乗せ換えることになったらどうなるか。
データ永続化装置がアーキテクチャレベルで変更することになったらどうすればよいのか。こういったリスクから距離を取るための手法として、クリーンアーキテクチャはよい選択肢になるのではないでしょうか。
- 投稿日:2019-03-27T00:52:58+09:00
Laravelでクリーンアーキテクチャ
この記事は次の記事の続編に近い内容となっています。
もし可能であればこちらの記事を読み進める前に、次の記事をご覧いただくとより内容がわかりやすいでしょう。Laravelで実践クリーンアーキテクチャ: https://qiita.com/nrslib/items/aa49d10dd2bcb3110f22
はじめに
皆さんこんな図をご存知でしょうか。
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妥協点
冒頭でお話したとおり、以前の記事では妥協していた部分があります。
この図の Presenter と UseCaseOutputPort の部分ですね。
こちらの図であれば 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')); } }いずれのアクションもリクエストからデータを処理してレスポンスを戻すという一般的な処理です。
しかし、この処理がすでにクリーンアーキテクチャの図からはかけ離れているのです。このコードを図に表すと次のようになります。
オリジナルの図(次の図)と比べるとだいぶ形がちがいますね。
元々の図では 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()); // ここでどうにかして表示できるように通知する } }この流れを整理すると次の順序で処理が行われます。
- Controller が InputPort (UserCreateUseCaseInterface) を呼び出す
- InputPort の実装である UserCreateInteractor に処理が移譲される
- UserCreateInteractor は Presenter (UserCreateUseCasePresenter) に結果を伝える
- Presenter の実装である UseCreatePresenter に処理が移譲される
これであれば次の図の再現になっているのではないでしょうか。
それでは調査していきましょう。なにはともあれ、まずは 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 ステータスコードなどが存在しているのがわかります。
ためしにコントローラの戻り値をなくしてみるとどうなるでしょうか。
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')); // ← 試しにコメントアウト }すると先ほど存在していたコンテントやステータスコードが現れなくなりました。
コントローラの戻り値がここに利用されているのは確定のようです。リクエストとレスポンスの流れがわかったところで、今度はコントローラの戻り値の具体的な生成方法を確認してみましょう。
コントローラの戻り値はview()という関数の戻り値ですので、view()の実装を探してみると helpers.php のコードが見つかります。helpers.phpif (! 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);これで戻り値を返却する以外の方法でレスポンスをつくることができそうです。
これでコア部分の魔改造ができたので次節からは次の図を準備していきます。
UseCaseOutputPort (OutputBoundary)
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
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
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
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 (処理の流れ)と照らし合わせると一致することがわかるでしょうか。
で、なにが嬉しいの?
今回のコードは MVC フレームワークの常識から逸脱したコードになっています。
正直な気持ちとして「これが最高だからこれで作ろうな!」と手放しに推せるコードではないです。それでもここまでコードを書いて、なぜ Presenter のようなまだるっこしいものを採用しているのかを見出すことができた気がしたので、それを書いてみます。
GUI のプログラムのパターンに古典的 MVC (Web の MVC とは異なるので注意)というものがあります。
図にすると次のイメージになります。
古典的 MVC ではユーザの操作は Controller に伝えられ、Controller はその入力から Model の処理を呼び出します。Model の処理結果は View に通知され、View がそれを描画することでユーザに伝えられます。
このパターンにおいて、着目されるのは責務を分けることになりがちです。
しかし、それ以外にも大事な要素があって、それは処理の方向だったりします。なにかをするとき、臨機応変な対応が求められるのと決まりきった手順が決まっているのでは後者の方が簡単ですよね。
プログラムの処理の流れも場合によってはあっちへこっちへいくようなプログラムよりも、常に一方向であった方が理解しやすいものです。つまり、古典的 MVC は複雑になりがちな GUI アプリケーションにおいて、責務を分散し、処理方向を一方向にすることで理解しやすいソフトウェアを作ろうというパターンだったりします。
さて、ここで改めてクリーンアーキテクチャの Flow of Control を確認してみます。
Presenter を用意することで戻り値を用意する必要がなくなりました。
結果として処理の方向は一方向に固定されます。
処理の流れが一方向ということは理解しやすいはずです。そう考えると、戻り値を戻す従来の形よりも、今回実装した構成の方がシンプルなソフトウェアではないでしょうか。
処理を一方向に固定することに意義がある。そういった意向がここにあるのではないのかと感じた次第でした。
まとめ
正直な話、今回のコードを実装しながら思っていたのは「物議を醸しだしそうなコードだな」でした。
もちろんこれこそが正しいコードである、とは考えておりません。
封印することも考えたのですが、アイデアとしては面白く感じたため、今回の記事を投稿するに至りました。この記事でお話したとおりクリーンアーキテクチャの構成を MVC フレームワークにあてはめてみるとかなり違和感を感じます。
MVC フレームワークの大前提を覆すようなコードになっていると言っても過言ではないと思います。しかし Controller をゲームのコントローラとして見立ててみると案外素直に受け入れられる可能性もなくはないと感じました。
ゲームのコントローラはボタンを押した結果をユーザに伝えたりはしません。(振動機能などはありますが)
押した結果をユーザに伝えるのは View であるモニタの役目です。
こう考えると Controller は戻り値など返さず、入力された情報だけに集中する方が自然ではないのでしょうか。クリーンアーキテクチャの目標のひとつは特定の技術からの独立です。
それがアプリケーションの防衛に繋がるからです。特定の技術に依存した形でソフトウェアを開発することはある種の危険性をはらみます。
もしもフレームワークが廃れ、別のフレームワークに乗せ換えることになったらどうなるか。
データ永続化装置がアーキテクチャレベルで変更することになったらどうすればよいのか。こういったリスクから距離を取るための手法として、クリーンアーキテクチャはよい選択肢になるのではないでしょうか。











