20191202のlaravelに関する記事は13件です。

Laravelにてテーブルの既存カラムをtinyint型に変更できない問題

環境

  • Laravel 5.7.28
  • MySQL 8.0.17

遭遇した問題

DBの容量を節約するため、既存カラムのデータ型をintからtinyintに変更するマーグレーションを実行した際、下記のようなエラーが表示された。
スクリーンショット 2019-12-02 18.37.06.png
tinyintegerなんてカラム型はないよ」と怒られているので、「あ、マイグレーションファイルにtinyIntegerではなくtinyintegerとタイピングしちゃったかな」なんて思って確認してみるも、間違っていない。

ちなみにマイグレーションファイルの中身は以下のような感じです。

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class ChangeContactTypeOfUsers extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
          $table->tinyInteger('contact_type')->change();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->integer('contact_type')->change();
        });
    }
}

原因

Laravel公式ドキュメントのMigrationの項を当たってみると、以下の通り。
スクリーンショット 2019-12-02 18.44.54.png

Only the following column types can be "changed"

の部分を和訳すると、「以下のカラムの型のみが変更可能です」となります。
試してみたところ、tinyintから別の型に変更することはできましたが、逆にtinyintには変更できませんでした。
つまり、Laravelではカラムの型をtinyintに変更することはできないということです。

また、例えばtinyintからintに変更するマイグレーションが通ったとしても、ロールバック時にはコケるので、tinyint型のカラムの型変更は行わないほうがよいでしょう。

tinyintに変更することで、格納可能サイズを超過するデータが生じる恐れがあるため、このような仕様となっているのでしょうか。
詳しい方いらっしゃいましたらコメントにてご教授いただけると幸いです。

対処法

一旦該当カラムをドロップし、再度tinyint型として追加することで対処しました。
結局、型変更のために2つのマイグレーションファイルの作成が必要になってしまいました。

※2019/12/4 追記
職場の方にご指摘いただき気付いたのですが、DBファサードを利用してSQLクエリを実行すれば解決する問題でした。

DB::statement("alter table users modify tel_type tinyint;");

学び

テーブルを作成する際には、格納され得るデータを予め想定し、適切にデータ型を決定する必要があるということを学習しました。

参考文献

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

【Laravel6】Vueがデフォルトでインストールされなくなっていた件【Vue.js】

前提

Laravelは5.3以降、yarn(npm)でインストールコマンドを実行すると
vueがデフォルトでインストールされるようになっていました。

package.jsonから色々消えた

Laravel6ではいくつかのパッケージがデフォルトから除外されたようで、
package.jsonの中身がすっきりしています。

Vue.js

masterのpackage.jsonからvueがいなくなっていました

2019/6/28に消されたようで、
当然、この日付以降に取得したLaravelでは、
yarn installをしてもvueでプロジェクトが作れません。
なので、手動で追加する必要があります。

例(追記参照:非正攻法のようです。)
$yarn add --dev vue vue-router

一方、Laravel5.8を確認してみるとvueは残っており、
更新履歴的に特に消える気配はなさそうなので、Laravel6からの方針と思われます。

他の消えたパッケージ

上記コミットを見てわかる通り、下記パッケージも同様にデフォルトでインストールされなくなりました。

  • bootstrap
  • jquery
  • popper.js

以上。
コミットメッセージを見ても特に消された経緯がわからなかったので、
本件について何か情報をお持ちの方がいらしたらコメントを頂けると幸いです。

追記

既存記事に解決策がありました

Laravel6でBootstrap, jQueryを使う方法

composer require laravel/ui
php artisan ui vue

を使うのが正攻法のようです。

公式記事に解説がありました

https://readouble.com/laravel/6.x/ja/frontend.html

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

Laravel SQLiteからMySQLに変更したら外部キー制約のマイグレーションが失敗してハマった

>php artisan -V
Laravel Framework 6.1.0

以下のusersに対して、user_idを外部キー制約として持つsubscribesテーブルを作りました。

Schema::create('users', function (Blueprint $table) {
  $table->bigIncrements('id');
  $table->string('name');
  $table->string('email')->unique();
  $table->timestamp('email_verified_at')->nullable();
  $table->string('password');
  $table->rememberToken();
  $table->timestamps();
});

SQLiteで動作していたマイグレーションだったので、そのままphp artisan migrateしたらエラーになりました。

実行したマイグレーション
Schema::create('subscribes', function (Blueprint $table) {
  $table->bigIncrements('id');
  $table->integer('user_id')->unsigned();
  $table->string('channel_id');
  $table->string('channel_title');
  $table->timestamps();

  // userが削除されたとき、それに関連するも一気に削除する制約
  $table->foreign('user_id')
        ->references('id')
        ->on('users')
        ->onDelete('cascade');
        });
Illuminate\Database\QueryException:
 SQLSTATE[HY000]: General error: 1215 
 Cannot add foreign key constraint
 (SQL: alter table `subscribes` add constraint 
 `subscribes_user_id_foreign` foreign key (`user_id`) 
 references `users` (`id`) on delete cascade) 

PHP - 【Laravel】外部キー制約があるテーブルのmigrateができません|teratail という記事をみて、user_idカラムを作るときの記述が異なることに気づいてその部分を修正することで、解決しました!

修正点
-  $table->integer('user_id')->unsigned();
+  $table->unsignedBigInteger('user_id');
正しいマイグレーション
Schema::create('subscribes', function (Blueprint $table) {
  $table->bigIncrements('id');
  $table->unsignedBigInteger('user_id');
  $table->string('channel_id');
  $table->string('channel_title');
  $table->timestamps();

  // userが削除されたとき、それに関連するも一気に削除する制約
  $table->foreign('user_id')
        ->references('id')
        ->on('users')
        ->onDelete('cascade');
        });

型が違っているからエラーになったっぽいのですが、それじゃあSQLiteはなぜ通った?という疑問が…。

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

Laravel+Vue.js+MySQLで入力内容の途中保存機能を実装してみた

