20191224のlaravelに関する記事は21件です。

Vue.js の フォーム とか tailwindcss とか Vue Router とか コンポーネント とか

この記事は、静岡 Advent Calendar 2019 の25日目の記事です。

はじめに

静岡の勉強会事情等々、皆様がいろいろと書いてくださっているので、自分はそちらの内容はまたの機会にすることにして、当初、Laravel を用いてテストについて何か書こうかなと思っておりました。

が、ちょっとばかり路線変更しまして、Vue.js の フォーム を中心に時間が許す限り何か思うがままに書こうかなと思います。
テストにつきましては、また別の機会にでも書きます。sqlite を使ってやるよくあるやつです。大した内容ではございません。
それから、次点として当初考えておりました、Laravel で過去に作った簡単なプロジェクトを Symfony(完全未経験) で書き直す。というものも考えてはおりましたが、風邪で昨日まで1週間寝込むという愚行を犯した自分には難しかったです。(今のところ娘に移っていないようなのが救いだったり...)

うだうだと書きましたが、それでは、早速本題に入りますよ。

今回は下記スクリーンショットのような form を想定しております。

スクリーンショット 2019-12-24 16.44.08.png

環境につきましては、バックエンドに Laravel (今回あまり関係ございませんが、ディレクトリ名とか Vue CLI のものとは異なります)を、フロントエンドに Vue.js / tailwindcss (Laravel Mixを使用)を使用いたします。

tailwindcss?? なんじゃそりゃ? という方は、こちらをご参照くださいませ。

CSSのクラスとして提供しているだけであり、個人的には、UI系のフレームワークより使い勝手がいいかなと思っています。作者さんはAdam Wathanです。
まぁ、Laravel / Vue 関連クラスターの著名な方なので、なんとなく安心感もありますね笑

何はともあれ、まずはセットアップから〜

Vue.js と tailwindcss のセットアップ

それでは、最初に Vue.js と tailwindcss の下準備とセットアップでもしましょうか。

どこか好きな ディレクトリ に新規で Laravel プロジェクト を作りましょう。

$ composer create-project laravel/laravel プロジェクト名 "バージョン指定.*"
Application key XXXXXXXXXうにゃうにゃでます set successfully.

set successfully. と出ていたらインストールが完了です。インストールが終わりましたら、今作成したプロジェクトのディレクトリへ移動しておきましょう。
また、エディタでもプロジェクトを開いておきます。

下記のコマンドを打って下準備をはじめましょう。

$ php artisan preset none

これで package.json の構成が最小限のものに変わったかと思います。
続きまして、今日の本題 Vue.js を入れていきましょう。

$ php artisan preset vue

これで、package.json でも "vue": "^2.5.7",な感じに追加されていることが確認できるかと思います。

npm or yarn で Vue Routertailwindcss を入れる

## Using npm
npm install vue-router tailwindcss --save-dev

## Using Yarn
yarn add vue-router tailwindcss --save-dev

package.json に、下記のものが追加されていればOKです。

package.json
"devDependencies": {
    "tailwindcss": "^1.1.4",
    "vue": "^2.5.7",       
    "vue-router": "^3.1.3"
},

あともう少しです。

CSS に tailwindcss を追加します

resources/assets/sass/app.scss に下記の内容を書きます。

app.scss
@tailwind base;

@tailwind components;

@tailwind utilities;

tailwindcssは、ビルド時にこれらのディレクティブを、生成されたすべてのCSSと交換します。

tailwindcss 構成ファイルを作成します

下記のコマンドを打ち込み、tailwind.config.jsを作成します。

npx tailwind init

これにより、tailwind.config.js が、プロジェクトのルートディレクトリの直下に作成され、最小限のファイルが作成されます。tailwindcssに何か追加したいときに追加していくものですね。
詳細はこちら

webpack.mix.js を修正する (Laravel5.5 の場合)

webpack.mix.js
let mix = require('laravel-mix');
let tailwindcss = require('tailwindcss')

/*
 |--------------------------------------------------------------------------
 | Mix Asset Management
 |--------------------------------------------------------------------------
 |
 | Mix provides a clean, fluent API for defining some Webpack build steps
 | for your Laravel application. By default, we are compiling the Sass
 | file for the application as well as bundling up all the JS files.
 |
 */

mix.js('resources/assets/js/app.js', 'public/js')
   .sass('resources/assets/sass/app.scss', 'public/css')
   .options({
       processCssUrls: false,
       postCss: [ tailwindcss('./tailwind.config.js') ],
});

webpack.mix.js を修正する。(Laravel6.0 の場合)

webpack.mix.js
const mix = require('laravel-mix');
const tailwindcss = require('tailwindcss')

/*
 |--------------------------------------------------------------------------
 | Mix Asset Management
 |--------------------------------------------------------------------------
 |
 | Mix provides a clean, fluent API for defining some Webpack build steps
 | for your Laravel application. By default, we are compiling the Sass
 | file for the application as well as bundling up all the JS files.
 |
 */

mix.js('resources/js/app.js', 'public/js')
   .sass('resources/sass/app.scss', 'public/css')
   .options({
       processCssUrls: false,
       postCss: [ tailwindcss('./tailwind.config.js') ],
});

npm or yarn で ビルド する

## Using npm
npm run watch

## Using yarn
yarn run watch

ちょっと長かったですね、あとちょっとです。

resources/views/layouts/app.blade.php の bladeテンプレートの、<div id="app"></div>の中を、下記のように変更します。

resources/views/layouts/app.blade.php
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">

    <title>{{ config('app.name', 'Laravel') }}</title>

    <!-- Styles -->
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
    <div id="app">
        <div class="h-screen">
            @yield('content')
        </div>
    </div>
    <!-- Scripts -->
    <script src="{{ asset('js/app.js') }}"></script>
</body>
</html>

それから、resources/views/home.blade.php も、下記のように変更しましょう。
resources/assets/js/components/App.vue を読み込むように記載しています。

resources/views/home.blade.php
@extends('layouts.app')

@section('content')
    <App></App>
@endsection

続きまして、resources/assets/js/components/App.vue も下記のように書きます。
ポイントは、<router-link to="/contacts/create">の箇所です。
/contacts/create のURLに遷移するように設定します。 ( Vue Router用の設定 )

resources/assets/js/components/App.vue
<template>
    <div>
        <router-link to="/contacts/create">
            <p>新規登録</p>
        </router-link>
    </div>
</template>

ここまで出来たので、冒頭に導入した Vue Router を使ってみます。
resources/assets/js/app.js と resources/assets/js/router.js を、それぞれ開きます。

まずは、resources/assets/js/app.js から修正していきますね。

下記のようにしておきましょう。

resources/assets/js/app.js
import Vue from 'vue';
import router from "./router";
import App from "./components/App";

const app = new Vue({
    el: '#app',
    components: {
        App
    },
    router
});

続いて、resources/assets/js/router.js では、ヒストリーモードを指定し、URLが、/contacts/create のときに、ContactsCreate のコンポーネントが表示されるようにします。

resources/assets/js/router.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import ContactsCreate from "./views/ContactsCreate";

Vue.use(VueRouter);

export default new VueRouter({
    routes: [
        { path: '/contacts/create', name: 'ContactsCreate', component: ContactsCreate }
    ],
    mode: 'history'
});

それでは、下準備が終わったところで、

まずは、ファイルを2つ用意しましょう。それぞれ、views/ContactsCreate.vue と、components/InputField.vue を作ります。

views/ContactsCreate.vue は、先ほど resources/assets/js/router.js で 指定したコンポーネントのファイルになりますね。

ContactsCreate.vue から作業を進めていきます。
一旦、props を用いまして、親コンポーネントであります ContactsCreate.vue から、
子コンポーネントの components/InputField.vue へ、
v-bind を 使って、label や placeholder の値を受け渡すところまで書いてみます。

views/ContactsCreate.vue
<template>
    <div>
        <form>
            <input-field name="name" label="お名前" placeholder="例:山田ルイ53世"/>
            <input-field name="email" label="メールアドレス" placeholder="例:test@test.jp"/>
            <input-field name="company" label="組織名" placeholder="例:藤原カンパニー" />
            <input-field name="birthday" label="生年月日" placeholder="例:MM/DD/YYYY"/>
            <div class="flex justify-end relative pb-4">
                <button class="py-2 px-4 text-blue-500 border border-blue-500 mr-5 hover:bg-gray-100 hover:border-blue-300 rounded">キャンセル</button>
                <button class="bg-blue-500 border border-blue-500 py-2 px-4 text-white hover:bg-blue-400 mr-1 rounded">新規登録</button>
            </div>
        </form>
    </div>
</template>

<script>
    import InputField from "../components/InputField";
    export default {
        name: "ContactsCreate",
        components: {
            InputField,
        },
        data() {
            return {
                form: {
                    'name': '',
                    'email': '',
                    'company': '',
                    'birthday': ''
                }
            }
        }
    }
</script>

<style scoped>

</style>

続きまして、components/InputField.vue です。
props に、'name', 'label', 'placeholder' を指定しております。

components/InputField.vue
<template>
    <div class="relative pb-4">
        <label :for="name" class="text-blue-500 pt-2 uppercase text-xs font-bold absolute">{{ label }}</label>
        <input :id="name" type="text" class="pt-8 w-full text-gray-900 border-b pb-2 focus:outline-none focus:border-blue-400" :placeholder="placeholder">
    </div>
</template>

<script>
    export default {
        name: "InputField",
        props: [
            'name', 'label', 'placeholder'
        ],
        data() {
            return {
                value: ''
            }
        }
    }
</script>

<style scoped>

</style>

tailwindcss の部分も書いておりますので、ちょっとごちゃごちゃと見難い部分があるかと思いますが、
これで、name や placeholder、label に値が受け渡されたのが確認できるかと思います。

次は、今やったこととは反対に、子コンポーネントから親コンポーネントへデータを受け渡してみましょう。

components/InputField.vue
// <input>タグに @input="updateField()" を追記
<template>
    <div class="relative pb-4">
        <label :for="name" class="text-blue-500 pt-2 uppercase text-xs font-bold absolute">{{ label }}</label>
        <input :id="name" type="text" class="pt-8 w-full text-gray-900 border-b pb-2 focus:outline-none focus:border-blue-400" :placeholder="placeholder" v-model="value" @input="updateField()">
    </div>
</template>

<script>
    export default {
        name: "InputField",
        props: [
            'name', 'label', 'placeholder'
        ],
        data() {
            return {
                value: ''
            }
        },
// 以下の methods:{} 部分を追加
        methods: {
            updateField() {
                this.$emit('update:field', this.value);
            }
        }
    }
</script>

最後に、views/ContactsCreate.vue に @update:field="form.name = $event" 等を<input-field/>の中に書いて終わりです。<input-field/>のコンポーネントが4つある感じになりますね。

views/ContactsCreate.vue
<template>
    <div>
        <form>
            <input-field name="name" label="お名前" placeholder="例:山田ルイ53世" @update:field="form.name = $event"/>
            <input-field name="email" label="メールアドレス" placeholder="例:test@test.jp" @update:field="form.email = $event"/>
            <input-field name="company" label="組織名" placeholder="例:藤原カンパニー" @update:field="form.company = $event"/>
            <input-field name="birthday" label="生年月日" placeholder="例:MM/DD/YYYY" @update:field="form.birthday = $event"/>
            <div class="flex justify-end relative pb-4">
                <button class="py-2 px-4 text-blue-500 border border-blue-500 mr-5 hover:bg-gray-100 hover:border-blue-300 rounded">キャンセル</button>
                <button class="bg-blue-500 border border-blue-500 py-2 px-4 text-white hover:bg-blue-400 mr-1 rounded">新規登録</button>
            </div>
        </form>
    </div>
</template>

<script>
    import InputField from "../components/InputField";
    export default {
        name: "ContactsCreate",
        components: {
            InputField,
        },
        data() {
            return {
                form: {
                    'name': '',
                    'email': '',
                    'company': '',
                    'birthday': ''
                }
            }
        }
    }
</script>

<style scoped>

</style>

いかがでしたでしょうか? いろいろと足らない部分は多々ございますが、一旦ここで筆を置かせていただき、自分の担当であります、洗濯に移ります。明日も朝から洗濯するぞ!

tailwindcss は、一見カオティックな印象ですが、慣れるとそうでもないようなところがなかなか面白いですね!

それでは皆様、

Merry Christmas and Happy New year

良いお年を〜

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

Vue.js の フォーム について

この記事は、静岡 Advent Calendar 2019 の25日目の記事です。

はじめに

静岡の勉強会事情等々、皆様がいろいろと書いてくださっているので、自分はそちらの内容はまたの機会にすることにして、当初、Laravel を用いてテストについて何か書こうかなと思っておりました。

が、ちょっとばかり路線変更しまして、Vue.js の フォーム を中心に時間が許す限り何か思うがままに書こうかなと思います。
テストにつきましては、また別の機会にでも書きます。sqlite を使ってやるよくあるやつです。大した内容ではございません。
それから、次点として当初考えておりました、Laravel で過去に作った簡単なプロジェクトを Symfony(完全未経験) で書き直す。というものも考えてはおりましたが、風邪で昨日まで1週間寝込むという愚行を犯した自分には難しかったです。(今のところ娘に移っていないようなのが救いだったり...)

うだうだと書きましたが、それでは、早速本題に入りますよ。

今回は上記スクリーンショットのような form を想定しております。

スクリーンショット 2019-12-24 16.44.08.png

環境につきましては、バックエンドに Laravel (今回あまり関係ございませんが、ディレクトリ名とか Vue CLI のものとは異なります)を、フロントエンドに Vue.js / tailwindcss (Laravel Mixを使用)を使用いたします。

tailwindcss?? なんじゃそりゃ? という方は、こちらをご参照くださいませ。

CSSのクラスとして提供しているだけであり、個人的には、UI系のフレームワークより使い勝手がいいかなと思っています。作者さんはAdam Wathanです。
まぁ、Laravel / Vue 関連クラスターの著名な方なので、なんとなく安心感もありますね笑

何はともあれ、まずはセットアップから〜

Vue.js と tailwindcss のセットアップ

それでは、最初に Vue.js と tailwindcss の下準備とセットアップでもしましょうか。

どこか好きな ディレクトリ に新規で Laravel プロジェクト を作りましょう。

$ composer create-project laravel/laravel プロジェクト名 "バージョン指定.*"
Application key XXXXXXXXXうにゃうにゃでます set successfully.

set successfully. と出ていたらインストールが完了です。インストールが終わりましたら、今作成したプロジェクトのディレクトリへ移動しておきましょう。
また、エディタでもプロジェクトを開いておきます。

下記のコマンドを打って下準備をはじめましょう。

$ php artisan preset none

これで package.json の構成が最小限のものに変わったかと思います。
続きまして、今日の本題 Vue.js を入れていきましょう。

$ php artisan preset vue

これで、package.json でも "vue": "^2.5.7",な感じに追加されていることが確認できるかと思います。

npm or yarn で Vue Routertailwindcss を入れる

## Using npm
npm install vue-router tailwindcss --save-dev

## Using Yarn
yarn add vue-router tailwindcss --save-dev

package.json に、下記のものが追加されていればOKです。

