20200227のlaravelに関する記事は11件です。

sutara79さんの「Laravel 5.7で基本的なCRUDを作る」にチャレンジした時の参考サイトまとめ

sutara79さんの「Laravel 5.7で基本的なCRUDを作る」にチャレンジした時の参考サイトまとめ

メイン
https://laraweb.net/

Laravel 6系でmake:authを使う方法
https://qiita.com/rei67/items/d6d0f5f6e58edbb17c09

ヘルパー関数 まとめ(asset etc..)
https://laraweb.net/knowledge/835/

viewメソッドの使い方
http://tektektech.com/views-helper/326/

翻訳文字列の取得(__メソッド)
https://readouble.com/laravel/5.7/ja/localization.html#retrieving-translation-strings

モデル結合ルート
https://cpoint-lab.co.jp/article/201906/10008/

コンポーネントとスロット
https://webplus8.com/laravel-blade-component/
https://debuglog.net/laravel-component/

クエリビルダ
https://www.wakuwakubank.com/posts/453-laravel-sql-basic/

MySQLの外部キー制約
https://qiita.com/suin/items/21fe6c5a78c1505b19cb

explode関数で文字列を分割
https://techacademy.jp/magazine/11415

オートロード
https://laraweb.net/surrounding/1642/

自作のヘルパー関数を追加
https://s8a.jp/laravel-custom-helper

バリデーションのis-invalid
http://www.tohoho-web.com/bootstrap/forms.html

バリデーション解説
https://qiita.com/kd9951/items/e797b17c03fc8e8f414b

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

Laravel Eloquent Collection で検索機能実装

Eloquent Collection で絞り込み

Laravel で検索機能を実装すると、Eloquent Builder の where句 で絞り込むことが多いかと思います。
(下記のイメージ)

EloquentBuilderのwhere句絞り込み
$productQuery = Product::query();
if ($request->input('price')) {
    $productQuery->where('price', '=' ,$request->input('price'));
}

if ($request->input('name')) {
     $productQuery->whereHas('user.userDetail', function (Product $productQuery) use ($request) {
         $productQuery->where('name', '=', $request->input('name'));
     });
 }

$products = $productQuery->get();
dd($products->get());

今回は、Eloquent Collection の filterメソッド を使って絞り込んでみようと思います。
filterメソッドはtrueまたはfalseを返します。

EloquentCollectionの絞り込み
$productQuery = Product::cursor();

if ($request->input('price')) {
     $priceCursor = $productQuery->filter(function(Product $productQuery) use ($request) {
        return $productQuery->price === $request->input('price');
     });
    $products = $priceCursor->all();
}else{
    $products = $productQuery->all();
}

dd($products);

※間違えているところがございましたらご指摘いただけると幸いです。

Eloquent Builder と Eloquent Collection の併用

あんまりないパターンだとは思いますが、検索項目の一部のみ Collection で絞り込む場合。
基本は Eloquent Builder で統一させるはずなので、なにかしらの諸事情によりやらなければならなくなったとき用に備忘録として残します。

流れ

  1. Eloquent Builder で SQL に where 付与
  2. SQL 実行して結果を LazyCollection (cursor) で取得
  3. LazyCollection::filter() で結果をフィルタリング
1.EloquentBuilderでSQLにwhere付与
$productQuery = Product::with('user.userDetail');
$productQuery->where('colors', $request->input('colors'));

2. SQL実行して結果をLazyCollectionのcursorメソッドで取得
// cursor()した時点でLaravel6.x であれば LazyCollectionになる
$productCursor = $productQuery->cursor();

3.LazyCollectionのfilter()で結果をフィルタリング
if ($request->input('name')) {
    $filtered = $productCursor->filter(function (Product $app) use ($request) {
        // filter()のコールバック関数の引数はBuilderではなくModelになりRelationshipsが使える
        return $app->user->userDetail->name === $request->input('name');
    });
$products = $filtered->all();

Eloquent Builder は基本、操作がミュータブルなので $productQuery の状態を変化(Builder自身を変化)しますが、
Eloquent Collection の filerメソッドは操作がイミュータブルな(自身の状態を変えずに新しいCollectionを作る)ので、変数をあてる必要があります。
ミュータブルかイミュータブルかどうかは、\Illuminate\Support\Collection のソースを見ます。
(戻り値が Collection ならだいたいイミュータブルではあるそうです。)

https://github.com/illuminate/support/blob/master/Collection.php

@ return staticとなってるのがイミュータブル
@ return $thisとなてるのはミュータブル

だそうです。
実際に今回使った filter() を確認してみると、
image.png

return new static とあるのでイミュータブルです。

流れのソースまとめ

商品テーブルの料金検索とネスト先の名前検索
$productQuery = Product::with('user.userDetail');
if ($request->input('price')) {
        $productQuery->where('price', $inputs['price']);
    });

$productCursor = $productQuery->cursor();

if ($request->input('name')) {
    $filtered = $productCursor->filter(function (Product $app) use ($request) {
        return $app->user->userDetail->name === $request->input('name');
    });
    $products = $filtered->all();
}else{
    $products = $productCursor->all();
}

参考文献

この記事は以下の情報を参考にしました。

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

Laravel 暗号化 初期化ベクトルが格納される場所

Laravelにはencryptdecryptという暗号化、復号のヘルパーメソッドが用意されています。

暗号化 6.x Laravel
https://readouble.com/laravel/6.x/ja/encryption.html

暗号化の結果を見ると、同じ文言でも全く違う文字列になっています。ということは、Laravelの暗号化ではランダム生成された初期化ベクトルを使っているということになりますが、データベースには初期化ベクトルのカラムはありません。初期化ベクトルはどこに保存されているのでしょうか。

前提知識

LaravelはAES-256-CBCという方式で暗号化をしています。この記事を理解するためには、AES、CBC、初期化ベクトルについてある程度の知識が必要です。

プログラマの暗号化入門
https://qiita.com/asksaito/items/1793b8d8b3069b0b8d68

Advanced Encryption Standard
https://ja.wikipedia.org/wiki/Advanced_Encryption_Standard

暗号利用モード
https://ja.wikipedia.org/wiki/%E6%9A%97%E5%8F%B7%E5%88%A9%E7%94%A8%E3%83%A2%E3%83%BC%E3%83%89

初期化ベクトル
https://ja.wikipedia.org/wiki/%E5%88%9D%E6%9C%9F%E5%8C%96%E3%83%99%E3%82%AF%E3%83%88%E3%83%AB

初期化ベクトル保存場所

encrypt で暗号化したデータに、初期化ベクトルが含まれています。ただ、base64エンコードされているので、そのままでは読めません。encrypt で暗号化したデータをbase64デコードすると以下JSONが得られますが、「iv」に初期化ベクトルがセットされています。

暗号化のbase64デコード結果
{
  "iv":"初期化ベクトル",
  "value":"暗号化された文言",
  "mac":"メッセージ認証符号"
}

動くコードで確認する

説明だけだとしっくりこないと思うので、実際に動くコードで確認しましょう。以下URLに今回使うコード一式をアップロードしました。

https://github.com/kaidouji85/laravel-encrypt-iv