グレンジ Advent Calendar 2019 4日目担当の soyo と申します。
グレンジでクライアントエンジニアをしております。
とはいえ、今年の記事もクライアントとはまったく関係ありません。

普段Googleフォームなどでアンケートを回答する際に、
「あれ、途中で保存することができないの?」って自分はたまに思います。

ユーザーが一項目ずつ入力したらサーバーに送信してデータベースに記録するから、
ページに再度アクセスしたら記録されている情報を自動的に反映するまで、
PHPを使って簡単に実装してみました。

目標

「ラジオボタンの選択内容」と「テキストの入力内容」を途中保存できるようにする
2.png

開発環境

  • macOS 10.14.6
  • PHP 7.3.8
  • Laravel 6.6.0
  • MySQL 8.0.18

フロントエンド

Vue.jsで入力内容の操作

今回の戦場はLaravelプロジェクトのwelcome画面にします。
まずはそこにVue.jsを導入して、ラジオボタン3つとテキストボックス1つを置きます。

resources/views/welcome.blade.php
...
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
...
<div id="app">
    ラジオボタン<br/>
    <input type="radio" value="1" v-model="radio">選択肢1<br/>
    <input type="radio" value="2" v-model="radio">選択肢2<br/>
    <input type="radio" value="3" v-model="radio">選択肢3<br/>
    <br/>
    テキスト入力<br/>
    <input type="text" v-model="text" placeholder="内容を入力">
    <br/>
</div>
...
public/js/main.js
const app = new Vue({
    el: '#app',
    data: {
        radio: '2',
        text: 'あいうえお'
    },
});

これでradiotextでラジオボタンとテキストボックスを操作することができます。
1.png

UUIDの作成と保存

javascriptで適当なUUIDを生成する方法がありまして、
生成したUUIDをJavaScript Cookieでローカルに保存するようにします。

resources/views/welcome.blade.php
...
<script src="https://cdn.jsdelivr.net/npm/js-cookie@beta/dist/js.cookie.min.js"></script>
...
public/js/main.js
const app = new Vue({
    el: '#app',
    data: {
        radio: '',
        text: '',
        uuid: ''
    },
    methods: {
        initUUID: function() {
            if (Cookies.get('uuid') !== undefined) {
                this.uuid = Cookies.get('uuid');
                return;
            }

            var d = new Date().getTime();
            var d2 = (performance && performance.now && (performance.now() * 1000)) || 0;
            this.uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
                var r = Math.random() * 16;
                if (d > 0){
                    r = (d + r) % 16 | 0;
                    d = Math.floor(d / 16);
                } else {
                    r = (d2 + r) % 16 | 0;
                    d2 = Math.floor(d2 / 16);
                }
                return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
            });

            // とりあえず期限を10年にする
            Cookies.set('uuid', this.uuid, { expires: 3650 });
        }
    }
});

app.initUUID();

これで画面を開く度にcookieからuuidを取得し、存在しない場合は生成できるようになりました。

サーバーとの通信

サーバーとの通信はaxiosで行います。

resources/views/welcome.blade.php
...
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
...
public/js/main.js
const app = new Vue({
...
    methods: {
        saveData: function(key, value) {
            let postData = {
                'user_id': this.uuid,
                'key': key,
                'value': value
            };

            axios.post("/saveData", postData).then(response => {
                // 成功
            }).catch(error => {
                // 失敗
            });
        },

        loadData: function () {
            let postData = {
                'user_id': this.uuid
            };

            axios.post("/loadData", postData).then(response => {
                // 成功
            }).catch(error => {
                // 失敗
            });
        }
    }
});

送信する内容についてですが、
文字を入力する度に送信してしまうとサーバーに負荷をかける可能性がありますので、
今回は連続する入力を無視してくれるLodashdebounceで制御します。

resources/views/welcome.blade.php
...
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
...
<div id="app">
    ラジオボタン<br/>
    <input type="radio" value="1" v-model="radio" @click="isRadioSelecting = true">選択肢1<br/>
    <input type="radio" value="2" v-model="radio" @click="isRadioSelecting = true">選択肢2<br/>
    <input type="radio" value="3" v-model="radio" @click="isRadioSelecting = true">選択肢3<br/>
    <br/>
    テキスト入力<br/>
    <input type="text" v-model="text" @input="isTextTyping = true" placeholder="内容を入力">
    <br/>
</div>
...
public/js/main.js
const app = new Vue({
    el: '#app',
    data: {
        ...
        isTextTyping: false,
        isRadioSelecting: false,
        ...
    },
    watch: {
        radio: _.debounce(function() {
            this.isRadioSelecting = false;
        }, 1000),

        text: _.debounce(function() {
            this.isTextTyping = false;
        }, 2000),

        isRadioSelecting: function(selecting) {
            if (selecting) {
                return;
            }
            this.saveData('radio', this.radio);
        },

        isTextTyping: function(typing) {
            if (typing) {
                return;
            }
            this.saveData('text', this.text);
        },
    },
    ...
});

これでラジオボタンは選択停止後1秒、テキストボックスは入力停止後2秒からサーバーにデータを送るようになりました。

最後に、ステータスをわかるためにvue2-notifyを使ってプッシュ通知を表示させます。

resources/views/welcome.blade.php
...
<script src="https://cdnjs.cloudflare.com/ajax/libs/element-ui/2.4.0/index.js"></script>
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
...

使い方の例

this.$notify.info({
    title: '受信',
    message: '内容読み取り完了'
});

これで、フロントエンドの方は必要な機能を揃えました。
完成したコードはこの記事の最後にまとめております。

サーバーサイド

データベース構造

テストのため、すごくシンプルなテーブルを作ります。

+------------+
| database() |
+------------+
| vue_test   |
+------------+

+------------+
| TABLE_NAME |
+------------+
| user_input |
+------------+

+-------------+-----------+
| COLUMN_NAME | DATA_TYPE |
+-------------+-----------+
| id          | int       |
| user_id     | varchar   |
| radio       | int       |
| text        | varchar   |
+-------------+-----------+

リクエストデータ処理クラス