package.json
"devDependencies": {
    "tailwindcss": "^1.1.4",
    "vue": "^2.5.7",       
    "vue-router": "^3.1.3"
},

あともう少しです。

CSS に tailwindcss を追加します

resources/assets/sass/app.scss に下記の内容を書きます。

app.scss
@tailwind base;

@tailwind components;

@tailwind utilities;

tailwindcssは、ビルド時にこれらのディレクティブを、生成されたすべてのCSSと交換します。

tailwindcss 構成ファイルを作成します

下記のコマンドを打ち込み、tailwind.config.jsを作成します。

npx tailwind init

これにより、tailwind.config.js が、プロジェクトのルートディレクトリの直下に作成され、最小限のファイルが作成されます。tailwindcssに何か追加したいときに追加していくものですね。
詳細はこちら

webpack.mix.js を修正する (Laravel5.5 の場合)

webpack.mix.js
let mix = require('laravel-mix');
let tailwindcss = require('tailwindcss')

/*
 |--------------------------------------------------------------------------
 | Mix Asset Management
 |--------------------------------------------------------------------------
 |
 | Mix provides a clean, fluent API for defining some Webpack build steps
 | for your Laravel application. By default, we are compiling the Sass
 | file for the application as well as bundling up all the JS files.
 |
 */

mix.js('resources/assets/js/app.js', 'public/js')
   .sass('resources/assets/sass/app.scss', 'public/css')
   .options({
       processCssUrls: false,
       postCss: [ tailwindcss('./tailwind.config.js') ],
});

webpack.mix.js を修正する。(Laravel6.0 の場合)

webpack.mix.js
const mix = require('laravel-mix');
const tailwindcss = require('tailwindcss')

/*
 |--------------------------------------------------------------------------
 | Mix Asset Management
 |--------------------------------------------------------------------------
 |
 | Mix provides a clean, fluent API for defining some Webpack build steps
 | for your Laravel application. By default, we are compiling the Sass
 | file for the application as well as bundling up all the JS files.
 |
 */

mix.js('resources/js/app.js', 'public/js')
   .sass('resources/sass/app.scss', 'public/css')
   .options({
       processCssUrls: false,
       postCss: [ tailwindcss('./tailwind.config.js') ],
});

npm or yarn で ビルド する

## Using npm
npm run watch

## Using yarn
yarn run watch

ちょっと長かったですね、それでは、下準備が終わったところで、

まずは、ファイルを2つ用意しましょう。それぞれ、views/ContactsCreate.vue と、components/InputField.vue を作ります。

ContactsCreate.vue から作業を進めていきます。
一旦、props を用いまして、親コンポーネントであります ContactsCreate.vue から、
子コンポーネントの components/InputField.vue へ、
v-bind を 使って、label や placeholder の値を受け渡すところまで書いてみます。

views/ContactsCreate.vue
<template>
    <div>
        <form>
            <input-field name="name" label="お名前" placeholder="例:山田ルイ53世"/>
            <input-field name="email" label="メールアドレス" placeholder="例:test@test.jp"/>
            <input-field name="company" label="組織名" placeholder="例:藤原カンパニー" />
            <input-field name="birthday" label="生年月日" placeholder="例:MM/DD/YYYY"/>
            <div class="flex justify-end relative pb-4">
                <button class="py-2 px-4 text-blue-500 border border-blue-500 mr-5 hover:bg-gray-100 hover:border-blue-300 rounded">キャンセル</button>
                <button class="bg-blue-500 border border-blue-500 py-2 px-4 text-white hover:bg-blue-400 mr-1 rounded">新規登録</button>
            </div>
        </form>
    </div>
</template>

<script>
    import InputField from "../components/InputField";
    export default {
        name: "ContactsCreate",
        components: {
            InputField,
        },
        data() {
            return {
                form: {
                    'name': '',
                    'email': '',
                    'company': '',
                    'birthday': ''
                }
            }
        }
    }
</script>

<style scoped>

</style>

続きまして、components/InputField.vue です。
props に、'name', 'label', 'placeholder' を指定しております。

components/InputField.vue
<template>
    <div class="relative pb-4">
        <label :for="name" class="text-blue-500 pt-2 uppercase text-xs font-bold absolute">{{ label }}</label>
        <input :id="name" type="text" class="pt-8 w-full text-gray-900 border-b pb-2 focus:outline-none focus:border-blue-400" :placeholder="placeholder">
    </div>
</template>

<script>
    export default {
        name: "InputField",
        props: [
            'name', 'label', 'placeholder'
        ],
        data() {
            return {
                value: ''
            }
        }
    }
</script>

<style scoped>

</style>

TailWind CSS の部分も書いておりますので、ちょっとごちゃごちゃと見難い部分があるかと思いますが、
これで、name や placeholder、label に値が受け渡されたのが確認できるかと思います。

次は、今やったこととは反対に、子コンポーネントから親コンポーネントへデータを受け渡してみましょう。

components/InputField.vue
// <input>タグに @input="updateField()" を追記
<template>
    <div class="relative pb-4">
        <label :for="name" class="text-blue-500 pt-2 uppercase text-xs font-bold absolute">{{ label }}</label>
        <input :id="name" type="text" class="pt-8 w-full text-gray-900 border-b pb-2 focus:outline-none focus:border-blue-400" :placeholder="placeholder" v-model="value" @input="updateField()">
    </div>
</template>

<script>
    export default {
        name: "InputField",
        props: [
            'name', 'label', 'placeholder'
        ],
        data() {
            return {
                value: ''
            }
        },
// 以下の methods:{} 部分を追加
        methods: {
            updateField() {
                this.$emit('update:field', this.value);
            }
        }
    }
</script>

最後に、views/ContactsCreate.vue に @update:field="form.name = $event" 等を<input-field/>の中に書いて終わりです。<input-field/>のコンポーネントが4つある感じになりますね。

views/ContactsCreate.vue
<template>
    <div>
        <form action="">
            <input-field name="name" label="お名前" placeholder="例:山田ルイ53世" @update:field="form.name = $event"/>
            <input-field name="email" label="メールアドレス" placeholder="例:test@test.jp" @update:field="form.email = $event"/>
            <input-field name="company" label="組織名" placeholder="例:藤原カンパニー" @update:field="form.company = $event"/>
            <input-field name="birthday" label="生年月日" placeholder="例:MM/DD/YYYY" @update:field="form.birthday = $event"/>
            <div class="flex justify-end relative pb-4">
                <button class="py-2 px-4 text-blue-500 border border-blue-500 mr-5 hover:bg-gray-100 hover:border-blue-300 rounded">キャンセル</button>
                <button class="bg-blue-500 border border-blue-500 py-2 px-4 text-white hover:bg-blue-400 mr-1 rounded">新規登録</button>
            </div>
        </form>
    </div>
</template>

<script>
    import InputField from "../components/InputField";
    export default {
        name: "ContactsCreate",
        components: {
            InputField,
        },
        data() {
            return {
                form: {
                    'name': '',
                    'email': '',
                    'company': '',
                    'birthday': ''
                }
            }
        }
    }
</script>

<style scoped>

</style>

いかがでしたでしょうか? いろいろと足らない部分は多々ございますが、一旦ここで筆を置かせていただき、自分の担当であります、洗濯に移ります。明日も朝から洗濯するぞ!

tailwindcss は、一見カオティックな印象ですが、慣れるとそうでもないようなところがなかなか面白いですね!

それでは皆様、

Merry Christmas and Happy New year

良いお年を〜

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

Vue.js の フォーム とか

この記事は、静岡 Advent Calendar 2019 の25日目の記事です。

はじめに

静岡の勉強会事情等々、皆様がいろいろと書いてくださっているので、自分はそちらの内容はまたの機会にすることにして、当初、Laravel を用いてテストについて何か書こうかなと思っておりました。

が、ちょっとばかり路線変更しまして、Vue.js の フォーム を中心に時間が許す限り何か思うがままに書こうかなと思います。
テストにつきましては、また別の機会にでも書きます。sqlite を使ってやるよくあるやつです。大した内容ではございません。
それから、次点として当初考えておりました、Laravel で過去に作った簡単なプロジェクトを Symfony(完全未経験) で書き直す。というものも考えてはおりましたが、風邪で昨日まで1週間寝込むという愚行を犯した自分には難しかったです。(今のところ娘に移っていないようなのが救いだったり...)

うだうだと書きましたが、それでは、早速本題に入りますよ。

今回は下記スクリーンショットのような form を想定しております。

スクリーンショット 2019-12-24 16.44.08.png

環境につきましては、バックエンドに Laravel (今回あまり関係ございませんが、ディレクトリ名とか Vue CLI のものとは異なります)を、フロントエンドに Vue.js / tailwindcss (Laravel Mixを使用)を使用いたします。

tailwindcss?? なんじゃそりゃ? という方は、こちらをご参照くださいませ。

CSSのクラスとして提供しているだけであり、個人的には、UI系のフレームワークより使い勝手がいいかなと思っています。作者さんはAdam Wathanです。
まぁ、Laravel / Vue 関連クラスターの著名な方なので、なんとなく安心感もありますね笑

何はともあれ、まずはセットアップから〜

Vue.js と tailwindcss のセットアップ

それでは、最初に Vue.js と tailwindcss の下準備とセットアップでもしましょうか。

どこか好きな ディレクトリ に新規で Laravel プロジェクト を作りましょう。

$ composer create-project laravel/laravel プロジェクト名 "バージョン指定.*"
Application key XXXXXXXXXうにゃうにゃでます set successfully.

set successfully. と出ていたらインストールが完了です。インストールが終わりましたら、今作成したプロジェクトのディレクトリへ移動しておきましょう。
また、エディタでもプロジェクトを開いておきます。

下記のコマンドを打って下準備をはじめましょう。

$ php artisan preset none

これで package.json の構成が最小限のものに変わったかと思います。
続きまして、今日の本題 Vue.js を入れていきましょう。

$ php artisan preset vue

これで、package.json でも "vue": "^2.5.7",な感じに追加されていることが確認できるかと思います。

npm or yarn で Vue Routertailwindcss を入れる

## Using npm
npm install vue-router tailwindcss --save-dev

## Using Yarn
yarn add vue-router tailwindcss --save-dev

package.json に、下記のものが追加されていればOKです。

package.json
"devDependencies": {
    "tailwindcss": "^1.1.4",
    "vue": "^2.5.7",       
    "vue-router": "^3.1.3"
},

あともう少しです。

CSS に tailwindcss を追加します

resources/assets/sass/app.scss に下記の内容を書きます。

app.scss
@tailwind base;

@tailwind components;

@tailwind utilities;

tailwindcssは、ビルド時にこれらのディレクティブを、生成されたすべてのCSSと交換します。

tailwindcss 構成ファイルを作成します

下記のコマンドを打ち込み、tailwind.config.jsを作成します。

npx tailwind init

これにより、tailwind.config.js が、プロジェクトのルートディレクトリの直下に作成され、最小限のファイルが作成されます。tailwindcssに何か追加したいときに追加していくものですね。
詳細はこちら

webpack.mix.js を修正する (Laravel5.5 の場合)

webpack.mix.js
let mix = require('laravel-mix');
let tailwindcss = require('tailwindcss')

/*
 |--------------------------------------------------------------------------
 | Mix Asset Management
 |--------------------------------------------------------------------------
 |
 | Mix provides a clean, fluent API for defining some Webpack build steps
 | for your Laravel application. By default, we are compiling the Sass
 | file for the application as well as bundling up all the JS files.
 |
 */

mix.js('resources/assets/js/app.js', 'public/js')
   .sass('resources/assets/sass/app.scss', 'public/css')
   .options({
       processCssUrls: false,
       postCss: [ tailwindcss('./tailwind.config.js') ],
});

webpack.mix.js を修正する。(Laravel6.0 の場合)

webpack.mix.js
const mix = require('laravel-mix');
const tailwindcss = require('tailwindcss')

/*
 |--------------------------------------------------------------------------
 | Mix Asset Management
 |--------------------------------------------------------------------------
 |
 | Mix provides a clean, fluent API for defining some Webpack build steps
 | for your Laravel application. By default, we are compiling the Sass
 | file for the application as well as bundling up all the JS files.
 |
 */

mix.js('resources/js/app.js', 'public/js')
   .sass('resources/sass/app.scss', 'public/css')
   .options({
       processCssUrls: false,
       postCss: [ tailwindcss('./tailwind.config.js') ],
});

npm or yarn で ビルド する

## Using npm
npm run watch

## Using yarn
yarn run watch

ちょっと長かったですね、あとちょっとです。

resources/views/layouts/app.blade.php の bladeテンプレートの、<div id="app"></div>の中を、下記のように変更します。

resources/views/layouts/app.blade.php
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">

    <title>{{ config('app.name', 'Laravel') }}</title>

    <!-- Styles -->
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
    <div id="app">
        <div class="h-screen">
            @yield('content')
        </div>
    </div>
    <!-- Scripts -->
    <script src="{{ asset('js/app.js') }}"></script>
</body>
</html>

それから、resources/views/home.blade.php も、下記のように変更しましょう。
resources/assets/js/components/App.vue を読み込むように記載しています。

resources/views/home.blade.php
@extends('layouts.app')

@section('content')
    <App></App>
@endsection

続きまして、resources/assets/js/components/App.vue も下記のように書きます。
ポイントは、<router-link to="/contacts/create">の箇所です。
/contacts/create のURLに遷移するように設定します。 ( Vue Router用の設定 )

resources/assets/js/components/App.vue
<template>
    <div>
        <router-link to="/contacts/create">
            <p>新規登録</p>
        </router-link>
    </div>
</template>

ここまで出来たので、冒頭に導入した Vue Router を使ってみます。
resources/assets/js/app.js と resources/assets/js/router.js を、それぞれ開きます。

まずは、resources/assets/js/app.js から修正していきますね。

下記のようにしておきましょう。

resources/assets/js/app.js
import Vue from 'vue';
import router from "./router";
import App from "./components/App";

const app = new Vue({
    el: '#app',
    components: {
        App
    },
    router
});

続いて、resources/assets/js/router.js では、ヒストリーモードを指定し、URLが、/contacts/create のときに、ContactsCreate のコンポーネントが表示されるようにします。

resources/assets/js/router.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import ContactsCreate from "./views/ContactsCreate";

Vue.use(VueRouter);

export default new VueRouter({
    routes: [
        { path: '/contacts/create', name: 'ContactsCreate', component: ContactsCreate }
    ],
    mode: 'history'
});

それでは、下準備が終わったところで、

まずは、ファイルを2つ用意しましょう。それぞれ、views/ContactsCreate.vue と、components/InputField.vue を作ります。

views/ContactsCreate.vue は、先ほど resources/assets/js/router.js で 指定したコンポーネントのファイルになりますね。

ContactsCreate.vue から作業を進めていきます。
一旦、props を用いまして、親コンポーネントであります ContactsCreate.vue から、
子コンポーネントの components/InputField.vue へ、
v-bind を 使って、label や placeholder の値を受け渡すところまで書いてみます。

