- 投稿日:2019-03-27T22:42:20+09:00
Laravel + Nuxt +Fargate にCodePipelineで継続的デプロイを実現する
前提
Laradock + Nuxt(SSRモード)をAWS Fargateで実行する
こちらでFargateで起動できていること
やりたいこと
githubのソースコードがmasterブランチにpushされた際にCodeBuildでDockerイメージをビルドし
Fargateにデプロイを自動化する
できればテストも実施する参考
LaravelアプリケーションをローカルでもAWSでもDockerで動かす
LaravelアプリケーションをCodePipeline/CodeBuildでECSに自動デプロイする
Laravelのセッション管理にRedisを指定し、AWSのElastiCacheを利用する
手順
- リポジトリにDocker関連のファイルを追加し修正する
- buildspec.ymlを追加する
- ソースをpushしデプロイできるか確認
- buildspec.ymlにS3から環境に合わせた.envファイルを取得するコマンドを追加
- 再度デプロイし、DBとRedisキャッシュから値を取得できるか確認する
1. リポジトリにDocker関連のファイルを追加し修正する
Dockerイメージをビルドするために必要なファイルをlaradockからコピーしてくる
mkdir docker cp -r ../laradock/nginx ./docker/nginx cp -r ../laradock/php-fpm ./docker/php-fpm cp ../laradock/docker-compose.production.yml docker-compose.production.ymlコピーしてきたらDockerfile-productionとdocker-compose.production.ymlのパスを修正する
/{projectname}/docker/nginx/Dockerfile-production
COPY ./docker/php-fpm/laravel.ini /usr/local/etc/php/conf.d COPY ./docker/php-fpm/xlaravel.pool.conf /usr/local/etc/php-fpm.d/# php.iniをコンテナに配置する COPY ./docker/php-fpm/php7.2.ini /usr/local/etc/php/php.ini# ソースコードをコンテナにコピーする COPY . ./{projectname}/docker/nginx/Dockerfile-production
# nginxの設定ファイルをコンテナ内に配置する COPY ./docker/nginx/nginx.conf /etc/nginx/ COPY ./docker/nginx/sites/ /etc/nginx/sites-available/ COPY ./docker/nginx/ssl/ /etc/nginx/ssl/ COPY ./docker/nginx/sites/default.conf /etc/nginx/conf.d/# ソースコードをコンテナにコピーする COPY . .docker-compose.production.yml
version: '3' services: ### PHP-FPM ############################################## php-fpm: build: context: . dockerfile: ./docker/php-fpm/Dockerfile-production # args: # - LARADOCK_PHP_VERSION=${PHP_VERSION} expose: - "9000" ### NGINX Server ######################################### nginx: build: context: . dockerfile: ./docker/nginx/Dockerfile-production ports: - "80:80" - "443:443" depends_on: - php-fpmビルドと同時にDockerコンテナを起動してみる
docker-compose -f docker-compose.production.yml build --no-cache nginx php-fpm docker-compose -f docker-compose.production.yml up -d nginx2. buildspec.ymlを追加する
build時の実行内容を記載する
touch buildspec.ymlbuildspec.yml
version: 0.2 phases: pre_build: commands: - echo Logging in to Amazon ECR... - aws --version - $(aws ecr get-login --no-include-email --region ${AWS_DEFAULT_REGION}) ## イメージ名を定義 - IMAGE_NAME_PHP_FPM=laradock_php-fpm - IMAGE_NAME_NGINX=laradock_nginx ## nginxとphp-fpmコンテナイメージのECRリポジトリURIを生成する - REPOSITORY_URI_PHP_FPM=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${IMAGE_NAME_PHP_FPM} - REPOSITORY_URI_NGINX=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${IMAGE_NAME_NGINX} ## ここは何をしてる? - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7) - IMAGE_TAG=${COMMIT_HASH:=latest} build: commands: - echo Build started on `date` - echo Building the Docker image... ## docker-composeでbuildを行う - docker-compose -f docker-compose.production.yml build --no-cache nginx php-fpm #- docker build -t ${REPOSITORY_URI_PHP_FPM}:latest -f docker/php-fpm/Dockerfile-production . #- docker build -t ${REPOSITORY_URI_NGINX}:latest docker/nginx/Dockerfile-production . - docker tag ${REPOSITORY_URI_PHP_FPM}:latest ${REPOSITORY_URI_PHP_FPM}:$IMAGE_TAG - docker tag ${REPOSITORY_URI_NGINX}:latest ${REPOSITORY_URI_NGINX}:$IMAGE_TAG post_build: commands: - echo Build completed on `date` - echo Pushing the Docker images... - docker push ${REPOSITORY_URI_PHP_FPM}:latest - docker push ${REPOSITORY_URI_PHP_FPM}:$IMAGE_TAG - docker push ${REPOSITORY_URI_NGINX}:latest - docker push ${REPOSITORY_URI_NGINX}:$IMAGE_TAG - echo Writing image definitions file... - IMAGE_DIFINITION_PHP_FPM="{\"name\":\"${IMAGE_NAME_PHP_FPM}\",\"imageUri\":\"${REPOSITORY_URI_PHP_FPM}:${IMAGE_TAG}\"}" - IMAGE_DIFINITION_NGINX="{\"name\":\"${IMAGE_NAME_NGINX}\",\"imageUri\":\"${REPOSITORY_URI_NGINX}:${IMAGE_TAG}\"}" - echo "[${IMAGE_DIFINITION_PHP_FPM},${IMAGE_DIFINITION_NGINX}]" > imagedefinitions.json artifacts: files: imagedefinitions.jsonCodePipelineを作成する
AWS CodeBuildを作成し、プロジェクトを新規作成する
デプロイ先を「AmazonECS」を選択する
デプロイの設定を行う
ビルドが失敗するので環境変数を変更する
環境変数の定義
環境変数 AWS_ACCOUNT_ID ビルドを実行する
ビルドを実行したら下記のエラーが発生
An error occurred (AccessDeniedException) when calling the GetAuthorizationToken operation: User: arn:aws:sts::636990608596:assumed-role/codebuild-GuildApp-CodeBuild-service-role/AWSCodeBuild-c714c40b-8e5e-466d-9d97-fda2158797d4 is not authorized to perform: ecr:GetAuthorizationToken on resource: * [Container] 2019/03/26 16:04:29 Command did not exit successfully $(aws ecr get-login --no-include-email --region ${AWS_DEFAULT_REGION}) exit status 255 [Container] 2019/03/26 16:04:29 Phase complete: PRE_BUILD Success: false [Container] 2019/03/26 16:04:29 Phase context status code: COMMAND_EXECUTION_ERROR Message: Error while executing command: $(aws ecr get-login --no-include-email --region ${AWS_DEFAULT_REGION}). Reason: exit status 255ロールが足りなかったみたい
【備忘録】CodeBuildでaws ecr get-loginコマンド実行時にエラーが発生する
下記のロールをアタッチした
AmazonEC2ContainerRegistryPowerUserその後細かいエラーを解消した結果
buildspec.yml
version: 0.2 phases: pre_build: commands: - echo Logging in to Amazon ECR... - aws --version - $(aws ecr get-login --no-include-email --region ${AWS_DEFAULT_REGION}) ## ECRイメージ名を定義 - IMAGE_NAME_PHP_FPM=laradock_php-fpm - IMAGE_NAME_NGINX=laradock_nginx ## nginxとphp-fpmコンテナイメージのECRリポジトリURIを生成する - REPOSITORY_URI_PHP_FPM=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${IMAGE_NAME_PHP_FPM} - REPOSITORY_URI_NGINX=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${IMAGE_NAME_NGINX} ## ここは何をしてる? - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7) - IMAGE_TAG=${COMMIT_HASH:=latest} build: commands: - echo Build started on `date` - echo Building the Docker image... ## docker-composeでbuildを行う #- docker-compose -f docker-compose.production.yml build --no-cache nginx php-fpm - docker build -t ${REPOSITORY_URI_PHP_FPM}:latest -f docker/php-fpm/Dockerfile-production . - docker build -t ${REPOSITORY_URI_NGINX}:latest -f docker/nginx/Dockerfile-production . - docker tag ${REPOSITORY_URI_PHP_FPM}:latest ${REPOSITORY_URI_PHP_FPM}:$IMAGE_TAG - docker tag ${REPOSITORY_URI_NGINX}:latest ${REPOSITORY_URI_NGINX}:$IMAGE_TAG post_build: commands: - echo Build completed on `date` - echo Pushing the Docker images... - docker push ${REPOSITORY_URI_PHP_FPM}:latest - docker push ${REPOSITORY_URI_PHP_FPM}:$IMAGE_TAG - docker push ${REPOSITORY_URI_NGINX}:latest - docker push ${REPOSITORY_URI_NGINX}:$IMAGE_TAG - echo Writing image definitions file... ## コンテナ定義の更新するためのファイルを作成する #- IMAGE_DIFINITION_PHP_FPM="{\"name\":\"${IMAGE_NAME_PHP_FPM}\",\"imageUri\":\"${REPOSITORY_URI_PHP_FPM}:${IMAGE_TAG}\"}" #- IMAGE_DIFINITION_NGINX="{\"name\":\"${IMAGE_NAME_NGINX}\",\"imageUri\":\"${REPOSITORY_URI_NGINX}:${IMAGE_TAG}\"}" - IMAGE_DIFINITION_PHP_FPM="{\"name\":\"php-fpm\",\"imageUri\":\"${REPOSITORY_URI_PHP_FPM}:${IMAGE_TAG}\"}" - IMAGE_DIFINITION_NGINX="{\"name\":\"nginx\",\"imageUri\":\"${REPOSITORY_URI_NGINX}:${IMAGE_TAG}\"}" - echo "[${IMAGE_DIFINITION_PHP_FPM},${IMAGE_DIFINITION_NGINX}]" > imagedefinitions.json artifacts: files: imagedefinitions.json3. ソースをpushしデプロイできるか確認
4. buildspec.ymlにS3から環境に合わせた.envファイルを取得するコマンドを追加
.env配置用のS3バケットを作成し、環境毎の.envファイルを配置する
CodeBuildプロジェクトにS3にアクセスできるポリシーを追加する
下記の取得する処理とCodeBuildに環境変数も追加する
buildspec.yml
## s3からenvファイルを取得する - aws s3 cp s3://${BUCKET_NAME}/staging/env ./.env5. 再度デプロイし、DBから値を取得できるか確認する
- DB配置用のプライベートサブネットを作成する
- プライベート用のルートテーブルを作成する
- RDSサービスでサブネットグループを作成する
- RDSサービスでパラメータグループを作成する
- AuroraDBの作成を行う
- laravelのdatabase.phpの変更を行う
- CodeBuild時に
php artisan migrateを実行し、Auroraにテーブル定義を行うDB配置用のプライベートサブネットを作成する
プライベートサブネット用のルートテーブルを作成する
RDSでサブネットグループを作成する
RDSでAuroraのパラメータグループを作成する
AuroraDBの作成を行う
laravelのdatabase.phpの変更を行う
Auroraはデフォルトで読み込みと書き込み専用が作成されるため
エンドポイントを切り替える必要がある参考サイト
Laravel の DB のマスター/スレーブでマスターで SELECT したりスレーブでトランザクションしたり/config/database.php
//'host' => env('DB_HOST', '127.0.0.1'), // 'port' => env('DB_PORT', '3306'), 'read' => [ 'host' => env('DB_SLAVE_HOST', '127.0.0.1'), 'port' => env('DB_SLAVE_PORT', '3306'), ], 'write' => [ 'host' => env('DB_MASTER_HOST', '127.0.0.1'), 'port' => env('DB_MASTER_PORT', '3306'), ],S3に配置している本番環境/ステージング環境用のenvファイルをあわせて修正する
コンテナ起動時に
php artisan migrateを実行し、Auroraにテーブル定義を行うマイグレーションを行うためにコマンドを起動時に実行したいため
startup.shファイル作成し、飛び出すように修正/docker/php-fpm/Dockerfile-prodfuction
#CMD ["php-fpm"] ADD ./docker/php-fpm/startup.sh /opt/startup.sh RUN sed -i 's/\r//g' /opt/startup.sh CMD ["/bin/bash", "/opt/startup.sh"]/docker/php-fpm/startup.sh
#!/bin/bash # migrate lunch php artisan cache:clear php artisan migrate # php-fpm sever lunch php-fpm
- 投稿日: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-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-27T01:16:13+09:00
技術選定まとめ
技術選定まとめ
おはこんにちばんわ
マンハッタンコードエンジニアのがーたろーです。今回プロジェクトで技術選定を行いました。
どのような観点、要素で技術を選定するべきなのかを学んだのでまとめたいと思います。技術選定に必要なもの
観点:エンジニア、開発チームが作業するのに最適だと思えるものを選定すること
上記の最適だと思えるものには
- 目的
- メリット
- できることの比較
これらを満たしていることが前提になります。
今回選定の候補に選ばれた言語は
1. Kotlin/SpringBoot
2. ruby/Ruby on Rails
3. php/Laravelの三つから選びます。
目的
API通信、管理画面、複数のサーバーとデータ通信をするためのサーバーを作る
オニオンアーキテクチャ、DDDの理解、学習
GraphQL使いたい
テスト駆動メリット&デメリット
kotlin/SpringBoot
メリット
今回のプロジェクトではアプリ側(スマートフォン)の開発も行うため、Andoroidの開発にKotlinを用いる。
そのため学習する言語がSwift+Kotlinの2言語で済む。
DDD(ドメイン駆動開発)と親和性が高いデメリット
インフラにかかる影響の判断が他の2言語と比べて手間がかかる。(tomcatの設定周り)
サーバーでKotlin(Java)を使う場合、Tomcatが必須になる。
Tomcatを利用する場合、Nginxとは異なりユーザーの最大同時接続数の管理がスレッドをいくつまで立てられるかの設定になる。
何スレッドまで利用するのかはサーバースペックを元に試算しなければ行けないためサーバーのスペック調査
設定数のMaxギリギリまで接続した場合の挙動のテストをして性能を担保しないといけない(やること増える)Ruby/Ruby on Rails
メリット
プロジェクトにRuby on Railsを触ったことのあるエンジニアが3人いる。
ディレクトリ構造など規約があるため、設計の話し合いコストが減る。
ライブラリがとにかく便利(使ったことあるやつがそのまま使えそう)デメリット
DDDとrailsの作りがおそらくケンカする。
DDDでやりたいことがrailsに合わせないと、そもそも作れなくなるためサーバー側だけ全体と作りが変わってきてしまう。PHP/Laravel
メリット
自由
DDDと親和性がある
railsを参考に作っているためライブラリの使い方が似ていそう。
ローカルでの環境作成が容易なため、開発に入るまでのスピードが早い。デメリット
規約、設計を設けないと開発現場が魔界と化す。できることの比較
候補 サーバ設定の難易度 GraphQL DDD 規約 テスト Kotlin/SpringBoot 性能試験が必要 チューニング OK ◉ ktlint Spek Mockit Ruby/Ruby on Rails nginx(むずかしくない) OK △ Rubocop Rspec PHP/Laravel nginx(むずかしくない) OK ○ PSR phpUnit phpspefc ミドルウェアはnginxを全部使う想定。
Kotlinだけ考えること増える。
規約、テストは使うものが違うだけで問題はない。ぶっちゃけどれ使いたい?
ここまで議論した結果、要件はどれでも実現可能
学習コスト的にはKotlinサーバーの書き方はAndroidとは異なる
やったことある人が多いのはメリットだが少ないのはデメリットではない(世界で使っている人が少ないのは別)
DDD・オニオンアーキテクチャに沿った開発をしたい。やらないともったいない。
↑これに力を入れたいのでデメリット=インフラ課題を増やしたくはない上記のことを考慮した結果
選ばれたのはPHP/Laravelでした
まとめ
今回私は開発チームの一員として使用する言語の技術選定を行いました。
その中で、何を観点に持たないといけないのか、懸念点、優先すべき事柄、そういった判断基準を身に着けるいい機会を頂けました。
この話をするときにもっといろんな知識があったらもっと別の視点でも話ができたんだろうなぁ、と次にする際にはいろんな知識を身につけてから挑戦したいなぁ、と思いました。インフラって全くワカンねぇなぁ。
ファンを増やしたいのでもしこの記事いいなぁ、お前やるじゃんとか思ったらファンメッセージください!
もし会社のHPに載っけてもいいぜって人いたらどうぞよろしくお願いします!
- 投稿日: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 は戻り値など返さず、入力された情報だけに集中する方が自然ではないのでしょうか。クリーンアーキテクチャの目標のひとつは特定の技術からの独立です。
それがアプリケーションの防衛に繋がるからです。特定の技術に依存した形でソフトウェアを開発することはある種の危険性をはらみます。
もしもフレームワークが廃れ、別のフレームワークに乗せ換えることになったらどうなるか。
データ永続化装置がアーキテクチャレベルで変更することになったらどうすればよいのか。こういったリスクから距離を取るための手法として、クリーンアーキテクチャはよい選択肢になるのではないでしょうか。








































