- 投稿日:2019-10-04T21:17:54+09:00
Laravel + Vue.js + Vuetifyでページネーションを実現する
バージョン
Laravel : 5.8
Vue.js : 2.5
Vuetify : 1.5作る画面
サーバーサイドにLaravel、フロントエンドにVue.js、デザインにVuetifyを使用して検索つきのページネーションを作ります。
データは何でもいいので、本を検索することにしました。
このような画面が出来上がります。
booksテーブルはtitle(タイトル)とpublishing_year(発行年)だけのシンプルなテーブルです。
Vuetifyとは
簡単に言うとCSSを使わずに、独自のHTMLタグでデザインを完成させてしまえる優れものです。
その他にもVue.jsで使えるマテリアルデザインコンポーネントはQuasarやBootstrapVueもあるみたいですが、何となくVuetifyが良さげな雰囲気を出しているので採用しました。
https://vuetifyjs.com/ja/インストール
まずはVue.jsとVuetifyを使う上で必要なライブラリをインストールします。
前述の通りVuetifyは1系を使います。npm install vue-router vuex vuetify@1 css-loader material-design-icons-iconfont vuex-persistedstateディレクトリ構成
主なファイルの配置です。
LaravelとVue.jsの一般的な構造なので特に問題ないと思います。├── app │ ├── Http │ │ └── Controller │ │ └── BookController.php │ └── Book.php ├── resources │ └── js │ ├── components │ │ ├── Book.vue │ │ ├── BookList.vue │ │ └── SearchArea.vue │ ├── app.js │ ├── bootstrap.js │ ├── router.js │ ├── store.js │ ├── util.js │ └── App.vue └── routes ├── api.php └── web.php余談ですがresources/js/componentsにBook.vueとBookList.vueがあるのが腑に落ちない方はAtomic Designがピッタリです。
https://uxdaystokyo.com/articles/glossary/atomic-design/ルーティング
ここからはコードをどんどん載せていきます。
routes/web.php<?php Route::get('/{any?}', function () { return view('index'); })->where('any', '.+');画面の変化はJavaScriptで行うことになるので、どのURLでも
index.blade.php
を呼びます。resources/views/index.blade.php<!doctype html> <html lang="ja"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>vuetify pagination</title> <script src="{{ mix('js/app.js') }}" defer></script> </head> <body> <div id="app"></div> </body> </html>Laravel Mixでコンパイルしたapp.jsを読み込みます。
VuetifyがあるのでCSSは使いません。webpack.mix.jsconst mix = require("laravel-mix"); mix.js("resources/js/app.js", "public/js") .version();webpack.mix.jsはこれだけです。
ここでもCSSは不要です。routes/api.php<?php Route::get('/books', 'BookController@index')->name('books');apiのルーティングは本を検索するためのものです。
JavaScript
resources/js/app.jsimport "./bootstrap"; import Vue from "vue"; import Vuetify from "vuetify"; Vue.use(Vuetify); import "vuetify/dist/vuetify.min.css"; import "material-design-icons-iconfont/dist/material-design-icons.css"; import router from "./router"; import store from "./store"; import App from "./App.vue"; new Vue({ el: "#app", router, store, components: { App }, template: "<App />" });app.jsにVuetifyを使用するための記載をします。
resources/js/router.jsimport Vue from "vue"; import VueRouter from "vue-router"; import BookList from "./components/BookList.vue"; Vue.use(VueRouter); const routes = [ { path: "/", component: BookList } ]; const router = new VueRouter({ mode: "history", routes }); export default router;router.jsでルートパスにアクセスしたらBookList.vueが呼ばれるように設定します。
resources/js/store.jsimport Vue from "vue"; import Vuex from "vuex"; import createPersistedState from "vuex-persistedstate"; Vue.use(Vuex); const store = new Vuex.Store({ plugins: [createPersistedState()] }); export default store;store.jsのvuex-persistedstateはブラウザをリロードしてもVuexのストアを保持してくれるもので、ログイン認証の永続化でよく使われるみたいです。
今回は特に使いませんが便利なので載せときましょう。resources/js/bootstrap.jsimport { getCookieValue } from "./util"; window.axios = require("axios"); // Ajaxリクエストであることを示すヘッダーを付与する window.axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest"; window.axios.interceptors.request.use(config => { // クッキーからトークンを取り出してヘッダーに添付する config.headers["X-XSRF-TOKEN"] = getCookieValue("XSRF-TOKEN"); return config; }); window.axios.interceptors.response.use( response => response, error => error.response || error );resources/js/util.js/** * クッキーの値を取得する * @param {String} searchKey 検索するキー * @returns {String} キーに対応する値 */ export function getCookieValue(searchKey) { if (typeof searchKey === "undefined") { return ""; } let val = ""; document.cookie.split(";").forEach(cookie => { const [key, value] = cookie.split("="); if (key === searchKey) { return (val = value); } }); return val; }bootstrap.jsはとutil.jsはCSRF対策のためのトークンをクッキーから取り出して、リクエストに含める処理です。
こちらのサイトを参考にさせてもらっています。
https://www.hypertextcandy.com/vue-laravel-tutorial-authentication-part-3/
素晴らしいサイトでかなりお世話になりました!Vue
resources/js/App.vue<template> <v-app> <v-content tag="div"> <v-container fluid> <RouterView /> </v-container> </v-content> </v-app> </template>やっとVuetifyのタグが登場しました。
<v-app>
で囲む必要があります。
公式を見れば便利なタグがたくさんあるので、こちらを参考に。
https://vuetifyjs.com/ja/components/paginationsresources/js/components/BookList.Vue<template> <div> <h3>Book List</h3> <search-area @search="searchBooks($event)"></search-area> <v-layout justify-end> <v-pagination v-model="page" :length="length"></v-pagination> </v-layout> <v-layout wrap> <v-flex sm6 pa-2 v-for="(book, key, index) in books" :key="index"> <book :title="book.title" :publishingYear="book.publishing_year"></book> </v-flex> </v-layout> </div> </template> <script> // Book.vueとSearchArea.vue(検索エリア)を読み込みます。 import Book from "../components/Book.vue"; import SearchArea from "../components/SearchArea.vue"; export default { data() { return { books: [], // 一覧データ page: 1, // 表示中のページ(v-paginationにバインド) length: 0, // ページネーションのリンクの数(v-paginationのprops) urlParams: "" // 検索パラメータ }; }, methods: { // 検索ボタンをクリックしたら呼ばれる async searchBooks(params) { // 検索パラメータをURLに付与してapiを叩く this.urlParams = params; let url = "/api/books?page=" + this.page + "&" + this.urlParams; const response = await axios.get(url); // 戻り値をデータに代入すれば表示が変わってくれます let books = response.data.data; this.books = books; this.length = response.data.last_page; } }, watch: { // ページネーションのリンクをクリックするとpageが変わる。 // pageを監視して、変更されたらsearchBooksを実行 page: function(newPage) { this.searchBooks(this.urlParams); } }, components: { Book, SearchArea } }; </script>BookList.Vueが今回の肝になるファイルで、ページネーションの処理はここで扱います。
pageというVue.jsで保持しているデータがトリガーになっていて、pageが変更されたら本を検索するapiが発火して、booksというデータが変更され、画面の表示が変わるという流れです。
通常のマルチページアプリケーションではページネーションのリンクのURLに直接遷移することが多いので、一番の違いはここではないでしょうか。resources/js/components/Book.Vue<template> <v-card class="indigo lighten-5"> <v-card-title class="pa-1">タイトル:{{title}}</v-card-title> <v-card-text class="pa-1">発行年:{{publishingYear}}</v-card-text> </v-card> </template> <script> export default { props: { title: { type: String, required: true }, publishingYear: { type: Number, required: true } } }; </script>Book.vueはpropsにタイトルの発行年があるだけで簡単です。
<v-card>
のclassは色をつけるためのもで、Vuetifyが提供してくれます。
たくさんあるので、これだけあれば困ることはないでしょう。
https://vuetifyjs.com/ja/styles/colorsresources/js/components/SearchArea.Vue<template> <div> <v-layout wrap> <v-flex sm4 pa-2> <v-text-field v-model="searchForm.title" label="タイトル"></v-text-field> </v-flex> <v-flex sm4 pa-2> <v-text-field v-model="searchForm.publishing_year" label="発行年"></v-text-field> </v-flex> <v-flex pa-2> <v-btn @click="clickHandler">検索</v-btn> </v-flex> </v-layout> </div> </template> <script> const querystring = require("querystring"); export default { data() { return { searchForm: { title: "", publishing_year: "" } }; }, methods: { clickHandler() { let params = querystring.encode(this.searchForm); this.$emit("search", params); } } }; </script>イベント名をsearch、引数をparams(検索パラメータ)にして検索ボタンにクリックイベントを定義しています。
paramsは検索項目に入力されている値をquerystringで文字列に変換してます。
title=A&publishing_year=2018
みたいな文字列になります。PHP
Laravel側で本を検索する処理を作ります。
app/Book.php<?php namespace App; use Illuminate\Database\Eloquent\Model; class Book extends Model { }Modelはあれば良いので中身は空です。
app/Http/Controllers/BookController.php<?php namespace App\Http\Controllers; use App\Book; use Illuminate\Http\Request; class BookController extends Controller { public function index(Request $request) { $per_page = 5; // 1ページあたりの件数 $input = $request->all(); $books = Book::select('id', 'title', 'publishing_year'); if (!empty($input['publishing_year'])) { $books = $books->where('publishing_year', $input['publishing_year']); } if (!empty($input['title'])) { $books = $books->where('title', 'LIKE', "%{$input['title']}%"); } $books = $books->paginate($per_page); return response()->json($books); } }検索してます。それだけですw
一応これで完成です!めでたし!!
- 投稿日:2019-10-04T19:13:08+09:00
Laravel-admin [インストール編]
しばらくlaravel-adminという管理画面作成用ライブラリを使ってみたので少しずつ記事にしていこうということで、まずインストールの手順からまとめていこうと思います。
プロジェクトの作成
Laravelのライブラリということでまず必要なものとなるのがLaravel。
Laravel-adminの公式ページによるとPHP7以降、Laravel5.5以降のものが必要とあるのでバージョンを指定してLaravelでプロジェクトを作成する。$ composer create-project --prefer-dist "laravel/laravel=5.5.*" プロジェクト名DBも必要になるので.envファイルも用意する。
.envDB_DATABASE=DB名 DB_USERNAME=ユーザ名 DB_PASSWORD=パスワードLaravel-admin導入
次のコマンドを実行する。
$ composer require encore/laravel-admin
vendor/encoreの中を見るとLaravel-adminが入っていることが確認できる。
次に下記のコマンドを実行する$ php artisan vendor:publish --provider="Encore\Admin\AdminServiceProvider"Laravel-adminが展開され、configやマイグレーションなどが追加される。
最後に下記のコマンドを実行することで必要な情報がDBに入る。$ php artisan admin:install
あとは
php artisan serve
などをして、http://localhost:8000/admin にアクセスする。
username、password共にadmin
と入れ、Dashboardが表示されれば成功。まとめ
最初からそれなりの画面が表示されているのであとは公式ページにあるクイックスタートの通りに進めていくと簡単に管理画面ができます。
これから少しずつ、スローペースではありますが、laravel-adminで試したことをまとめていきたいと思います。おまけ
ユーザの追加などをしようとするとConfig errorと出る。
追加はできるのですがやはりエラーが出たままなのはいい気分にはなれないので、config/filesystems.phpの一番下、local、public、s3と続いている下に以下を追加する。filesystems.php'admin' => [ 'driver' => 'local', 'root' => public_path('uploads'), 'visibility' => 'public', 'url' => env('APP_URL').'/uploads', ],するとエラーが表示されなくなる。
- 投稿日:2019-10-04T12:07:29+09:00
vue + laravel
axiosを使った通信
viewのaxiosを使用してTestsControllerのcreateメソッドに値を渡す。
またTestsControllerのcreateメソッドからviewに値を返し(res.data)viewで受け取っています。welcome.blade.phpaxios .post('http://localhost/public/test/create', postParam) .then((res) => { console.log(res.data); alert('TestController@createから帰ってきたデータは' + res.data.test); }).catch((ex) => { console.log('failed'); });TestsController<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; class TestsController extends Controller { /** * Display a listing of the resource. * * @return \Illuminate\Http\Response */ public function index() { Log::info('Showing user profile for user: '); return view('welcome'); } /** * Show the form for creating a new resource. * * @return \Illuminate\Http\Response */ public function create(Request $request) { Log::info('Showing user profile for user: '); $data = $request->all(); return $data; } }web.phpRoute::get('/test', 'TestsController@index'); Route::get('/test/create', 'TestsController@create'); Route::post('/test/create', 'TestsController@create');バリデーション
1.バリデーション定義を作成
StoreBlogPostは適当な名前です。
cmdphp artisan make:request StoreBlogPostapp\Http\Requestsの中に生成される
- ルールの追加
StoreBlogPostpublic function rules() { // 必須かつ最大文字数10文字 return [ 'test' => 'required|max:10', ]; }参考資料:https://www.slideshare.net/ssuser817ccb/laravel-bladevuejs
- 投稿日:2019-10-04T11:49:39+09:00
フォームリクエストでバリデーション前後にデータを加工する
prepareForValidation()
バリデーションする前に実行されます。
以下のようにリクエストデータを加工することが出来ます。protected function prepareForValidation() { $this->replace(['name' => 'Bob']) }validationData()
ここでreturnしたものが実際にバリデーションで使用されるデータになります。
元々は以下の通りall()
をreturnしてるだけです。Illuminate/Foundation/Http/FormRequest.phppublic function validationData() { return $this->all(); }なので、returnするデータを加工すればその値でバリデーションされます。
public function validationData(){ $data = $this->all(); $data['name'] = 'Bob'; return $data; }passedValidation()
5.8.33から追加。バリデーションに成功した後に実行されます。
以下のようにリクエストデータを加工することが出来ます。protected function passedValidation() { $this->replace(['name' => 'Bob']) }値のまとめ
場所 prepareForValidation() validationData() passedValidation() 入力 Alice Alice Alice バリデーション前 Bob Alice Alice バリデーション Bob Bob Alice バリデーション後 Bob Alice Bob
- 投稿日:2019-10-04T11:18:33+09:00
【Laravel】スマホでのForm入力時に数値入力パッドを使えるようにする
スマホの数字入力時に数値専用パッドを使えるようにする
これはそもそも Laravel 関係なく、HTML5の仕様の方で策定されており。
対応ブラウザ、スマホであれば使うことができる。実装は非常に簡単、type="tel" を利用する
最終的に出力されたHTMLの type に tel が設定されていれば
スマホの数値入力パッドが利用できる。<input placeholder="123-4567" size="8" maxlength="8" type="tel" name="zip">Laravelでの実装方法
Laravelの Form Facade を利用する場合 '第一引数' に type 属性を指定することができる。
{{ Form::input('tel', 'zip', null, ['placeholder' => '1020082', 'size' => 8, 'maxlength' => 8]) }}Form::textの引数に type="tel" と指定しても HTMLに吐き出される時に type="text" に書き換えられてしまうので注意だ。
numberを使うのか、telを使うのか
郵便番号の入力に
tel
を使うのは流石に違和感があると思う方も多い
しかし type="number" を利用するとこのような入力パッドになる。
- フリック入力で数値以外も入力できてしまう。
- ブラウザごとの挙動が結構違う
という点からあまりお勧めできない、もちろん pattern="[0-9]*" などを指定して入力を制限することもできるが
そこまでするなら普通に tel で良いのではないだろうか。
- 投稿日:2019-10-04T09:19:01+09:00
Laravelを使っていたら、EC-CUBE4系とも仲良くなれた
はじめに
先日、松戸市内で
Laravel
ドキュメントを読む勉強会を開催しました。この時の開催レポートです。当日は松戸駅近くのイベントスペースFANCLUBをお借りしましたー。ありがとうございました。
趣旨がちょっと変わったぞ・・・?
Laravelのドキュメントを原文で読むことで
Laravel
への理解を深めること英語力を向上させることが目的の一石二鳥イベント。ただ、それだけではなくドキュメントを読んでいくうちに、段々と設計思想の話にもなり、非常に学びの多い会となりました。
特に、私が所属しているJoolenには、EC-CUBEのスペシャリストがおり、その方との会話の中での気づきが大きかったのでこちらを中心に書き残しておきます。今回、話題にできたテーマは以下の通りです。1
共通点
.env
を使っていることデータベースへの接続文字列などは
.env
ファイルに持たせることができます。
本番環境では、.env
ファイルではなく環境変数から設定を取得できるので、セキュリティの観点から、その様にしましょうという話で盛り上がりました。
もちろん、APP_ENV
の指定で、参照する.env
ファイルを切り替えることができることも同じです。EC-CUBE4系以降でも使えるテクニックです。
切替え方法が参考になる記事composerを使っていること
共に
composer
を使うことができるので双方ともにパッケージのインストールなどで悩むことはあまりなさそうです。ただ、composer create-project
で雛形を作れることに、EC-CUBE経験者は驚いていました(パッケージをインストールする以外にも機能があったんだ!)DI(Dependency Injection)が使える
EC-CUBE4系もLaravelもDIを使うことができます。ただし、Laravelはコンストラクタだけではなくメソッドでもインジェクションをすることができます。テストがしやすくて良いですね
相違点
まぁ、全く異なるフレームワークなので相違点ばかりなのは当たり前ですが。。。
ルーティング方法
EC-CUBE4系では、
Controller
のアノテーションでルーティングや返すテンプレートを定義します。
EC-CUBEのController
(抜粋)/** * 会員登録画面. * * @Route("/entry", name="entry") * @Template("Entry/index.twig") */ public function index(Request $request) { ... }一方、Laravelではroutes配下の
web.php
など、Routing
は独立しています。<?php /* |-------------------------------------------------------------------------- | Web Routes |-------------------------------------------------------------------------- | | Here is where you can register web routes for your application. These | routes are loaded by the RouteServiceProvider within a group which | contains the "web" middleware group. Now create something great! | */ Route::get('/', function () { return view('welcome'); })->middleware('auth');コレは双方にとって、少し新鮮だった様です。ちなみに、
Laravel
のルーティングファイルは分割することができます。(質問された)<?php namespace App\Providers; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; use Illuminate\Support\Facades\Route; class RouteServiceProvider extends ServiceProvider { /* * This namespace is applied to your controller routes. * * In addition, it is set as the URL generator's root namespace. * * @var string */ protected $namespace = 'App\Http\Controllers'; /* * Define your route model bindings, pattern filters, etc. * * @return void */ public function boot() { // parent::boot(); } /** * Define the routes for the application. * * @return void */ public function map() { $this->mapApiRoutes(); $this->mapWebRoutes(); // } /* * Define the "web" routes for the application. * * These routes all receive session state, CSRF protection, etc. * * @return void */ protected function mapWebRoutes() { Route::middleware('web') ->namespace($this->namespace) ->group(base_path('routes/web.php')); } /* * Define the "api" routes for the application. * * These routes are typically stateless. * * @return void */ protected function mapApiRoutes() { Route::prefix('api') ->middleware('api') ->namespace($this->namespace) ->group(base_path('routes/api.php')); } }ディレクトリ定義の自由さ
よく言われることですが、
Models
ディレクトリが無いことに驚かれました。ただ、一方でこれはLaravel
を利用する技術者が自らのベストプラクティスを適用できるという意味にもなります。逆にイケてない設計をすると、あとあと苦労するという噂もありますが。。。
EC-CUBEではいわゆる、リポジトリパターンをきっちり採用していますのでLaravel
側でも同じ様な設計をすることで、双方の人材交流はやりやすくなるかなー、と思いました。
Laravelでリポジトリパターン
まぁ、基本的にはその企業やチームの文化やスキルセットに合わせた設計で良いか、というオチでしたが。。。まとめ
EC-CUBE経験者とLaravel経験者で意見交換をすることで、思いも寄らない気づきをたくさん得ることができました。参加してくださった方々、本当にありがとうございました。
pretty urls以降は、時間の都合でざっと眺めた程度になっちゃいました ↩