views/ContactsCreate.vue
<template>
    <div>
        <form>
            <input-field name="name" label="お名前" placeholder="例:山田ルイ53世"/>
            <input-field name="email" label="メールアドレス" placeholder="例:test@test.jp"/>
            <input-field name="company" label="組織名" placeholder="例:藤原カンパニー" />
            <input-field name="birthday" label="生年月日" placeholder="例:MM/DD/YYYY"/>
            <div class="flex justify-end relative pb-4">
                <button class="py-2 px-4 text-blue-500 border border-blue-500 mr-5 hover:bg-gray-100 hover:border-blue-300 rounded">キャンセル</button>
                <button class="bg-blue-500 border border-blue-500 py-2 px-4 text-white hover:bg-blue-400 mr-1 rounded">新規登録</button>
            </div>
        </form>
    </div>
</template>

<script>
    import InputField from "../components/InputField";
    export default {
        name: "ContactsCreate",
        components: {
            InputField,
        },
        data() {
            return {
                form: {
                    'name': '',
                    'email': '',
                    'company': '',
                    'birthday': ''
                }
            }
        }
    }
</script>

<style scoped>

</style>

続きまして、components/InputField.vue です。
props に、'name', 'label', 'placeholder' を指定しております。

components/InputField.vue
<template>
    <div class="relative pb-4">
        <label :for="name" class="text-blue-500 pt-2 uppercase text-xs font-bold absolute">{{ label }}</label>
        <input :id="name" type="text" class="pt-8 w-full text-gray-900 border-b pb-2 focus:outline-none focus:border-blue-400" :placeholder="placeholder">
    </div>
</template>

<script>
    export default {
        name: "InputField",
        props: [
            'name', 'label', 'placeholder'
        ],
        data() {
            return {
                value: ''
            }
        }
    }
</script>

<style scoped>

</style>

tailwindcss の部分も書いておりますので、ちょっとごちゃごちゃと見難い部分があるかと思いますが、
これで、name や placeholder、label に値が受け渡されたのが確認できるかと思います。

次は、今やったこととは反対に、子コンポーネントから親コンポーネントへデータを受け渡してみましょう。

components/InputField.vue
// <input>タグに @input="updateField()" を追記
<template>
    <div class="relative pb-4">
        <label :for="name" class="text-blue-500 pt-2 uppercase text-xs font-bold absolute">{{ label }}</label>
        <input :id="name" type="text" class="pt-8 w-full text-gray-900 border-b pb-2 focus:outline-none focus:border-blue-400" :placeholder="placeholder" v-model="value" @input="updateField()">
    </div>
</template>

<script>
    export default {
        name: "InputField",
        props: [
            'name', 'label', 'placeholder'
        ],
        data() {
            return {
                value: ''
            }
        },
// 以下の methods:{} 部分を追加
        methods: {
            updateField() {
                this.$emit('update:field', this.value);
            }
        }
    }
</script>

最後に、views/ContactsCreate.vue に @update:field="form.name = $event" 等を<input-field/>の中に書いて終わりです。<input-field/>のコンポーネントが4つある感じになりますね。

views/ContactsCreate.vue
<template>
    <div>
        <form>
            <input-field name="name" label="お名前" placeholder="例:山田ルイ53世" @update:field="form.name = $event"/>
            <input-field name="email" label="メールアドレス" placeholder="例:test@test.jp" @update:field="form.email = $event"/>
            <input-field name="company" label="組織名" placeholder="例:藤原カンパニー" @update:field="form.company = $event"/>
            <input-field name="birthday" label="生年月日" placeholder="例:MM/DD/YYYY" @update:field="form.birthday = $event"/>
            <div class="flex justify-end relative pb-4">
                <button class="py-2 px-4 text-blue-500 border border-blue-500 mr-5 hover:bg-gray-100 hover:border-blue-300 rounded">キャンセル</button>
                <button class="bg-blue-500 border border-blue-500 py-2 px-4 text-white hover:bg-blue-400 mr-1 rounded">新規登録</button>
            </div>
        </form>
    </div>
</template>

<script>
    import InputField from "../components/InputField";
    export default {
        name: "ContactsCreate",
        components: {
            InputField,
        },
        data() {
            return {
                form: {
                    'name': '',
                    'email': '',
                    'company': '',
                    'birthday': ''
                }
            }
        }
    }
</script>

<style scoped>

</style>

いかがでしたでしょうか? いろいろと足らない部分は多々ございますが、一旦ここで筆を置かせていただき、自分の担当であります、洗濯に移ります。明日も朝から洗濯するぞ!

tailwindcss は、一見カオティックな印象ですが、慣れるとそうでもないようなところがなかなか面白いですね!

それでは皆様、

Merry Christmas and Happy New year

良いお年を〜

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

Laravel Job Queueで バッチ運用した話

やりたい事

  • 毎日、スクレイピング処理を実行
  • スクレイピングした結果をGoogleスプレッドシートに書き込む
  • 大量の処理が走るので、PHPメモリエラーになる可能性が高い
  • スプレッドシートの結果を毎日、バックアップする
  • スプレッドシートの結果を毎日、メールで送る

やった事

  • ①スケジューラの設定
  • ②バッチコマンドクラスの作成
  • ③ジョブキューの仕組みを実装
  • ④GASでスプレッドシート書き込みの処理を実装
  • ⑤supervisorの設定

0.各キーワード説明

Job Queueとは?

ジョブキューとはジョブをキューで管理するものです。
キューとはFIFO(First In First Out)を実現するデータ構造です。
キューに登録されたモノは、キューに登録した順に処理されます。
スクリーンショット 2019-12-24 21.53.56.png

参考:http://tech.voyagegroup.com/archives/495474.html

全体構成

スクリーンショット 2019-12-24 21.52.52.png

①スケジューラの設定

Kernel.phpに朝9時でバッチを動くように設定

<?php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
    /**
     * The Artisan commands provided by your application.
     *
     * @var array
     */
    protected $commands = [
        //
    ];

    /**
     * Define the application's command schedule.
     *
     * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
     * @return void
     */
    protected function schedule(Schedule $schedule)
    {
        // バッチを実行
        $schedule->command('batch:daily')->dailyAt('09:00');
    }

    /**
     * Register the commands for the application.
     *
     * @return void
     */
    protected function commands()
    {
        $this->load(__DIR__.'/Commands');

        require base_path('routes/console.php');
    }
}

②バッチコマンドクラスの作成

コマンドでの処理
- 本日分のジョブキューを詰める
- 前日分のジョブの実行結果をメールで送る

<?php

namespace App\Console\Commands;

use App\Entities\JobStatus;
use App\Entities\Project;
use App\Jobs\ProcessGoogleSearch;
use App\Mail\DailyBatchResultMail;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Mail;

class dailyBatchCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'batch:daily';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = '1日1回実行されるバッチ用コマンド';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     * @throws \Exception
     */
    public function handle()
    {
        $error = $this->SendJobStatusesByEmail();

        if ($error !== null) {
            throw new \Exception($error);
        }

        $error = $this->updateIsActive();

        if ($error !== null) {
            throw new \Exception($error);
        }

        $this->refreshQueue();
        $projects = Project::where('is_active','=', 1)->get();
        $this->registeredJobStatuses($projects);
        $this->pushQueue($projects);
    }

    // 以下、省略

}

③ジョブキューの仕組みを実装

※サンプルコードなので、細かい部分は省略。

<?php

namespace App\Jobs;

use App\Entities\JobStatus;
use App\Entities\Project;
use App\Enums\JobState;
use App\Services\CustomSearchService;
use App\Services\SearchService;
use App\Services\SpreadsheetService;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Log;

class ProcessSearch implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * 最大試行回数
     *
     * @var int
     */
    public $tries = 0;

    /**
     * 詳細検索するかどうか(TRUE)
     */
    const IS_DETAIL_SEARCH_TRUE = 1;

    /**
     * 詳細検索するかどうか(FALSE)
     */
    const IS_DETAIL_SEARCH_FALSE = 0;

    private $project;
    private $jobStatus;
    private $searchService;
    private $customSearchService;
    private $spreadsheetService;

    /**
     * Create a new job instance.
     *
     * @param Project $project
     * @param JobStatus $jobStatus
     */
    public function __construct(Project $project, JobStatus $jobStatus)
    {
        $this->project = $project;
        $this->jobStatus = $jobStatus;

        $this->searchService = new SearchService();
        $this->customSearchService = new CustomSearchService();
        $this->spreadsheetService = new SpreadsheetService();
    }

    /**
     * Execute the job.
     *
     * @return void
     * @throws \Exception
     */
    public function handle()
    {
        // 一部、処理を省略
        // JobStatusの更新
        $jobStatus = $this->jobStatus;
        $jobStatus->status = \App\Enums\JobState::RUNNING;
        $jobStatus->started_at = Carbon::now();
        $jobStatus->save();

        $parameter = [];

        $keyword = $jobStatus->keyword;

        if (!empty($keyword)) {
            $searchCount = $this->project->search_count;

            // 検索を実行
            // サービスクラスの具体的な処理は省略
            $parameter['searchResults'][] = $this->customSearchService
                ->execute($keyword, $searchCount, self::IS_DETAIL_SEARCH_TRUE, $jobStatus);

            sleep(mt_rand(config('variables.google_search_min_wait'), config('variables.google_search_max_wait')));
        }

        // GAS側で使う
        $parameter['sheetName'] = $this->project->name;
        $parameter['searchCount'] = $searchCount;

        // スプレッドシート書き出し
        [$spreadSheetUrl, $error] = $this->spreadsheetService->outputSpreadsheet($parameter);

        // JobStatusの更新
        if (empty($error)) {
            $jobStatus->status = JobState::SUCCESS;
            $jobStatus->spreadsheet_url = $spreadSheetUrl;
        } else {
            $jobStatus->status = JobState::FAILED;
        }
        $jobStatus->finished_at = Carbon::now();
        $jobStatus->save();
    }

    /**
     * 失敗したジョブの処理
     *
     * @param \Exception $exception
     * @return void
     */
    public function failed(\Exception $exception)
    {
        // JobStatusの更新
        $jobStatus = JobStatus::where('project_id', $this->project->id)->first();
        $jobStatus->status = \App\Enums\JobState::FAILED;
        $jobStatus->finished_at = Carbon::now();
        $jobStatus->save();

        // 失敗の通知をユーザーへ送るなど…
        \Log::error($exception->getMessage());
    }

}


④GASでスプレッドシート書き込みの処理を実装

  • PHPからパラメータを取得
    スクリーンショット 2019-12-24 23.38.05.png

  • Excelファイルを作成して、メール送信
    スクリーンショット 2019-12-24 22.26.38.png

  • Excelファイルを作成して、Googleドライブにバックアップ
    スクリーンショット 2019-12-24 22.27.24.png

⑤supervisorの設定

スクリーンショット 2019-12-24 22.28.02.png
スクリーンショット 2019-12-24 22.28.08.png

まとめ

  • 大量の処理が走るような機能があった際に、Job Queueを使うとPHPメモリエラーを防げる可能性がある。
  • メールの大量送信やスクレイピング、高速でAPIを叩くような処理には向いてそう。

参考記事

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

画像投稿がメインのサービスをはじめて運営してみて、失敗したこと、改善したこと、やろうとしていること

こんにちは、キッド✈️と申します。
東南アジア発のスタートアップスタジオ、GAOGAOのサーバーサイドエンジニアです。
このQiitaはGAOGAO Advent Calendar 2019の25日目の記事です。

先日、@rikuhiroseさんと共同で開発している、旅行サービスをリリースしました。ユーザーによる画像投稿がメインになるサービスです。

リリースしたサービスはこちらです。
20191214003023.png
FRIP | 思わず友達を旅行に誘いたくなる、 おすすめなスポットがあつまる旅行口コミサイト

実は、ユーザーによる画像投稿がメインのサービスを運営するのはこれがはじめてです。

実際に画像投稿メインのサービスを運営してみて、失敗したことがあったので、備忘録としてこちらに残すことにしました。

失敗したこと

(1)ファイルサイズが大きい画像のアップロードに失敗する

サービスをリリースしてすぐ、ありがたいことに友達が投稿をしてくれようとしました..!!嬉しい!!

と、喜んだのもつかの間。

投稿フォームを入力し、投稿する画像を選択し、いざアップロードしたら、画像が重すぎてエラーになりました。

スクリーンショット 2019-12-24 22.46.53.png

理由としては、PHPのupload_max_filesizeを超えてしまったために、サーバー側まで画像を渡すことができませんでした。

お恥ずかしいのですが、PHPのupload_max_filesizeの上限を変更しておらず、上限に引っかかってしまったのです。

解決策:

php.iniのpost_max_sizeとupload_max_filesizeの上限を変更しました。

FRIPはHerokuで運用しています。

HerokuでPHPのupload_max_filesizeを変更するには、publicディレクトリ配下に、.user.ini という名前のファイルを作成し、そちらに書き込むことで反映させることができます。

.user.ini
post_max_size = 20M
upload_max_filesize = 5M

(2)投稿に30秒以上かかってしまう

さて、upload_max_filesizeの上限をあげたことで、無事にサイズの大きな画像もアップロードできるようになりました。しかし、今度は別の問題が発生しました。

リリース当初、画像アップロードは、フォーム送信後にサーバーサイドで全てアップロード処理を行っていました。

スクリーンショット 2019-12-24 23.29.28.png

ですが、ファイルサイズの大きい画像で、かつ複数枚ともなると、フォーム送信してから完了するまでの時間が長い長い...。計測したら、フォーム送信から完了まで30秒かかってしまうこともざらにありました。

これではユーザーが使ってくれない...。

解決策:

画像アップロードは、画像を選択したら非同期でアップロードを行い、アップロードして画像のidを返すようにしました。

フォームを全て入力し、送信する時点で、画像はアップロードされるようにしたことで、フォーム送信から完了までの時間を大幅に短縮することができました。

(3)ファイルサイズが大きすぎてサイトが劇的に遅くなる

さて、無事に投稿はスムーズにできるようになりました。

しかし、今度はユーザーがアップロードした画像のファイルサイズが大きすぎて、サイトが劇的に遅くなりました...。

画像アップロードはS3にアップロードして、テーブルにs3へのurlを保存。CloudFront経由でS3の画像を読み込んでいました。

お恥ずかしいのですが、ユーザーがアップロードした画像は何も改変せず、そのままS3にあげていました。

旅行系サービスなのでユーザーが投稿する画像は、ファイルサイズが大きくなりがちです。

スクリーンショット 2019-12-24 22.40.44.png

トップページで152 × 152 pixelsでしか表示していないのに、読み込んでいる画像は4032 × 3024 pixels。そりゃ重くなるわけだ..。

今現状、投稿は16個ほどですが、読み込みが劇的に遅くなってしまいました。

解決策:

画像をリサイズ後に、S3へアップロードするようにしました。

FRIPはLaravelを使っているのですが、Laravel向けのライブラリで、簡単に画像をリサイズできるIntervention/imageがあり、こちらでリサイズする処理を入れました。

画像をS3にアップロードする前にこちらの処理を入れています。このように書くことで、width:600で、画像のアスペクト比を維持したままリサイズすることができます。

image.php
\InterventionImage::make($image)
  ->resize(600, null, function ($constraint) {
  $constraint->aspectRatio();
  $constraint->upsize();
})->save();

