- 投稿日:2019-12-24T23:51:07+09:00
Vue.js の フォーム とか tailwindcss とか Vue Router とか コンポーネント とか
この記事は、静岡 Advent Calendar 2019 の25日目の記事です。
はじめに
静岡の勉強会事情等々、皆様がいろいろと書いてくださっているので、自分はそちらの内容はまたの機会にすることにして、当初、Laravel を用いてテストについて何か書こうかなと思っておりました。
が、ちょっとばかり路線変更しまして、Vue.js の フォーム を中心に時間が許す限り何か思うがままに書こうかなと思います。
テストにつきましては、また別の機会にでも書きます。sqlite を使ってやるよくあるやつです。大した内容ではございません。
それから、次点として当初考えておりました、Laravel で過去に作った簡単なプロジェクトを Symfony(完全未経験) で書き直す。というものも考えてはおりましたが、風邪で昨日まで1週間寝込むという愚行を犯した自分には難しかったです。(今のところ娘に移っていないようなのが救いだったり...)うだうだと書きましたが、それでは、早速本題に入りますよ。
今回は下記スクリーンショットのような form を想定しております。
環境につきましては、バックエンドに 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 Router と tailwindcss を入れる
## Using npm npm install vue-router tailwindcss --save-dev ## Using Yarn yarn add vue-router tailwindcss --save-devpackage.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.jslet 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.jsconst 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.jsimport 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.jsimport 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
良いお年を〜
- 投稿日:2019-12-24T23:51:07+09:00
Vue.js の フォーム について
この記事は、静岡 Advent Calendar 2019 の25日目の記事です。
はじめに
静岡の勉強会事情等々、皆様がいろいろと書いてくださっているので、自分はそちらの内容はまたの機会にすることにして、当初、Laravel を用いてテストについて何か書こうかなと思っておりました。
が、ちょっとばかり路線変更しまして、Vue.js の フォーム を中心に時間が許す限り何か思うがままに書こうかなと思います。
テストにつきましては、また別の機会にでも書きます。sqlite を使ってやるよくあるやつです。大した内容ではございません。
それから、次点として当初考えておりました、Laravel で過去に作った簡単なプロジェクトを Symfony(完全未経験) で書き直す。というものも考えてはおりましたが、風邪で昨日まで1週間寝込むという愚行を犯した自分には難しかったです。(今のところ娘に移っていないようなのが救いだったり...)うだうだと書きましたが、それでは、早速本題に入りますよ。
今回は上記スクリーンショットのような form を想定しております。
環境につきましては、バックエンドに 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 Router と tailwindcss を入れる
## Using npm npm install vue-router tailwindcss --save-dev ## Using Yarn yarn add vue-router tailwindcss --save-devpackage.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.jslet 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.jsconst 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
良いお年を〜
- 投稿日:2019-12-24T23:51:07+09:00
Vue.js の フォーム とか
この記事は、静岡 Advent Calendar 2019 の25日目の記事です。
はじめに
静岡の勉強会事情等々、皆様がいろいろと書いてくださっているので、自分はそちらの内容はまたの機会にすることにして、当初、Laravel を用いてテストについて何か書こうかなと思っておりました。
が、ちょっとばかり路線変更しまして、Vue.js の フォーム を中心に時間が許す限り何か思うがままに書こうかなと思います。
テストにつきましては、また別の機会にでも書きます。sqlite を使ってやるよくあるやつです。大した内容ではございません。
それから、次点として当初考えておりました、Laravel で過去に作った簡単なプロジェクトを Symfony(完全未経験) で書き直す。というものも考えてはおりましたが、風邪で昨日まで1週間寝込むという愚行を犯した自分には難しかったです。(今のところ娘に移っていないようなのが救いだったり...)うだうだと書きましたが、それでは、早速本題に入りますよ。
今回は下記スクリーンショットのような form を想定しております。
環境につきましては、バックエンドに 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 Router と tailwindcss を入れる
## Using npm npm install vue-router tailwindcss --save-dev ## Using Yarn yarn add vue-router tailwindcss --save-devpackage.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.jslet 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.jsconst 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.jsimport 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.jsimport 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
良いお年を〜
- 投稿日:2019-12-24T23:37:15+09:00
Laravel Job Queueで バッチ運用した話
やりたい事
- 毎日、スクレイピング処理を実行
- スクレイピングした結果をGoogleスプレッドシートに書き込む
- 大量の処理が走るので、PHPメモリエラーになる可能性が高い
- スプレッドシートの結果を毎日、バックアップする
- スプレッドシートの結果を毎日、メールで送る
やった事
- ①スケジューラの設定
- ②バッチコマンドクラスの作成
- ③ジョブキューの仕組みを実装
- ④GASでスプレッドシート書き込みの処理を実装
- ⑤supervisorの設定
0.各キーワード説明
Job Queueとは?
ジョブキューとはジョブをキューで管理するものです。
キューとはFIFO(First In First Out)を実現するデータ構造です。
キューに登録されたモノは、キューに登録した順に処理されます。
参考:http://tech.voyagegroup.com/archives/495474.html
全体構成
①スケジューラの設定
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でスプレッドシート書き込みの処理を実装
⑤supervisorの設定
まとめ
- 大量の処理が走るような機能があった際に、Job Queueを使うとPHPメモリエラーを防げる可能性がある。
- メールの大量送信やスクレイピング、高速でAPIを叩くような処理には向いてそう。
参考記事
- https://speakerdeck.com/bumptakayuki/laravel-job-queuede-batutiyun-yong-sitahua
自分の発表資料も参考により具体的に書いてみました。
- 投稿日:2019-12-24T23:32:27+09:00
画像投稿がメインのサービスをはじめて運営してみて、失敗したこと、改善したこと、やろうとしていること
こんにちは、キッド✈️と申します。
東南アジア発のスタートアップスタジオ、GAOGAOのサーバーサイドエンジニアです。
このQiitaはGAOGAO Advent Calendar 2019の25日目の記事です。先日、@rikuhiroseさんと共同で開発している、旅行サービスをリリースしました。ユーザーによる画像投稿がメインになるサービスです。
リリースしたサービスはこちらです。
FRIP | 思わず友達を旅行に誘いたくなる、 おすすめなスポットがあつまる旅行口コミサイト実は、ユーザーによる画像投稿がメインのサービスを運営するのはこれがはじめてです。
実際に画像投稿メインのサービスを運営してみて、失敗したことがあったので、備忘録としてこちらに残すことにしました。
失敗したこと
(1)ファイルサイズが大きい画像のアップロードに失敗する
サービスをリリースしてすぐ、ありがたいことに友達が投稿をしてくれようとしました..!!嬉しい!!
と、喜んだのもつかの間。
投稿フォームを入力し、投稿する画像を選択し、いざアップロードしたら、画像が重すぎてエラーになりました。
理由としては、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.inipost_max_size = 20M upload_max_filesize = 5M(2)投稿に30秒以上かかってしまう
さて、upload_max_filesizeの上限をあげたことで、無事にサイズの大きな画像もアップロードできるようになりました。しかし、今度は別の問題が発生しました。
リリース当初、画像アップロードは、フォーム送信後にサーバーサイドで全てアップロード処理を行っていました。
ですが、ファイルサイズの大きい画像で、かつ複数枚ともなると、フォーム送信してから完了するまでの時間が長い長い...。計測したら、フォーム送信から完了まで30秒かかってしまうこともざらにありました。
これではユーザーが使ってくれない...。
解決策:
画像アップロードは、画像を選択したら非同期でアップロードを行い、アップロードして画像のidを返すようにしました。
フォームを全て入力し、送信する時点で、画像はアップロードされるようにしたことで、フォーム送信から完了までの時間を大幅に短縮することができました。
(3)ファイルサイズが大きすぎてサイトが劇的に遅くなる
さて、無事に投稿はスムーズにできるようになりました。
しかし、今度はユーザーがアップロードした画像のファイルサイズが大きすぎて、サイトが劇的に遅くなりました...。
画像アップロードはS3にアップロードして、テーブルにs3へのurlを保存。CloudFront経由でS3の画像を読み込んでいました。
お恥ずかしいのですが、ユーザーがアップロードした画像は何も改変せず、そのままS3にあげていました。
旅行系サービスなのでユーザーが投稿する画像は、ファイルサイズが大きくなりがちです。
トップページで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 への自動アップロードで即時に画像変換する|クラスメソッドブログ最後に
記事からも分かりますように大変未熟ですので、画像アップロードのフローで、もっとこうした方がいいよ!など改善アドバイスやフィードバックいただけたらすごく嬉しいです?♂️
何卒よろしくお願いいたします?♂️
メリークリスマス!
- 投稿日:2019-12-24T21:56:32+09:00
Laravelワカンネ(゚⊿゚)から「完全に理解した()」までステップアップ
タイトルで釣るはじめに
この記事は Laravel Advent Calendar 2019 の24日目の記事です
この記事を書こうと思った理由
ネタが思いつかなかった
Laravelを業務で触れて1年が経とうかというのに未だLaravelの基本的な機能を理解しているか
と言われたら首が引きちぎれる勢いで首を横に振ってしまいます。始めた当初は、実行の流れ(ライフサイクル)も含めてふぁさーど??さーびすこんてな??だったので、
きっと同じように理解しないまま使ってる(または葬られた)人もいるだろうと思い書きました。そもそもLaravel使うメリットは
シンプルなMVC構造かつORMなどの機能だけでなく、
強力なルーティングフィルターや柔軟なオートローダーなどをサポートしており、
開発者は設計を意識せずに開発ができる。↑とりあえずよく目にするやつ
Laravelが実行されるまでの流れってどうなってるの
LaravelのアプリケーションではURLにアクセスした時にエントリポイント(
public/index.php
)が参照される仕組みになっており、
その中でオートロードファイルの読み込みアプリケーションの設定・実行が行われています。エントリポイントの中身(
public/index.php
)実行の流れ(ライフサイクル)
実行までのフローを見るとこんな感じになっていて、
ユーザーがURLにアクセスしてから① ~ ⑦までの流れで実行されています。MVC(Model View Controller)
上記の実行フローの画像⑥の部分でコントローラーを起点にモデルやビューの参照が行われている
ルーティング
先ほどの実行フローの画像④でルータでは、URLとコントローラーを紐付けています。
どう言う事??ってなるので画像で説明(文字で説明できないボンクラ)上記画像ではフローを端折ってますが、左のユーザーが
http://test/welcome
と言うURLにアクセスした時に、
エントリーポイントからルータ(routes/web.php
)が呼び出され、URLの最後のパスwelcom
と紐付くコントローラーを検出します。どうやって検出しているかというと下記の様に設定することでLaravelが認識することが出来ます。
...と言う風な流れで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.phpclass 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; }便利や....。
サービスプロバイダ
名前が似ているのでサービスプロバイダとサービスコンテナ何が違うの??となっている方もいると思いますので説明。
サービスプロバイダとはフレームワークやアプリケーションに含まれるサービス(機能)の初期処理を行う目的で用意されています。
Laravelではサービス(機能)毎に初期処理を定義して実行する仕組みがあり、
その仕組みや実際に初期処理の実装を行うクラスのことをサービスプロバイダ
と呼びます。
サービス
=> 機能
プロパイダー
=> 供給者つまり機能(サービス)を供給する(プロバイド)クラスと言えます。
文章だけではあまりイメージ出来ないと思いますが、先ほどのサービスコンテナで利用した
bind
はControllerに直接書かずに、
サービスプロバイダに切り分けて定義するって事だけ分かってればとりあえず良いかなと個人的には思ってます実際のサービスコンテナの例
app/Providers/PhoneServiceProvider.phpnamespace 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.phpRoute::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リクエストをフィルタリングする際などのメカニズムとして用意されている。
(中間という意味では一緒)ここで先ほどの実行のフロー(ライフサイクル)の画像を再度貼ります。
下記画像の⑤ミドルウェアを見ての通り、④ルーターから⑤ミドルウェアが実行され、その後に⑥のコントローラーを参照しているのが分かります。
しかし、画像を良く見てみると矢印の通り⑥→⑤→④と処理が戻っているのが分かると思います。
この通り、ミドルウェアではコントローラーの呼び出し前と呼び出し後にミドルウェアに設定された処理を行うことが出来ます。(と言えば語弊が生まれるかもしれないが)(実行の流れ画像再添付)
コントローラーの前後に設定出来るのは分かったけど具体的に何が出来るの?という人もいると思います。
Laravelでログイン認証を実装した事がある人であれば何となく分かると思いますが、実はこのミドルウェアを使って、ユーザが認証済みかどうかの確認を影で行なっています。
それだけではなく、バリデーションやコンテンツの置換など様々な機能が用意されています。Laravelの素敵なところは上記の様なデフォルト機能だけでなく、自分で簡単にミドルウェアを自作してコントローラーの前後に設定が出来るところです。
随時追加していきます(
時間がなかった)最後に
Laravel完全に理解した()状態にステップアップできたでしょうか。
結構記事によって説明が異なってたりするので自分の知識も間違ってる箇所あるかと思います。。
誤りに気付いた方はマサカリと思わず編集リクエストまたは指摘していただけると幸いです。
- 投稿日:2019-12-24T21:49:02+09:00
【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.phpDB::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.phpclass User extends Model { // protected $fillable = ['name','age','sex']; }コントローラーの編集
とりあえず、ユーザー一覧を取得するindexメソッドを作成
app/Http/controllers/UsersController.phppublic function index() { $users = User::all(); return view('index')->with('users', $users); }全体ビューを作成
共通化するビューのフォルダとファイルを作成
$ cd resources/views $ mkdir layouts $ cd layouts $ vi defalt.blade.phpdefalt.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> @endsectionlaravelサーバーを立ち上げて確認
$ php artisan serve --host ***.****.**.** --port 8000こんな感じでできていれば、okです。
検索機能を実装していきます。
検索画面を作成
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.phppublic 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); } }終了です。
- 投稿日:2019-12-24T21:16:27+09:00
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.ymlDocker環境
docker-compoe
appコンテナにはLaravelのビルドインサーバーを起動する設定を記載しています
docker-compose.ymlversion: '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: localDockerfile
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 -dlaravelインストール
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
node_moduleのインストール
$ docker-compose exec app yarnHMRの設定
ホットリロードの確認用に、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.jsmix.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
へアクセスして、コンポーネントの内容が表示されれば完了です。
コンポーネントを修正して、変更が反映されることを確認しましょう。参考
終わりに
ホットリロードでフロントエンド開発が捗ります。
注意点としては、HMRで監視するファイルはJavascriptやCSSなので、blade自体を更新した場合はブラウザの更新が必要となります。
- 投稿日:2019-12-24T19:29:28+09:00
AWS LambdaでLaravelを動かす
最近サーバーレス(Lambda)に興味があったので始めてみるかとは思ったものの
好きな言語であるPHPは標準でサポートされておらず。。。
どうにか動かす方法はないかと調べていたところ、brefなるものを使えば簡単に動かせそう!しかもLaravelも動かせる⁉
だったので、その手順をまとめてみました。準備
以下のものが必要になります.
- AWSアカウント
- AWS CLI
- AWSアカウントアクセスキー及びシークレットキー
- npmコマンド
こちらの準備については記事の内容ではないのでスキップします。
serverlessコマンドをインストール
まずはserverlessコマンドをインストールします。
こちらはLambdaをデプロイする際に使用します。$ npm install -g serverlessserverlessコマンドのインストールが完了したら、AWSアクセスキー及びシークレットアクセスキーを設定します
$ serverless config credentials --provider aws --key <key> --secret <secret>注意点として、AWS CLIのデフォルトアカウントを設定している場合は
そのアカウントが使用されるので上記の手順は不要です。Laravelプロジェクト作成
まずはcomposerコマンドでlaravelプロジェクトを作成しましょう
$ composer create-project laravel/laravel serverless-laravel --prefer-distbrefを追加
$ cd serverless-laravel $ composer require bref/bref注意点として、brefの最新バージョンを使用するにはPHP7.2以上が要求されます。
PHP7.1以下で使用したい場合はbref0.5未満を使用する必要があるみたいです(未確認)
参考: https://bref.sh/docs/installation.html#brefserverless.ymlを用意
serverless.ymlをプロジェクトディレクトリ直下に作成します。
serverless.ymlservice: 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" layerLambda上でLaravelを動かすためにコードを変更
Lambdaでは
/tmp
ディレクトリ以外への書き込みは出来ないため、Laravelが書き込みを行う領域を変える必要があります。bootstrap/app.php# $appを作成している行の直後に記載 $app->useStoragePath($_ENV['APP_STORAGE'] ?? $app->storagePath());.envVIEW_COMPILED_PATH=/tmp/storage/framework/views SESSION_DRIVER=array LOG_CHANNEL=stderrapp/Providers/AppServiceProvider.phppublic 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が表示されるのでアクセスしてみます。
デプロイしたものを削除する
removeコマンドで削除できます。
$ serverless remove
- 投稿日:2019-12-24T18:10:43+09:00
laravel/installer がメジャーバージョンアップした時の対応
2019年5月にv2.0、11月にv3.0が出てる。プロジェクトのcomposerは更新してもglobalのほうは気にしてない人が多い。v1.xがインストールされたままな人も多いと思う。
最新バージョンがあるか確認
composer global outdatedglobalの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
を付ける。
- 投稿日:2019-12-24T16:17:30+09:00
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のコード直接読んで仕組みを理解したいところですが、今は覚書として。
- 投稿日:2019-12-24T15:56:16+09:00
AlpineLinux on Docker × Laravelが遅い話
開発環境だけアプリがめちゃくちゃ重い
新しいチームに配属されて最初の感想です。
本番ではめちゃめちゃ早いのに開発環境がむちゃくちゃに遅い。
DXが最悪だったので原因を調査したところAlpineからCentOSに乗り換えたら3倍ぐらい早くなったという話です。
ベンチマーク
長々した説明は置いといて、結論から言うと「よくわからないが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ページを測定対象とする
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)を使用しているという状況であれば、まず一旦そのイメージの妥当性を確認すべきなのかなと。
他のディストリビューションをベースにした場合との速度計測をして正しい技術選定を行った方がよいのではと思う次第。
- 投稿日:2019-12-24T15:20:58+09:00
【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')
title
にPage 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
に変数を代入します。以上で説明は終わりです。
疑問、気になるところがございましたら、質問、コメントよろしくお願いします!!!
- 投稿日:2019-12-24T12:48:04+09:00
【Laravel】カスタムページネーションで、最初と最後のページに遷移するボタンと、今のページと総ページ数を出現させる
Laravelのページネーションで、以下のように
・最初と最後のページに遷移するボタン
・今のページと総ページ数(全体ページ数)
を出現させる(使用できるようにする)方法です。やりたいこと
上述の通り、以下のキャプチャのようなページネーションを作成する
ボタンはそれぞれ、
①最初のページへのリンク
②前のページへのリンク
③次のページへのリンク
④最後のページへのリンク
となっています。環境
- PHP:バージョン7.3.7
- Laravel:バージョン5.8
- OS:Windows10
デフォルトのページネーション
まずはLaravelデフォルトのページネーションから。
①前のページへのリンク
②次のページへのリンク
②番号のページへのリンク
となっています。記述も至って簡単で、ページングしたいページのビュークラスに、
{{ $tests->links() }}と記述するだけ($testsはページング対象のデータの入った変数)。
ちなみに、これは、
\vendor\laravel\framework\src\Illuminate\Pagination\resources\views配下の
default.blade.php
がベースになっています。最初と最後のページに遷移するボタンと、今のページと総ページ数を出現させる
1. カスタムページング用のファイルの作成
\resources\views配下に、\vendor\paginationディレクトリ(フォルダ)を作り、その配下にカスタムページング用のファイルを作成します。
今回は、「pagination_view.blade.php」というファイル名にしました。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) }}">«</a> </li> // 前のページへのリンク {{-- Previous Page Link --}} <li class="page-item {{ $paginator->onFirstPage() ? ' disabled' : '' }}"> <a class="page-link" href="{{ $paginator->previousPageUrl() }}">‹</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> {{ $page }}</span></li> // 現在のページと最後の総ページの間の「/」 / // 総ページ数(=最後のページ) <li class="active" aria-current="page"><span>{{ $paginator->lastPage() }} </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() }}">›</a> </li> // 最後のページへのリンク {{-- Last Page Link --}} <li class="page-item {{ $paginator->currentPage() == $paginator->lastPage() ? ' disabled' : '' }}"> <a class="page-link" href="{{ $paginator->url($paginator->lastPage()) }}">»</a> </li> </ul> @endif3. カスタムページネーション用のファイルの読み込み
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
- 投稿日:2019-12-24T12:09:18+09:00
【Laravel】routeメソッドで生成されるURLを相対パスにする
Laravel 6.0
route
メソッドの第3引数にfalse
を渡すと相対パスを返してくれるらしい。(デフォルトはtrue)route関数はデフォルトとして絶対URLを生成します。相対URLを生成したい場合は、第3引数にfalseを渡してください。
$url = route('routeName', ['id' => 1], false);helpers.phpif (! 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
- 投稿日:2019-12-24T12:07:52+09:00
【Laravel】routeメソッドやリダイレクトで生成されるURLのホスト名を指定する
経緯
Laravel6.0
・Laravelはルートを指定していない場合
$_SERVER
から取得する。・
$_SERVER
の値はサーバーの設定により決まる。
PHP: $_SERVER - Manual・サーバーの設定が複雑で期待する値が取得できなかった。
・リダイレクトやrouteメソッドで生成されるURLに影響がでて正しく遷移できない。
結論
UrlGeneratorにルートURLを設定することで、生成されるURLを固定できる
app/Privider/AppServiceProvider.phppublic function boot() { // ルートURLを設定 Illuminate\Support\Facades\URL::forceRootUrl(\config('app.url')); // 必要に応じてsslを強制する if (\config('app.env') !== 'local') { Illuminate\Support\Facades\URL::forceScheme('https'); } }.envAPP_URL=https://foo.comソースコード(ざっくり)
Illuminate/Routing/UrlGenerator.php
にforcedRoot
を指定することで生成されるURLを変更できる。Illuminate/Routing/UrlGenerator.phppublic 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.phppublic 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('DOCUMENTROOT');// \$_SERVERのインデックスのうち、命名がHTTP_*に該当するもの
// \$_SERVER['HTTP_USER_AGENT']
$request->header->get('user-agent');優先順位的には
- $this->forcedRoot
- $_SERVER['HTTP_HOST']
- $_SERVER['SERVER_NAME']
- $_SERVER['SERVER_ADDR']
になりそう。多分。
参考
- 投稿日:2019-12-24T11:32:19+09:00
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
composer
and install passportdocker 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 theHasApiTokens
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:routes
をAuthServiceProvider
に追加しますNext we add the
passport:routes
to theAuthServiceProvider
<?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:
より良いAPI応答を行うためのライブラリです。
Is a library to make better API responses.
インストールする前に、
docker-compose
を停止し、一時的なcomposer
コンテナを実行します。Before installing it, we stop
docker-compose
and run a temporarycomposer
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" } }
- 投稿日:2019-12-24T10:47:24+09:00
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 は 今回はUserUserview.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はい。並び替わりましたね♪
- 投稿日:2019-12-24T01:26:01+09:00
LaravelでスクレイピングしたデータをSeederに逆生成してみた話
やりたい事
- 某サイトからスクレイピングしてきたデータをDBに保存。
※某サイトからは許可を得ている。- ローカルの開発環境でもある程度、テストデータとして、Seederは用意しておきたい。
- PHPUnitを実行する時にテストデータを作っておきたい。
導入方法
1.fabpot/goutteをインストール
$ composer require fabpot/goutte2.スクレイピングするバッチを作成
<?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が存在せず、自分でデータを取得しなければいけない案件には向いてそう!
参考記事
- 投稿日:2019-12-24T01:19:04+09:00
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/
を作りました。
-Laradock
はgit clone https://github.com/laradock/laradock.git
したものです。
-/project-z/docker-laravel/
はproject-z
でcomposer create-project --prefer-dist laravel/laravel docker-laravel -vvv
したものです。
permission deniedを順番に紹介
LaravelのDB設定のために
/project-z/docker-laravel/.env
を修正しようとするとpermission denied
最初はdocker compose up出来たのに、何ん回目でいきなりnginxのssl周りでpermission denied
permission deniedの原因と解決
LaravelのDB設定のために
/project-z/docker-laravel/.env
を修正しようとするとpermission denied最初はdocker compose up出来たのに、何ん回目でいきなりnginxのssl周りでpermission denied
よくわからないけど
/storage/logs/
周りでpermission deniedよくわからないけど
/storage/framework/views/
周りでpermission denied
- 原因 : Laravelのソースコードの権限をDockerコンテナの中と外で適切に両方を対応してない。
- 解決 : Laradockの
/laradock/.env
にあるWORKSPACE_PUID
とWORKSPACE_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_PUID
とWORKSPACE_PGID
、PHP_FPM_PUID
とPHP_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
- 投稿日:2019-12-24T00:01:08+09:00
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を発行することができました。