ユーザー入力内容をデータベースに書き込む・読み取り処理を行います。

app/Http/Controllers/UserInputController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class UserInputController extends Controller
{
    public function saveData(Request $request)
    {
        DB::table('user_input')->updateOrInsert(
            [
                'user_id' => $request->input('user_id')
            ],
            [
                $request->input('key') => $request->input('value')
            ]
        );
    }

    public function loadData(Request $request)
    {
        $user_id = $request->input('user_id');
        $data = [
            'result' => DB::table('user_input')->where('user_id', $user_id)->first()
        ];
        return $data;
    }
}

ルーティング

routes/web.php
...
Route::post('/saveData', 'UserInputController@saveData');
Route::post('/loadData', 'UserInputController@loadData');
...

コードまとめ

resources/views/welcome.blade.php
<!DOCTYPE html>
<html>
    <head>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/js-cookie@beta/dist/js.cookie.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/element-ui/2.4.0/index.js"></script>
        <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
    </head>
    <body>
        <div id="app">
            ラジオボタン<br/>
            <input type="radio" value="1" v-model="radio" @click="isRadioSelecting = true">選択肢1<br/>
            <input type="radio" value="2" v-model="radio" @click="isRadioSelecting = true">選択肢2<br/>
            <input type="radio" value="3" v-model="radio" @click="isRadioSelecting = true">選択肢3<br/>
            <br/>
            テキスト入力<br/>
            <input type="text" v-model="text" @input="isTextTyping = true" placeholder="内容を入力">
            <br/>
        </div>
    </body>
    <script src="{{ asset('/js/main.js') }}"></script>
</html>

public/js/main.js
const app = new Vue({
    el: '#app',
    data: {
        radio: '',
        text: '',
        isTextTyping: false,
        isRadioSelecting: false,

        uuid: ''
    },
    watch: {
        radio: _.debounce(function() {
            this.isRadioSelecting = false;
        }, 1000),

        text: _.debounce(function() {
            this.isTextTyping = false;
        }, 2000),

        isRadioSelecting: function(selecting) {
            if (selecting) {
                return;
            }
            this.saveData('radio', this.radio, 'ラジオボタン');
        },

        isTextTyping: function(typing) {
            if (typing) {
                return;
            }
            this.saveData('text', this.text, 'テキスト入力');
        },
    },
    methods: {
        initUUID: function() {
            if (Cookies.get('uuid') !== undefined) {
                this.uuid = Cookies.get('uuid');
                return;
            }

            var d = new Date().getTime();
            var d2 = (performance && performance.now && (performance.now() * 1000)) || 0;
            this.uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
                var r = Math.random() * 16;
                if (d > 0){
                    r = (d + r) % 16 | 0;
                    d = Math.floor(d / 16);
                } else {
                    r = (d2 + r) % 16 | 0;
                    d2 = Math.floor(d2 / 16);
                }
                return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
            });


            Cookies.set('uuid', this.uuid, { expires: 3650 });
        },

        saveData: function(key, value, description) {
            let postData = {
                'user_id': this.uuid,
                'key': key,
                'value': value
            };
            axios.post("/saveData", postData).then(response => {
                this.$notify.info({
                    title: '送信',
                    message: '内容保存済み:' + description
                });
            }).catch(error => {
                this.$notify.error({
                    title: '送信',
                    message: '送信に失敗しました'
                })
            });
        },

        loadData: function () {
            let postData = {
                'user_id': this.uuid
            };

            axios.post("/loadData", postData).then(response => {
                let data = response.data['result'];
                if (data == null) {
                    this.$notify.info({
                        title: '受信',
                        message: '新規ユーザー'
                    });
                    return;
                }

                this.radio = data['radio'];
                this.text = data['text'];
                this.$notify.info({
                    title: '受信',
                    message: '内容読み取り完了'
                });
            }).catch(error => {
                this.$notify.error({
                    title: '受信',
                    message: '受信に失敗しました'
                })
            });
        }
    }
});

app.initUUID();
app.loadData();
app/Http/Controllers/UserInputController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class UserInputController extends Controller
{
    public function saveData(Request $request)
    {
        DB::table('user_input')->updateOrInsert(
            [
                'user_id' => $request->input('user_id')
            ],
            [
                $request->input('key') => $request->input('value')
            ]
        );
    }

    public function loadData(Request $request)
    {
        $user_id = $request->input('user_id');
        $data = [
            'result' => DB::table('user_input')->where('user_id', $user_id)->first()
        ];
        return $data;
    }
}
routes/web.php
<?php

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return view('welcome');
});

Route::post('/saveData', 'UserInputController@saveData');
Route::post('/loadData', 'UserInputController@loadData');

最後に

Vue.jsが使いやすくて、サードパーティのライブラリもたくさんあって、
導入と実装がかなり楽でした(cocos2d-xとunityと比べるとねw)

また、項目を増やす度にテーブルにカラムを追加するのはさすがに面倒ですね。
その場合はテーブルのスキーマをユーザーID、項目ID、内容にして、
select文でユーザーIDと項目IDで検索して、その結果を処理して反映すればいいと思います。

ありがとうございました。

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

Github ActionsでLaravelのテストとECRへpushするまで

目標

Laravelで作成したアプリに対して、Github Actionsを利用してテストの実行及びECRへのイメージのpushまで行います。

前提条件

  • Laravelアプリケーション作成済み
  • Laravel実行用イメージ作成済み
  • ECRのリポジトリ作成済み

Github Actionsについて

いわゆるCI / CDです。細かい部分は既に多くご紹介されているので省略いたします。
公式

Public Repositoryだと無料みたいですね。
PrivateだとFreeプランでは1ヶ月2000分までだとか。

Laravelのテスト実行

こちらは既にテンプレートが用意されており、変更するところといえば実行タイミングくらいでしょうか。MySQLやRedisが欲しい場合は別途追加する必要があります。
今回はテストなのでDBもsqliteで、実行タイミングもmasterへのpushという指定をしていますが、実際はかなり柔軟に設定できます。
リファレンス

name: Laravel

on:
  push:
    branches:
    - master