また、こちらのリサイズ機能を追加する前に投稿された画像は未対応なままです。

そちらについては、今後はCloudinaly経由でリサイズして配信する予定しています。

今後やること

Cloudinalyから画像配信

現在は、画像アップロードまでに画像を一定のサイズにリサイズしてから、S3にアップロードし、CloudFront経由でS3の画像を読み込んでいます。

しかし、将来的にはデバイスごとに配信する画像のサイズを改変したいと思いました。

そこで便利なのが、Cloudinalyです。

Cloudinalyでは、画像のパスに「w_500」と、widthを指定するだけ、そのサイズにリサイズしてくれます。
https://res.cloudinary.com/frip/image/upload/c_scale,w_500/v1574897562/sample.jpg

すでにS3で画像管理していても、CloudinalyならリソースURLのパスを紐づけるマッピング設定を行うことで、即時に自動アップロード・画像変換を行うことができます。

詳しくはこちらの記事に解説を譲ります。僕もこちらの記事を参考に設定しました。
S3 から Cloudinary への自動アップロードで即時に画像変換する|クラスメソッドブログ

最後に

記事からも分かりますように大変未熟ですので、画像アップロードのフローで、もっとこうした方がいいよ!など改善アドバイスやフィードバックいただけたらすごく嬉しいです?‍♂️

何卒よろしくお願いいたします?‍♂️

メリークリスマス!

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

Laravelワカンネ(゚⊿゚)から「完全に理解した()」までステップアップ

タイトルで釣る

images (1).jpeg

はじめに

この記事は Laravel Advent Calendar 201924日目の記事です

この記事を書こうと思った理由

ネタが思いつかなかった
Laravelを業務で触れて1年が経とうかというのに未だLaravelの基本的な機能を理解しているか
と言われたら首が引きちぎれる勢いで首を横に振ってしまいます。

始めた当初は、実行の流れ(ライフサイクル)も含めてふぁさーど??さーびすこんてな??だったので、
きっと同じように理解しないまま使ってる(または葬られた)人もいるだろうと思い書きました。

そもそもLaravel使うメリットは

シンプルなMVC構造かつORMなどの機能だけでなく、
強力なルーティングフィルターや柔軟なオートローダーなどをサポートしており、
開発者は設計を意識せずに開発ができる。

↑とりあえずよく目にするやつ

Laravelが実行されるまでの流れってどうなってるの

LaravelのアプリケーションではURLにアクセスした時にエントリポイント(public/index.php)が参照される仕組みになっており、
その中でオートロードファイルの読み込みアプリケーションの設定・実行が行われています。

エントリポイントの中身(public/index.php)

スクリーンショット 2019-12-22 23.41.11.png

実行の流れ(ライフサイクル)

実行までのフローを見るとこんな感じになっていて、
ユーザーがURLにアクセスしてから① ~ ⑦までの流れで実行されています。

スクリーンショット 2019-12-22 23.40.58.png

MVC(Model View Controller)

上記の実行フローの画像⑥の部分でコントローラーを起点にモデルやビューの参照が行われている

スクリーンショット 2019-12-22 23.54.23.png

ルーティング

先ほどの実行フローの画像④でルータでは、URLとコントローラーを紐付けています。
どう言う事??ってなるので画像で説明(文字で説明できないボンクラ

スクリーンショット 2019-12-23 0.04.57.png

上記画像ではフローを端折ってますが、左のユーザーがhttp://test/welcomeと言うURLにアクセスした時に、
エントリーポイントからルータ(routes/web.php)が呼び出され、URLの最後のパス welcom と紐付くコントローラーを検出します。

どうやって検出しているかというと下記の様に設定することでLaravelが認識することが出来ます。

スクリーンショット 2019-12-23 0.21.21.png

...と言う風な流れでURLとコントローラーが紐づけられ、後はコントローラーを起点にModelから値を取得してViewに渡せば先ほどのMVCの画像と繋がりユーザーにページが表示される様になると言うわけです。

画像は全部LTした時の使い回し

追記
この記事書いた後にたまたま見つけた記事が詳しく書いてて、良記事だったので置いとく(最初から知りたかった)
少し初学者には難しいかもなので、上記を理解してみると良いかも。。
[Laravel] ドキュメントの概念について調べて見た

目にしただけで睡魔に襲われる機能(多分自分だけ)

  • DI(依存性の注入)
  • サービスコンテナ(DIコンテナ)
  • サービスプロバイダ
  • ファサード
  • ミドルウェア

僕の知識と文才では知識が0の人に100教えるのは無理(と言うか自分がそこまで理解してない)なので
雰囲気だけ伝われば幸い。。

この辺に関しては良記事が溢れているので、触りだけ説明して後は記事に横流しという他力本願スタイルでいかせてもらう

DI(依存性の注入)

Laravelを調べていると良く目にするDI(依存性の注入)DIコンテナ(サービスコンテナ)という言葉
実はLaravelに限った機能という訳ではなく、プログラムの世界では幅広く使われています。

だからDIって何だよ!!ってなってる方はどうか怒りをお沈めください。
DIというのは簡単に言うと クラスの内部でインスタンス生成(new)するのではなく、外部で用意して注入してね と言う事です。

めっちゃ噛み砕いて言うとクラスの中でnewすんなってこと。(多分叩かれる)
じゃあ外部でインスタンスを用意するってどう言うこと??ってなると思う。
コードの例で見てみましょう。

ユーザーの携帯を鳴らすと言う簡単な処理を実行しているこの処理を...

index.php
<?php

class User
{
    protected $phone;

    public function __construct()
    {
        $this->phone = new Phone();
    }

    public function UserCallPhone()
    {
        $this->phone->call();
    }
}

class Phone
{
    public function call()
    {
        return "プルプル...";
    }
}

$user = new User();

下記の様に外部(クラス外)でPhoneクラスをインスタンス化して、Userクラスに注入すれば外部からインスタンスを用意した。つまり依存性の注入が出来たということになる。

index.php
<?php

class User
{
    protected $phone;

    public function __construct(Phone $phone)
    {
        $this->phone = $phone;
    }

    public function UserCallPhone()
    {
        $this->phone->call();
    }
}

class Phone
{
    public function call()
    {
        return "プルプル...";
    }
}

// ここでPhoneクラスをインスタンス化
$phone = new Phone();
// Phoneクラスのインスタンスを引数に渡す
$user = new User($phone);

これでUserクラスはPhoneクラスとの疎結合に成功しました。
(インターフェースに分けないと本当の意味で依存関係を解決したとは言わないかも。。)

このクラス同士の依存を無くして疎結合にしようね。ってのがDI。

参考
この記事凄く分かりやすいのでぜひ
LaravelのDIコンテナはどう使われているのか

サービスコンテナ(DIコンテナ)

上記のDI(依存性の注入)で何と無くDIは理解したけどサービスコンテナってなんだよ!!って方、ちゃんと説明します。
実はLaravelを学習する上で結構大事な要素だったりします。

例によって、簡単に説明すると依存関係を解決するために行なっていた外部からの注入(上記
DI参考)をまとめて担ってくれるのがサービスコンテナです。
噛み砕くと(また怒られる)サービスコンテナは下記の様な解釈でいいと思います。

サービス => インスタンス化(new)
コンテナ => 入れ物

再度、先ほどのUserクラスとPhoneクラスのDIをサービスコンテナを使って、再現してみます。

index.php
class User
{
    protected $phone;

    public function __construct()
    {
        // インスタンスの生成方法を登録する
        app()->bind('Phone', function(){
            return new Phone();
        });

        // サービスコンテナが生成したインスタンスを取得
        $this->phone = app()->make('Phone');
    }

    public function UserCallPhone()
    {
        $this->phone->call();
    }
}

class Phone
{
    public function call()
    {
        return "プルプルプル...";
    }
}

$user = new User();

先ほどは外部でPhoneクラスをインスタンス化して、Userクラスに外部から注入していましたが、
上記では__construct()内で何やらごにょごにょしている様です。

バインド(bind)

bind()にクラス名を渡してそのインスタンスの生成方法をサービスコンテナに登録する

app()->bind('Phone', function(){
    return new Phone();
});

リゾルブ(resolve)

指定されたインスタンスをサービスコンテナが生成したインスタンスを取得

サービスコンテナが生成したインスタンスを返すことを解決(resolve)という

$phone = app()->make('Phone');

各コードの説明である通りbindでインスタンス生成方法をサービスコンテナに登録して、
サービスコンテナからインスタンスを取得することでDIを実現している。

つまりPhoneクラスのインスタンス(new)をサービスコンテナが代わりに作って渡してくれている

さらにサービスコンテナでは下記の様にも書けます!!
この様なやり方をメソッドインジェクション(この場合コンストラクタなのでコンストラクタインジェクション)と言います。

index.php
    // Phoneクラスのインスタンスを$phoneに渡している
    public function __construct(Phone $phone)
    {
        $this->phone = $phone;
    }

便利や....。

参考
最高にわかるDIコンテナ(特にPHPにフォーカスした
DI・DIコンテナ、ちゃんと理解出来てる・・?

サービスプロバイダ

名前が似ているのでサービスプロバイダとサービスコンテナ何が違うの??となっている方もいると思いますので説明。

サービスプロバイダとはフレームワークやアプリケーションに含まれるサービス(機能)の初期処理を行う目的で用意されています。
Laravelではサービス(機能)毎に初期処理を定義して実行する仕組みがあり、
その仕組みや実際に初期処理の実装を行うクラスのことをサービスプロバイダと呼びます。

サービス => 機能
プロパイダー => 供給者

つまり機能(サービス)を供給する(プロバイド)クラスと言えます。

文章だけではあまりイメージ出来ないと思いますが、先ほどのサービスコンテナで利用したbindはControllerに直接書かずに、
サービスプロバイダに切り分けて定義するって事だけ分かってればとりあえず良いかなと個人的には思ってます

実際のサービスコンテナの例

app/Providers/PhoneServiceProvider.php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class PhoneServiceProvider extends ServiceProvider
{
    public function register()
    {
        app()->bind('Phone', function(){
            return new Phone();
        });
    }

    public function boot()
    {
        //
    }
}

register()とboot()というメソッドが用意されていますが、こちらは実行されるタイミングの違いです。

ファサード

ファサードとはLaravelで用意されている独自クラスのメソッドをどこでも呼び出して使用できる様にした機能の事です。
これだけだとピンとこないかもしれませんが、ルーティングを設定するときに無意識に使っているはずです。

routes/web.php
Route::get('/', 'HomeController@index');

Route::getの部分が正にファサードです。

ここでいくつか疑問があると思います。一つずつ説明しましょう。

  • Q. 結局ファサードって何

    • A. 処理をフレームワークに肩代わりさせて、手軽に使える様に出来る機能(めっちゃ噛み砕くと)
  • Q. 何でuseもパスもなしでこんな簡潔に書けるの??

    • A. config/app.phpでエイリアスを設定しているから
config/app.php
'aliases' => [
    'Route' => Illuminate\Support\Facades\Route::class,
];

Laravelで用意された標準機能だけでなく、自作で作って設定することも可能です。

参考
(Laravel第6回目)ファサード(Facade)からサービスコンテナを学ぶ
Laravelファサードの作り方からその構造まで徹底解説入門

ミドルウェア

インフラ界隈だとOSとアプリケーション間の中間的な役割を担っている機能のことだが、
LaravelではHTTPリクエストをフィルタリングする際などのメカニズムとして用意されている。
(中間という意味では一緒)

ここで先ほどの実行のフロー(ライフサイクル)の画像を再度貼ります。

下記画像の⑤ミドルウェアを見ての通り、④ルーターから⑤ミドルウェアが実行され、その後に⑥のコントローラーを参照しているのが分かります。
しかし、画像を良く見てみると矢印の通り⑥→⑤→④と処理が戻っているのが分かると思います。
この通り、ミドルウェアではコントローラーの呼び出し前と呼び出し後にミドルウェアに設定された処理を行うことが出来ます。(と言えば語弊が生まれるかもしれないが)

(実行の流れ画像再添付)

スクリーンショット 2019-12-22 23.40.58.png

コントローラーの前後に設定出来るのは分かったけど具体的に何が出来るの?という人もいると思います。
Laravelでログイン認証を実装した事がある人であれば何となく分かると思いますが、実はこのミドルウェアを使って、ユーザが認証済みかどうかの確認を影で行なっています。
それだけではなく、バリデーションやコンテンツの置換など様々な機能が用意されています。

Laravelの素敵なところは上記の様なデフォルト機能だけでなく、自分で簡単にミドルウェアを自作してコントローラーの前後に設定が出来るところです。

随時追加していきます(時間がなかった)

最後に

Laravel完全に理解した()状態にステップアップできたでしょうか。

結構記事によって説明が異なってたりするので自分の知識も間違ってる箇所あるかと思います。。
誤りに気付いた方はマサカリと思わず編集リクエストまたは指摘していただけると幸いです。

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

【Laravel】検索機能の実装

laravelで検索機能の実装

要件定義

今回はユーザー一覧から検索機能を実装する。
ユーザーの管理対象項目は名前、年齢、性別

データベース設計

プロジェクト名:serch
モデル名:User
テーブル名:users

プロジェクトファイルの作成

$ composer create-project --prefer-dist laravel/laravel='5.8' serch

.envファイルにデータベースの設定

database.sqliteファイルの作成

Userモデルとマイグレーションファイルを作成

$ php artisan make:model User -m
Model created successfully.
Created Migration: 2019_12_21_043638_create_users_table

マイグレーションファイルの修正

public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name');
            $table->string('sex');
            $table->integer('age');
            $table->timestamps();
        });

マイグレーションの実行

$ php artisan migrate
Migration table created successfully.
Migrating: 2019_12_21_043638_create_users_table
Migrated:  2019_12_21_043638_create_users_table (0.01 seconds)

UsersControllerの作成

$ php artisan make:controller UsersController
Controller created successfully.

usersテーブルにデータを挿入するseederファイルを作成

$ php artisan make:seeder UsersTableSeeder
Seeder created successfully.

seederファイルに書き込むデータを記載する。

database/seeds/UsersTableSeeder.php
      DB::table('users')->insert([
        [
        'name' => '岩崎蓮',
        'age' => 34,
        'sex' => '男',
        'created_at' => new Datetime(),
        'updated_at' => new Datetime()
      ],
        [
        'name' => '町田里奈',
        'age' => 20,
        'sex' => '女',
        'created_at' => new Datetime(),
        'updated_at' => new Datetime()
      ],
        [
        'name' => '横田拓也',
        'age' => 25,
        'sex' => '男',
        'created_at' => new Datetime(),
        'updated_at' => new Datetime()
      ],
        [
        'name' => '矢野萌',
        'age' => 25,
        'sex' => '女',
        'created_at' => new Datetime(),
        'updated_at' => new Datetime()
      ],
        [
        'name' => '沼田舞',
        'age' => 31,
        'sex' => '女',
        'created_at' => new Datetime(),
        'updated_at' => new Datetime()
      ],

    ]);
    }

DatabaseSeederに実行できるように編集

    public function run()
    {
        // $this->call(UsersTableSeeder::class);
        $this->call(UsersTableSeeder::class);
    }