コードの動かし方は、リンク先を参照してください。コードの本体はLaravelカスタムコマンドで、ファイルパスはapp/Console/Commands/EncryptIV.phpです。ボイラープレートのせいで行数が無駄に多いですが、handleメソッドがサンプルコードのメインです。プログラムを実行すると、前セクションで紹介した「暗号化のbase64デコード結果」が出力されます。

app/Console/Commands/EncryptIV.php
public function handle()
    {
        // 勉強用に暗号化前の情報をコンソール出力している
        // ただ、セキュリティ上の問題があるので、本番環境では絶対にこのようなコードを書いてはいけない

        $origin = "オリジナルメッセージ";
        $this->info("origin: {$origin}");

        $encrpted = encrypt($origin);
        $this->info("encrpted: {$encrpted}");

        $base64Decoded = base64_decode($encrpted);
        $this->info("base64Decoded: {$base64Decoded}");
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel】環境構築からSNS連携まで

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

【Laravel】環境構築から各機能の実装

目次

環境構築
ログイン機能
SNS API連携
ブログ投稿機能
WYSIWYGエディタ
問合せ機能

Laravel 環境構築

https://qiita.com/sano1202/items/6021856b70e4f8d3dc3d

Laravel ログイン機能

https://php-junkie.net/framework/laravel/login/
https://readouble.com/laravel/5.6/ja/authentication.html

Laravel SNS API連携

https://mseeeen.msen.jp/laravel-socialite/

Laravel ブログ投稿機能

https://manablog.org/laravel_bulletin_board/
https://blog.hiroyuki90.com/articles/laravel-bbs/

うまくいかなかった部分を補足
PJ作成
$ composer create-project laravel/laravel bbc --prefer-dist

DBテーブル作成
$ php artisan make:migration create_posts_table --create=posts
$ php artisan make:migration create_categories_table --create=categories
$ php artisan make:migration create_comments_table --create=comments

モデル作成
$ php artisan make:model Post
$ php artisan make:model Category
$ php artisan make:model Comment

DBシーダー作成
$ php artisan make:seeder PostCommentSeeder
$ php artisan db:seed --class=PostCommentSeeder
$this->call(PostCommentSeeder::class);

コントローラ作成
$ php artisan make:controller PostsController
$ php artisan make:controller CommentsController

リクエスト作成(バリデーションに使用する)
$ php artisan make:request PostRequest
$ php artisan make:request CommentsRequest

Laravel WYSIWYGエディタ

https://qiita.com/qwe001/items/9a86839db4da5a7ab763

Laravel 問合せ機能

https://www.webopixel.net/php/1316.html

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

XLSXWriterの導入

参考

環境

  • PHP 5.6
  • Laravel 5.4
  • LaravelExcel 2.1
  • XLSXWriter

LaravelExcelを利用し、DBから取得したデータをExcelファイルに書き込んでExportする処理を作っていたが、データ件数が大量になると

Allowed memory size of ○○○○○ bytes exhausted

というメモリオーバーのエラーが出た。

PHPでExcelファイルを作成する際にはしばしばぶち当たるこの問題。
今回は顧客の要望をまったく満たせないパフォーマンスだったので、ライブラリをXLSXWriterに変更してみた。

パッケージのインストール

composer require mk-j/PHP_XLSXWriter

簡単な使い方

use XLSXWriter;

// 中略

// シート名
const SHEET_NAME = 'ユーザーリスト';

// ヘッダー情報
const SHEET_HEADER = [
    'ID' => 'integer',
    'NAME' => 'string',
    'BIRTHDAY' => 'yyyy/mm/dd',
];

$writer = new XLSXWriter();

// ヘッダー行の書き込み
$writer->writeSheetHeader(self::SHEET_NAME, self::SHEET_HEADER);

// データ行書き込み
foreach ($dataList as $data) {
    $writer->writeSheetRow(self::SHEET_NAME, $data);
}

// 出力
$filePath = './tmp/user_list.xlsx';
$writer->writeToFile($filePath);

// XLSXWriterはストレージに保存するしか方法がない?ようなので、Exportしたい時は出力後ファイル削除
if (file_exists($filePath)) {
    header('Content-Description: File Transfer');
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename="' . basename($filePath) . '"');
    header('Expires: 0');
    header('Cache-Control: must-revalidate');
    header('Pragma: public');
    header('Content-Length: ' . filesize($filePath));
    $readFile = readfile($filePath);
    if ($readFile) {
        unlink($filePath);
    }
}
exit;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「チーム共有機能・Views生成時の命名規則修正」LaravelDB.com

◇Laravel DB.com を初めて知った人はこちらから

1.Laravel DB.com ってなに?前回の紹介記事です!

2.Qiita記事LaravelDB.comの紹介 >>

3.次にLaravelDB.comを知った人は以下へどうぞ。

4.LaravelDB.comへログイン >>

◇NewUpdate[2020-02-25~]

1.Update:Views生成時フォルダ名の命名規則

以前と現在のLaravelDB.comのView生成時の命名規則は以下です。

記法 複/単
以前 キャメルケース 複数形 testDemos
現在 スネークケース 単数形 test_demo

※キャメルケースは、複合語をひと綴りとして、要素語の最初を大文字で書き表す記法。
※アンダースコア( _ )を区切記号として単語をつなげる記法。

何故、いま「Viewsの命名規則」を変えたのか?
Auth関連の処理を追加・変更しようとする際に「現在の記法(スネークケース)」でないと動作しないケースがあるため変更しました。

2.New:チーム共有機能(他のメンバーとER図を共有)

LaravelDB.comをリリース後に ”Feedbackで一番多かった” のが、「Team開発やメンターとのオンラインMTG用にER図を共有したい」でした。

「どうやったら?シンプルに実装でき、ユーザーが簡単に操作できるか?」の仕様検討が最も時間がかかりました。
数日間悩んだ結果、一つの方法を昨日考えつきました。仕様と実装方法が見えてきたらそこからは1日半程度で完成し、その翌日にはリリースしました。スピードは時間を有意義に使うための重要なスキルです。これで、オンライン上でテーブルの設計が共有でき、CRUDが簡単に生成できることでしょう。
また 追加機能・改修等があれば今後もアップデート記事を書いていきます。

【シェアData「送信側」】

テーブル設計をシェアする機能のことです。

1 [シェアData]作成


POINT:
この時点で共有データが作成されます。
見せたい相手にIDを渡しておいて、変更があれば「Create a [Share ID]」ボタンを押すと毎度データ更新されることを知っておきましょう! 「データ更新したので見てください!」って後から言えるってことですね。

2 [シェアData] IDをコピー

ここでコピーしたIDを相手に知らせます。

【シェアData 「受信側」】

1 [シェアData]読み込み

相手は送られてきた[シェアData]IDを貼り付けます。
こちらのIDを知っていれば誰でも読み込めます!見て欲しい人に渡しましょう!
※LOGIN(Googleアカウントで)しないと見れないことは認識しておいてください。

2.「Read」ボタンをクリックしてデータを受信表示しましょう。

3.シェアデータの複製が完了!!

受信側にデータが入りました!受信側もそのデータを活用できるようになります。
チーム・メンタリング等のケースでも利用可能です。

※受信後は「別名を付けて保存しておくと良いでしょう!」


LaravelDB.com解説ページ一覧

コード書かずに超スピード開発~(DEMO動画あり)~最新版『 Laravel DB.com 』

https://qiita.com/daisu_yamazaki/items/068595670bdc2b6fe3fc

LaravelDB.com 対応カラム一覧

https://qiita.com/daisu_yamazaki/items/92dc3cc599a264c3fb0f

LaravelDB.com テーブル命名ルール

https://qiita.com/daisu_yamazaki/items/1cb5987cc6d1008def82

LaravelDb.com integerの注意点

https://qiita.com/daisu_yamazaki/items/f2e6d58cfa20fa81fd54

LaravelDB.com Faker(テストデータ投入)

https://qiita.com/daisu_yamazaki/items/57669e8fa2c256d85c95

Twitter

LaravelDB.com

以上

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

NewUpdate!!「チーム共有機能・Views生成時の命名規則修正」LaravelDB.com

◇Laravel DB.com を初めて知った人はこちらから

1.Laravel DB.com ってなに?前回の紹介記事です!

2.Qiita記事LaravelDB.comの紹介 >>

3.次にLaravelDB.comを知った人は以下へどうぞ。

4.LaravelDB.comへログイン >>

◇NewUpdate[2020-02-25~]

1.Update:Views生成時フォルダ名の命名規則

以前と現在のLaravelDB.comのView生成時の命名規則は以下です。

記法 複/単
以前 キャメルケース 複数形 testDemos
現在 スネークケース 単数形 test_demo

※キャメルケースは、複合語をひと綴りとして、要素語の最初を大文字で書き表す記法。
※アンダースコア( _ )を区切記号として単語をつなげる記法。

何故、いま「Viewsの命名規則」を変えたのか?
Auth関連の処理を追加・変更しようとする際に「現在の記法(スネークケース)」でないと動作しないケースがあるため変更しました。

2.New:チーム共有機能(他のメンバーとER図を共有)

LaravelDB.comをリリース後に ”Feedbackで一番多かった” のが、「Team開発やメンターとのオンラインMTG用にER図を共有したい」でした。

「どうやったら?シンプルに実装でき、ユーザーが簡単に操作できるか?」の仕様検討が最も時間がかかりました。
数日間悩んだ結果、一つの方法を昨日考えつきました。仕様と実装方法が見えてきたらそこからは1日半程度で完成し、その翌日にはリリースしました。スピードは時間を有意義に使うための重要なスキルです。これで、オンライン上でテーブルの設計が共有でき、CRUDが簡単に生成できることでしょう。
また 追加機能・改修等があれば今後もアップデート記事を書いていきます。

【シェアData「送信側」】

テーブル設計をシェアする機能のことです。

1 [シェアData]作成


POINT:
この時点で共有データが作成されます。
見せたい相手にIDを渡しておいて、変更があれば「Create a [Share ID]」ボタンを押すと毎度データ更新されることを知っておきましょう! 「データ更新したので見てください!」って後から言えるってことですね。

2 [シェアData] IDをコピー

ここでコピーしたIDを相手に知らせます。

【シェアData 「受信側」】

1 [シェアData]読み込み

相手は送られてきた[シェアData]IDを貼り付けます。
こちらのIDを知っていれば誰でも読み込めます!見て欲しい人に渡しましょう!
※LOGIN(Googleアカウントで)しないと見れないことは認識しておいてください。

2.「Read」ボタンをクリックしてデータを受信表示しましょう。

3.シェアデータの複製が完了!!

受信側にデータが入りました!受信側もそのデータを活用できるようになります。
チーム・メンタリング等のケースでも利用可能です。

※受信後は「別名を付けて保存しておくと良いでしょう!」


LaravelDB.com解説ページ一覧

コード書かずに超スピード開発~(DEMO動画あり)~最新版『 Laravel DB.com 』

https://qiita.com/daisu_yamazaki/items/068595670bdc2b6fe3fc

LaravelDB.com 対応カラム一覧

https://qiita.com/daisu_yamazaki/items/92dc3cc599a264c3fb0f

LaravelDB.com テーブル命名ルール

https://qiita.com/daisu_yamazaki/items/1cb5987cc6d1008def82

LaravelDb.com integerの注意点

https://qiita.com/daisu_yamazaki/items/f2e6d58cfa20fa81fd54

LaravelDB.com Faker(テストデータ投入)

https://qiita.com/daisu_yamazaki/items/57669e8fa2c256d85c95

Twitter

LaravelDB.com

以上

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

Update「チーム共有機能・Views生成時の命名規則修正」LaravelDB.com

◇Laravel DB.com を初めて知った人はこちらから

1.Laravel DB.com ってなに?前回の紹介記事です!

2.Qiita記事LaravelDB.comの紹介 >>

3.次にLaravelDB.comを知った人は以下へどうぞ。

4.LaravelDB.comへログイン >>

◇NewUpdate[2020-02-25~]

1.Update:Views生成時フォルダ名の命名規則

以前と現在のLaravelDB.comのView生成時の命名規則は以下です。

記法 複/単
以前 キャメルケース 複数形 testDemos
現在 スネークケース 単数形 test_demo

※キャメルケースは、複合語をひと綴りとして、要素語の最初を大文字で書き表す記法。
※アンダースコア( _ )を区切記号として単語をつなげる記法。

何故、いま「Viewsの命名規則」を変えたのか?
Auth関連の処理を追加・変更しようとする際に「現在の記法(スネークケース)」でないと動作しないケースがあるため変更しました。

2.New:チーム共有機能(他のメンバーとER図を共有)

LaravelDB.comをリリース後に ”Feedbackで一番多かった” のが、「Team開発やメンターとのオンラインMTG用にER図を共有したい」でした。

「どうやったら?シンプルに実装でき、ユーザーが簡単に操作できるか?」の仕様検討が最も時間がかかりました。
数日間悩んだ結果、一つの方法を昨日考えつきました。仕様と実装方法が見えてきたらそこからは1日半程度で完成し、その翌日にはリリースしました。スピードは時間を有意義に使うための重要なスキルです。これで、オンライン上でテーブルの設計が共有でき、CRUDが簡単に生成できることでしょう。
また 追加機能・改修等があれば今後もアップデート記事を書いていきます。

【シェアData「送信側」】

テーブル設計をシェアする機能のことです。

1 [シェアData]作成


POINT:
この時点で共有データが作成されます。
見せたい相手にIDを渡しておいて、変更があれば「Create a [Share ID]」ボタンを押すと毎度データ更新されることを知っておきましょう! 「データ更新したので見てください!」って後から言えるってことですね。

2 [シェアData] IDをコピー

ここでコピーしたIDを相手に知らせます。

【シェアData 「受信側」】

1 [シェアData]読み込み

相手は送られてきた[シェアData]IDを貼り付けます。
こちらのIDを知っていれば誰でも読み込めます!見て欲しい人に渡しましょう!
※LOGIN(Googleアカウントで)しないと見れないことは認識しておいてください。

2.「Read」ボタンをクリックしてデータを受信表示しましょう。

3.シェアデータの複製が完了!!

受信側にデータが入りました!受信側もそのデータを活用できるようになります。
チーム・メンタリング等のケースでも利用可能です。

※受信後は「別名を付けて保存しておくと良いでしょう!」


LaravelDB.com解説ページ一覧

コード書かずに超スピード開発~(DEMO動画あり)~最新版『 Laravel DB.com 』

https://qiita.com/daisu_yamazaki/items/068595670bdc2b6fe3fc

LaravelDB.com 対応カラム一覧

https://qiita.com/daisu_yamazaki/items/92dc3cc599a264c3fb0f

LaravelDB.com テーブル命名ルール

https://qiita.com/daisu_yamazaki/items/1cb5987cc6d1008def82

LaravelDb.com integerの注意点

https://qiita.com/daisu_yamazaki/items/f2e6d58cfa20fa81fd54

LaravelDB.com Faker(テストデータ投入)

https://qiita.com/daisu_yamazaki/items/57669e8fa2c256d85c95

Twitter

LaravelDB.com

以上

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

Laravel で Fat Model を防ぐ 5 つの Tips

この記事について

前回 Fat Controller について同様の記事を書きました。
Laravel で Fat Controller を防ぐ 5 つの Tips - Qiita

引き続いて Fat Model 編です。Fat Model に関しては、Laravel を使っているとわりとそうなる傾向にあるなぁという印象を持っているので、どうすれば解消できるのか悩んでいる方は多いんじゃないかと想像しています。それらを防ぐために使えるテクニックというか、Tips を5つばかり紹介したいと思います。

はじめに

Fat Model とは?

簡潔にいえば、行数が多く(個人的には Model だと 200 行超えると Fat 感を感じます、みなさんはいかがでしょうか)、行数が多いがために処理の流れを追うことが難しく、しばしば不具合の原因になるクラスのことをいうのだと思います。ただ行数が多い、というだけでなく、複数のクライアント(呼び出し側)で処理が重複していたり、メソッドの中で条件分岐が発生したりして、ひとつの変更が他の部分に及ぼす影響を検知しづらい状態にあるクラスを想定しています。

巨大なクラスであっても、そこに含まれるデータや関数が高凝集・低結合であれば問題ないと考えます。大事なのは「不具合の原因になる」ということであって、すべての巨大なクラスが悪である、ということではないと思っています。

Fat Model の問題点

まず前提として、この記事では Model は Controller と View 以外のものを指しています。前述の Policy や FormRequest なども Model の一種と捉えています。が、ここでは主に Illuminate\Database\Eloquent\Model (以下 Eloquent Model)を継承したクラスを対象にします。おそらく、最も Fat Model になりやすいのはここだと思うからです。

  • 大量のメソッドがあり、ひとつのプロパティを操作するメソッドが複数(しかも多く)存在するため、データモデルの変更に追従するのが難しい。それによって、データモデルを変更すると、予期せぬ不具合が発生することがある
  • ひとつひとつのメソッドが長く、処理の流れを追うことが難しい。それによって、局所的な変更が処理全体に影響することがあり、予期せぬ不具合が発生することがある
  • Model の複数のバリエーションを同一クラス内で扱っており、バリエーションの違いを判定するための条件分岐があちこちに発生する。それによって、バリエーションが増えた際にコードの変更が漏れ、予期せぬ不具合が発生することがある

Fat Model になる主な要因として、テーブルと対になった Eloquent Model のみが存在し、基底クラスの制約上リレーションシップ、クエリスコープ、アクセサ・ミューテータが一緒くたになっているため、そこから処理を委譲しにくい、というのがあるんじゃないかと思います。個人的には、適切に処理がカプセル化できているのであれば、Fat Model は( Fat Controller に比べれば)悪いことではないと思っていて、逆に Eloquent Model が Slim になっている場合、本来カプセル化しておくべき処理が外部に漏れている可能性があって、そちらのほうがずっと危険だと考えます。

Laravel と同じく Active Record パターンを採用している Ruby on Rails でも同様の問題が取り上げられており、普及した時期が Laravel よりも早いため、議論の過程は参考になるのではないかと思います。

Railsのファットモデル問題に対処する前に読んでほしい記事 - Qiita

Fat Model を防ぐ 5 つの Tips

  1. リクエストのデータを処理する関数は FormRequest に書く
  2. レスポンスのデータを処理する関数は ViewModel あるいは Resource に書く
  3. 複雑なロジックを別のクラスに委譲する
  4. Repository パターンを導入する
  5. コンテキストやバリエーションごとに継承したクラスを用意する

1. リクエストのデータを処理する関数は FormRequest に書く

Fat Controller 編と同じです。

モデルにあるメソッドで Request を受け取ってゴニョゴニョしている処理がある場合は、FormRequest へ移動させましょう。

単純な例は Fat Controller 編の例を見ていただくとして、複雑なクエリビルディングがある場合の例を書いておきます。

// Model
public function scopeMatched(Builder $builder, Request $request) {
    $keyword = $request->keyword;
    $containsCompleted = $request->contains_completed;
    // 他にもたくさんのパラメータがある
    $orderBy = $request->order_by;

    return $builder
        ->where('keyword', 'LIKE', "%{$keyword}%")
        ->when(!$containsCompleted, function ($builder) {
            $builder->whereIn('status', ['todo', 'doing']);
        })
        // 他にもたくさんのメソッドチェーンがある
        ->orderBy($orderBy);
}

入力パラメータが複雑になる場合は、Scope クラスを経由して組み立てることができます。

公式ドキュメント ( https://laravel.com/docs/6.x/eloquent#global-scopes )では Scope の使い方は boot メソッドで addGlobalScope を使う例しか載っていませんが、別にこれは addGlobalScope 経由でしか使えないというわけではなく、アドホックなクエリビルディングでも以下のように使うことができます。

// FormRequest
public function filterScope() {
    return new MatchedScope($this->all());
}

// Scope
public function apply(Builder $builder, Model $model) {
    $builder
        ->where('keyword', 'LIKE', "%{$this->keyword}%")
        ->when(!$this->containsCompleted, function ($builder) {
            $builder->whereIn('status', ['todo', 'doing']);
        })
        ->orderBy($this->orderBy);
}

// Controller
$tasks = Task::matched($request->filterScope())->get();

// Model
public function scopeMatched(Builder $builder, MatchedScope $scope) {
    $scope->apply($builder, $this);
    return $builder;
}

2. レスポンスのデータを処理する関数は ViewModel あるいは Resource に書く

こちらも Fat Controller 編と同じです。

とはいえ、Eloquent に備わったアクセサ/ミューテータを活用したいなら、Model に書かざるを得ません。ここは利便性とのトレードオフなのかな、とも思います。

アクセサに複雑な処理あるいは重複した処理があれば、それを別クラスにするのはおすすめです。

以下では、アクセサを利用せず、Resource 側で整形するパターンを紹介します。

例として、日付と時刻を別々に持っており、日付・時刻ともにオプショナル、時刻のありなしでフォーマットが異なる、というルールがあったとき、

// Model
public function getReservedAtAttribute(): string {
    if (empty($this->attributes['reserved_at_date'])) {
        return '';
    }
    if (empty($this->attributes['reserved_at_time'])) {
        return Carbon::parse($this->attributes['reserved_at_date'])->format('Y-m-d');
    }
    $dt = sprintf('%s %s', $this->attributes['reserved_at_date'], $this->attributes['reserved_at_time']);
    return Carbon::parse($dt)->format('Y-m-d H:i');
}

この処理をまるっと別のクラスに移し、Resource クラスで変換するように変更してみます。

// OptionalTimeDateTimeFormatter
public function __construct(string $date, string $time) {
    // snip
}
public function format(): string {
    if (empty($this->date)) {
        return '';
    }
    if (empty($this->time)) {
        return Carbon::parse($this->date)->format('Y-m-d');
    }
    $dt = sprintf('%s %s', $this->date, $this->time);
    return Carbon::parse($dt)->format('Y-m-d H:i');
}

// Resource
public function toArray($request): array {
    $dateTimeFormatter = new OptionalTimeDateTimeFormatter(
        $this->reserved_at_date,
        $this->reserved_at_time
    );
    return [
        'reserved_at' => $dateTimeFormatter->format(),
        // snip
    ];
}

トレイトにするのでもいいかもしれません。

個人的には、Resource は便利な半面ややトリッキーな部分も多いので(詳細は省きますが)、あまりこのクラスにロジックを書かないほうがいいと思っていますが、委譲するクラスの生成と単一の API 呼び出しだけであれば、許容範囲かな、とは思います。

3. 複雑なロジックを別のクラスに委譲する

2 のケースとやることはほとんど同じです。

例として、TODO アプリケーションで見積もり時間と実績時間から進捗率を算出して返すようなメソッドがあるとします。単純な四則演算ではなく、見積もりは 1w とか 3h とかの形式で設定でき、実績時間は 00:00 のように分までの時間を足し合わせていく方式とします。

// Task Model
public function getProgressRateAttribute(): ?float {
    if ($this->estimate === null) {
        return null;
    }
    $suffixCoefficientMap = ['w' => 5 * 24 * 60, 'd' => 24 * 60, 'h' => 60];
    $matches = [];
    if (!preg_match("/\A(\d+)([wdh]{1})\z/", $this->estimate, $matches)) {
        throw new \LogicException('estimate invalid format.')
    }
    list(, $value, $suffix) = $matches;
    $estimate = $matches[1] * $suffixCoefficientMap[$matches[2]];

    // このあと実績時間の計算が続く...

    return $estimate / $sumWorkTimes;
}

まぁまぁ長くなりますね(途中で力尽きました)。

これを、

// Task Model
public function getProgressRateAttribute(): ?float {
    if ($this->estimate === null) {
        return null;
    }

    $rate = new ProgressRate($this->estimate, $this->work_times);
    return $rate->value();
}

みたいにすれば、1/4 くらいになります。メソッドが長くなってきたらプライベートメソッドに切り出したりすると思いますが、 $this が使われてなかったり、特定のプロパティしか使ってないようなら別のクラスに切り出しやすいです。

4. Repository パターンを導入する

Repository パターンをご存じない方はググってみてください。本記事では Martin Fowler さんの定義のみ載せておきますが、アーキテクチャ全体に関わることなので、導入に際しては十分に検討したほうがいいでしょう。

https://martinfowler.com/eaaCatalog/repository.html

Eloquent が肥大化する要因のひとつに、コレクション操作と単一のインスタンス操作がミックスされていることがあろうかと思います。

// コレクション
$tasks = Task::matched($request->filters())->get();

// 単一インスタンス
$task->doSomething();

上記2つのメソッドはいずれも Model 側に存在するわけですが、Repository クラスを導入すれば、コレクション操作を分離することができます。

// コレクション
$tasks = $taskRepository->find(new MatchedScope($request->filters()));

// 単一インスタンス
$task->doSomething();

上の例では、Criteria の代わりに Scope を使っています。本来 Repository は永続化層の変更に柔軟に対応するため、というのがあるので、Criteria を汎用的なつくりにしてクエリビルダーへの依存性をなくしておいたほうが、本来の使い方に則しているかなと思いますが、コレクション操作を分離するだけであれば不要と考えます。

5. コンテキストやバリエーションごとに継承したクラスを用意する

ある特定のコンテキストだったり、ある状態にもとづいて別のバリエーションが存在するようなケースで、クラスを分けることを検討してもいいと思います。

例として(あまり適切な例じゃないかもしれないですが)、タスクが「タスク」と「リマインダー」に分かれているものの、データベースには同じテーブルに入っていて、task_type みたいなフィールドの値で分類している、とします。

そうしたケースで、「リマインダー」だけが実行可能なユースケースがあり、そうしたユースケースで使われるメソッドが「タスク」クラスに混在しているとき、「リマインダー」用のメソッドを分離することができます。

// Task
class Task extends Model {
    public static function boot() {
        parent::boot();
        static::addGlobalScope('task', function (Builder $builder) {
            $builder->where('task_type', 'task');
        })
    }
}

// Reminder
class Reminder extends Model {
    protected $table = 'tasks';

    public static function boot() {
        parent::boot();
        static::addGlobalScope('reminder', function (Builder $builder) {
            $builder->where('task_type', 'reminder');
        })
    }  
}

// Controller
// task_type=task の ID が指定された場合は 404 になる
public function snooze(Reminder $reminder, SnoozeReminderRequest $request) {
    $reminder->snooze($request->time); // snooze は Reminder だけの機能
    return response()->json(null, Response::HTTP_NO_CONTENT);
}

Task クラスのメソッドを使う場合は、 extends Task でもいいでしょう。

ただし、適切に分離せずオープンクローズド原則に違反するようなつくりになってしまうと、却って不具合の原因になることもあるので、そのコンテキストあるいはバリエーションが完全に独立しているか、分離すべきメソッドのみ分離しているか、など、慎重に検討してください。

おわりに

いかがでしたでしょうか。例をひねりだすのに苦労して、もしかしたら不適切な例もあるかもしれませんが、Fat Model を解消したいとお悩みの方の一助になれば幸いです。

他にも Fat Model 解消法をお持ちの方、コメント欄にて教えていただけると大変助かります :bow:

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

Docker Toolboxを使ってDockerでLaravel開発環境を作ってみる

はじめに

今までVagrant+VirtualBox、XAMPP、Laravel Homesteadと開発環境を構築してきましたが、現在本格的にエンジニア志望の身の上であるということと、友人がDockerで環境構築をしたという話を聞いたので私もこれからの開発環境をDockerに絞ってやっていこうと思い今回挑戦してみましたので備忘録として書きます。

一応Githubにも慣れる目的でpushしてありますが、基本的には後述の参考記事の手順通りにやったことをWindows10 Homeでやるために私がしたことを追記して書いたようになりますので、作ったDocker環境は同じです。

https://github.com/shitikamiyako/Docker-Laravel-Demo

最初に行っておきたいこと

Dockerを使うならMacかWindowsならせめてWindows10 Proを使うことをおすすめします、Homeだとまーややこしいというより、Dockerを使うメリットの半分くらいなくなっている気がしなくもないです。
Linuxに明るい人だとLinuxでやる人もいるそうな。

構築する環境

windows10 home(Ryzen 5 3600)
docker v18.06.1-ce(途中でv19.03.1にアップデートします)
VirtualBox 6.0.16

Dockerとは?

私が語るよりも詳しいことは下記の記事をご覧になってください。

さくらナレッジ様より Docker入門(第一回)~Dockerとは何か、何が良いのか~

Dockerでプログラマが最低限知るべきことが、最速でわかるチュートリアル

さくらナレッジさんの記事より引用させていただくと、Dockerとは簡潔に言うと以下のような仕組みです。

Dockerは、Linuxのコンテナ技術を使ったもので、よく仮想マシンと比較されます。VirtualBoxなどの仮想マシンでは、ホストマシン上でハイパーバイザを利用しゲストOSを動かし、その上でミドルウェアなどを動かします。それに対し、コンテナはホストマシンのカーネルを利用し、プロセスやユーザなどを隔離することで、あたかも別のマシンが動いているかのように動かすことができます。そのため、軽量で高速に起動、停止などが可能です。

イメージとコンテナについて十全に理解するにはLinux及びDockerについてもう少し深く勉強する必要があるので、あくまで今の私の感覚で話をすると、イメージはコンテナを作る材料のようなものだと思うとわかりやすいかもしれません。
例えば、今回はLaravelをDockerに導入するために

Nginx(サーバー) 、PHP(言語)、 MySQL(データベース)

の3つを用意しないといけないのですけど、Dockerの場合これを用意するにはまず各々のイメージをDocker Hubのレポジトリから引っ張ってこないといけない(pullするという)ので、まずそれを行います。
そしてそれを使用するために用意してきたイメージから各コンテナを作ることになります。
こうすることで各コンテナがサーバー・PHP実行環境・データベースの役割を持つのであとはPHP実行環境にLaravelをインストールしてしまえばLaravel環境の完成ということになります。
よく言われるDockerの利点の1つがここで、開発環境によって使うイメージを組み合わせることで容易に異なる環境を構築することができます。
例えば上記の例においてもこれとは別にデータベースをMySQLではなくMariaDBにしたLaravel環境を用意したいということであれば、MariaDBイメージを用意すればいいということになりますね。

Dockerインストール

DockerはMacを使ってると話が早いみたいなのですが、そうでなければ早くも詰まるポイントその1です。

通常、Macの場合であればDocker for Macと呼ばれるインストーラーからインストールできるのですがWindowsの場合だとかなりややこしいです。

まず、使用しているWindowsがWindows10 Proの64bit版であるかどうかで分岐します。
そうであるなら、Docker for Windowsというインストーラーが使えるのでこれを使いましょう。
違う場合、つまり今回のパターンだと残念ながらそのままだとDockerは使えずDocker Toolboxというものを使い、VirtualBox下でDockerを使うことになります。
仮想マシンを使わないのがDockerの利点の1つのはずなので虚無の気持ちを覚えますが、受け入れましょう。

まずGithubから最新版をダウンロードします。
DockerToolbox

しかし、ここでも詰まるポイントがあってCPUがAMD製つまりRyzenを使用されてる場合はなんとバージョンによってインストールの際弾かれてしまう場合があります。
私はRyzenを使っているので見事に弾かれました。
その場合は、バージョンを下げたもので試してみてください、私は冒頭のバージョンで成功した後、最新版にアップグレードすることで事なきことを得ました。
さて、ToolboxをインストールするとDocker QuickStart Terminalというものがあるはずなのでそれを起動してください。
インストールの際にVirtual BoxやGitなどWindows10 HomeでDockerを使うために必要なものを同時にインストールするかどうか聞かれますが、各々で必要なければチェックを外しましょう。

無事にこのようにクジラが出てきたらDockerのインストール完了です。
以後Dockerを起動する際もDocker QuickStart Terminalから起動できます。
2020-02-25_02h50_00.png

あと、これも詰まるポイントの1つなのですが仮想環境を使うためには
Hyper-vの機能を有効にしないといけません。
これはCPUがIntel製かAMD製か、さらにWindowsがProかHomeかで、もっというとマザーボードがどこのものかで有効にする手段が違うのですが、このあたりはお使いの環境を調べてHyper-v 有効というキーワードでググっていただければ。
私の場合は、BIOSの設定から有効にしないといけませんでした。

開発環境の構築をしていく

docker-composeでLaravelの開発環境を整える方法とその解説
(以下参考記事1と呼称させて頂きます。)

Laravelの開発環境をDockerを使って構築する

詳しいことは上記の記事を参照して頂いた方がわかりやすく話も早いのですが、今回の場合はWindows10 Homeでの導入になりますので、私からも順を追って話をさせていただきます。

1.Virtual Boxのポートフォワーディングの確認

まずは、Windows10 Homeの場合必ず確認しなればならないのがここです。
これ、今冷静に考えればWindows10 HomeでのDockerの使用はVirtual Boxをかませてその中の仮想マシン(Linux)で動かすわけですから、当然仮想マシン側のポートもちゃんと設定してあげないとlocalhostすらままならないんですけど、なぜか私はそれに気づかず8時間くらい溶かしてました。
こんな人は他にはいないと思いますけど、後々あれ? とならないようにまずこちらから設定しましょう。

Virtual Boxを起動して、Dockerで使用している仮想マシンを選択して設定のところをクリックします。
どれを使用しているんだ? となる方はDockerを起動してdocker-machine lsとコマンドプロンプトで打つと、起動している仮想マシンの詳細が出るのでそこのNAMEの部分がDockerで使用されている仮想マシンの名前になりますので確認してみてください。
導入直後のままならおそらくdefaultという名前のはずです。

さて、設定をクリックしたあとはネットワークの項目を選択、高度と書かれているところのプルダウンメニューを表示させると下記のようにポートフォワーディングの項目が出るのでそこをクリックしましょう。
2020-02-26_02h38_29.png

すると下記のような画面が出てくるので、右側のプラスマークのアイコンからポートの設定ができます。

2020-02-26_02h39_03.png

名前・ホストIP(ローカル環境なので、127.0.0.1)、ホストポート、ゲストポートを設定します。
このあと、Dockerの方でもホスト⇒ゲストとポートの設定のをするのですが、ややこしいことにWindows10 HomeでのDocker利用は

Windows⇒Virtual Box⇒Docker

つまり

大本のホスト⇒Dockerから見たホスト(Windowsから見るとゲスト)⇒ゲスト

といったような構造になっているため、Docker側でVirtual Box⇒Dockerでのポートの設定をしても、Windows⇒Virtual Box側でポートを通してやらないと暖簾に腕押しということになってしまうのです。
ネットワークの知識に少し明るいとこういうところは当然なのかもしれないですが、私みたいなまだまだビギナーという人には落とし穴になりがちだと思うので最初に確認して頂きたいです。

2.docker-compose.ymlの記述

参考記事1から引用させていただくと

docker-compose.ymlには、yaml形式で、複数のコンテナで構成されるサービスを構築・実行する手順等を記述します。
それぞれのコンテナについて、イメージ作成時にベースにするDockerイメージやDockerfileの指定・ホスト側からDockerコンテナ側へのポートフォワーディングの指定・Dockerコンテナに共有する(ホスト側の)ファイル群の指定などを定義することができます。

ということになります。
つまり、このコンテナ(役割)を使った環境を作りますよという設計図のようなものですね。
なお、今回私は以下のようなディレクトリ構造で構築しましたので以後の説明はこれに沿っているものと考えてください。

・docker-compose-laravel(ルートフォルダ)
├── demo-docker-app(Laravel)
├── docker
│ └── web
│ └── default.conf
│  └── php
│ └── Dockerfile
├── docker-compose.yml
└── index.html
└── index.php

ディレクトリの構築が終わったらdocker-compose.ymlに以下のように記述します。

version: '3'
services:
  web:
    image: nginx:1.15.6
    ports:
      - '8000:80'
    volumes:
      - ./docker/web/default.conf:/etc/nginx/conf.d/default.conf
      - .:/var/www/html

上記はNginxを実行するコンテナを定義しています。
versionはdocker-compose.ymlのファイルフォーマットのバージョン指定。

serviceはコンテナの名前の定義で、インテンドが1つ下がった部分が実行するコンテナの定義となります。
なので、docker-compose.yml記述でのインテンド上げ下げは正確に行わければいけないので注意しましょう、インテンドが間違っていると必ずエラーが出てうまくいきません。

imageはDockerイメージの指定になります。
このコンテナではNginxを使いたいのでバージョンを含めて上記のように指定します。

portsは前述の通り、ポートフォワーディングの設定になります。
1で設定したものと同じになるように設定しましょう。
ちなみにホストのポート:ゲストのポートという記述になりますが、ゲスト側のポートの下2桁の00は省略します。

volumesはホスト・コンテナ間でのファイル共有の指定で、ホスト側のパス:コンテナ側のパスの形式で記述します。
Homestead使ったことがある人ならわかると思うのですが、要はホスト側で編集した内容等がゲスト側にも反映されるように同期をとるようなものです。
いちいちゲスト側の環境でコード書いたりしなくてもいいようにすると思っていただければ。

./docker/web/default.conf:/etc/nginx/conf.d/default.conf

と書いてありますがこれはホスト側のパス:ゲスト側のパスという記述になります。

.:/var/www/html

つまり上記の場合はホスト側のルートフォルダの内容はゲスト側の/var/www/html部分に反省されていますよということになります。

また、volumesを設定することファイルやディレクトリは、永続化(コンテナを削除してもホスト側にファイルやディレクトリが残る)させることができるようです。

3.default.confの記述

このファイルはNginxの設定ファイルとなります。
以下のように基準してください。

server {
    listen 80;
    root  /var/www/html;
    index index.html;
    access_log /var/log/nginx/access.log;
    error_log  /var/log/nginx/error.log;
}

listenはリクエストを受け付けるIPアドレスやポート番号の設定です。
上記の場合はすべてのIPアドレスの80番ポートでリクエストを受け付けるということになります。
root, indexでは、それぞれ、ドキュメントルートのディレクトリ・インデックスとして使われるファイル名を定義します。
indexの部分は先程の通りでindexは適当な確認用のindex.html作っておいてそれを設定してください。
access_log, error_logはそれぞれのログ出力先の設定となります。

4.Nginxの動作確認

ここまでの工程でローカルでWebサーバーを立ち上げる準備ができたので立ち上げてみます。
ディレクトリをルートフォルダに移して以下のコマンドを実行します。

docker-compose up -d

このコマンドはdocker-compose.ymlの記述どおりにコンテナを作って、起動してくださいという意味になります。
-dのオプションはバックグラウンドでコンテナを実行するためのコマンドになります。
これがない場合は、upで実行された時にコンテナはすぐに終了してしまいます。

無事に作成・開始が成功すると以下のように表示されます。

Creating network "docker-compose-laravel_default" with the default driver
Creating docker-compose-laravel_web_1 ... done

ブラウザでhttp://localhost:8000 にアクセスすると、index.htmlが表示されます。
8000の部分は設定したポート番号に合わせてください。

5.PHP実行環境の準備

NginxでWebサーバーを立ち上げたので今度はそこでPHPを動作できるようにします。
そのためにはPHP-FPMというのものを使うのですが、このあたりの詳しいことは参考記事1で紹介されている、以下の記事をご覧になってください。私も今読んでいます。
nginx と PHP-FPM の仕組みをちゃんと理解しながら PHP の実行環境を構築する

さて、コンテナを追加するのでdocker-compose.ymlを編集していきます。
以下の通りに記述してみてください。

version: '3'
services:
  web:
    image: nginx:1.15.6
    ports:
      - '8000:80'
    depends_on:
      - app
    volumes:
      - ./docker/web/default.conf:/etc/nginx/conf.d/default.conf
      - .:/var/www/html
  app:
    image: php:7.2-fpm
    volumes:
      - .:/var/www/html

servicesの項目に、appを追加します。
そして、webの項目にdepends_onを追加してそこにappを指定しておきます。
これはサービスの依存関係を表すもので、今回はNginxがPHPに依存をしているということになります。
この定義によって、Dockerはコンテナの起動時にサービスの依存関係に基づいてコンテナを起動するようになるので、PHPコンテナが起動した後にNginxコンテナが起動するということになります。
あとの項目はNginxのときと同じですね、イメージを指定してファイル共有の定義をします。

次に、PHPが動くようにNginxの設定を変更します。
以下のように記述してください。

server {
    listen 80;
    root /var/www/html;
    index index.php index.html index.htm;
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    location / {
        try_files $uri $uri/ /index.php$is_args$args;
    }

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

indexの項目を見るとindex.phpとindex.htmが追加されています。
先程はindex.htmlをインデックスページとして設定しましたが、このよう複数設定すると先に記述があるファイルから優先してインデックスページに設定されます。
今回はphpを実行したいのでindex.phpを先頭に記述することになります。

locationの項目はURIごとにどのファイルを配信するのか設定する項目です。
最初のlocationではURIのパスにファイルがあるかどうかを確認し、なかった場合はディレクトリがあるかを確認し、いずれもなかった場合はindex.phpを返すという意味になります。
次のlocationではNginxがPHP-FPMにリクエストを渡すための設定をしています。
詳しいことは先述の記事を確認してください。

ここまでが終わったら動作確認です。
index.htmlと同じように適当な確認用のindex.phpを作成して、配置してください。
そのあとは先程と同じようにdocker-compose up -dを行います。
もし、先程実行したまま作業を続けている場合は、docker-compose downというコンテナを停止・削除するコマンドを実行してから行ってください。
実行するとhttp://localhost:8000 でindex.phpが表示されます。
phpinfo();のコマンドを仕込んでおくとPHPが実行されているかよりわかりやすくなると思います。

6.MySQLコンテナを用意する

最後にDBとしてMySQLを準備をします。
docker-compose.ymlを編集してMySQLのコンテナを定義します。

version: '3'
services:
  web:
    image: nginx:1.15.6
    ports:
      - '8000:80'
    depends_on:
      - app
    volumes:
      - ./docker/web/default.conf:/etc/nginx/conf.d/default.conf
      - .:/var/www/html
  app:
    image: php:7.2-fpm
    volumes:
      - .:/var/www/html
    depends_on:
      - mysql
  mysql:
    image: mysql:5.7
    environment:
      MYSQL_DATABASE: sample
      MYSQL_USER: user
      MYSQL_PASSWORD: password
      MYSQL_ROOT_PASSWORD: password
    ports:
      - "3306:3306"
    volumes:
      - mysql-data:/var/lib/mysql
volumes:
  mysql-data:

ここまで来るとservicesはweb、app、mysqlで構成されたサービスだということがわかりやすくなりますね。

environmentはMySQLコンテナでの環境変数の設定です。
Laravelでの.envファイルにあるMySQLの項目と同じですね。
またここでのvolumesはmysql-dataをサービス内で共通化して、他のコンテナからも参照できるようにするための設定のようです。

ここまで終わったら動作確認をしましょう。
upしたままならdownをしてから、もう一度docker-compose up -dを行います。
そうしたら以下のコマンドでMySQLのコンテナに入ることができます。

docker-compose exec mysql bash

コンテナに入るとあとは通常のMySQLの操作と同じなので

mysql -h localhost -u -p

でログインしてください、ユーザー名とパスワードは先程設定したものを入力します。
MySQLに入ったらshow databases;でデータベースを確認し、docker-compose.ymlで定義したデータベースが作成できているか確認してください。

7.Laravelのインストール

では、いよいよLaravelのインストールになります。
Composerがどうのとかそういうのはここでは割愛します。
一応私も手前味噌ですがLaravel導入に関して備忘録を残してありますので、よろしければそちらをご覧になってください。

まず、Dockerfileというものを作成します。
これはビルドするイメージの構成の定義です。
docker-compose.ymlで定義したのでは? と思った方、私も思いました。
どうやらこちらは必要なパッケージその他を指定した上でイメージをどう構成するかということを定義するもののようです。
先程だけだとPHPしか入りませんが、今度はComposerなどを入れたいのでそのためにDockerfileで指示書を書くというイメージでしょうか。

FROM php:7.2-fpm
# install composer
RUN cd /usr/bin && curl -s http://getcomposer.org/installer | php && ln -s /usr/bin/composer.phar /usr/bin/composer
RUN apt-get update \
&& apt-get install -y \
git \
zip \
unzip \
vim
RUN apt-get update \
    && apt-get install -y libpq-dev \
    && docker-php-ext-install pdo_mysql pdo_pgsql
WORKDIR /var/www/html

これが今回のDockerfileです。
FROMでイメージ(厳密にはDocker Hubレジストリで公開されているベース)を指定し、
curlコマンドでcomposerをインストール、さらにapt-getコマンドでgit、zip、unzip、vimをインストール、最後にdocker-php-ext-installコマンドでPDOをインストールするという意味になります。
RUNでこれらをDockerイメージのビルド時にコンテナで実行するように定義しています。
WORKDIRはRUNなどの命令が実行される際のディレクトリを指定します。

Dockerfileを作ったら、docker-compose.ymlのPHPの部分、つまりappの部分をDockerfileに基づくように修正します。

version: '3'
services:
  web:
    image: nginx:1.15.6
    ports:
      - '8000:80'
    depends_on:
      - app
    volumes:
      - ./docker/web/default.conf:/etc/nginx/conf.d/default.conf
      - .:/var/www/html
  app:
    build: ./docker/php
    volumes:
      - .:/var/www/html
    depends_on:
      - mysql
  mysql:
    image: mysql:5.7
    environment:
      MYSQL_DATABASE: sample
      MYSQL_USER: user
      MYSQL_PASSWORD: password
      MYSQL_ROOT_PASSWORD: password
    ports:
      - "3306:3306"
    volumes:
      - mysql-data:/var/lib/mysql
volumes:
  mysql-data:

imageの部分をbuildにしてDockerfileがあるディレクトリを指定します。
こうすることでDockerfileに基づいてイメージを作成し、appコンテナ(PHPコンテナ)が出来上がるということになります。

あとはLaravelを入れるだけです。
毎度のようにdocker-compose up -dを行い、今度はPHPコンテナに入りたいのでdocker-compose exec app bashを行って以下のコマンドを実行してLaravelをインストールします。

composer create-project --prefer-dist laravel/laravel プロジェクト名

無事にLaravelのインストールが完了したら.envファイルのMySQLの項目をdocker-compose.ymlで定義したものに合わせて設定してください。
DB_HOSTの項目はmysqlと指定します。

最後にNginxの設定をLaravelに合わせたものに修正します。
以下の通りです。

server {
    listen 80;
    root /var/www/html/my-laravel-app/public;
    index index.php index.html index.htm;
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;
    location / {
        try_files $uri $uri/ /index.php$is_args$args;
    }
    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass app:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }
}

rootの部分をLaravelのpubilcフォルダに合わせることでここがルートディレクトリになります。

ここまで終わったら動作確認です。
まず、docker-compose downを実行して、docker-compose up -ddocker-compose exec app bashでPHPコンテナに入ってマイグレーションを行います。
あとは以下のようにLaravelプロジェクトのフォルダにディレクトリを移してmigrateするだけですね。

cd プロジェクト名
php artisan migrate

マイグレーションが正常に行われたことを確認したら、再度docker-compose down を実行して、docker-compose up -d`を行い、http://localhost:8000 を確認してください。
Laravelのウェルカムページが表示できたら無事終了です、お疲れさまでした。

おまけ

Docker Toolboxのアップグレードについてはこちらを参照してください。

参考:Windows10 Homeで導入したdockerを最新にアップデートする方法

手順としてはアップグレードしたいバージョンのDocker ToolBoxをダウンロードしてきて、導入の時と同じことをするだけです。
Select Componentsは導入と同じように入れる必要のないもの(この場合はアップグレードする必要のないもの)はチェックを外し、次のSelect Additional Tasksもすべてチェックを外します。

インストールが終わったらdocker -vでバージョンを確認して、バージョンが上がったことを確認したらdocker-machine upgrade defaultで仮想マシン側のDockerもバージョンを上げておきます。(厳密にはdocker-machineは仮想マシン側でのDockerのホストを生成して管理しているものみたいです)
これでアップグレードは完了です。

あとがき

最初はGit経由でやろうとしていましたが、ポートフォワーディングの罠にハマりうまく行かないと思い込んでしまったので今回Dockerfileとdocker-compose.ymlで1からDocker環境を構築してみました。
災い転じて福となすとばかりに、とてもわかりやすい記事がありましたのでそれを参照しながら、うまく行かない原因がポートフォワーディングの罠だったことなど色々発見しながらDockerにおける環境構築の基本を学習できたのでとても有意義でした。
でも、フォロワーさんに「Linuxを扱えるようにならないとね?(威圧)」というありがたくも耳が痛すぎるリプライを頂いて、それに納得・承知をした上でやっぱり言わせてください。

Docker環境を整備したい! という方はできればMacも導入してください。

Linux、いずれは向き合わないといけないなぁ。

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