jobs:
  laravel-tests:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v1
    - name: Copy .env
      run: php -r "file_exists('.env') || copy('.env.example', '.env');"
    - name: Install Dependencies
      run: composer install -q --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist
    - name: Generate key
      run: php artisan key:generate
    - name: Create Database
      run: |
        mkdir -p database
        touch database/search.sqlite
    - name: Execute tests (Unit and Feature tests) via PHPUnit
      env:
        DB_CONNECTION: sqlite
        DB_DATABASE: database/database.sqlite
      run: vendor/bin/phpunit

masterへのpushが完了すると、

Screen Shot 2019-12-02 at 15.08.02.png

という感じで結果を見ることができます。
特にテストも追加していないデフォルトの状態でテストを実行した場合、実行時間は合計35秒でした。このあと何回か実施したところ50秒くらいのもあったので、そんくらいのブレでおさまる感じみたいですね。

ECRへのpush

テストだけで終わるのもさみしいので、ECRへイメージをpushするところまでやりたいと思います。
こちらも既にテンプレートが用意されているというか、もっといえばECSへのデプロイまでやってくれるテンプレートがありましたので、まずはそのうちのECRへのイメージのpushのところを使ってやってみようと思います。

on:
  push:
    branches:
      - master

name: Deploy to Amazon ECS

jobs:
  deploy:
    name: Deploy
    runs-on: ubuntu-latest

    steps:
    - name: Checkout
      uses: actions/checkout@v1

    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ap-northeast-1

    - name: Login to Amazon ECR
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v1

    - name: Build, tag, and push image to Amazon ECR
      id: build-image
      env:
        ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        ECR_REPOSITORY: sample_repository
        IMAGE_TAG: ${{ github.sha }}
      run: |
        docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
        docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
        echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"

変更するところはリポジトリ名です。今回は「sample_repository」というリポジトリ名にしているので、こちらを変更しています。

        ECR_REPOSITORY: sample_repository

また、「${{ secrets.AWS_ACCESS_KEY_ID }}」という形で利用できる変数は以下を参考に設定してみてください。今回設定するのは以下2点です。

  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY

登録手順

実行すると以下のようになるかなと思います。
Screen Shot 2019-12-02 at 15.27.46.png

終わりに

今回の設定のままだとイメージのタグが${{ github.sha }}で設定されてしまうので、ブランチのタグから取得するような形にするときれいにイメージを保存できそうですね。

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

Laravelでセッションを利用できるまでの準備の流れ

laravelでのsession

laravelではセッションの起点が\Illuminate\Session\Middleware\StartSessionというミドルウェアになります。

/Illuminate/Session/Middleware/StartSession
public function handle($request, Closure $next)
{
    if (! $this->sessionConfigured()) {
        return $next($request);
    }

    $request->setLaravelSession(
        $session = $this->startSession($request)
    );

    $this->collectGarbage($session);

    $response = $next($request);

    $this->storeCurrentUrl($request, $session);

    $this->addCookieToResponse($response, $session);

    $this->saveSession($request);

    return $response;
}

$this->sessionConfigured()でセッションを開始するかどうかを判定しています。

laravelではセッションの設定はconfig/sessionに記述していきます。そのファイルがなかったり、driverがnullだったりするとセッションは使われません。

sessionの開始

$request->setLaravelSession()はリクエストのプロパティに生成したセッションをセットしているだけなので、実質的な処理は$this->startSessionになります。

/Illuminate/Session/Middleware/StartSession
protected function startSession(Request $request)
{
    return tap($this->getSession($request), function ($session) use ($request) {
        $session->setRequestOnHandler($request);

        $session->start();
    });
}

sessionの取得

$this->getSession()でセッションの取得を行っていきます。

/Illuminate/Session/Middleware/StartSession
public function getSession(Request $request)
{
    return tap($this->manager->driver(), function ($session) use ($request) {
        $session->setId($request->cookies->get($session->getName()));
    });
}

/Illuminate/Session/SessionManagerdriver()を呼んでドライバーの取得をしています。driver()自体はスーパークラスの/Illuminate/Support/Managerに実装されており、最終的にSessionManager::createNativeDriverが呼ばれます。

ドライバーとはセッションの値をどこに保存するかを決めるもので、デフォルトではfileになっていて、databaseなどにも変更できるようです。

/Illuminate/Session/SessionManager
protected function createNativeDriver()
{
    $lifetime = $this->config->get('session.lifetime');

    return $this->buildSession(new FileSessionHandler(
        $this->container->make('files'), $this->config->get('session.files'), $lifetime
    ));
}

$this->buildSession()にセッション用のファイルハンドラを渡してセッションを生成してそうです。

/Illuminate/Session/SessionManager
protected function buildSession($handler)
{
    return $this->config->get('session.encrypt')
            ? $this->buildEncryptedSession($handler)
            : new Store($this->config->get('session.cookie'), $handler);
}

$this->config->get('session.encrypt')はデフォルトではfalseなので、ここで本体である/Illuminate/Session/Storeが生成されています。

ここで第一引数に渡している文字列はcookieのNameとして使われています。(APP_NAME_session)ブラウザでも確認することができますね!

セッションIDのセット

セッションをのインスタンスを取得したので、getSession()に戻って、tap関数でidをセットしていきます。

$session->setId($request->cookies->get($session->getName()));

先程new Store()の第一引数に渡した文字列を使って、cookieからidを取得しています。もしidがなければidを生成してセットしてくれます。

セッションの開始

無事にセッションを取得できたので、セッションを開始してきます。

$session->setRequestOnHandler($request)はデフォルトでは関係ないので割愛)

/Illuminate/Session/Middleware/StartSession
protected function startSession(Request $request)
{
    return tap($this->getSession($request), function ($session) use ($request) {
        $session->setRequestOnHandler($request);

        $session->start();
    });
}
/Illuminate/Session/Store
public function start()
{
    $this->loadSession();

    if (! $this->has('_token')) {
        $this->regenerateToken();
    }

    return $this->started = true;
}