seederの実行

$ php artisan db:seed
Seeding: UsersTableSeeder
Database seeding completed successfully.

ルーティングの設定

route/web.php
//検索結果を表示する
Route::get('/serch','UsersController@serch');

//ユーザー一覧と検索画面
Route::get('/','UsersController@index');

Userモデルに書き込み可能な権限を加える

app/User.php
class User extends Model
{
    //
        protected $fillable = ['name','age','sex'];
}

コントローラーの編集

とりあえず、ユーザー一覧を取得するindexメソッドを作成

app/Http/controllers/UsersController.php
  public function index() {
      $users = User::all();
      return view('index')->with('users', $users);
    }

全体ビューを作成

共通化するビューのフォルダとファイルを作成

$ cd resources/views
$ mkdir layouts
$ cd layouts
$ vi defalt.blade.php

defalt.blade.phpの編集

resources/views/layouts/defalt.blade.php
<!doctype html>
<html lang="ja">
  <head>
    <title>ユーザー管理</title>
  <!-- 必要なメタタグ -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
  </head>
  <body>
    <div class="container" style="margin-top:50px;">
      <ul class="nav justify-content-end">
    <li class="nav-item">
      <a class="nav-link active" href="{{ url('/')}}">検索と一覧</a>
    </li>
  </ul>
      @yield('content')
    </div>
    ...

  <!-- オプションのJavaScript -->
  <!-- 最初にjQuery、次にPopper.js、次にBootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
  </body>
</html>

indexビューを作成

resources/views/index.blade.php
@extends('layouts.defalt')

@section('content')
<h1>検索条件を入力してください</h1>
<form action="{{ url('/serch')}}" method="post">
  {{ csrf_field()}}
  {{method_field('get')}}
  <div class="form-group">
    <label>名前</label>
    <input type="text" class="form-control col-md-5" placeholder="検索したい名前を入力してください" name="name">
  </div>
  <div class="form-group">
    <label>年齢</label>
    <input type="text" class="form-control col-md-5" placeholder="年齢を入力してください" name="age" value="{{ old("name")}}">
  </div>

  <div class="form-group">
     <label>年齢の条件</label>
     <select class="form-control col-md-5" name="age_condition">
       <option selected value="0">選択...</option>
       <option value="1">以上</option>
       <option value="2">以下</option>
     </select>
   </div>

  <div class="form-group">
     <label>性別</label>
     <select class="form-control col-md-5" name="sex">
       <option selected value="0">選択...</option>
       <option value="1"></option>
       <option value="2"></option>
     </select>
   </div>

  <button type="submit" class="btn btn-primary col-md-5">検索</button>
</form>
@if(session('flash_message'))
<div class="alert alert-primary" role="alert" style="margin-top:50px;">{{ session('flash_message')}}</div>
@endif
<div style="margin-top:50px;">
<h1>ユーザー一覧</h1>
<table class="table">
  <tr>
    <th>ユーザー名</th><th>年齢</th><th>性別</th>
  </tr>
@foreach($users as $user)
  <tr>
    <td>{{$user->name}}</td><td>{{$user->age}}</td><td>{{$user->sex}}</td>
  </tr>
@endforeach
</table>
</div>
@endsection

laravelサーバーを立ち上げて確認

$ php artisan serve --host ***.****.**.** --port 8000

こんな感じでできていれば、okです。

スクリーンショット 2019-12-22 10.16.36.png

検索機能を実装していきます。

検索画面を作成

resources/views/serch.blade.php
@extends('layouts.defalt')

@section('content')
<div style="margin-top:50px;">
<h1>検索結果</h1>
@if(isset($users))
<table class="table">
  <tr>
    <th>ユーザー名</th><th>年齢</th><th>性別</th>
  </tr>
  @foreach($users as $user)
    <tr>
      <td>{{$user->name}}</td><td>{{$user->age}}</td><td>{{$user->sex}}</td>
    </tr>
  @endforeach
</table>
@endif
@if(!empty($message))
<div class="alert alert-primary" role="alert">{{ $message}}</div>
@endif
</div>
@endsection

検索メソッドを作成。コントローラーの編集

app/Http/Controllers/USersController.php
public function serch(Request $request) {
      $keyword_name = $request->name;
      $keyword_age = $request->age;
      $keyword_sex = $request->sex;
      $keyword_age_condition = $request->age_condition;

      if(!empty($keyword_name) && empty($keyword_age) && empty($keyword_age_condition)) {
      $query = User::query();
      $users = $query->where('name','like', '%' .$keyword_name. '%')->get();
      $message = "「". $keyword_name."」を含む名前の検索が完了しました。";
      return view('/serch')->with([
        'users' => $users,
        'message' => $message,
      ]);
    }

    elseif(empty($keyword_name) && !empty($keyword_age) && $keyword_age_condition == 0){
          $message = "年齢の条件を選択してください";
          return view('/serch')->with([
            'message' => $message,
          ]);
    }
    elseif(empty($keyword_name) && !empty($keyword_age) && $keyword_age_condition == 1){
      $query = User::query();
      $users = $query->where('age','>=', $keyword_age)->get();
      $message = $keyword_age. "歳以上の検索が完了しました";
      return view('/serch')->with([
        'users' => $users,
        'message' => $message,
      ]);
    }
    elseif(empty($keyword_name) && !empty($keyword_age) && $keyword_age_condition == 2){
      $query = User::query();
      $users = $query->where('age','<=', $keyword_age)->get();
      $message = $keyword_age. "歳以下の検索が完了しました";
      return view('/serch')->with([
        'users' => $users,
        'message' => $message,
      ]);
    }
    elseif(!empty($keyword_name) && !empty($keyword_age) && $keyword_age_condition == 1){
      $query = User::query();
      $users = $query->where('name','like', '%' .$keyword_name. '%')->where('age','>=', $keyword_age)->get();
      $message = "「".$keyword_name . "」を含む名前と". $keyword_age. "歳以上の検索が完了しました";
      return view('/serch')->with([
        'users' => $users,
        'message' => $message,
      ]);
    }
    elseif(!empty($keyword_name) && !empty($keyword_age) && $keyword_age_condition == 2){
      $query = User::query();
      $users = $query->where('name','like', '%' .$keyword_name. '%')->where('age','<=', $keyword_age)->get();
      $message = "「".$keyword_name . "」を含む名前と". $keyword_age. "歳以下の検索が完了しました";
      return view('/serch')->with([
        'users' => $users,
        'message' => $message,
      ]);
    }
    elseif(empty($keyword_name) && empty($keyword_age) && $keyword_sex == 1){
      $query = User::query();
      $users = $query->where('sex','男')->get();
      $message = "男性の検索が完了しました";
            return view('/serch')->with([
              'users' => $users,
              'message' => $message,
            ]);
    }
    elseif(empty($keyword_name) && empty($keyword_age) && $keyword_sex == 2){
      $query = User::query();
      $users = $query->where('sex','女')->get();
      $message = "女性の検索が完了しました";
            return view('/serch')->with([
              'users' => $users,
              'message' => $message,
            ]);
    }
    else {
      $message = "検索結果はありません。";
      return view('/serch')->with('message',$message);
      }
}

終了です。

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

DockerなLaravel6でHMR(Hot Module Replacement)

はじめに

Laravelでvue.jsを使った開発をするときは
laravel-mix付属のホットリロード機能(HMR)が便利です。

ただ、ホストで動かすことが想定されており、dockerベースでHMRな開発環境を作ろうとして戸惑ったので情報を整理します。

概要

Laravel6 + vue.js on Docker な環境でHMRをする設定を説明します。
Laradockは使いません。

ホスト側にバージョン管理が必要なツールはあまり入れたくないため、すべてDockerコンテナ内で完結させる方針です。
(yarnの実行はコンテナ内で実行すると遅いのですが、Ubuntu 上ではそれほど違いが感じられなかったため、コンテナ内で行う場合について記載しております。)

環境

  • Ubuntu 18.04
  • Docker 19.03.5
  • docker-compose 1.25.0
  • Laravel 6

結論

  • WebサーバーはLaravelのビルドインサーバーを使う
  • PHPとNode.jsは同じコンテナに設定
  • Webpackのプロキシ機能で8080のアクセスを8000ポート(ビルドインサーバー)に転送

ソースはこちら
https://github.com/odaryo/laravel6_vuejs_HMR

環境構築について

ディレクトリ構成

┬- docker (Dockerの設定)
│    └- app
│        ├- conf.d  (php設定ファイル)
│        │   ├- php_settings.ini
│        │   └- xdebug.ini
│        └- Dockerfile
├- src  (Laravelディレクトリ)
└- docker-compose.yml

Docker環境

docker-compoe

appコンテナにはLaravelのビルドインサーバーを起動する設定を記載しています

docker-compose.yml
version: '3'

services:
  # laravel
  app:
    build:
      context: ./
      dockerfile: ./docker/app/Dockerfile
    depends_on:
      - db
    links:
      - db
    volumes:
      - ./app:/app:cached
      - ./docker/app/conf.d/xdebug.ini:/usr/local/etc/php/conf.d/xdebug.ini
      - ./docker/app/conf.d/php_settings.ini:/usr/local/etc/php/conf.d/php_settings.ini
    command: bash -c "php artisan serve --host 0.0.0.0" # 起動時にビルドインサーバーを起動
    ports:
      - "8000:8000"  # php artisan serve 用のポート
      - "8080:8080"  # HMR用のポート

  # DB (mysql)
  db:
    image: mysql:8
    environment:
      - MYSQL_DATABASE=testdb
      - MYSQL_USER=testuser
      - MYSQL_PASSWORD=password
      - MYSQL_ROOT_PASSWORD=root
      - TZ=Asia/Tokyo
    restart: always
    volumes:
      - dev_mysql_data:/var/lib/mysql
    ports:
      - "33306:3306"

volumes:
  dev_mysql_data:
    driver: local

Dockerfile

PHPコンテナとNode.jsコンテナに分けるとうまく行かなかったため、マルチステージビルドの形でPHPコンテナにNode.jsを追加する

Dockerfile
# Nodeイメージ
FROM node:13-alpine as node

# PHPイメージ
FROM php:7.3-alpine

# Laravel環境に必要なパッケージをインストール
RUN apk update \
    && apk upgrade \
    && apk add --no-cache \
        bash \
        git \
        unzip \
        libpng \
        libpng-dev \
        libjpeg \
        icu \
        icu-dev \
        icu-libs \
        libxml2 \
        libxml2-dev \
        openssl \
        openssl-dev \
    && docker-php-ext-install \
        pdo_mysql \
        mysqli \
        gd \
        mbstring \
        intl \
        xml \
        opcache \
    && docker-php-ext-enable intl mbstring \
    && apk --update --no-cache add autoconf g++ make \
    # xdebugインストール
    && pecl install -f xdebug \
    && docker-php-ext-enable xdebug \
    && apk del --purge autoconf g++ make

# Composerのインストール
RUN curl -sS https://getcomposer.org/installer | php ;mv composer.phar /usr/local/bin/composer;
RUN composer global require hirak/prestissimo \
    && composer global require phpunit/phpunit

# Nodeコンテナからyarnとnodeをコピー
COPY --from=node /opt/yarn-v* /opt/yarn
COPY --from=node /usr/local/bin/node /usr/local/bin/

# 使いやすいようにシンボリックリンク作成
RUN ln -s /opt/yarn/bin/yarn /usr/local/bin/yarn \
    && ln -s /opt/yarn/bin/yarnpkg /usr/local/bin/yarnpkg

# ホスト側とuser_id, group_idを合わせる
ARG USER_ID
ARG GROUP_ID

RUN addgroup -g ${GROUP_ID} -S app-user && \
    adduser -u ${USER_ID} -S app-user -G app-user

USER app-user:app-user

# Setup working directory
WORKDIR /app

参考

構築手順

docker起動

$ docker-compose build
$ docker-compose up -d

laravelインストール

appコンテナに入って実行します。
※laravelインストールまではコンテナが起動しないため、runコマンドで実行する必要があります。

$ docker-compose run app /bin/bash
$ composer create-project --prefer-dist laravel/laravel .

vue.jsを使う設定

laravel 6.0からデフォルトではインストールされないため、laravel/uiからインストールします。

$ composer require laravel/ui --dev
$ php artisan ui vue
$ exit

ここでdockerを再起動しておく

$ docker-compose up -d

http://localhost:8000にアクセスして、スタート画面が表示されたらOK
image.png

node_moduleのインストール

$ docker-compose exec app yarn

HMRの設定

ホットリロードの確認用に、welcome.blade.phpのbody内にVue.jsのComponentを追加しておく
Componentはインストール時に作成されるサンプルを使用

welcome.blade.php
<div id="app">
    <example-component></example-component>
</div>
<script src="{{ mix('js/app.js') }}"></script>

webpack.mix.jsにHMRの設定を追加

8080へのアクセスではエラーとなりCannot GET /が表示されてしまうため、ビルドインサーバーの8000ポートへプロキシしてやります

webpack.mix.js
mix.webpackConfig({
    devServer: {
        host: '0.0.0.0',
        port: 8080,
        proxy: {
            '*': 'http://0.0.0.0:8000'
        },
        // Windows(Docker for windows)の場合は下記を追加する
        // watchOptions:{
        //     aggregateTimeout:200,
        //     poll:5000
        // },
    }
});

ホットリロードの実行

$ docker-compose exec app yarn hot

http://localhost:8080へアクセスして、コンポーネントの内容が表示されれば完了です。
コンポーネントを修正して、変更が反映されることを確認しましょう。

image.png

参考

終わりに

ホットリロードでフロントエンド開発が捗ります。

注意点としては、HMRで監視するファイルはJavascriptやCSSなので、blade自体を更新した場合はブラウザの更新が必要となります。

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

AWS LambdaでLaravelを動かす

最近サーバーレス(Lambda)に興味があったので始めてみるかとは思ったものの
好きな言語であるPHPは標準でサポートされておらず。。。
どうにか動かす方法はないかと調べていたところ、brefなるものを使えば簡単に動かせそう!しかもLaravelも動かせる⁉
だったので、その手順をまとめてみました。

準備

以下のものが必要になります.

  • AWSアカウント
  • AWS CLI
  • AWSアカウントアクセスキー及びシークレットキー
  • npmコマンド

こちらの準備については記事の内容ではないのでスキップします。

serverlessコマンドをインストール

まずはserverlessコマンドをインストールします。
こちらはLambdaをデプロイする際に使用します。

$ npm install -g serverless

serverlessコマンドのインストールが完了したら、AWSアクセスキー及びシークレットアクセスキーを設定します

$ serverless config credentials --provider aws --key <key> --secret <secret>

注意点として、AWS CLIのデフォルトアカウントを設定している場合は
そのアカウントが使用されるので上記の手順は不要です。

Laravelプロジェクト作成

まずはcomposerコマンドでlaravelプロジェクトを作成しましょう

$ composer create-project laravel/laravel serverless-laravel --prefer-dist

brefを追加

$ cd serverless-laravel
$ composer require bref/bref

