太陽がまぶしかったから

C'etait a cause du soleil.

Docker for Mac で PHP 7.2 x Laravel 5.6 のコンテナを作成して開発

PHPフレームワークLaravel入門 第2版

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 番に接続。

f:id:bulldra:20180501141846j:plain

 できた。一度このような 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 をインストール。

PHPフレームワークLaravel入門 第2版

PHPフレームワークLaravel入門 第2版

 ここからは上記参考書を元に作成。

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 側のファイルに反映。

f:id:bulldra:20180501144051p:plain

 編集内容が画面に反映された。ファイル反映を再起動スクリプト等に書いておくことでローカル側の編集内容が 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 にアクセス。

f:id:bulldra:20180501204422p:plain

 でてきた。こういうのって素直に嬉しい。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>

f:id:bulldra:20180501205759j:plain

 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 レスポンスを利用する

 ブラウザとサーバーの間でやりとりされるリクエストとレスポンスは、引数の型指定でそれぞれ、RequestResponse を追加しておくことで、 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>

f:id:bulldra:20180501221601j:plain

 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 文字列に変換している。

f:id:bulldra:20180502080416j:plain

 できた。

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 では imagedocker pull の役割をする。 mysql コンテナは myapp_dbmyapp_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_ciMySQLの照合順序 - 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

 前回学習した テンプレート内の繰り返し構文でレコードオブジェクトを繰り返し表示。

f:id:bulldra:20180504082352j:plain

 とりあえず超簡単な検索機能が実装できた。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 から利用することは考える。

PHPフレームワークLaravel入門 第2版

PHPフレームワークLaravel入門 第2版