20191004のlaravelに関する記事は6件です。

Laravel + Vue.js + Vuetifyでページネーションを実現する

バージョン

Laravel : 5.8
Vue.js : 2.5
Vuetify : 1.5

作る画面

サーバーサイドにLaravel、フロントエンドにVue.js、デザインにVuetifyを使用して検索つきのページネーションを作ります。
データは何でもいいので、本を検索することにしました。
このような画面が出来上がります。
m4p3.gif

booksテーブルはtitle(タイトル)とpublishing_year(発行年)だけのシンプルなテーブルです。
book_table.png

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.js
const 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.js
import "./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.js
import 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.js
import 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.js
import { 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/paginations

resources/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/colors

resources/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

一応これで完成です!めでたし!!

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

Laravel-admin [インストール編]

しばらくlaravel-adminという管理画面作成用ライブラリを使ってみたので少しずつ記事にしていこうということで、まずインストールの手順からまとめていこうと思います。

プロジェクトの作成

Laravelのライブラリということでまず必要なものとなるのがLaravel。
Laravel-adminの公式ページによるとPHP7以降、Laravel5.5以降のものが必要とあるのでバージョンを指定してLaravelでプロジェクトを作成する。

$ composer create-project --prefer-dist "laravel/laravel=5.5.*" プロジェクト名

DBも必要になるので.envファイルも用意する。

.env
DB_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', 
],

するとエラーが表示されなくなる。

参考
laravel-adminインストール手順
Laravel-admin 公式

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

vue + laravel

axiosを使った通信

viewのaxiosを使用してTestsControllerのcreateメソッドに値を渡す。
またTestsControllerのcreateメソッドからviewに値を返し(res.data)viewで受け取っています。

welcome.blade.php
        axios
            .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.php
Route::get('/test', 'TestsController@index');
Route::get('/test/create', 'TestsController@create');
Route::post('/test/create', 'TestsController@create');

バリデーション

1.バリデーション定義を作成

StoreBlogPostは適当な名前です。

cmd
php artisan make:request StoreBlogPost

app\Http\Requestsの中に生成される

  • ルールの追加
StoreBlogPost
public function rules()
    {
                // 必須かつ最大文字数10文字
        return [
            'test' => 'required|max:10',
        ];
    }

参考資料:https://www.slideshare.net/ssuser817ccb/laravel-bladevuejs

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

フォームリクエストでバリデーション前後にデータを加工する

prepareForValidation()

バリデーションするに実行されます。
以下のようにリクエストデータを加工することが出来ます。

protected function prepareForValidation()
{
    $this->replace(['name' => 'Bob'])
}

validationData()

ここでreturnしたものが実際にバリデーションで使用されるデータになります。
元々は以下の通り all() をreturnしてるだけです。

Illuminate/Foundation/Http/FormRequest.php
public 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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel】スマホでのForm入力時に数値入力パッドを使えるようにする

スマホの数字入力時に数値専用パッドを使えるようにする

これはそもそも Laravel 関係なく、HTML5の仕様の方で策定されており。
対応ブラウザ、スマホであれば使うことができる。

実装は非常に簡単、type="tel" を利用する

最終的に出力されたHTMLの type に tel が設定されていれば
スマホの数値入力パッドが利用できる。

<input placeholder="123-4567" size="8" maxlength="8" type="tel" name="zip">

image.png

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" を利用するとこのような入力パッドになる。

image.png

  • フリック入力で数値以外も入力できてしまう。
  • ブラウザごとの挙動が結構違う

という点からあまりお勧めできない、もちろん pattern="[0-9]*" などを指定して入力を制限することもできるが
そこまでするなら普通に tel で良いのではないだろうか。

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

Laravelを使っていたら、EC-CUBE4系とも仲良くなれた

はじめに

先日、松戸市内でLaravelドキュメントを読む勉強会を開催しました。この時の開催レポートです。当日は松戸駅近くのイベントスペースFANCLUBをお借りしましたー。ありがとうございました。
会場

趣旨がちょっと変わったぞ・・・?

Laravelのドキュメントを原文で読むことでLaravelへの理解を深めること英語力を向上させることが目的の一石二鳥イベント。ただ、それだけではなくドキュメントを読んでいくうちに、段々と設計思想の話にもなり、非常に学びの多い会となりました。
特に、私が所属しているJoolenには、EC-CUBEのスペシャリストがおり、その方との会話の中での気づきが大きかったのでこちらを中心に書き残しておきます。

今回、話題にできたテーマは以下の通りです。1

  1. installation
    1. Server Requirements(サーバの要件)
    2. Installing Laravel(インストール方法)
    3. Configuration(設定方法)
  2. Web Server Configuration
    1. Directory Configuration(ディレクトリ設定)
    2. pretty urls(index.phpは隠そうよ、というお話)
  3. Directory Structure(ディレクトリ構成)
  4. Routing(ルーティング設定)

共通点

.envを使っていること

データベースへの接続文字列などは.envファイルに持たせることができます。
本番環境では、.envファイルではなく環境変数から設定を取得できるので、セキュリティの観点から、その様にしましょうという話で盛り上がりました。
もちろん、APP_ENVの指定で、参照する .envファイルを切り替えることができることも同じです。EC-CUBE4系以降でも使えるテクニックです。
切替え方法が参考になる記事

composerを使っていること

共にcomposerを使うことができるので双方ともにパッケージのインストールなどで悩むことはあまりなさそうです。ただ、composer create-projectで雛形を作れることに、EC-CUBE経験者は驚いていました:smile:(パッケージをインストールする以外にも機能があったんだ!)

DI(Dependency Injection)が使える

EC-CUBE4系もLaravelもDIを使うことができます。ただし、Laravelはコンストラクタだけではなくメソッドでもインジェクションをすることができます。テストがしやすくて良いですね:thumbsup:

相違点

まぁ、全く異なるフレームワークなので相違点ばかりなのは当たり前ですが。。。

ルーティング方法

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経験者で意見交換をすることで、思いも寄らない気づきをたくさん得ることができました。参加してくださった方々、本当にありがとうございました。


  1. pretty urls以降は、時間の都合でざっと眺めた程度になっちゃいました:sweat_smile: 

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