注意点として、brefの最新バージョンを使用するにはPHP7.2以上が要求されます。
PHP7.1以下で使用したい場合はbref0.5未満を使用する必要があるみたいです(未確認)
参考: https://bref.sh/docs/installation.html#bref

serverless.ymlを用意

serverless.ymlをプロジェクトディレクトリ直下に作成します。

serverless.yml
service: serverless-laravel

provider:
    name: aws
    region: ap-northeast-1
    runtime: provided
    environment:
        # Laravel environment variables
        APP_STORAGE: '/tmp'

plugins:
    - ./vendor/bref/bref

functions:
    website:
        handler: public/index.php
        timeout: 28 # in seconds (API Gateway has a timeout of 29 seconds)
        layers:
            - ${bref:layer.php-73-fpm}
        events:
            -   http: 'ANY /'
            -   http: 'ANY /{proxy+}'
    artisan:
        handler: artisan
        timeout: 120 # in seconds
        layers:
            - ${bref:layer.php-73} # PHP
            - ${bref:layer.console} # The "console" layer

Lambda上でLaravelを動かすためにコードを変更

Lambdaでは/tmpディレクトリ以外への書き込みは出来ないため、Laravelが書き込みを行う領域を変える必要があります。

bootstrap/app.php
# $appを作成している行の直後に記載
$app->useStoragePath($_ENV['APP_STORAGE'] ?? $app->storagePath());
.env
VIEW_COMPILED_PATH=/tmp/storage/framework/views
SESSION_DRIVER=array
LOG_CHANNEL=stderr
app/Providers/AppServiceProvider.php
    public function boot()
    {
        if (! is_dir(config('view.compiled'))) {
            mkdir(config('view.compiled'), 0755, true);
        }
    }

デプロイ準備

デプロイを行う前に、開発用パッケージなど不要なものを削除します。
ただし、require-devでインストールした依存関係が削除されるので注意してください

$ composer install --prefer-dist --optimize-autoloader --no-dev

デプロイ

$ serverless deploy
.....
endpoints:
  ANY - https://XXXXXXXXXX.execute-api.us-east-1.amazonaws.com/dev
  ANY - https://XXXXXXXXXX.execute-api.us-east-1.amazonaws.com/dev/{proxy+}

デプロイが完了すると、コンソールにAPI GatewayのエントリーポイントのURLが表示されるのでアクセスしてみます。

image.png

デプロイしたものを削除する

removeコマンドで削除できます。

$ serverless remove

参考:
https://bref.sh/docs/

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

laravel/installer がメジャーバージョンアップした時の対応

2019年5月にv2.0、11月にv3.0が出てる。プロジェクトのcomposerは更新してもglobalのほうは気にしてない人が多い。v1.xがインストールされたままな人も多いと思う。

最新バージョンがあるか確認

composer global outdated

globalのcomposer.jsonを直接書き換えるか再度インストールすれば最新バージョンになる。

composer global require laravel/installer

全体を更新。

composer global update

気になった変更点

--auth付けると最初からlaravel/uiも加えた状態でプロジェクトが作られる。

laravel new project --auth

そもそもlaravel/laravelのrequire-devに加えて欲しかったけどinstallerのフラグで対応された。
Laravel5.8までと同じように作るなら--authを付ける。

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

Laravel Blade で extendsとincludeでyieldの挙動が変わる

起きたこと

Laravel 6.6 (執筆時 最新版)のBladeファイルで、includeしたテーマのyield部分に、sectionが挿入されないことがあったのでメモ。

ファイル

こんなベースファイルに

base.blade.php
<!DOCTYPE html>
<head>
    @yield("head")
</head>

<body>
    @yield("body")
</body>
</html>

こんなheadタグのパーツを作って

head.blade.php
@section("head")
<title>@yield("title")</title>
@endsection

こんな実装bladeを作ったとする。

index.blade.php
@extends("base") // baseを継承

@include("head") // headをinclude

@section("title")  
index   //includeしたheadに、タイトル[index]を挿入
@endsection

@section("body")
body   //extendsしたbaseに、[body]を挿入
@endsection

これで生成されるhtmlは

<!DOCTYPE html>

<head>
<title></title>    <!-- タイトルが挿入されていない -->
</head>

<body>
body               <!-- bodyは挿入されている -->
</body>
</html>

このように、 @section("body") はきちんとyieldに挿入されるが @section("title") の部分は無視される。

思い通りの動き方のためには

index.blade.php
@extends("base")

// titleが挿入されるテンプレート head をincludeする前に
// section("title")を設定する
@section("title", "index")  

@include("head")

@section("body")
body
@endsection

このようにするとhead内のyieldにきちんと挿入される。

どうやら

  • extendsしたテンプレートへ挿入したいsectionはどこに書いても良い
    • extends宣言の前でも後でも良い
  • includeしたテンプレートへ挿入したいsectionは、includeするよりも先にsectionを書かないといけない
    • sectionの記述後にincludeしないといけない

といった制約がある様子。
どう日本語で表現したらいいのか難しいですが、多重yieldとか、includeしたテンプレートへのyieldとか、そういった感じでしょうか。

そのうちLaravelのコード直接読んで仕組みを理解したいところですが、今は覚書として。

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

AlpineLinux on Docker × Laravelが遅い話

開発環境だけアプリがめちゃくちゃ重い

新しいチームに配属されて最初の感想です。

本番ではめちゃめちゃ早いのに開発環境がむちゃくちゃに遅い。

DXが最悪だったので原因を調査したところAlpineからCentOSに乗り換えたら3倍ぐらい早くなったという話です。

image.png


image.png

ベンチマーク

長々した説明は置いといて、結論から言うと「よくわからないがdockerで動かしているphpが異常に重い」ということだけがわかった。

これアプリケーションコードが重いというわけではなく、Alpine Linux on Docker で動いているLaravelが全体的に処理が遅いという体感があったので実際に計測してみた。

以下のPHPコードをAlpineとCentOSで動かして比較してみる。CentOSのDockerfileはPHP入れる処理だけなので省略。

test.php
<?php
ini_set('memory_limit', '-1');
function benchmark($i) {

    if($i ==! 0) {
        benchmark($i - 1);
    }
    function() { return sha1("test${i}");};
    function() { return md5("test${i}");};
}

$time_start = microtime(true);
foreach (range(1, 10000) as $i) {
    benchmark($i);
}

$time = microtime(true) - $time_start;
echo "{$time} sec";

コードの内容は至って簡単で、メモリ上限を無くし再帰的処理でハッシュを取得していくだけ。

 /t/test  docker run --rm -v /tmp/test/test.php:/test.php centos-php php /test.php
8.2858350276947 sec
/t/test  docker run --rm -v /tmp/test/test.php:/test.php centos-php php /test.php
8.2832388877869 sec
/t/test  docker run --rm -v /tmp/test/test.php:/test.php centos-php php /test.php
8.2994618415833 sec

/t/test  docker run --rm -v /tmp/test/test.php:/test.php php:7.4.1-fpm-alpine php /test.php
8.1350269317627 sec
/t/test  docker run --rm -v /tmp/test/test.php:/test.php php:7.4.1-fpm-alpine php /test.php
8.0924451351166 sec
/t/test  docker run --rm -v /tmp/test/test.php:/test.php php:7.4.1-fpm-alpine php /test.php
8.0950059890747 sec

これは特に問題なさそう(むしろAlpineの方が早い

Laravelでのベンチマーク

実際にLaravelで測定した時の結果を貼っていく

用意が面倒だったので開発環境での弊社サービスのLPページを測定対象とする

https://album.8122.jp/

alpineでのdockerfileは以下の通り

FROM php:7.3-fpm-alpine
ENV LD_PRELOAD /usr/lib/preloadable_libiconv.so php

RUN apk --no-cache update \
&& apk add --no-cache $PHPIZE_DEPS postgresql-dev libpng-dev libjpeg-turbo-dev icu-dev \
&& docker-php-ext-configure gd --with-jpeg-dir=/usr/include/ --with-png-dir=/usr/include/ \
&& docker-php-ext-configure intl --enable-intl \
&& docker-php-ext-install exif pdo_pgsql gd intl opcache pcntl
RUN pecl install xdebug \
&& docker-php-ext-enable xdebug

WORKDIR /var/www/html

対してCentOSベースのDockerfileは以下の通り

FROM centos:7.5.1804

#locale 追加
RUN sed -i -e '/override_install_langs/s/$/,ja_JP.utf8/g' /etc/yum.conf

RUN curl -sL https://rpm.nodesource.com/setup_10.x | bash - \
  && yum install -y http://rpms.famillecollet.com/enterprise/remi-release-7.rpm \
  https://s3-ap-northeast-1.amazonaws.com/sen-infra/rpms/centos/7/x86_64/pgdg-centos10-10-2.noarch.rpm \
  && yum install -y postgresql10 \
  nodejs \
  zlib-devel \
  glibc-common \
  make \
  libpng-devel \
  cronie \
  && yum install -y --enablerepo=remi,remi-php73 \
  php \
  php-opcache \
  php-mbstring \
  php-pdo \
  php-pecl-memcache \
  php-pecl-memcached \
  php-pecl-redis \
  php-pecl-imagick \
  php-mcrypt \
  php-mysqlnd \
  php-xml \
  php-gd \
  php-devel \
  php-pgsql \
  php-pecl-ssh2 \
  php-process \
  php-intl \
  php-pear \
  php-pecl-apcu \
  php-pecl-apcu-bc \
  php-pecl-zip \
  php-fpm \
  && rm -rf /var/cache/yum/* \
  && yum clean all


COPY ./php-fpm.conf /etc/php-fpm.conf

RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin \
  && mv /usr/local/bin/composer.phar  /usr/local/bin/composer \
  && composer global require hirak/prestissimo

WORKDIR /var/www/html

CMD ["/usr/sbin/php-fpm","-F","-y","/etc/php-fpm.conf"]

LPページなのでミドルウェアもプロパイダーも挟んではいない(はず

まずはAlpineから

yoshiken@yskn:~/Git/sen/album$ time curl localhost
real    0m0.392s
user    0m0.013s
sys 0m0.004s
yoshiken@yskn:~/Git/sen/album$ time curl localhost
real    0m0.392s
user    0m0.013s
sys 0m0.000s
yoshiken@yskn:~/Git/sen/album$ time curl localhost
real    0m0.409s
user    0m0.012s
sys 0m0.004s

平均して約0.4sec

対してCentOSベースでは

yoshiken@yskn:~/Git/sen/album$ time curl localhost
real    0m0.064s
user    0m0.007s
sys 0m0.007s
yoshiken@yskn:~/Git/sen/album$ time curl localhost
real    0m0.062s
user    0m0.003s
sys 0m0.012s
yoshiken@yskn:~/Git/sen/album$ time curl localhost
real    0m0.052s
user    0m0.014s
sys 0m0.000s

平均して0.06sec前後といったところ。

よくわからん

ここまで書いていてなんだが、何故PHPとしてのパフォーマンスが上のAlpineがLaravelを介すると急激に遅くなるかわかっていない。

調べた感じAlpineだと遅いという点では色々困ってる人が見当たってるが、具体的な解決策、根本原因は未だ解明されていない。

[5.2] Slow response times running within a php-7 Docker container · Issue #12228 · laravel/framework

php - PHP7 + Laravel +Nginx is terribly slow on Docker - Stack Overflow

AlpineでPythonが遅くなるのはAlpineの独自パッケージによる差分が原因とされているが、PHPでは最初のベンチマークの時点では大きな乖離がなかった。
https://superuser.com/questions/1219609/why-is-the-alpine-docker-image-over-50-slower-than-the-ubuntu-image

言いたいこと

いろんな記事で「DockerとAlpine使って爆速で環境構築!!!」という文章を見かけるが、一旦待ってほしい。

ローカル環境では急速なスケールをする必要もないのでイメージが大きかろうが特に問題は無いわけであって、重要なのは使うフレームワークや言語に最適化された環境である。

もし、本番環境でコンテナ運用して開発と同じイメージ(特にAlpineLinux)を使用しているという状況であれば、まず一旦そのイメージの妥当性を確認すべきなのかなと。
他のディストリビューションをベースにした場合との速度計測をして正しい技術選定を行った方がよいのではと思う次第。

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

【Laravel】テンプレートでビューを楽に作る

はじめに

ビューファイルを作成する時にテンプレートを利用することで簡単に全体の統一感を出すことができます。
では、説明していきます

テンプレートの使い方

まずベースとなるテンプレートの利用方法について説明します。

テンプレートファイルの作成

layoutsフォルダの中にapp.blade.phpのファイルを作成してください。
そして、下記の内容を記述してください。

resources/views/layouts/app.blade.php
<html>
    <head>
        <title>アプリ名 - @yield('title')</title>
    </head>
    <body>
        @section('sidebar')
            ここがメインのサイドバー
        @show

        <div class="container">
            @yield('content')
        </div>
    </body>
</html>

@section
  使用目的はコンテンツの区画を定義することです。
  最後には@showを使用します。
  @section('sidebar')記述でsidebarという区画を定義しています。
@yield
  表示内容を定義するためのものです。値を表示する場所を指定します。
  @yield('title')titleという変数を使う場所を指定しています。

ビューファイルの編集

ビューファイルでのレイアウトの参照方法について説明します。

resources/views/child.blade.php
@extends('layouts.app')

@section('title', 'Page Title')

@section('sidebar')
    @parent

    <p>ここはメインのサイドバーに追加される</p>
@endsection

@section('content')
    <p>ここが本文のコンテンツ</p>
@endsection

@extends

@extends('layouts.app')
  layouts/app.blade.phpを継承します。

@section

@section('title', 'Page Title')
  titlePage Titleを代入する

@section

@section('sidebar')
    @parent
    <p>ここはメインのサイドバーに追加される</p>
@endsection

@section('sidebar')sidebarに代入する値を設定します。
@parentは継承元のapp.blade.phpの内容を表しています。
@endsection@sectionの終わりです。

コンポーネントの使い方

コンポーネントとは部分的に使うテンプレートです。

resources/views/alert.blade.php
<div class="alert alert-danger">
    <div class="alert-title">{{ $title }}</div>
    {{ $slot }}
</div>

$slotには下記の@component内の@slot以外の内容が入ります。

ビューファイルでのコンポーネントの参照方法を説明します。

resources/views/child.blade.php
@component('alert')
    @slot('title')
        Forbidden
    @endslot
    You are not allowed to access this resource!
@endcomponent

@component('alert')alert.blade.phpを参照します。
@slot('title')$titleに変数を代入します。

以上で説明は終わりです。

疑問、気になるところがございましたら、質問、コメントよろしくお願いします!!!

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

【Laravel】カスタムページネーションで、最初と最後のページに遷移するボタンと、今のページと総ページ数を出現させる

Laravelのページネーションで、以下のように

・最初と最後のページに遷移するボタン
・今のページと総ページ数(全体ページ数)

を出現させる(使用できるようにする)方法です。

やりたいこと

上述の通り、以下のキャプチャのようなページネーションを作成する

image.png

ボタンはそれぞれ、

①最初のページへのリンク
②前のページへのリンク
③次のページへのリンク
④最後のページへのリンク

となっています。

環境

  • PHP:バージョン7.3.7
  • Laravel:バージョン5.8
  • OS:Windows10

デフォルトのページネーション

まずはLaravelデフォルトのページネーションから。

image.png


①前のページへのリンク
②次のページへのリンク
②番号のページへのリンク

となっています。

記述も至って簡単で、ページングしたいページのビュークラスに、

{{ $tests->links() }}

と記述するだけ($testsはページング対象のデータの入った変数)。

ちなみに、これは、
\vendor\laravel\framework\src\Illuminate\Pagination\resources\views配下の
default.blade.php
がベースになっています。

image.png

最初と最後のページに遷移するボタンと、今のページと総ページ数を出現させる

1. カスタムページング用のファイルの作成

\resources\views配下に、\vendor\paginationディレクトリ(フォルダ)を作り、その配下にカスタムページング用のファイルを作成します。
今回は、「pagination_view.blade.php」というファイル名にしました。

image.png

2. カスタムページネーション用のファイルの編集

作成したカスタムページネーション用のファイルに以下のように記述します。

①最初のページへのリンク
②前のページへのリンク
③次のページへのリンク
④最後のページへのリンク

の部分については、@akkino_D-En さんの
Laravelでリンク数を可変で決められるペジネーションの自作方法
をそのまま使用させていただきました。

\resources\views\vendor\pagination\pagination_view.blade.php
@if ($paginator->hasPages())
    <ul class="pagination" role="navigation">
        // 最初のページへのリンク
        {{-- First Page View --}} 
            <li class="page-item {{ $paginator->onFirstPage() ? ' disabled' : '' }}">
            <a class="page-link" href="{{ $paginator->url(1) }}">&laquo;</a>
            </li>

        // 前のページへのリンク
        {{-- Previous Page Link --}} 
        <li class="page-item {{ $paginator->onFirstPage() ? ' disabled' : '' }}">
            <a class="page-link" href="{{ $paginator->previousPageUrl() }}">&lsaquo;</a>
        </li>


        {{-- Pagination Elements --}} 
        @foreach ($elements as $element)
            {{-- "Three Dots" Separator --}}
            @if (is_string($element))
                <li class="disabled" aria-disabled="true"><span>{{ $element }}</span></li>
            @endif

            {{-- Array Of Links --}}
            @if (is_array($element))
                @foreach ($element as $page => $url)
                    @if ($page == $paginator->currentPage())
                        // 現在のページ
                        <li class="active" aria-current="page"><span>&nbsp;{{ $page }}</span></li>
                        // 現在のページと最後の総ページの間の「/」
                        &nbsp;/&nbsp;
                        // 総ページ数(=最後のページ)
                        <li class="active" aria-current="page"><span>{{ $paginator->lastPage() }}&nbsp;</span></li>
                    @endif
                @endforeach
            @endif
        @endforeach

        // 次のページへのリンク
        {{-- Next Page Link --}}
        <li class="page-item {{ $paginator->currentPage() == $paginator->lastPage() ? ' disabled' : '' }}">
            <a class="page-link" href="{{ $paginator->nextPageUrl() }}">&rsaquo;</a>
        </li>

        // 最後のページへのリンク
        {{-- Last Page Link --}}
        <li class="page-item {{ $paginator->currentPage() == $paginator->lastPage() ? ' disabled' : '' }}">
        <a class="page-link" href="{{ $paginator->url($paginator->lastPage()) }}">&raquo;</a>
        </li>
    </ul>
@endif

3. カスタムページネーション用のファイルの読み込み

Viewファイルのページネーション使用箇所に以下のように記述します。

{{ $tests->links('vendor/pagination/pagination_view') }}

検索ページなどで、ページング後も検索条件を保持させたい場合は以下のように記述するとよいでしょう。
「appends(request()->query())」の部分で、検索条件を保持してくれます。

{{ $tests->appends(request()->query())->links('vendor/pagination/pagination_view') }}

参考

今回は以下を参考にさせていただきました。ありがとうございました!

①<< < > >>の部分
Laravelでリンク数を可変で決められるペジネーションの自作方法 | Qiita
②総ページの部分
Laravel 5.5 データベース:ペジネーション | ReadDouble
③デフォルトのページネーション、$paginator->currentPage()の部分
Laravelでカスタムページネーションを作成 | Qiita

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

【Laravel】routeメソッドで生成されるURLを相対パスにする

Laravel 6.0

routeメソッドの第3引数にfalseを渡すと相対パスを返してくれるらしい。(デフォルトはtrue)

ヘルパ 6.x Laravel

route関数はデフォルトとして絶対URLを生成します。相対URLを生成したい場合は、第3引数にfalseを渡してください。
$url = route('routeName', ['id' => 1], false);

helpers.php
if (! function_exists('route')) {
    /**
     * Generate the URL to a named route.
     *
     * @param  array|string  $name
     * @param  mixed  $parameters
     * @param  bool  $absolute
     * @return string
     */
    function route($name, $parameters = [], $absolute = true)
    {
        return app('url')->route($name, $parameters, $absolute);
    }
}
index.blade.php
<form action="{{route('foo/bar', [], false)}}" method="post">
 ...