loadSession()でfileからセッションに格納している値を読み出してきます。

その中に_token(CSRF用)がなければトークンを生成します。

以上でセッションを利用する準備が整いました!

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

Laravel Mixで圧縮時にconosole.logを削除する

Laravel Mixがterser-webpack-pluginの設定を中継してくれるので、
webpack.mix.jsに、この設定を含めておくだけでnpm run prod時にconosole.logが削除される。
npm run watchnpm run devするときには設定をスルーしてくれる。

webpack.mix.js
//console.log削除設定
mix.options({
    terser: {
        terserOptions: {
            compress: {
                drop_console: true
            }
        }
    }
});

以下バージョンで動作確認済み

package.json
"laravel-mix": "^4.1.4",

ありがたやありがたや

参考リンク

Laravel Mix Options
terser-webpack-plugin

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

Laravel Mixで圧縮時にconosole.*を削除する

Laravel Mixがterser-webpack-pluginの設定を中継してくれるので、
webpack.mix.jsに、以下の設定を含めておくだけでnpm run prod時にconosole.logが削除される。
npm run watchnpm run devするときには設定をスルーしてくれる。

webpack.mix.js
//console.log削除設定
mix.options({
    terser: {
        terserOptions: {
            compress: {
                drop_console: true
            }
        }
    }
});

以下バージョンで動作確認済み

package.json
"laravel-mix": "^4.1.4",

ありがたやありがたや

参考リンク

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

Mac × MAMP × Laravelで接続した時に起こった事

事象

・なんかLaravelインストールしたらMAMPの「MY WEBSITE」に接続できなくなった
 ※正確に言うと、「The requested URL /~ was not found on this server.」の404エラーが出ました。

原因

・Laravelインストールしたと思ったらできてなかった....
→composer導入した後に、インストールしたつもりになってた

・Laravel側で複数設定が必要なファイルに対して追加できていなかった。
 ■database.php
  →使用しているデータベースの情報を追加できていなかった

・MAMP側の設定ファイルでLaravelの追加ができていなかった
 ■httpd-vhosts.conf
  →設定されているPort番号がLaravelの行で設定している
   Port番号と一致していなかった。
   NameVitualHost *:oooo
   ※上記とここのファイルでLaravelの追加が必要
 ■httped.conf
  →ここでVitural hostsの下の行でコメントアウトされている文章の#を外す
   (ここは出来てきた)

所感

・ターミナル時にコマンド実行して大量のメッセージが流れた時に、
 読み取る前にやった気になってたかも
・MAMPを初めて導入した時より早く事象の解決が出来た気がした。

 //MAMP導入時→12,3時間くらい
 //今回→6,7時間くらい

参考元

PKunitoさんとCodedayの作者様、理解しやすい記事を作成いただいてありがとうございました。

・Laravel開発:1.環境構築をMAMPを使用して作成する
 https://qiita.com/PKunito/items/6a3bb187ca3c67de4519

・MAMPを使用してLaravelアプリをMySQLに接続する方法
 https://codeday.me/jp/qa/20190324/474003.html  

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

Laradockを用いてDocker/Apache/PHP7.2/MySQL/Laravelの開発環境を構築する

Laradockクローン

ルートディレクトリにて、下記を実行。
$ git clone https://github.com/LaraDock/laradock.git
.envをenv-exampleからコピーして作成。
$ cd laradock
$ cp env-example .env
プロジェクト作成

まずは、ワークスペースを起動。
$ cd laradock
$ docker-compose up -d workspace
ワークスペースに入る。
$ docker-compose exec workspace bash
Laravelのプロジェクトを作成

composer create-project laravel/laravel web

dockerを一旦終了

exit

$ docker-compose down
laradock/.envのpathを作成したプロジェクトに変更。

Point to the path of your applications code on your host

APP_CODE_PATH_HOST=../new_project

apache2の設定変更

ハマったところ。
apache2を使用するので、laradock/apache2/sites/default.apache.confを変更。
ServerName localhost
DocumentRoot /var/www/public/
Options Indexes FollowSymLinks

各バージョンを指定(.env)
PHP_VERSION=7.2
MYSQL_VERSION=latest
(mysql/Dockerfile)
ARG MYSQL_VERSION=5.7

dockerにてコンテナを起動。
$ docker-compose up -d mysql apache2 workspace
localhostにアクセス。

docker-compose stop

既存のLaravelプロジェクトを配置する場合
webに展開
docker-compose exec --user=laradock workspace bash # workspaceへ入る
composer install
laradock@hoge:/var/www$ exit # workspaceから抜ける
$ docker-compose restart # コンテナ再起動
http://localhost/ にアクセス

Laravel のプロジェクトを Homestead 環境で 起動させました。
http://localhost:8000/ にアクセスするとエラーがでました。

RuntimeException がでる
RuntimeException
No application encryption key has been specified.
encryption key がないとあります。

key を生成する
php artisan key:generate
Application key [base64:Wdhku6YSePiOh0XjqauthSaeOhzwRKxasFjbuuHXz0w=] set successfully.
Application key が生成されました。

再度アクセス
http://localhost:8000/ にアクセスすると Laravel の初期画面が表示されました

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

Laradockを利用してLaravelの開発環境を作る

この記事は CODEBASE okinawa Advent Calendar 2019 2日目の記事です :coffee:

本記事では、Laradockを使って、Laravelの開発環境を構築する手順について書こうと思います。

