Laravel を Docker で使ってみよう
今更ながらにプログラミング熱が高まっているので、GW中に Webアプリケーションでも作成してみようと、PHP の Laravel 環境を作ってみることとした。ローカル環境を汚したくないので Docker を利用。 Laradock のような作成済み環境もあったが、PHP自体にも初めて触るので、いちから作成してみる。
Docker for Mac はインストール済みの前提とする。 Docker はコンテナ型仮想化環境。VMware や VirtualBox などの仮想環境に比べて軽量かつDockerfileという簡単な設定ファイルからコンテナイメージファイルを作成できるようにすることで手順書がほとんどいらなくなる。いわゆる Infrastructure as Code の根幹である。
Dockerfile からコンテナイメージを作成
FROM centos:latest RUN yum update -y RUN yum -y install aws-cli autoconf \ && yum -y install git vim unzip wget httpd \ && yum -y install libzip libzip-devel opnessl RUN yum -y install epel-release \ && yum -y install http://rpms.famillecollet.com/enterprise/remi-release-7.rpm \ && yum search php72 \ && yum -y install php72 RUN ln -s /usr/bin/php72 /usr/bin/php RUN yum -y install php-openssl php72-php-mbstring php72-php-pdo php72-php-gd php72-php-dom php72-php-mysql RUN curl -sS https://getcomposer.org/installer \ | php -- --install-dir=/usr/local/bin --filename=composer RUN composer global require "Laravel/installer=~1.1" RUN composer global update WORKDIR /var/www RUN composer create-project laravel/laravel laravelapp --prefer-dist WORKDIR /var/www/laravelapp RUN composer update CMD php artisan serve --host=0.0.0.0 --port=8000
上記を、 Dockerfile というファイル名で保存(各コマンドの意味は後述)。
$ docker build -t my-php-app .
Dockerfile のビルドを実施。
$ docker run --name phpapp -d -p 50000:8000 my-php-app
ビルドされたイメージを起動を phpapp という名前(--name)でバックグラウンド起動(-d)。Docker 側のホストマシン側のポート番号50000 を Docker側の 8000 番に接続。
できた。一度このような Dockerfile ができれば次からやるときは誰がやっても簡単に環境を作成できるようになるが、実際にイメージファイルを作るまではそれなりに大変だった。
Dockerfile の詳説
CentOS のイメージを取得
FROM centos:latest RUN yum update -y RUN yum -y install aws-cli autoconf \ && yum -y install git vim unzip wget httpd \ && yum -y install libzip libzip-devel opnessl
まずは Cent OS の最新バージョンイメージをダウンロードしてきて必要ライブラリをインストール。Cent OS だから大変だったんじゃないか説があるが、エンタープライズ領域だと Cent OS / RHELに決め打ちされる可能性が高いことを念頭においた。 これだけで Cent OS の環境の作れてしまう時代。
PHP 7.2 をインストール
RUN yum -y install epel-release \ && yum -y install http://rpms.famillecollet.com/enterprise/remi-release-7.rpm \ && yum search php72 \ && yum -y install php72 RUN ln -s /usr/bin/php72 /usr/bin/php RUN yum -y install php-openssl php72-php-mbstring php72-php-pdo php72-php-gd php72-php-dom
PHP 7.2 と Laravel に必要なPHPエクステンションをインストール。 Cent OS の通常リポジトリには PHP の最新版が入っていないため、いわゆるRemi リポジトリを追加する必要があるとのこと。必要な Extension は Laravel公式ドキュメント を参照。
Composer をインストール
RUN curl -sS https://getcomposer.org/installer \ | php -- --install-dir=/usr/local/bin --filename=composer
Laravel をインストールする前段階として、 PHP のパッケージ管理プログラムの Composer をインストール。
ここからは上記参考書を元に作成。
Laravel をインストール
RUN composer global require "Laravel/installer=~1.1" RUN composer global update
Composer からLaravel をインストールして update。 installer=~1.1
を入れなかったり、 update をしないとサンプルアプリケーション作成時に落ちる。
Laravel プロジェクトの作成と起動
WORKDIR /var/www RUN composer create-project laravel/laravel laravelapp --prefer-dist WORKDIR /var/www/laravelapp RUN composer update CMD php artisan serve --host=0.0.0.0 --port=8000
laravelapp というプロジェクトを作成して artisan というテストサーバーを起動。通常通りに artisan を起動して 127.0.0.1 に割り当てるとホストマシンからの参照で ERR_EMPTY_RESPONSE が出るため、 0.0.0.0 に設定。ここの試行錯誤に一番時間がかかったりもした。
プロジェクトをローカルで編集して反映
Docker イメージは 状態を持たないため、起動するたびに作成時の状態に戻る Immutable Infrastructure という性質を持っている。このため、プロジェクトのファイルをローカルにもってきてから編集し、起動時直後にローカル側のファイルを反映する仕組みとする。
$ docker cp phpapp:/var/www/laravelapp ./
Docker上のファイルをコピーするのは docker cp コマンドで行う。サンプルプロジェクトの View テンプレートは laravelapp/resources/views
で管理されているので、適当に編集。
$ docker cp laravelapp/ phpapp:/var/www/
ローカルファイルの内容を Docker 側のファイルに反映。
編集内容が画面に反映された。ファイル反映を再起動スクリプト等に書いておくことでローカル側の編集内容が Docker 側に自動反映されるようにしておく。
docker stop phpapp docker rm `docker ps -a -q` docker run --name phpapp -d -p 50000:8000 my-php-app docker cp laravelapp/ phpapp:/var/www/
本来的には git から最新のソースを取ってくるようにしたり、DBサーバーをたてたりすべきだが、これ以上の Yak Shaving を続けるのも本筋からずれるので、まずは参考書を読み進められる状態にするまで。自分で手を動かすと意外に大変だな。
ルーティング機能を理解する
Webサービスを作るのにあたって、最初に理解すべきなのが URL パターンと起動処理の対応定義であろう。 Laravel では routes/web.php
に、作成されているとのこと。
<?php Route::get('/', function () { return view('welcome'); });
一般的にこの手の定義は XML や JSON で作られると思われるが、web.php
の中身はプログラムそのもの。この例では /
に GET メソッドでアクセスしたら view('welcome') の結果を返却する。routes/channels.php
などもあるが、まずは web.php
を使えばよさそう
テンプレートエンジン blade を使ってみる
welcome
を引数に view 関数を利用すると blade という Laravel 組み込みのテンプレートエンジンが起動され、 laravelapp/resources/views/welcome.blade.php
をレンダリングした結果を返す。ディレクトリやファイル名は規約的に決まっているようだ。 welcome.blade.php
の中身の一部は以下の通り。
@if (Route::has('login')) <div class="top-right links"> @auth <a href="{{ url('/home') }}">Home</a> @else
html を中心としながら if 文や変数展開がある感じ。Webアプリケーションは html を出力する際の print 文地獄を回避するために、テンプレートエンジンが整備される必然性があるのだろう。
ルーティングとテンプレートエンジンを組み合わせる
上記を踏まえて hello.blade.php
というテンプレートを追加してみる。
<!doctype html> <html lang="{{ app()->getLocale() }}"> <head> <meta charset="utf-8"> <style> .flex-center { align-items: center; display: flex; justify-content: center; } </style> </head> <body> <div class="flex-center">Hello World !!</div> </body> </html>
このテンプレートエンジンの起動と /hello
というパスを web.php
で紐付ける。
<?php Route::get('/hello', function() { return view('hello'); });
ファイルをサーバーに反映して http://localhost:50000/hello
にアクセス。
でてきた。こういうのって素直に嬉しい。view
関数の結果はあくまで文字列なので、 web.php
内で定義する関数の戻り値に直接 <html>〜
とやってもよいが地獄なので試さない。
ルートパラメータの利用
ルートを定義する際には {パラメータ名}
でにはURL文字列の一部をパラメータとして利用することができる。 {パラメータ名?}
とすることで必須項目ではなくなり、関数側でデフォルト値が定義可能。パラメータは view の第二引数に連想配列として渡すことで、テンプレートエンジンから利用できる。
<?php Route::get('/hello/{target?}', function($target='world') { return view('hello', ['target' => $target]); });
<body> <div class="flex-center">Hello {{ $target }} !!</div> </body>
URL文字列を変数としてスムーズに扱えると、いわゆる RESTful API が作りやすくなるので、なかなかよさそう。
コントローラーを作成する
model: アプリケーションデータ、ビジネスルール、ロジック、関数
view: グラフや図などの任意の情報表現
controller: 入力を受け取りmodelとviewへの命令に変換する
Model View Controller - Wikipedia
自分の知ってるMVCモデルではビジネスロジックはモデルの責務だと考えているが、Laravel における MVC モデルにおいてはモデルはあくまでDBアクセスに特化してコントローラー側に処理を書いていくのが一般的rしい。php artisan make:controller
コマンドでコントローラーの雛形が作成可能。バックグラウンド起動している Docker コンテナに命令するには docker exec
を利用する。
$ docker exec phpapp php artisan make:controller HelloController
$ docker cp phpapp:/var/www/laravelapp/app laravelapp/
上記のコマンドで、HelloController
というコントローラークラスを生成してをローカルに取得。 確認するとapp/Http/Controllers/HelloController.php
が作成されている。大したファイルではないので、手動で作った方が早いか。
コントローラーをルーティングに割り当てる
HelloController.php
を以下のように編集。追加した関数の責務も html の返却なので、 view
を使って blade を起動させる。ついでに、現在日付を表示させてみる。
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class HelloController extends Controller { public function hello($target='world') { return view('hello', ['target' => $target, 'date' => date("Y/m/d")]); } }
web.php
に クラス名@関数名
形式で URLに対応して起動するアクションを定義。
<?php Route::get('/hello/{target?}', 'HelloController@hello');
URLに定義したパラメータは対応する関数を実行する際の引数となる。
シングルアクション定義
クラスに定義される関数が一種類の場合には __invoke
関数として定義することで、ルーティング定義をクラス名だけに省略できる。
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class HelloController extends Controller { public function __invoke($target='world') { return view('hello', ['target' => $target, 'date' => date("Y/m/d")]); } }
<?php Route::get('/hello/{target?}', 'HelloController@hello');
Laravel で HTTP リクエストと HTTP レスポンスを利用する
ブラウザとサーバーの間でやりとりされるリクエストとレスポンスは、引数の型指定でそれぞれ、Request
と Response
を追加しておくことで、 web.php
で特に引数定義しなくても利用可能となる。ちょっとキモい。
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Http\Response; class HelloController extends Controller { public function __invoke(Request $req, Response $res, $target='world') { return view('hello', ['target' => $target, 'url' => $req->url(), 'status' => $res->status()]); } }
<!doctype html> <html lang="{{ app()->getLocale() }}"> <head> <meta charset="utf-8"> <style> .flex-center { align-items: center; display: flex; justify-content: center; } </style> </head> <body> <div class="flex-center">{{ $status }} Hello {{ $target }} !!</div> <div class="flex-center">{{ $url }}</div> </body> </html>
View が返しているのも厳密には html ではなく、レンダリング結果が格納されたResponse オブジェクトだそう。ここまでの開発でブラウザからのリクエストを受け取り、それに応じて処理を切り分けることが可能となった。
view メソッドのテンプレートで探索ディレクトリを指定
view
メソッドでは view('ディレクトリ名.ファイル名')
形式で呼び出し先の resources/views
以下に探索ディレクトリを指定できる。機能ごとにディレクトリを分けるのが一般的だそう。テンプレートファイルには PHP の構文をそのまま入れることもできるが、地獄しか見えないので Controller 側で必要最低限の連想配列を生成して起動した方がよさそう。
Laravel でURLクエリストリングの取得と表示
GET メソッドの末尾につける ?q1=x86&q2=z80
といったURLクエリストリングについては、 Request
オブジェクトの query
メソッドから取得できる。(参考:HTTPリクエスト 5.6 Laravel)
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Http\Response; class HelloController extends Controller { public function __invoke(Request $req, Response $res, $target='world') { $param = [ 'target' => $target, 'url' => $req->url(), 'query' => json_encode($req->query()), 'q1' => $req->query('q1', 'Empty'), 'status' => $res->status() ]; return view('hello.top', $param); } }
引数を指定しないと連想配列形式ですべてを取得。1つ目の引数で名前、2つ目の引数でデフォルト値を指定可能。isset 三項演算子などの定型文を書く必要がないのが楽。連想配列そのままはテンプレートに渡せないようなので、 JSON 文字列に変換している。
できた。
Laravel で入力フォーム
POSTメソッドを呼び出す入力フォームをテンプレートに追加する。Laravel では {{ csrf_field() }}
を入れておくことで、クロスサイトリクエストフォージェリと呼ばれる他サイトを経由した入力を防ぐためのトークンを発行することができる。
<form method="POST" action="/hello"> {{ csrf_field() }} <input type="text" name="target" value="" /> <input type="submit" name="入力" /> </form>
ルーティングを設定して、GET と POST で呼び出す関数を切り分ける。
<?php Route::get('/hello/{target?}', 'HelloController@doGet'); Route::post('/hello', 'HelloController@doPost');
doPost()
にフォームからの入力を受け取ってテンプレート用の連想配列に格納する処理を追加する。
<?php /* 中略 */ public function doPost(Request $req, Response $res) { $target = $req->input('target') != null ? $req->input('target') : 'wolrd'; $param = [ 'target' => $target, 'url' => $req->url(), 'query' => json_encode($req->query()), 'q1' => $req->query('q1', 'Empty'), 'status' => $res->status() ]; return view('hello.top', $param); }
$req->input('target')
で <input name="target"〜
の入力内容を取得可能。 query()
と同様に二つの目の引数でデフォルト値を設定可能であるが、「入力値が存在しない時」≠「空文字入力」であるため、期待通りには動かない。
Laravelのデフォルトグローバルミドルウェアスタックには、TrimStringsとConvertEmptyStringsToNullミドルウェアが含まれています。これらのミドルウェアは、App\Http\Kernelクラスにリストされています。これらのミドルウェアは自動的にリクエストの全入力フィールドをトリムし、それと同時に空の文字列フィールドをnullへ変換します。これにより、ルートやコントローラで、ノーマライズについて心配する必要が無くなります。
HTTPリクエスト 5.6 Laravel
Laravel におけるデフォルト中間処理で入力フィールドのトリムと空文字列の null 化が自動的に行われるため、 null チェックのみが正解。ここで empty()
判定をしてしまうと数値の 0
が空文字列判定されて正しく動かない。PHPェ。
blade テンプレートにおける制御構文
view
関数で呼び出す blade テンプレート内には @if
などの判定制御ディレクティブを書くことができる。
@if(strval($target) != null) <div class="flex-center">if target = {{ $target }}</div> @else <div class="flex-center">not if target = {{ $target }}</div> @endif @isset($target) <div class="flex-center">isset target = {{ $target }}</div> @else <div class="flex-center">not isset target = <{{ $target }}/div> @endisset @empty($target) <div class="flex-center">empty target = {{ $target }}</div> @else <div class="flex-center">not empty target = {{ $target }}</div> @endempty
@empty
は案の定で target=0
が空文字判定されてしまうので使いどころが難しい。@if
ディレクティブは権限に応じた機能トグルのON/OFFをイージーに実装するのに使えるだろう。繰り返しディレクティブも可能。
@forelse ($data as $d) <p>{{ $d }}</p> @empty <p>なーんもない</p> @endforelse
N件のデータを取得した場合に行列の要素を取り出して処理する用途が思い浮かぶ。 @for
や @while
など一通りのディレクティブがあるが、 @forelse
が便利。件数が0件だった場合の処理を @empty
に書けるので、@if ($data->count() >= 1)
的な入れ子制御が不要となる。foreach
構文の引数の順番が JavaScript や Python などと逆で気持ち悪い。blade テンプレート内で PHP自体 を書けるディレクティブもある。
@php /* PHPスクリプト */ @endphpt
制御処理自体はなるべくコントローラー側で吸収すべきであろうが、画面内の特定要素の出し分け制御や繰り返し表示はテンプレート側に書いた方が直感的。ブラウザサイドの JavaScript でこの手の制御をすると、ブラウザ毎の単体テストや事象再現が複雑になりがちなのでサーバーサイドで出力が確定できるのがシンプルでよい。
レイアウトの継承と可変部分の定義
このテンプレート処理では HTML 全体の定義を書いてきたが、実際のアプリケーションを作成する場合にはヘッダ・フッタ・メニューなどの使い回し要素が発生する。 blade ではベースとなるレイアウトに @yeild
と @section
で可変部分を定義して、個別レイアウト側で可変部分を補完するようなことができる。
views/layouts/base.blade.php
<!doctype html> <html lang="{{ app()->getLocale() }}"> <head> <meta charset="utf-8"> <style> .flex-center { align-items: center; display: flex; justify-content: center; } </style> <title>@yield('title')</title> </head> <body> @section('menu') <ul> <li><a href="/hello">Hello</a></li> <li>@show</li> </ul> @yield('contents') copyright 2018 bulldra. </body> </html>
views/hello/top.blade.php
@extends('layouts.app')
で上記ベースレイアウトを取得。
@extends('layouts.base') @section('title','hello') @section('menu') @parent 追加メニュー @endsection @section('contents') <form method="POST" action="/hello"> {{ csrf_field() }} <input type="text" name="target" value="" /> <input type="submit" name="入力" /> </form> @forelse ($data as $d) <p>{{ $d }}</p> @empty <p>なーんもない</p> @endforelse @endsection
ベースレイアウト側で @yeild
を定義すると、個別レイアウト側で定義した同名の @section
で置き換えられる。ベースレイアウト側で定義した @section
は個別レイアウト側の同名 @section
で 呼び出して @parent
でベースレイアウト側の定義を呼び出しながら @show
部分を書き換えられる。 @parent
を使わない場合は単純な上書きとなるので、 @yeild
の動作と変わらない。
コンポーネントとサブビューの組み込み
ベースレイアウトを作成する方法は便利だが、警告メッセージなどの特定状態で表示される部品を定義するには適さない。このような場合には部品として定義したテンプレートをコンポーネントまたはサブビューとして利用する。
views/components/error.blade.php
<style> .message { border:double 3px; } </style> <div class="message">ERROR! {{ $err_message }} </div>
views/hello/top.blade.php
@extends('layouts.base') @section('title','hello') @section('menu') @parent 追加メニュー @endsection @section('contents') <form method="POST" action="/hello"> {{ csrf_field() }} <input type="text" name="target" value="" /> <input type="submit" name="入力" /> </form> @forelse ($data as $d) <p>{{ $d }}</p> @empty <p>なーんもない</p> @endforelse @component('components.error') @slot('err_message') ダメダメ1 @endslot @endcomponent @component('components.error', ['err_message'=>'ダメダメ2']) @endcomponent @php $err_message = 'ダメダメ3' @endphp @include('components.error') @endsection
@component
でテンプレート部品を指定して、 @slot
内で引数を定義する。 @include
を利用した場合には呼び出し元の変数がそのまま利用できるので、マクロ展開されているイメージ。継承とコンポーネントの関係はオブジェクト指向における継承と委譲の関係と理解。
Note: @eachを使ってレンダされるビューは、親のビューから変数を継承しません。子ビューで親ビューの変数が必要な場合は、代わりに@foreachと@includeを使用してください。
Bladeテンプレート 5.6 Laravel
@each
によるコンポーネントの繰り返し構文もあるが、殆んどのシーンでは @foreach
を使うべきだと思われるので割愛。
PHP Laravel で MySQL コンテナに接続したい
ここまででテンプレートの使い方を学習していたが、具体的なデータ操作がないと飽きてしまうので、先に MySQL のコンテナを作成してPHP Laravel のコンテナから 接続できるようにしたくなった。
複数の Docker コンテナを管理するには docker-compose が便利。 docker-compose.yml
の定義から複数のコンテナを作成してリンク定義が行える。今回の docker-compose.yml
は以下の通りに作成する(password は任意に変更)。
version: '2' services: mysql: image: "mysql:5.7" container_name: docker_mysql environment: MYSQL_ROOT_PASSWORD: root_password MYSQL_DATABASE: myapp_db MYSQL_USER: myapp_user MYSQL_PASSWORD: password volumes: - ./db/mysql_data:/var/lib/mysql command: > mysqld --character-set-server=utf8 --collation-server=utf8_bin --skip-character-set-client-handshake ports: - "3306:3306" phpapp: build: ./app/ container_name: docker_app ports: - "50000:8000" links: - mysql cli: image: "mysql:5.7" container_name: docker_cli environment: MYSQL_ROOT_PASSWORD: notuse links: - mysql
docker-compose では image
が docker pull
の役割をする。 mysql
コンテナは myapp_db
に myapp_user
で接続する初期設定を行った MySQL の本体。volumes
の定義を行うことで、ローカル側のファイルシステムに MySQL のデータを格納してデータの永続化をさせている。
phpapp
は Laravel コンテナ作成時の Dockerfile ディレクトリを指定。Docker 同士のDBテスト接続のために、 cli
も作成しておく。それぞれの links
の定義で Docker コンテナ間の接続許可を定義している。
MySQL の起動確認とリモート接続確認
docker-compose で一括バックグラウンド起動。
$ docker-compose up -d
起動された MySQL コンテナにの sh に入ってローカルの MySQL に接続。
$ docker exec -it docker_mysql bash root@0bf0c8305cc0:/# mysql -u root -p Enter password:
データベース一覧を取得して、環境変数に設定したDBが作成されていることを確認。
mysql> show databases -> ; +--------------------+ | Database | +--------------------+ | information_schema | | myapp_db | | mysql | | performance_schema | | sys | +--------------------+ 5 rows in set (0.05 sec)
作成ユーザーと作成DBで改めて接続。
root@0bf0c8305cc0:/# mysql -u myapp_user -p myapp_db Enter password:
MySQL コンテナの仮想 IP アドレスを取得。
root@0bf0c8305cc0:/# exit $ docker inspect docker_mysql | grep IPAddress
cli コンテナに接続して MySQL コンテナへのリモート接続を確認。
$ docker exec -it docker_cli bash root@4d53c5870591:/# mysql -h 172.19.0.x -u myapp_user -p myapp_db Enter password:
ここままで Docker コンテナ同士でのDB接続ができることを確認した。全てがうまく行ってる前提であれば必要ないけど、ひとつひとつ確認していった方が安全。自分の場合はマウントしたボリュームにファイルが残っていると環境変数を設定してもDBが作られない事象にちょっとハマった。
テストデータの作成とローカル接続
Port 転送設定をしているため、ホストマシンのSQLクライアントから 127.0.0.1:3306 で接続可能。テストテーブルとテストデータを作成しておく。
create table company ( `company_id` bigint(20) unsigned primary key auto_increment, `company_name` text not null, `mail_address` text ); insert into company (company_name, mail_address) values ('現代観光株式会社', 'aaa@bbb'), ('現代日本株式会社', 'abc@bbb'), ('現代空間株式会社', 'aa@babb'), ('株式会社現代視覚', 'aaca@babb'), ('株式会社幽霊的身体', 'acaa@bcbb') ;
MySQL の Docker イメージで日本語を扱えるようにするために、 mysqld
コマンドでの文字コード指定を docker-compose.yml
内に定義している。ちなみに utf8_unicode_ci
は MySQLの照合順序 - Qiita で挙げられているような面白曖昧検索になるので使うな派。
自分でビルドするイメージなら Dockerfile にコマンドを書き込んでいけばよいのだけど、公式イメージを外からカスマイズするための前提知識が辛かった。断片情報を集めるよりも、 library/mysql - Docker Hub の公式情報を読み込むべき。
Cent OS の Laravel コンテナから MySQL に接続する
Laravel のデータベース定義は config/database.php
にあるので、こちらをよしなに編集するだけ……だとうまくいかなくて、 .env 内の以下サンプル定義を削除しないとそちらが優先されるようだ。
DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=homestead DB_USERNAME=homestead DB_PASSWORD=secret
.env を編集するだけではキャッシュが優先されるため、 artisan のキャッシュクリアコマンドを実行しておく。
docker exec docker_app php artisan config:cache
ここまでの準備をすれば Laravel から MySQL に接続できると思ったが、 could not find Driver
が発生。 phpinfo() を確認すると、PDO drivers
が sqlite にのみ対応。 Laravel アプリ作成時点で php72-php-mysql
が入っていなかったのが問題だったようだ。Dockerfile の yum を編集して Docker イメージをビルドし直し。無事に接続。
Laravel のクエリビルダーを利用する
Controller の POST メソッドで MySQL データの取得をテストしてみる。
public function doPost(Request $req, Response $res) { $target = $req->input('target') ?? ''; $items = DB::table('company')->where('company_name', 'like', '%' . $target . '%')->get(); $param = [ 'target' => $target, 'items' => $items ]; return view('hello.top', $param); }
DB:table
でテーブル名と where 句を指定して、get()
で取得。レコードオブジェクトの配列が作成されるので、 blade テンプレートに受け渡す。
@section ('table') <table> <tr> <th>company_id</th> <th>company_name</th> <th>mail_address</th> </tr> @forelse ($items as $i) <tr> <td>{{ $i->company_id }}</td> <td>{{ $i->company_name }}</td> <td>{{ $i->mail_address }}</td> </tr> @empty <tr><td colspan="3">なーんもない</td></tr> @endforelse </table> @endsection
前回学習した テンプレート内の繰り返し構文でレコードオブジェクトを繰り返し表示。
とりあえず超簡単な検索機能が実装できた。Web + DB のコンテナ間通信が実現できるとやってる感がでてきてよい感じ。ゆっくりながらも進んでいこう。
Laravel で RESTful API 作成
いまどきの Single Page Application 構成を見据えて View は React で作ることにしたため、 Laravel では API を通じたデータの入出力に専念させたい。このような用途に利用されるのが、 RESTful API である。RESTful API とは REpresentational State Transfer の原則に基づいて設計された API で一意の URI と HTTP メソッドによってリソースへのアクセスを提供する。CRUD と HTTP メソッドの関係は以下の通り。
CRUD | 意味 | メソッド | 備考 |
---|---|---|---|
Create | 作成 | POST / PUT | 指定IDが存在しなれば新規作成する更新(いわゆる UPSERT) は PUT |
Read | 読み込み | GET | Content-Type: application/json 形式で取得 |
Update | 更新 | PUT | 更新後の結果を返却する |
Delete | 削除 | DELTE | 論理削除は PUT( GEC の LB が対応してないので DELTE は使わないほうよいそう ) |
これらの処理を Laravel で実装していく。
artisan で REST コントローラーを生成
Laravel では artisan によって RESTful API に適したコントローラーのテンプレートを生成することができる。
$ docker exec -it docker_web bash [root@2665067dae23 laravelapp]# php artisan make:controller CompnayApiController --resource
上記コマンドを実行すると、 app/http/Controllers
内に指定したコントローラーが生成されているので、 DB へのアクセス処理を書いていく。
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; class CompanyApiController extends Controller { public function index() { $item = DB::table('company') ->orderBy('company_id','desc') ->get(['company_id','company_name']); return response(json_encode($item), 200) ->header('Content-Type', 'application/json'); } public function show($id) { $item = DB::table('company') ->where('company_id', $id) ->first(['company_id','company_name']); if($item !== null) { return response(json_encode($item), 200) ->header('Content-Type', 'application/json'); } else { return response('', 404); } } }
それぞれを web.php で紐付ける。
<?php Route::get('/', function () { return view('welcome'); }); Route::get('/api/company', 'CompanyApiController@index'); Route::get('/api/company/{id}', 'CompanyApiController@show');
このようにすることで、 {endpoint}/api/company/
への GET アクセスで一覧データ、{endpoint}/api/company/{id}
なら個別データを JSON で戻せるようになる。
さらに create()
では MySQL への Insert を行って自動採番されたIDを含むレコードを返却させる。 insertGetId($param);
が便利。
public function create(Request $req) { $param = [ 'company_name' => $req->input('company_name') ]; $last_id = DB::table('company')->insertGetId($param); $item = [ 'company_id' => $last_id, 'company_name' => $req->input('company_name') ]; return response(json_encode($item), 201); }
こちらは POST メソッドに紐付ける。
Route::post('/api/company', 'CompanyApiController@create');
POSTを実行する際にはデフォルトで CSRF 対策が必須となっているが、 状態を持たない RESTful API において CSRF のトークンを求めるのは適さないため、 app/http/Middleware/VerifyCsrfToken.php
に以下の記述を行って除外対象とする。
protected $except = [ 'api/*' ];
curl と jq で Restful API のテスト
GETメソッドであればブラウザから直接アドレスを叩けばよいが、 POST についてブラウザが生成するのは面倒であるため curl コマンドと jq コマンドのテストを行う。 jq については homebrew からインストールしておく必要がある。
$ curl localhost:50000/api/company | jq -r % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 422 0 422 0 0 135 0 --:--:-- 0:00:03 --:--:-- 135 [ { "company_id": 5, "company_name": "株式会社幽霊的身体" }, { "company_id": 4, "company_name": "株式会社現代視覚" }, { "company_id": 3, "company_name": "現代空間株式会社" }, { "company_id": 2, "company_name": "現代日本株式会社" }, { "company_id": 1, "company_name": "現代観光株式会社" } ]
POSTコマンドを実行する。-X
でメソッド -H
リクエストヘッダを指定してペイロードを -d
で指定している。
$ curl localhost:50000/api/company -X POST -H 'Content-Type:application/json' -d '{"company_name":"追加株式会社"}' | jq -r % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 108 0 71 100 37 21 11 0:00:03 0:00:03 --:--:-- 21 { "company_id": 77, "company_name": "追加株式会社" }
データベースを覗くと上記のデータが登録されている。エラーハンドリングなどの課題が残っているが、とりあえずは表示と登録の API が動くことを確認した。次回はこの API を React から利用することは考える。