</form>

おまけ

そもそも相対パスにしようと思ったのは、
サーバーの事情で生成されるURLのドメインが期待するものになっていなかったので、
絶対パスではなく、相対パスでURLを生成したかったから。

でもよく考えたらrouteメソッドで生成されるURLは相対パスにできても
リダイレクトは絶対パスが生成されていたのでドメインが違えば正しくリダイレクトできない。

どのみちURLのドメインを明示的に指定する必要があった。

↓リダイレクトも含めて生成されるURLを固定する方法
【Laravel】routeメソッドやリダイレクトで生成されるURLのホスト名を指定する - Qiita

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

【Laravel】routeメソッドやリダイレクトで生成されるURLのホスト名を指定する

経緯

Laravel6.0

・Laravelはルートを指定していない場合$_SERVERから取得する。

$_SERVERの値はサーバーの設定により決まる。
PHP: $_SERVER - Manual

・サーバーの設定が複雑で期待する値が取得できなかった。

・リダイレクトやrouteメソッドで生成されるURLに影響がでて正しく遷移できない。

結論

UrlGeneratorにルートURLを設定することで、生成されるURLを固定できる

app/Privider/AppServiceProvider.php
    public function boot()
    {
        // ルートURLを設定
        Illuminate\Support\Facades\URL::forceRootUrl(\config('app.url'));

        // 必要に応じてsslを強制する
        if (\config('app.env') !== 'local') {
            Illuminate\Support\Facades\URL::forceScheme('https');
        }
    }
.env
APP_URL=https://foo.com

ソースコード(ざっくり)

Illuminate/Routing/UrlGenerator.phpforcedRootを指定することで生成されるURLを変更できる。

Illuminate/Routing/UrlGenerator.php

    public function formatRoot($scheme, $root = null)
    {
        if (is_null($root)) {
            if (is_null($this->cachedRoot)) {
                // forcedRootが設定されている場合は優先されるみたい。
                $this->cachedRoot = $this->forcedRoot ?: $this->request->root();
            }

            $root = $this->cachedRoot;
        }

        $start = Str::startsWith($root, 'http://') ? 'http://' : 'https://';

        return preg_replace('~'.$start.'~', $scheme, $root, 1);
    }

    // forcedRootを設定するためのメソッドがある
    public function forceRootUrl($root)
    {
        $this->forcedRoot = rtrim($root, '/');

        $this->cachedRoot = null;
    }

forcedRootが指定されていない場合は$_SERVERから値を取得しているみたい。

symfony/http-foundation/Request.php
 public function getHost()
    {
        // ここの処理がいまいちわかってない
        // プロキシを指定したときなどに関係ありそう
        if ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_X_FORWARDED_HOST)) {
            $host = $host[0];

        // $_SERVERから取得している
        } elseif (!$host = $this->headers->get('HOST')) {
            if (!$host = $this->server->get('SERVER_NAME')) {
                $host = $this->server->get('SERVER_ADDR', '');
            }
        }

Symfony2のコントローラについてまとめた(後半) - OTOBANK Engineering Blog

// $SERVER['DOCUMENT_ROOT']
$request->server->get('DOCUMENT
ROOT');

// \$_SERVERのインデックスのうち、命名がHTTP_*に該当するもの
// \$_SERVER['HTTP_USER_AGENT']
$request->header->get('user-agent');

優先順位的には

  1. $this->forcedRoot
  2. $_SERVER['HTTP_HOST']
  3. $_SERVER['SERVER_NAME']
  4. $_SERVER['SERVER_ADDR']

になりそう。多分。

参考

LaravelのURLジェネレーターで.envに設定したホスト名を使用する。 - Qiita

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

Laravel + PassportでAPIを作成する

Making an API with Laravel + Passport

以前の投稿を読んでいる場合は、Laravelプロジェクトのセットアップ方法を既に知っているので、それから続けて、パスポートをインストールしてJWT APIを作成します

If you have followed my previous posts, you already know how to setup a Laravel project, continuing from that, we will install passport to create an JWT Api

必要条件 / Requisites:

Laravel Project

Docker

Laravel/Passport

Laravel-Shovel

laravelプロジェクトフォルダー内に、一時的なdockerコンテナーを作成して「composer」を使用し、パスポートをインストールします

Inside the laravel project folder, we create a temporary docker container to use composerand install passport

docker run --rm -v $(pwd):/app composer require laravel/passport

この命令にはしばらく時間がかかります...完了したら、 docker-composeを開始できます

This instruction will take a while ...once done, we can start the docker-compose

sudo docker-compose up --build

実行されると、「http:// localhost /」に移動して確認できます

Once its running, we can verify by going to http://localhost/

新しい移行を実行し、パスポートをインストールします

We run the new migrations and Install passport

sudo docker-compose exec app-server php artisan migrate
sudo docker-compose exec app-server php artisan passport:install

これで、お気に入りのエディター(私の場合はPHPStorm)を使用してプロジェクトを開き、 Userモデルを編集してHasApiTokens特性を追加できます。

We can now open the project using our favorite editor, in my case PHPStorm, and edit the User model to add the HasApiTokens trait.

<?php

namespace App;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];

次に、 passport:routesAuthServiceProviderに追加します

Next we add the passport:routes to the AuthServiceProvider

<?php

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
use Laravel\Passport\Passport;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        // 'App\Model' => 'App\Policies\ModelPolicy',
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();
        Passport::routes();
        //
    }
}

config \ auth.phpでApi認証プロバイダーをパスポートに変更します

We change the Api Auth provider to passport in the config\auth.php

'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'api' => [
            'driver' => 'passport',
            'provider' => 'users',
            'hash' => false,
        ],
    ],

次に、 AuthControllerを作成します

Now we create our AuthController

docker-compose exec app-server php artisan make:controller API\\AuthController

この新しいコントローラーはdockerによって作成されたため、ファイルの書き込み許可を変更する必要がある場合があります

Since this new controller was made by docker, you might need to change the write permission of the file

sudo chown -R myUser:myUser mylaravelproject/

エディターで新しいAuthControllerを開くことができます。関数については、itsolutionstuff.com

ただし、機能に若干の変更が加えられています

Now we can open the new AuthController in our editor, as for the functions, I took the example from the itsolutionstuff.com

But with some slight changes in the functions

<?php

namespace App\Http\Controllers\API;

use App\Http\Controllers\Controller;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;

class AuthController extends Controller
{
    /**
     * Register api
     *
     * @return \Illuminate\Http\Response
     */
    public function register(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'name' => 'required',
            'email' => 'required|email',
            'password' => 'required|confirmed',
            'password_confirmation' => 'required|same:password',
        ]);

        if($validator->fails()){
            return response()->json(["error"=>$validator->errors()],422);
        }

        $input = $request->all();
        $input['password'] = bcrypt($input['password']);
        $user = User::create($input);
        $success['token'] =  $user->createToken('MyApp')->accessToken;
        $success['name'] =  $user->name;

        return response()->json($success);
    }

    /**
     * Login api
     *
     * @return \Illuminate\Http\Response
     */
    public function login(Request $request)
    {
        if(Auth::attempt(['email' => $request->email, 'password' => $request->password])){
            $user = Auth::user();
            $success['token'] =  $user->createToken('MyApp')-> accessToken;
            $success['name'] =  $user->name;

            return  response()->json([$success]);
        }
        else{
            return response()->json(["error"=>"Unauthorized"],422);
        }
    }
}

routes \ api.php内にルートを追加します

Add the routes inside routes\api.php

Route::post('register', 'API\AuthController@register');
Route::post('login', 'API\AuthController@login');

お気に入りのRESTクライアントを使用して、登録とログインをテストできます。「不眠症」を使用しています

You can test the register and login, using your favorite REST client, I'm using insomnia

http://localhost/api/register
{
    "name":"My Name",
    "email":"secremeail@email.com",
    "password":"securepassword",
    "password_confirmation":"securepassword"
}
http://localhost/api/login
{
    "email":"secremeail@email.com",
    "password":"securepassword"
}

これで、基本的なAPIを実装しましたが、より良い応答をするために、以下を使用します。

With this, we have implemented our basic API, but to make a better response, we use:

laravel-shovel

より良いAPI応答を行うためのライブラリです。

Is a library to make better API responses.

インストールする前に、 docker-composeを停止し、一時的なcomposerコンテナを実行します。

Before installing it, we stop docker-compose and run a temporary composer container.

docker run --rm -v $(pwd):/app composer require stephenlake/laravel-shovel

ミドルウェア内のルートをグループ化します

We group the routes inside the middleware

Route::group(['middleware' => ['ApiRequest',"ApiResponse"]],function (){
    Route::post('register', 'API\AuthController@register');
    Route::post('login', 'API\AuthController@login');
});

すべてのAuthController応答を通常の応答に変更する必要があります

We have to change all the AuthController responses to normal resposes

<?php

namespace App\Http\Controllers\API;

use App\Http\Controllers\Controller;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;

class AuthController extends Controller
{

    public function register(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'name' => 'required',
            'email' => 'required|email',
            'password' => 'required|confirmed',
            'password_confirmation' => 'required|same:password',
        ]);

        if($validator->fails()){
           return response()->json(["messages"=>$validator->errors()],422)
               ->withMeta("message","Validation error");
        }

        $input = $request->all();
        $input['password'] = bcrypt($input['password']);
        $user = User::create($input);
        $success['token'] =  $user->createToken('MyApp')->accessToken;
        $success['name'] =  $user->name;

        return response($success);

    }


    public function login(Request $request)
    {
        $credentials = [
            'email' => $request->email,
            'password' => $request->password
        ];

        if (auth()->attempt($credentials)) {
            $token = auth()->user()->createToken('MyApp')->accessToken;
            return response(["token"=>$token]);
        } else {
            throw new \Exception("Invalid Credentials",422);
        }
    }
}

また、「app / Exceptions / handler.php」を処理する例外にjson応答を追加します

Also add a json response in the expection handled app/Exepctions/handler.php

public function render($request, Exception $exception)
    {
        if($request->acceptsJson())
        {
            return response()
                ->json(["messages"=>$exception->getMessage()],500)
                ->withMeta("message","internal server error");
        }
        return parent::render($request, $exception);
    }

このライブラリは、メタヘッダーを追加するすべてのAPI応答を標準化します