普段の開発環境は、Laradockを利用していないのですが、Laravelを最近さわりはじめたという方からLaradockでの開発環境構築につまずいた(migrateに失敗する)と相談を受けた際に、サクッと解決方法を提示できなかったので、改めて自分でも触ってみてみました (PHPフレームワークLaravel Webアプリケーション開発を参考にした、と話していたので、本記事でも同じような手順で書いてみました :pray:

Laradockとは

Laradock is a full PHP development environment based on Docker.

ざっくりの説明だと、Laravelの開発環境をDockerをベースでサクッと提供してくれるものです。(詳しくは 公式のページ をみていただけると、わかりやすいかもです :pencil:

環境構築手順

前提

  • Dockerがインストールされている事  
  • gitがインストールされている事

Laradockのダウンロード

  • 以下のコマンドを実行する
# 作業ディレクトリ作成
$ mkdir laravel_docker

# 移動
$ cd laravel_docker

# laradockをclone
$ git clone https://github.com/LaraDock/laradock.git

コンテナの初期化

  • 以下のコマンドを実行する
# laradockディレクトリに移動
$ cd laradock

# envファイルをコピー
$ cp env-example .env

# コンテナの起動 (若干時間かかる)
$ docker-compose up -d nginx mysql workspace phpmyadmin

# コンテナがちゃんと起動してるか確認
$ docker ps
CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS                                      NAMES
42218ff4fdeb        laradock_phpmyadmin   "/docker-entrypoint.…"   10 seconds ago      Up 8 seconds        0.0.0.0:8080->80/tcp                       laradock_phpmyadmin_1
ce8875359faa        laradock_nginx        "/bin/bash /opt/star…"   47 seconds ago      Up 9 seconds        0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp   laradock_nginx_1
229e9c9b0fb9        laradock_php-fpm      "docker-php-entrypoi…"   49 seconds ago      Up 47 seconds       9000/tcp                                   laradock_php-fpm_1
3f398da51a14        laradock_workspace    "/sbin/my_init"          50 seconds ago      Up 48 seconds       0.0.0.0:2222->22/tcp                       laradock_workspace_1
761b70cc0bd2        laradock_mysql        "docker-entrypoint.s…"   51 seconds ago      Up 10 seconds       0.0.0.0:3306->3306/tcp, 33060/tcp          laradock_mysql_1
8f28241ced91        docker:dind           "dockerd-entrypoint.…"   51 seconds ago      Up 49 seconds       2375-2376/tcp                              laradock_docker-in-docker_1

Laravelプロジェクトの作成

  • 以下のコマンドを実行する
# workspaceコンテナに接続
$ docker-compose exec --user=laradock workspace bash

# Laravelプロジェクト作成
$ composer create-project laravel/laravel sampleapp --prefer-dist "6.*.*"

# コンテナから出る
$ exit
  • laradockディレクトリの .env ファイルの設定を変更する

  • 変更前

# Point to the path of your applications code on your host
APP_CODE_PATH_HOST=../
  • 変更後
# Point to the path of your applications code on your host
APP_CODE_PATH_HOST=../sampleapp
  • 変更した設定を反映させるため、コンテナを再起動させる
# サービスの停止
$ docker-compose stop

# サービスの起動
$ docker-compose up -d nginx mysql
  • ブラウザから http://localhost にアクセスして、LaravelのWelcom画面が表示を確認できたら :ok:

Laravel.png

php artisan migrate ができることを確認する

  • エラーその1(DBに接続できてない)
laradock@fd5cdbe63d4b:/var/www$ php artisan migrate

   Illuminate\Database\QueryException  : SQLSTATE[HY000] [2002] Connection refused (SQL: select * from information_schema.tables where table_schema = laravel and table_name = migrations and table_type = 'BASE TABLE')

  at /var/www/vendor/laravel/framework/src/Illuminate/Database/Connection.php:665
    661|         // If an exception occurs when attempting to run a query, we'll format the error
    662|         // message to include the bindings with SQL, which will make this exception a
    663|         // lot more helpful to the developer instead of just the database's errors.
    664|         catch (Exception $e) {
  > 665|             throw new QueryException(
    666|                 $query, $this->prepareBindings($bindings), $e
    667|             );
    668|         }
    669|

  Exception trace:

  1   PDOException::("SQLSTATE[HY000] [2002] Connection refused")
      /var/www/vendor/laravel/framework/src/Illuminate/Database/Connectors/Connector.php:70

  2   PDO::__construct("mysql:host=127.0.0.1;port=3306;dbname=laravel", "root", "", [])
      /var/www/vendor/laravel/framework/src/Illuminate/Database/Connectors/Connector.php:70

  Please use the argument -v to see more details.
  • DBに接続できてないのはlaravelの.envの設定ができていないっぽいので、Laradockのデフォルトの設定に合わせて修正しました

  • 変更前

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=
  • 変更後(Laradockのデフォルトの設定に合わせる)
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=default
DB_USERNAME=default
DB_PASSWORD=secret
  • 上記修正後、あらためて php artisan migrate すると以下のエラーが発生

  • エラーその2(MySQLサーバー側で使われている認証方式がLaravel側でサポートされていないっぽい)

laradock@fd5cdbe63d4b:/var/www$ php artisan migrate

   Illuminate\Database\QueryException  : SQLSTATE[HY000] [2054] The server requested authentication method unknown to the client (SQL: select * from information_schema.tables where table_schema = default and table_name = migrations and table_type = 'BASE TABLE')

  at /var/www/vendor/laravel/framework/src/Illuminate/Database/Connection.php:665
    661|         // If an exception occurs when attempting to run a query, we'll format the error
    662|         // message to include the bindings with SQL, which will make this exception a
    663|         // lot more helpful to the developer instead of just the database's errors.
    664|         catch (Exception $e) {
  > 665|             throw new QueryException(
    666|                 $query, $this->prepareBindings($bindings), $e
    667|             );
    668|         }
    669|

  Exception trace:

  1   PDOException::("PDO::__construct(): The server requested authentication method unknown to the client [caching_sha2_password]")
      /var/www/vendor/laravel/framework/src/Illuminate/Database/Connectors/Connector.php:70

  2   PDO::__construct("mysql:host=mysql;port=3306;dbname=default", "default", "secret", [])
      /var/www/vendor/laravel/framework/src/Illuminate/Database/Connectors/Connector.php:70

  Please use the argument -v to see more details.
  • MySQLユーザに対する認証方式を変更する
$ docker exec -it laradock_mysql_1 sh
# mysql -uroot -proot -A
mysql> alter user 'default'@'%' identified with mysql_native_password by 'secret';
  • laradockのmysql/my.cnfを修正する(default_authentication_plugin=mysql_native_passwordを追加する)
# The MySQL  Client configuration file.
#
# For explanations see
# http://dev.mysql.com/doc/mysql/en/server-system-variables.html

[mysql]

[mysqld]
sql-mode="STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION"
character-set-server=utf8
default_authentication_plugin=mysql_native_password // ⬅️追加

  • 上記修正した後にあらためて php artisan migrate する。うまくいったのでこれで大丈夫そう :ok:
laradock@fd5cdbe63d4b:/var/www$ php artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (0.07 seconds)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table (0.04 seconds)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated:  2019_08_19_000000_create_failed_jobs_table (0.02 seconds)

まとめ

  • 今までLaradockを使ったことなかったけど、Laravelの環境構築すぐできて便利 :pencil:
  • いろんなものが入ってるので、わかりにくところは多そう
  • 困ってる方の質問を受けた時にサクッとこれぐらいこたえれれるようにがんばろ :muscle:

参考

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

LaravelのMailMessageクラスをカスタマイズする

php artisan make:notificationで通知のひな型を生成するとNotificationを継承した以下の様なコードが現れます。

public function toMail($notifiable)
{
    return (new MailMessage)
                ->line('The introduction to the notification.')
                ->action('Notification Action', url('/'))
                ->line('Thank you for using our application!');
}

このひな型の中では、MailMessageクラスを使ってメールの文章が構築されていますが、このMailMessageクラスに関するドキュメントがあまりないため、恐らく多くのケースで、まったく別のコードに置き換えるか、雰囲気でカスタマイズしていたりするのではないでしょうか?

MailMessageクラスと上手く付き合う事で、HTMLメールとテキストメールの2パターン分のbladeを作成&更新する手間から解放される事ができます。
この記事では、MailMessageクラスの仕組みとカスタマイズ方法を解説していきます。

Laravelのバージョン

Laravel 5.7

文章の構築

文章の構築はsubject(), greeting(), line()などのメソッドを呼び出して行います。
各メソッドがメール上のどこに出力されるかは、以下の図の通りです。
左の画像が構築されるHTMLメールで、右の画像が構築する処理、赤矢印で対応を表しています。

2019-12-01_21h46_03.png

上図の補足ですが、subjectはメールの件名となります。
①には.envAPP_ENVの値が出力されます。
②はリンクになっておりaction()の第二引数で指定したURLが出力されます。
また、level()の指定により色が変わります。
③はアクションボタンの出力がある場合のみ出力される注意書きです。
英語が出力されている箇所については、@langを通して出力されているので
Translation Stringsを使って置き換える事ができます。

テキストメールしか対応していないメールクライアントの場合、以下の様に表示されます。

2019-12-01_21h43_31.png

使用されるbladeについて

デフォルトでは以下のbladeを使用してレンダリングされます。

HTMLメールの場合

テキストメールの場合

①の内容を確認すると、本文部分のテキストをIlluminate\Mail\Markdown::parse()を通しているためHTMLメールではmarkdown記法が使える様です。
逆に、④では、Illuminate\Mail\Markdown::parse()を通していない他、strip_tags関数を通してHTMLタグを取り除いている事が分かります。
なお、③と⑥は同じファイルです。

bladeのカスタマイズ方法

③⑥はMailMessagemarkdown('your-blade-name')で指定したbladeに差し替える事ができます。
①②は/resources/views/vendor/mail/html以下に同名のbladeを配置すると、そちらが使われます。
④⑤は/resources/views/vendor/mail/markdown以下に同名のbladeを配置すると、そちらが使われます。

Markdownについて

MailMessageではMarkdown記法が使用でき、パースにはparsedownが使用されています。(デモページ
使える文法はGitHub Flavoredに準拠している様です。

なお、テキストメールには変換前のmarkdownがそのまま出力されます。

スタイルのカスタマイズについて

HTMLメールで適応されるCSSをthemeと呼んでいる様です。

デフォルトで適応されるCSS(theme)は以下となります。
Illuminate/Mail/resources/views/html/themes/default.css

カスタマイズしたい場合は/resources/views/vendor/mail/html/themes/default.cssを作成すると、そちらが使われます。

改行する方法

デザインの都合上、1つのline()の中で改行したいケースがあります。
(改行を含む文字列を入力しても、出力時には取り払われてしまう。brタグも同様。)
その場合は以下の様に書く必要があります。

->line(new HtmlString("1行目のテキスト<br>\n2行目のテキスト"))

キモは<br>\nを両方書くことです。
これにより、HTMLメールとテキストメールの両方で改行されます。

最後に

以上が、MailMessageの仕組みとカスタマイズ方法となります。
お役に立てましたら「いいね」を頂けると嬉しいです!!

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

Laravel のモデルクラスをどこに配置するか問題について考えてみる

この記事について

Laravel Advent Calender 2019 2日目の記事です。

Laravel では、モデルクラスの置き場所が決められておらず、デフォルトで作成される User クラスは app 直下に置かれています。とはいえ、app 直下にすべてのモデルクラスを置いてしまうと、ツリービューで見たときの視認性が悪くなってしまうので、できれば役割やコンテキストごとに分割して配置したい、という気持ちになります。

これまで10近く Laravel を使ったアプリケーションに携わってきて、様々な構成を見てきましたが、わりと最近はひとつの形に収斂されてきてる印象を受けるので、問題提起を兼ねて、様々なパターンについてメリット/デメリットを考察しつつ、どういう配置がいいのか探ってみようという試みです。

はじめに

環境

  • Laravel 6.6.0

Model/モデルの定義

本記事では「Model」(アルファベット表記のもの)は Eloquent Model を指し、「モデル」(カタカナ表記のもの)は概念的なものを指します。本記事ではビューとコントローラー以外はすべてモデルとして扱います。

パターン一覧

  1. デフォルト
  2. Models
  3. Entities, ValueObjects, Services, etc
  4. アプリケーションと独立した Domain

初期構成

必要になったらディレクトリができるので、初期状態はすっきりしています。

$ tree -L 1 app
app
├── Console
├── Exceptions
├── Http
├── Providers
└── User.php

これに、Events, Notifications, Policies といった標準で規定されたディレクトリが加わります(これらもカスタマイズは可能ですが、特別な理由がなければそのまま使うほうがいいでしょう。

パターン1: デフォルト

前述の通り、app 直下に配置するパターンです。

配置例

tree -L 1 app         
app
├── Console
├── Customer.php
├── Deliverer.php
├── Exceptions
├── Http
├── Order.php
├── Providers
└── User.php

メリット

php artisan make:model Hoge と実行すると app/Hoge.php ができます。最少のタイプ数で作成できるのがメリットです。

デメリット

こちらも前述の通り、ツリービューで見たときにずらずらと Model のファイルが並んでしまうので、視認性が悪くなります。

所感

中には、ツリービューは見ないあるいは視認性の悪さは気にならない、という方もいるかもしれませんが、私は無理だったので、数個程度のファイルでアプリケーションが構成されているのでなければ、こちらのパターンは選択しないでしょう。

パターン2: Models

app/Models 以下に配置するパターンです。

Laravel4 の時代には models ディレクトリがあったんですが、5 になってなくなりました。4時代から触っていて、それに慣れていたので、なくなったときは、えーなんでなくしたの?と思いました。

配置例

$ tree -L 2 -d app
app
├── Console
├── Exceptions
├── Http
│   ├── Controllers
│   └── Middleware
├── Models
│   ├── Base
│   ├── Delivery
│   └── Order
└── Providers

メリット

パターン3 との対比になりますが、このパターンだと、役割ごとではなくコンテキストあるいは集約ルートごとの分割が容易になります。

注文と配送というコンテキストがあるとして、Models 以下のようにコンテキストごとに分割して配置することができます。さらにコンテキストごとに Entities, Services などをつくってもいいでしょう。

デメリット

こちらもパターン3 との対比になりますが、コンテキストがひとつないしはそれほど多くなく、コンテキストごとに振る舞いが変わらないようなドメインの場合は、階層が増えるだけであまり意味がなくなってしまうかもしれません。

所感

いまのところこれがいちばんしっくりきています。Policy や Observer のような Model に密接に関わるクラスをどこに配置するか(デフォルトか Models 以下か)というのは悩ましいところではあるんですが、いまのところはデフォルトがいいのかな、と感じています。

パターン3: Entities, ValueObjects, Services, etc

app/Entities, app/Services など、モデルの種類ごとにディレクトリを切って配置するパターンです。最近はわりとこのパターンに遭遇することが多いです(書籍やインターネット上のリソースで推奨しているものがあるんでしょうか)。

配置例

$ tree -L 2 -d app                                                                     
app
├── Console
├── Entities
│   ├── Deliver
│   └── Order
├── Exceptions
├── Http
│   ├── Controllers
│   └── Middleware
├── Providers
├── Services
│   ├── Deliver
│   └── Order
└── ValueObjects
    ├── Deliver
    └── Order

メリット

パターン2 との対比になりますが、コンテキストごとに分けたいのであれば、種類ごとのディレクトリの下で分割する形になります。その結果、種類ごとのディレクトリの下にそれぞれディレクトリができることになるので、コンテキストがひとつあるいはごく少なければ、いちばん簡潔な構成かもしれません。

デメリット

普段これでやっててあまりデメリットは感じてないですが、強いて挙げるとすれば、クラスの種類に引きずられて、関連の強いクラスが分断されてしまう恐れがあるとか、実態は値オブジェクトでないのに ValueObjects の中にあって混乱する、とかでしょうか。

所感

パターン2を選ぶか3を選ぶか、というのは、極論で言えば好みの問題、ということになる気はします。個人的には、モデルの種類(Entity なのか Service なのか)というのはあまり気にならなくて、どちらかといえば、どのコンテキストのクラス(オブジェクト)なのか、のほうに意識があるので、パターン2 を推しますが、チームでよく話し合って決めればいいのかな、と思います。

パターン4: アプリケーションと独立した Domain

最近また盛り上がりを感じるドメイン駆動設計的な、クリーンアーキテクチャ的な、フレームワークへの依存性をゼロにする、あるいは極力小さくする、という戦略のもとにつくられる、ディレクトリ構成です。

私はこのような方針で Laravel を採用しているプロジェクトには関わったことがなく、細かいメリット・デメリットは想像の範囲内でしかわからないので、配置例のみ記載することにします。もし実際に採用されている方がいれば、コメントにてメリット・デメリットを教えていただけるとありがたいです。

実装の詳細はこちらの記事を参考にするといいかもしれません。

独立したコアレイヤパターン - Shin x Blog

配置例

$ tree -L 2 -d ./app ./domain
./app
├── Console
├── Exceptions
├── Http
│   ├── Controllers
│   └── Middleware
├── Infrastructure
│   └── Repositories
└── Providers
./domain
├── Delivery
│   ├── Entities
│   └── Repositories
└── Order
    ├── Entities
    └── Repositories

上記例では、リポジトリパターンを導入し、domain/{Context}/Repositories 配下には interface を、app/Infrastructure/Repositories 配下には実装クラスをそれぞれ配置します。原則的に domain 以下は POPO (Plain Old PHP Object) なクラスになるので、フレームワークに対して疎結合にできるメリットがあります。

あと、「Policy や Observer のような Model に密接に関わるクラス」との関係をどうするか、という問題があって、これは、

  1. 使わないで自前で仕組みを用意する
  2. 使うが中身はドメインモデルに委譲できるようにする

といった解決策がありそうです。そこら辺も事前に決めておく必要があるでしょう。

おわりに

個人的にはパターン2 のように app/Models 以下にすべてを配置する形を推したいですが、パターン3 のようにクラスの種類ごとにディレクトリを切る形でもいまのところそれほど不満はありません。

上記以外のメリット・デメリットを感じている方、あるいは上記以外で、ウチではこんな構成でやってます、というのがあれば、メリット・デメリット合わせて教えていただけると助かります :bow:

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