This library will standarize all api responses adding meta headers

{
  "meta": {
    "code": 200,
    "status": "success",
    "message": "OK"
  },
  "data": {
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIxIiwianRpIjoiMTMzYzU3NjVlZmJjYTdmODQ0NDdlMTE4ZWUyZDc1YWI5YzY0MmQ3NTE2MjIzNWM1Y2FjNDNlNjI5ZDIyMzU1MzMzMzY1M2U2Yjc2ZTJhNzIiLCJpYXQiOjE1NzcxNTQwMzcsIm5iZiI6MTU3NzE1NDAzNywiZXhwIjoxNjA4Nzc2NDM3LCJzdWIiOiIxIiwic2NvcGVzIjpbXX0.KIoArr6X69eRLDluWWgEOS5gAlLmLxtYKhMURgzsmgmLooVV6EJDHGx11gnQ7_WmagtbredLabHXeSks6DQ2A8tGeqFyrVxnCRddAySxHDxhzF0VF2wF9rn_1OduDcC3xOVdrXPj-VkxToHLyW3e6A714XSTxHgzynEKBh2JtDIRN3lCt13_1F8iD9ocGHPLBrW-XFhV4Iw2atSyL8N5qQH29wsopwWZCoTqqAN2whgfylyCTlXFAcQWe0AOJEzc39jpTudkiSXAKKFuS1hCjLYYdiuae-NJGOTutDD3CzFjYrO-Kvq3-QBX7go4uNftOVwuARsvlBuyCfPbpzCM8FVfuZCEHMv7YODCrCs2s305PvsGAsIIcKB_9_dpx0nO-lZy9_Hsn6HKAztCkLBNQponLAM10pah36xaq5c_mOwRMIltRArfDi-QuteIP4XUYXJXDV96bw2BQGNlybbOU0z7x7ocLmlP7xh4NBZFjUs5eM02U6eJykewxr8UpIkoyi6N3-ZxKKhdIWfeW6jRFe7IHlKQ2QTR1Hp33zGPlwTIW8dTe8UYo_FXdQojsFV7uxc9GUUDFimrJT3cem6JvcUEWsQtbxRv3tyyMST4P5gZpr-bANS2z6DiseuQRjHU9ZNYX9rm62GS8Wo_sNXxbfayTrFk6mBsuNn33kY1T8o"
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

vue.js laravel で いいね機能を作ろう

定義

・ユーザーにも、記事にも、色々なテーブルに対応できるように
・vue.js , laravel を利用
・コンポーネントにして使いまわしが効くように
・redis でも考えたが、そんなにめちゃくちゃいいねされる訳じゃないし、
いいね順にソートするとき微妙かなと考えたのでmysqlで

今回は ユーザー に対し いいねできるように。

テーブルを作成

users

id,name


iines

id,foreign_key,model(varchar255),user_id,updated_at,created_at


ここまででMYSQLの設定完了

User と iine を関連付ける。

User.php
//User.id  Iine の foreign_key 。さらには model が User のものを紐付ける。
public function iines()
{
    return $this->hasMany('App\Iine','foreign_key')->where('model', 'User');
}

いいねをしたり、いいねしているか取得する。

OkwsController.php
//いいねを取得
public static function getIine(request $request)
{
    $model = new Iine;

    $model->foreign_key = $request->foreign_key;
    $model->user_id = $request->user_id;
    $model->model = $request->model;


    $tmp = $model
        ->select('id')
        ->first();

    //要素の存在チェック bool
    if(!empty($tmp->id)){
        $res = true;
    } else {
        $res = false;
    }

    return response()->json(['res'=> $res]);

    }

//いいねを追加
    public static function addIine(request $request)
    {
    $model = new Iine;

    $model->foreign_key = $request->foreign_key;
    $model->user_id = $request->user_id;
    $model->model = $request->model;



    $tmp = $model
        ->select('id')
        ->first();


    //要素の存在チェック bool
    if(!empty($tmp->id)){
        $res = false;
        $model
            ->where('id',$tmp->id)
            ->delete();//削除
    } else {
        $res = true;
        $model->save();
    }

    return response()->json(['res'=> $res]);

}

あとは、web.phpにも動くようにルーティングを追加。

ここまででLaravel側の設定を完了。

つづいて、vue.js。
まずは、使い回しができるようにComponent化しておく。

IineComponent.vue

<template>

    <div>
        <button v-if="flag" @click="addIine">
            いいね済
        </button>
        <button @click="addIine" v-else>
            いいね
        </button>

    </div>

</template>

<script>
    export default {
        props: ['foreign_key','user_id','model'],

        data () {
            return {
                flag:false,
            };
        },

        created () {
            this.getIine();
        },

        methods: {

            //足跡を追加
            addIine(){
                let dataform = new FormData();
                dataform.append('foreign_key',this.foreign_key);
                dataform.append('user_id',this.user_id);
                dataform.append('model',this.model);
                axios.post('/okws/addIine/', dataform).then(e => {
                    this.flag = e.data.res;
                    console.log("いいね成功");
                }).catch((error) => {
                    console.log("エラー");
                });
            },
            //足跡を追加
            getIine(){
                let dataform = new FormData();
                dataform.append('foreign_key',this.foreign_key);
                dataform.append('user_id',this.user_id);
                dataform.append('model',this.model);
                axios.post('/okws/getIine/', dataform).then(e => {
                    this.flag = e.data.res;
                    console.log("いいねできたか取得");
                }).catch((error) => {
                    console.log("エラー");
                });
            },

        },
    }
</script>





つづいては、 Userview.vue から コンポーネントを読み出そう。
foreign_key に 訪問先ページの User.id。
user_id に 現在ログインしている User.id
model は 今回はUser

Userview.vue
<!--500,100,'User'-->
<iine-component :foreign_key="$route.params.id" :user_id="$root.user.id" :model="model"></iine-component>



Userview.vue は app.js でルーティングを忘れずに。

ここまででいいね機能をつけることができました。

■ いいね順に並び替えよう

//        関連モデル hasmany iineをいくつ持っているか で並び替え
        $res = User::withCount('iines')
            ->orderBy('iines_count', 'desc')
            ->get();


        foreach ($res as $v) {
            echo "記事のID : " . $v->id."<br>";
            echo "いいね数 : " . $v->iines_count."<br>";
        }

結果

記事のID : 10186
いいね数 : 2
記事のID : 10185
いいね数 : 1
記事のID : 0
いいね数 : 0

はい。並び替わりましたね♪

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

LaravelでスクレイピングしたデータをSeederに逆生成してみた話

やりたい事

  • 某サイトからスクレイピングしてきたデータをDBに保存。
    ※某サイトからは許可を得ている。
  • ローカルの開発環境でもある程度、テストデータとして、Seederは用意しておきたい。
  • PHPUnitを実行する時にテストデータを作っておきたい。

導入方法

スクリーンショット 2019-12-24 0.19.32.png

1.fabpot/goutteをインストール

$ composer require fabpot/goutte

2.スクレイピングするバッチを作成

<?php

namespace App\Console\Commands;

use App\Entity\Article;
use Goutte\Client;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;

/**
 * Class ScrapingCommand
 * @package App\Console\Commands
 */
class ScrapingCommand extends Command
{

    /**
     * スクレイピング先のURL
     */
    const SCRAPING_URL = 'http://example.com';

    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'command:scraping_command';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {

        //インスタンス生成
        $client = new Client();

        //取得とDOM構築
        $crawler = $client->request('GET', self::SCRAPING_URL);

        //要素の取得
        $tr = $crawler->filter('table tr')->each(function($element){
            echo $element->text()."\n";
        });

        // 以下略

        // 取得したデータをArticleエンティティに設定 ※例なので、ざっくり書いてます。
        $article = new Article();
        // 本当は良い感じに取得したデータをエンティティに詰める
        $article->title = $tr;
        // 以下略

        DB::beginTransaction();
        try {
            $article->save();
            DB::commit();

        } catch (\Exception $exception) {
            Log::error('記事の更新に失敗しました', [
                'exception' => $exception->getMessage(),
                'file' => __FILE__,
                'method' => __FUNCTION__,
                'line' => __LINE__
            ]);
            DB::rollBack();
        }

        Log::notice('スクレイピングバッチの実行が成功しました。');
    }
}

3.orangehill/iseedをインストール

https://github.com/orangehill/iseed

$ composer require --dev "orangehill/iseed"

を実行。

config/app.phpにProviderの設定を追加

'providers' => [

        /*
         * データベースからLaravelのSeederを逆生成する
         */
        Orangehill\Iseed\IseedServiceProvider::class
    ],

4.下記のコマンドを実行すればテーブルの内容に応じたSeederクラスが生成される。

$ php artisan iseed {table_name} 

を実行。

5.Seederクラス生成後のイメージ

<?php

use Illuminate\Database\Seeder;

class ArticlesTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        \DB::table('articles')->delete();

        \DB::table('articles')->insert([
                0 => [
                    'id' => 1,
                    'title' => 'タイトル',
                    'descrition' => '記事の説明文',
                    'category' => 'Tech',
                ],
                1 => [
                    'id' => 2,
                    'title' => 'タイトル',
                    'descrition' => '記事の説明文',
                    'category' => 'Tech',
                ],
                2 => [
                    'id' => 3,
                    'title' => 'タイトル',
                    'descrition' => '記事の説明文',
                    'category' => 'Tech',
                ]
            ]
        );
    }
}

使ってみた感想

  • けっこうコマンドの実行時間も短いし、良かった
  • データ量が多いとファイルサイズが大きくなってしまうので、上手くfor文とかで重複データはコードで簡潔にまとめてもらえたら尚嬉しい!
  • お客さんがマスタデータを提供していない or APIが存在せず、自分でデータを取得しなければいけない案件には向いてそう!

参考記事

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

Laradockで作ったLaravelプロジェクトで出るpermission deniedに関してかく

こんにちは、Fusicのmockmockチームエンジニアのジホです。

この記事は

  • Fusic Advent Calendar24日目の記事です。
  • Linux上でLaradockを使ったLaravelプロジェクト構築で会ったpermission deniedに関して書きます。

書く理由は

Laradockのdocker-compose.ymlを見てでかい。どこ見ればいいのがわからない時に参考になって欲しいからです。

環境

Linux

  • CentOS Linux release 7.7.1908 (Core)

Docker Compose

  • docker-compose version 1.25.0, build b42d419

Docker

  • Docker version 19.03.5, build 633a0ea

構成図

phpというディレクトリーの配下にLaradockがあるLaradock、Laravalがある/project-z/docker-laravel/を作りました。
- Laradockgit clone https://github.com/laradock/laradock.gitしたものです。
- /project-z/docker-laravel/project-zcomposer create-project --prefer-dist laravel/laravel docker-laravel -vvvしたものです。
directoryTree1.PNG
directoryTree2.PNG

permission deniedを順番に紹介

  • LaravelのDB設定のために/project-z/docker-laravel/.envを修正しようとするとpermission denied
    souceCodePermissionDotEnv.png

  • 最初はdocker compose up出来たのに、何ん回目でいきなりnginxのssl周りでpermission denied
    secondDockerComposeUpPermission.PNG

  • よくわからないけど/storage/logs/周りでpermission denied
    afterFixSecondDockerComposeUPPermission.PNG

  • よくわからないけど/storage/framework/views/周りでpermission denied
    viewPermission.PNG

permission deniedの原因と解決

  • LaravelのDB設定のために/project-z/docker-laravel/.envを修正しようとするとpermission denied

    • 原因 : Laravelのソースコードの権限をDockerコンテナの中と外で適切に両方を対応してない(user idとgroup idが揃えてない) souceCodePermission.png centosAndLaradocUID.png
    • 解決 : Laradockの/laradock/.envにあるWORKSPACE_PUIDWORKSPACE_PGIDをDockerコンテナの外を基準として揃える。VSCodeなどで作業するなら、Dockerコンテナの外で作業することになるからです。 set1001.png
  • 最初はdocker compose up出来たのに、何ん回目でいきなりnginxのssl周りでpermission denied

    • 原因 : /laradock/php-fpm/DockerfileがDockerコンテナの中でnginxのuserのidとgroup idをdefaultとして1000に設定していた。(user idとgroup idが揃えてない) usermodWwwdata.png
    • 解決 : Laradockの/laradock/.envにあるPHP_FPM_PUIDPHP_FPM_PGID修正する set1001wwwdata.png
  • よくわからないけど/storage/logs/周りでpermission denied

  • よくわからないけど/storage/framework/views/周りでpermission denied

    • 原因 : Laravelのソースコードの権限をDockerコンテナの中と外で適切に両方を対応してない。
    • 解決 : Laradockの/laradock/.envにあるWORKSPACE_PUIDWORKSPACE_PGIDをDockerコンテナの外を基準として揃える。VSCodeなどで作業するなら、Dockerコンテナの外で作業することになるからです。
    • Laravelプロジェクト生成時点次第でsudo chown vagrant -R ...を1回する必要がある可能性があります。例えば、composer create-project --prefer-dist laravel/laravel docker-laravel -vvvした時点でdocker-laravelのファイルとディレクトリー権限がずれていることがあります。

まとめ

  • Dockerコンテナの中と外のuserのidとgroup idが揃えているか確認しましょう。
  • WORKSPACE_PUIDWORKSPACE_PGIDPHP_FPM_PUIDPHP_FPM_PGIDをちゃっと設定したら大体大丈夫です。
    • /laradock/.envにあります(Laradockのenvファイル)

参考

https://laradock.io/getting-started/
https://docs.docker.com/compose/compose-file/#env_file
https://laravel.com/docs/6.x

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

EloquentのJOINで結合テーブルに論理削除を効かせる

EloquentでModelに論理削除(SoftDeletes)を設定しておくと、デフォルトでdeleted_at is nullをWHERE条件に入れてSQLを発行してくれます。

しかし、テーブル結合(JOIN)をした場合、結合したテーブルに対して論理削除の抽出条件が効きません。

$users = User::join('deptments', 'users.deptment_id', '=', 'deptments.id')->get();
select
  * 
from
  `users` 
  inner join `deptments` 
    on `users`.`deptment_id` = `deptments`.`id` 
where
  `users`.`deleted_at` is null

それならとwhereを設定してみるも、これだとLEFT JOINの場合に結合しなかったusersテーブルのレコードが抽出されなくなってしまいます。

$users = User::join('deptments', 'users.deptment_id', '=', 'deptments.id')
             ->where('deptments.deleted_at', null)->get();
select
  * 
from
  `users` 
  inner join `deptments` 
    on `users`.`deptment_id` = `deptments`.`id` 
where
  `deptments`.`deleted_at` is null 
  and `users`.`deleted_at` is null

第二引数にクロージャを渡すとJOIN句の中で複数の抽出条件を使えるようになります。

$users = User::join('deptments', function ($join) {
            $join->on('users.deptment_id', '=', 'deptments.id')
                 ->where('deptments.deleted_at', null);
         })->get();
select
  * 
from
  `users` 
  inner join `deptments` 
    on `users`.`deptment_id` = `deptments`.`id` 
    and `deptments`.`deleted_at` is null 
where
  `users`.`deleted_at` is null

意図したSQLを発行することができました。

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