20191126のlaravelに関する記事は11件です。

【Laravel】VirtualBox+Vagrantで環境構築

こんにちは!!
PHPの始めることになりました!
使用するフレームワークがLaravelです
VirtualBox+Vagrantを使って環境構築したので
今回はVirtualBox+Vagrantを使ってLaravelの環境構築のやり方についてまとめました!!

やること

ゼロからlaravel-projectという名前のアプリケーションを作ることにします!
内容は、、
まず、VirtualBox+Vagrantを使ってLaravelの環境構築をします。
その後、laravel-projectという名前のアプリケーションを作ります!
それでは始めましょう!!

前提

前提条件が以下になります
ホストOS
  macOS(Mojave)
使用するツール
  virtual box
    仮想環境を作成するツール
  vagrant
    仮想環境を操作するツール

仮想環境を構築しましょう

それでは仮想環境を構築していきましょう

VirtualBox、Vagrantのインストール

下記の公式サイトからVirtualBox、Vagrantダウンロードしてください
ダウンロード後は指示に従いインストールしてください
VirtualBox 公式サイト
  https://www.virtualbox.org/wiki/Downloads
Vagrant 公式サイト
  https://www.vagrantup.com/
インストール終了後、PCを再起動してください

Homesteadを追加します

Vagrant boxのlaravel/homesteadを追加します
仮想マシンのOSの種類を追加するイメージです

ちなみに、laravel/homesteadの他にubuntuやCentOsなどがあります
公式サイトから追加可能なboxを見ることができます
興味ある方は他のboxも追加してみてください!

ターミナル
$ vagrant box add laravel/homestead

追加されたboxはvagrant box listで確認できます

ターミナル 実行結果(例)
$ vagrant box list
laravel/homestead (virtualbox, 8.2.1)   ←これが表示されればOKです!
ubuntu/bionic64   (virtualbox, 20190705.0.0)

Homesteadの設定

Homesteadの取得

Homesteadの設定に必要なファイルをgitで取ってきます
ついでにvagrantで使用する共有ファイル(laravel好きな名前でOK)も作成します
共有ファイルについては後ほど説明します

ターミナル
$ mkdir laravel
$ cd laravel
$ git clone https://github.com/laravel/homestead.git Homestead
$ cd Homestead
$ bash init.sh

Homestead.yamlの編集

Homestead.yamlの一部を下記のように編集します

Homestead.yaml(変更前)
folders:
    - map: ~/Code
      to: /home/vagrant/Code

変更前の意味
  ホストOS上の~/Codeディレクトリを
  Vagrant上で/home/vagrant/Codeディレクトリとして扱う

Homestead.yaml(変更後)
folders:
    - map: ~/Desktop/laravel
      to: /home/vagrant/Code

変更後の意味
  ホストOS上の~/Desktop/laravelディレクトリを
  Vagrant上で/home/vagrant/Codeディレクトリとして扱う

仮想環境の立ち上げ

vagrantを立ち上げ、接続しましょう

ターミナル
$ cd ~/Desktop/laravel/Homestead
$ vagrant up
$ vagrant ssh

接続後、下記が表示されれば完了です

 _                               _                 _ 
| |                             | |               | |
| |__   ___  _ __ ___   ___  ___| |_ ___  __ _  __| |
| '_ \ / _ \| '_ ` _ \ / _ \/ __| __/ _ \/ _` |/ _` |
| | | | (_) | | | | | |  __/\__ \ ||  __/ (_| | (_| |
|_| |_|\___/|_| |_| |_|\___||___/\__\___|\__,_|\__,_|


vagrant上でLaravelのアプリケーションの作成

Laravelのアプリケーションを作っていきましょう

composerをダウンロードする

ターミナル
$ php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
$ php -r "if (hash_file('sha384', 'composer-setup.php') === 'a5c698ffe4b8e849a443b120cd5ba38043260d5c4023dbf93e1558871f1f07f58274fc6f4c93bcfd858c6bd0775cd8d1') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
$ php composer-setup.php
$ php -r "unlink('composer-setup.php');"

composer.pharができれば完了です

composerが導入できているか確認しましょう

ターミナル 実行結果
$ composer -v
   ______
  / ____/___  ____ ___  ____  ____  ________  _____
 / /   / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/
/ /___/ /_/ / / / / / / /_/ / /_/ (__  )  __/ /
\____/\____/_/ /_/ /_/ .___/\____/____/\___/_/
                    /_/
Composer version 1.9.1 2019-11-01 17:20:17

プロジェクトを作成しよう

下記がプロジェクトの作成コマンドです
laravel-projectの部分を好きなアプリ名に変更してください

ターミナル
composer create-project laravel/laravel laravel-project --prefer-dist

サーバーを立ち上げよう

下記がphpサーバーを立ち上げるコマンドです

ターミナル
php artisan serve

サーバーが立ち上がっているか確認しましょう
http://192.168.10.10をクリックして確認してください

以上で、ゼロからlaravel-projectという名前のアプリケーションを作成完了です!!

参考記事

Laravel公式サイト
https://readouble.com/laravel/

mac+VirtualBox+Vagrant with HomesteadでLaravel
https://vamola.info/programming/mac-laravel/

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

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

Ajax通信で403エラーになる

Ajax通信が403エラーで返ってきていた原因の調査にとても時間を取られたので、備忘録として残します。

環境

  • php: 7.1.30
  • Laravel: 5.4.36
  • Webサーバー: Apache 2.4

返ってきていたエラー

Ajaxでデータを更新するフォームで、送信したのにうまく更新されていない
->Laravelのログを見てみる
->ログには何も出ていない
->Chromeのコンソールで確認した結果、以下のエラーが返ってきていました。

PUT https://xxx/yyy/zzz 403 (Forbidden)

Ajax通信が403エラーでうまく動作していません。なぜかアクセス禁止の扱いを受けてしまっています。

調査

  • Apacheのエラーログの確認

Laravelのログに何も出ていない->そこまで処理が来ていないということなので、Apacheのエラーログを確認することにしました。(ここまでに結構な時間がかかってしまいました)

Apacheのエラーログを確認すると、以下のエラーが出ていました。

[Thu Nov 21 19:42:57.735794 2019] [authz_core:error] [pid 22270] [client xx.yyy.zzz.nn:12345] AH01630: client denied by server configuration: /home/xxx/public_html/public/xxxxxx, referer: https://xxx/yyy/zzz

とりあえず

AH01630: client denied by server configuration

のエラー文言で検索してみるも、"アクセス制限の記述方法がApache2.2と2.4で変わっている"系の記事ばかりで、解決に繋がるようなめぼしい情報は見つかりませんでした。
とりあえずApacheの設定をどこか変更するのだろうと、闇雲に設定ファイルの中を覗き回っているうちに、1日が終わってしまいました。


ところで、最初にChromeのコンソールで確認したエラー文言は

  • PUTで送信して403のエラー

というものでした。
もしかしてPUTメソッドが禁止されているのでは??
と自分で思いついたわけではなくヒントをいただいたので、該当箇所を確認することに。

 /etc/httpd/conf.d/userdir.conf

<Directory "/home/*/public_html">
    AllowOverride FileInfo AuthConfig Limit Indexes
    Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec
    Require method GET POST OPTIONS
</Directory>

↓これ

Require method GET POST OPTIONS

PUTは許可されていませんでした。

解決

PUTではなくPOSTを使えばいいということがわかったので、Apacheの設定を変更せずともエラーを解消することができました。

  • ルーティングのメソッド変更
Route::put('/xx/{xxx}/xx/{xxx}', 'SomethingApiController@somethingUpdate')

Route::post('/xx/{xxx}/xx/{xxx}', 'SomethingApiController@somethingUpdate')

  • Ajax時のメソッド変更
type: "PUT",

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

Laravel Adminでmigrate してデータが消えたら

Laravel Adminを使っていて、migrate freshしたら、初期の管理者データが消えてしまいました。
さくっと直せるようにLaravel Adminで使われているSeederを利用して、データを投入できるようにします

Seeder 作成

php artisan make:seeder AdminTablesSeeder 

vendor/encore/laravel-admin/src/Auth/Database/AdminTablesSeeder.phpにLaravelAdminのSeederがあるので中身をコピーし、
ディレクトリ構成が異なるので、インポートを多少書き足します。
そうすると下記のように書き換えられます。

AdminTablesSeeder
<?php

use Illuminate\Database\Seeder;
use Encore\Admin\Auth\Database\Administrator;
use Encore\Admin\Auth\Database\Role;
use Encore\Admin\Auth\Database\Permission;
use Encore\Admin\Auth\Database\Menu;

class AdminTablesSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        // create a user.
        Administrator::truncate();
        Administrator::create([
            'username' => 'admin',
            'password' => bcrypt('admin'),
            'name'     => 'Administrator',
        ]);

        // create a role.
        Role::truncate();
        Role::create([
            'name' => 'Administrator',
            'slug' => 'administrator',
        ]);

        // add role to user.
        Administrator::first()->roles()->save(Role::first());

        //create a permission
        Permission::truncate();
        Permission::insert([
            [
                'name'        => 'All permission',
                'slug'        => '*',
                'http_method' => '',
                'http_path'   => '*',
            ],
            [
                'name'        => 'Dashboard',
                'slug'        => 'dashboard',
                'http_method' => 'GET',
                'http_path'   => '/',
            ],
            [
                'name'        => 'Login',
                'slug'        => 'auth.login',
                'http_method' => '',
                'http_path'   => "/auth/login\r\n/auth/logout",
            ],
            [
                'name'        => 'User setting',
                'slug'        => 'auth.setting',
                'http_method' => 'GET,PUT',
                'http_path'   => '/auth/setting',
            ],
            [
                'name'        => 'Auth management',
                'slug'        => 'auth.management',
                'http_method' => '',
                'http_path'   => "/auth/roles\r\n/auth/permissions\r\n/auth/menu\r\n/auth/logs",
            ],
        ]);

        Role::first()->permissions()->save(Permission::first());

        // add default menus.
        Menu::truncate();
        Menu::insert([
            [
                'parent_id' => 0,
                'order'     => 1,
                'title'     => 'Dashboard',
                'icon'      => 'fa-bar-chart',
                'uri'       => '/',
            ],
            [
                'parent_id' => 0,
                'order'     => 2,
                'title'     => 'Admin',
                'icon'      => 'fa-tasks',
                'uri'       => '',
            ],
            [
                'parent_id' => 2,
                'order'     => 3,
                'title'     => 'Users',
                'icon'      => 'fa-users',
                'uri'       => 'auth/users',
            ],
            [
                'parent_id' => 2,
                'order'     => 4,
                'title'     => 'Roles',
                'icon'      => 'fa-user',
                'uri'       => 'auth/roles',
            ],
            [
                'parent_id' => 2,
                'order'     => 5,
                'title'     => 'Permission',
                'icon'      => 'fa-ban',
                'uri'       => 'auth/permissions',
            ],
            [
                'parent_id' => 2,
                'order'     => 6,
                'title'     => 'Menu',
                'icon'      => 'fa-bars',
                'uri'       => 'auth/menu',
            ],
            [
                'parent_id' => 2,
                'order'     => 7,
                'title'     => 'Operation log',
                'icon'      => 'fa-history',
                'uri'       => 'auth/logs',
            ],
        ]);

        // add role to menu.
        Menu::find(2)->roles()->save(Role::first());
    }
}


seederの呼び出しに追加します

DatabaseSeeder
<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        $this->call([
            AdminTablesSeeder::class,
        ]);
    }
}

Composerのオートローダー再生成

composer dump-autoload

実行

php artisan db:seed
#または
php artisan db:seed --class=AdminTablesSeeder 

参考資料

http://cmz.wp.xdomain.jp/?p=1009

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

【laravel】 DBの存在するテーブルとカラムとカラムの型を取得するあとdoctrine/dbalでjson型が扱えない件

環境

MySQL 5.7
Laravel 5.8

存在するテーブル

tableNames = [];
foreach (DB::select('SHOW TABLES') as $table) {
    $dbName = config('database.connections.mysql.database');
    $tableNames[] = $table->{'Tables_in_' . $dbName};
}

存在するカラムとその型

型を取得

// 存在するカラムを取得
$columns = Schema::connection('mysql')->getColumnListing($tableName);

$columnTypes = [];
foreach ($columns as $column) {
    // カラムタイプを取得
    $columnTypes[$column] = Schema::connection('mysql')->getConnection()->getDoctrineColumn($tableName, $column)->toArray()['type'];
}

int の場合 Doctrine\DBAL\Types\IntegerType のようにとれる。
これはSQLの型とPHPの型をマッピングするクラス。

型判定

if ($type instanceof Doctrine\DBAL\Types\StringType) {
    // ごにょごにょ
}

json型でエラー

Doctrine DBALが2.4以下だとjson型がサポート外とのこと。
エラーが出るので以下のようにjson_arrayにマッピングさせると扱えるようになる。

DB::getDoctrineSchemaManager()->getDatabasePlatform()->registerDoctrineTypeMapping('json', 'json_array');
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

『Laravel + Vue.js』プロジェクト導入前の3つの心得

はじめに

Vue #2 Advent Calendar 2019 の7日目の記事です

この記事は
企業レベルで『プロジェクトとしてVue.jsを導入してみての感想』の共有を行うことが目的です、
そしてその中でも私の感じた『Laravel + Vue.jsプロジェクト導入前の3つの心得』についてご紹介致します。
記事内容に、間違いなどございましたらコメント欄でやさしくリプライしていただければ嬉しいです(^^♪
今回初投稿なので・・・

前提条件

・ウェブフレームワークは既にLaravelを使用
・プロジェクト目的は、速度改善、脱スパゲッティーコード化、新規技術導入などを含む
・具体的なプロジェクト内容は、スマホのウェブページ、コンポーネント化
・Vue.jsの単一コンポーネントの利用(CDNではない)

TL;DR

上記の条件下で私の感じた3つの心得

その1:JSのバンドルサイズ意識しよう
その2:ライブラリ導入の選定は早めに行おう
その3:なんでもVue.jsでやろうとせずスコープを決めよう

では詳しく紹介していきます

心得その1:JSのバンドルサイズを意識してコーディングしよう

単一コンポーネントを利用する際、Laravelでは「laravelmix」と呼ばれるwebpackの進化系みたいなもの(warpper)を利用してアセットのコンパイルを行わないといけないのですが、
この際、バンドルされたJSファイルがすぐに肥大化するので最初から意識してコーディングしてねという話です。

laravemixの設定ファイルはデフォルトでは以下みたいな感じになっています(Laravel 5.x系)

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

/*
 |--------------------------------------------------------------------------
 | 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');

webpack知識の重要性

事前に「laravelmix抜き」の"本来のwebpackでVue.js"を使用する場合は、利用者さんがきちんとwebpackを理解してVue.jsを利用している可能性が高いかもしれませんが
あなたがもし今回初めてVue.jsを利用するVue.js初学者さんでwebpack利用経験がないのであれば、
「laravelmixで利用するwebpack」が別物だということを心得ておく必要があるかもしれません。

実際Laravelで利用するlaravelmixは、webpackへの詳細な知識がなくてもある程度簡単に動作してくれます。
しかしwebpackについて理解なしにwebpackのwrapperのlaravelmixから利用するのは危険です。
あなたがもしwebpackの知識をきちんと理解しているのであればこの設定ファイルの設定見直しだけでもページの速度改善になります。
ちなみにwebpackの設定はlaravelmixより詳細に設定項目があり、より細かに調整ができます。

バンドルサイズ肥大化の原因

webpack.mix.js
mix.js('resources/js/app.js', 'public/js')
    .sass('resources/sass/app.scss', 'public/css');

上記の設定ファイルの中を見てみましょう。
laravelmixでバンドルする対象のファイルの記述が設定されていますね。
(resources/js/app.jsがVue.jsのVueインスタンスファイルにあたります。)
こちらのファイルが1ファイルにバンドルされて、public/js配下にコンパイルされます。(npm run~コマンド実行時)
肥大化する原因は私なりですが以下の2つのいずれかです。

原因1:ライブラリの追加しすぎ
バンドルしたJSファイルはデフォルトのままで既に2万行近くあるので、そのままではとても重いです。
加えて使いたいライブラリなどが追加されれば、追加した分だけバンドルサイズが大きくなっていきます。

原因2:Atomic Designを細かく分けすぎる
今回のプロジェクトでは部品化する際のデザインパターンを「Atomic Design」に則り進めたのですが、部品を細かくしすぎてもあまりよくありません。Atomレベルでさえ1つの.vueファイルで3KBくらいはあるのでコンパイル時にバンドルするJSサイズが肥大化する原因になってしまうのです。よって、再利用性の少ない「Atomレベルは作成しない」などルールを決めてもよいかもしれません。

おまけ:extract設定してライブラリファイル分割する方法

webpack.mix.js
mix
   .js("resources/assets/js/app.js", "public/js") 
   .extract(["vue", "axios", "vuex"])//bootstrap.js内でimportしているライブラリー名を記述
   .version(); 

バンドルで1ファイルになるとお伝えしましたが、laravelmixではextractに記述するだけでライブラリファイル分割可能です。
extractにライブラリ指定してあげれば、指定したライブラリ群がバンドルJSファイルから外にだされ、別ファイルとして(manifest, vendor.js)分割できます。このような設定はlaravelmixのアセットコンパイル公式の各バージョン毎に記述の仕方が書いてあります。Vue.jsをLaravelで書き始める前にぜひご一読ください。

バンドルしたJSファイルサイズ目安

JSのバンドルサイズの目安は一休.comさんのスマホサイトのパフォーマンスについてのブログにも記載がありますが、
"一休"さんのスマホサイトのプロジェクト時では、1ページ300KBを上限にして、それ以下に抑えるようにコーディング意識していたりします。

私のプロジェクトではこのようなルール決めを一切行いませんでした。結果、バンドル後のJSファイルがかなり肥大化してしまい重いスマホサイトが出来上がってしまいました。( ;∀;)

解消策:より具体的な目標を立てる

プロジェクト開始前にメンバー間で、具体的なバンドルサイズ目標をたてることが効果的です。1ページ300KB以下のバンドルJSファイルと残りのリソース700KBまでに抑えるなどより具体的な数値を設定しましょう。
他には、『Googleのpage speed insights(速度計測ツール)で70点以上を獲得する』などでもよいかもしれません。大事なのは達成できなさそうだとしても「意識すること」です。そのための目標の設定は重要かと思います。

ちなみに
グーグル推奨値のリソースの合計サイズが1.6MBとの発表の記事を以前読んだことがあります。これは3G回線のモバイル端末でストレスなくアクセスできる最低の基準です。(実際には1.6MBでもまだ重い気がするので。)1.6MBは全体のリソースサイズなので、この中でJSはさらに小さく抑えるべきです。こういった基準値をプロジェクト前に調べてルール決めを行い認識合わせすることは大事です。
せっかく導入したのなら、ページが重くならないように気を付けましょう。

心得その2:ライブラリ導入の選定は早めにしよう

これもかなり悩ましかったのですが、「とりあえずVue導入して、それからあとのこと考えればいいさ」みたいに考えていたので
Vue.js導入を最優先したのですが(ダメ人間)
導入前のライブラリの選定はものすごく大事です。選定を間違えると、先ほどの話のJSのバンドルサイズの肥大化につながります。

加えて後から導入すると改修がより困難なものなどもあるので早めに検討してから導入したほうが良いでしょう。
ライブラリ選定も設計の一部だと心得てきちんと選定しましょう

初めから導入したかったライブラリ

・vuex(状態管理ライブラリ)
・vue-cli(cli)
・storybook(UIコンポーネント管理系)
・vue-router(ルーティング系)
・SPAも可能ならNuxt.js

中でもvuexは最初に導入しておけば手戻りが発生しなかったかと後悔しています。
propsでの受け渡しが親子孫までの関係のあるモジュールを作成した場合、vuexを導入した方がより簡単に人為的ミスを無くしてスマートにコードを書くことができると感じました。しかしvuexも完璧ではなく、全てvuex化してしまうと逆に重くなってしまったりするので、トレードオフを忘れずに。
vue-cliについてはあればおそらく前準備をコマンドライン入力で簡単にはじめられたことでしょう・・・参考サイト:『これでわかるはじめてのvue-cli』
storybookが初めからあればデザイナーとのコンポーネントのデザイン及び設計についてのやり取りがもっとスムーズになったことでしょう・・・参考サイト:Qiita記事『Storybook for Vue 入門』
vue-routerがあれば遅延ローディングルートを利用して各ルートコンポーネントごとに別々にチャンクして、訪れたルートの時だけロードできるようにルーティングできたでしょう・・・参考サイト:公式リファレンス『Vue Router』

おまけ:一般的にWeb関連の企業で導入されているnode_modulesのライブラリ群

株式会社LINE様のVue.jsの勉強会で行われたUIT#5イベントでは多くの企業が以下のライブラリを導入してVue.jsのWebページなどを実装していました。参考までに共有します。
・vue-router
・vue-cli
・storybook
・vue-whiteroom(storybookに変わるライブラリ)
・Nuxt(SPA化など検討する場合のVue.jsのライブラリ)

イベント情報詳細:UIT#5 わたしたちにとってのVue.js

解消策:プロジェクトスタート前にライブラリ調査・選定

vuexの導入を例に挙げたように使う可能性があるライブラリ群は早めに決めておきましょう。ただし、不要なライブラリは取り除かないとすぐに肥大化します。必要か不必要か今一度メンバーで確認し必要最低限のライブラリ選定を心がけ、同じようなライブラリが存在するか確認、もし存在すれば、より軽いサイズの代用ライブラリを導入しましょう。

又、ライブラリ自体をバンドルJSファイル外で読むこともできるので「バンドルに何を含めるのか」よく考えて選定してください。

心得その3:なんでもVue.jsでやろうとせずスコープを決めよう

今回のVue.jsの導入はLaravelでのVue.js導入だったのですが、プロジェクト開始直後私の頭の中では『新しい技術の良さを他のエンジニアに理解してもらう』ことに比重を置いていたので、Vue.jsをたくさん利用しようと考えていました。広める為にできるだけVue.jsでコーディングしようとしていたのです。その結果、本来Laravel側で事足りるものですらVue化してしまいました。JSバンドルサイズは3万行を超え、SPの初期読み込み時間はtestmysiteで(3Gで)10秒ほども出てしまうほど重いものになりました。JSのファイル数を減らせば読み込みが早くなると思っていた私ですが、1ファイルが非常に長い場合も同様にペナルティーになってしまいます。なんでもVue.jsでやろうとしてしまうと結果ページが重くなることさえありうるのです。(私の設計ミスの可能性もありますが・・・)

プロジェクトが進んでいくにつれ私の作成したVue.jsページはただ単に重い不便なページになっていました。( ;∀;)
ではどうすればよかったのでしょうか・・・

私が今回、具体的に失敗した点は、
Laravelで利用できるインクルードなどを捨てすべてVue.jsのコンポーネントに修正していたところにあります。既存の動いているコードはどんなコードであれ資産です。なんでも改修しようとするのでなく、不便さや不要箇所、エラーやバグがある場合のみ改修すればよかったのかもしれません。
既にLaravelのインクルードで実装されていたヘッダー部分の共通部品までVue化を行い、ほかにもフッターなどのすでにあるモジュールですらすべてVueのコンポーネントにしてしまったのです。プロジェクトの目的にもよるとは思うのですが、パフォーマンスの観点でいうと部品が増えればバンドルしたJSファイルのサイズも比例して大きくなるので動いているモジュールすべてをスコープに含むことは間違っていたかもしれません。
Vue.jsの実装パターンはいろいろあるということを忘れてはいけません。
ページ全体をVue化してSPA化したり、一部品だけをVue化したり。
これはすべてに言える話なのですが『大事なのは何の目的で、そのライブラリ、又はそのフレームワークを利用しているのか念頭に置いてコーディングすること』です

解消策:Vue.js適用範囲のスコープを決める

『どれがLaravel側で行うべきこと』で、『どれがVue.js側で行うべきこと』かということを意識する。これらは、おそらく「心得レベル」の話ではなく、「設計レベル」の話になってしまうかと思われます。しかし、Vue.jsをプロジェクトに導入する際は、適用範囲のスコープを決めて実装することが重要だと感じました。加えて、スコープの妥当性、適用範囲の妥当性についても考えましょう。
せっかくLaravelでVue.jsを使っているのに、Laravelの良さを捨てて何でもかんでもVue化してしまうのはもったいないこと。
Vue化する際は必ず、よりいい方法をトレードオフすることがとても重要だと強く感じました。

結論

その1:バンドルサイズを意識したコーディングを心がけないと重いサイトになる
その2:ライブラリ選定を早めにしないと手戻りが発生する
その3:Vue.jsでなんでもやろうとすると保守しにくいサイトになる

総じて
Vue.jsのプロジェクト導入を検討しているそこのアナタ!簡単に導入できるからと言って侮らないこと!

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

LaravelにESLintとPrettierを導入する時にやったことの全て

はじめに

この記事ではLaravel5.8ESLintPrettierを導入します。

LaravelのMVCモデルでは、Viewの部分は *.blade.phpファイルによって書くことが一般的です。
しかし最近では、JavaScriptの描画技術の幅やページの描画スピードの速さなどを理由として、bladeは完全に捨ててVue.jsにフロントエンドの全てを任せる構成をとるプロジェクトが多いと感じています。

今回、ESLintを導入する対象となったLaravel5.8のプロジェクトは僕がPMをしているプロジェクトです。

このプロジェクトでも、Laravelのbladeはほとんど使用しておらず、画面描画は resourses/js/に置かれているVue.jsによって行っています。

今回の記事の対象者は、LaravelでVue.jsを使っているor使いたい方全てです。

なぜESLintとPrettierを使うべきなのか

「なぜESLintPrettierを併用して使うべきなのか」ということについては、Laravelに限らず、広く議論されていることですので、ここでは触れません。
この議論の結論を言えば、「ESLintだけ導入」あるいは「Prettierだけ導入」というのは諸事情があって微妙だからどちらも使おうね、ということです。

以下ではESLintPrettierを導入するにあたり、僕が感じている利点について触れておきます。

集団開発でコードフォーマットが統一されていないのはかなりしんどい

「何を綺麗と感じるか」は人それぞれです。
しかし、一方で個々の感性が異なるために、しばしば地獄をみることになります。

  • 「あれ?末尾のセミコロンは前全部消したはずなのにまたAさんが足し直してる、、、」
  • 「キャメル?パスカル?命名規則が混在している、、、」
  • 「シングルクォーテーションとダブルクォーテーションが混在している、、、」

これを十人十色などと言ってはいけません。阿鼻叫喚です。
コードのフォーマットや変数の命名規則が理由で議論が始まったりプルリクエストが通らないとしたら、せっかくLaravelを使っているのにあなたの開発スピードは早いとは言えなくなりませんか?

  • フォーマットや変数の命名規則は開発者間で統一する
  • 開発者ごとのフォーマット調整による余計な差分はなくし、コードレビューの可読性をあげる

この重要性を理解しましょう。コードレビューは常にロジックにのみ集中するべきです。

1人で開発するからといってフォーマットが煩雑なのは許されない

僕の価値観では、コードというものは「誰が見ても美しくあるべき」です。あなたがフォーマッターを使わずに書いたコードは、他人が見ても必ず美しいと思えるでしょうか。また、開発者が感じる「美しさ」は誰が保証してくれるのでしょうか?

「ソースコードは1人で扱うから」とは言っても、いつかもう1人のメンバーが加わる場合もあります。
そう言った際に無駄なコストを払わないように、1人だとしてもフォーマットには基準を設けて整形しておくのがマナーだと僕は考えています。

ESlintPrettierは非常に優秀なフォーマッターなので、「ESLintとPrettierが共通のルールによって整形してくれたのだから信じよう」と開発者は思うのです。

いつESLintとPrettierを導入するのか

上記で「ESLintPrettierを導入しなさい」と布教したのですが、僕も最近になってようやくこれらのフォーマッターを導入したのです。

実は、ESLintPrettierを導入するまでにGitHubでのcommitは500を超えており、そこそこの規模のプロジェクトになっていました。ですから、かなりの数の *.js *.vueファイルが存在していました。

導入パターン

ESLintを全てのファイルにかける(=現存する*.jsファイルや*.vueファイルを一気に整形する)場合、非常にコンフリクトが発生しやすくなります。

対象の全てのファイルの全体に対して改行ポイントやスペース、コンマや命名規則、変数宣言や比較演算子などありとあらゆるコードを修正することを想像すれば「コンフリクトが発生しやすい」というのは火を見るより明らかです。そして、そのコンフリクトは激しくなりがちであることも理解できるでしょう。

ESLintを導入するタイミングはおおよそ次のパターンに分類できます。

  1. 今からLaravelをセットアップするあなた
    →後悔はさせないので、いますぐ導入しましょう。最もEasyです。このタイミングで気づいたあなたは偉い!

  2. すでにそこそこの規模になっているあなた
    →プロジェクト全体で大きな変更がないタイミングを伺いましょう。そこそこの努力と忍耐が必要です。

  3. かなりの規模になっていて集団開発が盛んになっているあなた
    →この時点で何かしらのフォーマッターを入れていないのはまずい状況ですが、導入のタイミングも難しいです。大量のコンフリクトを覚悟して慎重にタイミングを見計らいます。開発者全体で打ち合わせなどをする必要があるかもしれません。

僕は2番に該当していたつもりですが、それでも一気にESLintを全てのファイルにかけて本当に血の海を見ました。全ファイルが血を流して倒れました。

それでもなんとかコンフリクトが少なくなるようなタイミングを見計って行えば、ギリギリ耐えます。
あなたのフォーマッターへの想いだけを信じてください。

ESLintとPrettierを導入する

やっと本題です。しかしやることはそんなに多くありません。

今回は、プロジェクトの途中からESLintPrettierを導入します。(上記での2or3番を想定しています。1番でももちろん全く問題なし!)
ここから先はLaravelがどうこうというより、ESLintPrettier自体をよく理解していることが重要です。

パッケージをインストールする

今回必要なパッケージは以下になります。たくさんあります。
以下のパッケージは開発時にしか必要ありませんので --save-devオプションをつけてインストールしてあげます。

$ npm install babel-eslint eslint eslint-config-prettier eslint-config-standard eslint-friendly-formatter eslint-loader eslint-plugin-html eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-node eslint-plugin-prettier eslint-plugin-promise eslint-plugin-standard eslint-plugin-vue laravel-mix-eslint --save-dev

ESLintの設定ファイルを作成する

続いて、.eslintrc.jsファイルを新しく作成します。
このファイルではESLintとPrettierの整形ルールを定めます

$ cd yourLaravelProject
$ vim .eslintrc.js
.eslintrc.js
module.exports = {
  root: true,
  parserOptions: {
    parser: 'babel-eslint',
  },
  env: {
    browser: true,
  },
  extends: [
    'standard',
    'plugin:vue/recommended',
    'plugin:prettier/recommended',
    'prettier/vue',
  ],
  plugins: [
    'vue',
    'prettier', // prettierをESLintと併用します
  ],
  rules: {
    // ESLintが使用する整形ルールのうち、自分がoffにしたいルールなどを指定する
    'vue/no-v-html': 'off', // v-htmlの使用について
    'vue/prop-name-casing': 'off', // Propsの変数の命名規則について
    'no-console': 'off', // console.log()の使用について
    'no-unused-vars': 'off', // 使われていない変数について
    'camelcase': 'off', // camelcaseについて

    // この先はPrettierのルール
    "prettier/prettier": [ 
      "error",
      {
        printWidth: 120,
        tabWidth: 2,
        useTabs: false,
        singleQuote: true,
        trailingComma: 'all',
        bracketSpacing: true,
        arrowParens: 'avoid',
        semi: false,
      },
    ]
  }
}

この設定ファイルは結構厳しめにルールを設けているつもりです。
そもそもせっかくコードフォーマッターを導入するのにルールを緩くしておく必要はないからです。

しかし、もしプロジェクトの途中での導入に際してあまりルールを厳しく入れたくないという場合は、extends:[]の部分を見直すと良いと思います。

このあたりは下記サイトが参考になりました。
(参考)Vue.jsスタイルガイドとeslint-plugin-vue検証ルールのマッピング

またPrettierのルールもあなたの好みです。
Prettierのルールは公式ドキュメントを参考に変更すると良さそうです。
(参考)Options - Prettier

ファイル変更時にESLintを使用する

webpackが上記の整形ルールを使用してファイル変更をwatchしてくれるようにwebpack.mix.jsを編集します。

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

/*
 |--------------------------------------------------------------------------
 | 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.
 |
 */

// ESLintに関する設定(この部分を丸ごと追記するイメージです)
if (!mix.inProduction()) { // 本番環境ではESLintは使用しない
  mix.webpackConfig({
    module: {
      rules: [
        {
          enforce: 'pre',
          exclude: /node_modules/,
          loader: 'eslint-loader',
          test: /\.(js|vue)?$/,
        },
      ],
    },
  })
}

// watchするファイルやポート番号などに関する設定(今回の内容とは関係ありません)
mix
  .js('resources/js/app.js', 'public/js')
  .sass('resources/sass/app.scss', 'public/css')
  .browserSync({ // browserSyncの設定
    files: ['resources/js/**/*', 'resources/sass/**/*', 'resources/views/**/*', 'public/css/**/*'],
    port: 3000,
    ui: {
      port: 3001,
    },
    proxy: 'localhost:8000', //php artisan serveで立ち上げた8000番をProxyする
  })

// 本番環境ではバージョン付けによるキャッシュ対策を施す(今回の内容とは関係ありません)
if (mix.inProduction()) {
  mix.version()
}

これで基本的なセットアップは全て終了です。

$ npm run watch

を唱えることによっていつも通り開発を開始しましょう。コンソールでESLintとPrettierが大量のエラーを吐いてくれるはずです。

実際に動作を確認する

あなたのコンソールを見てください。
例えば次のようになっている場合、

/Users/yourName/Laravel/resources/js/pages/A/B.js
350:1  error  Delete `····`
       prettier/prettier

これはPrettierが怒っている様子です。

このエラーが指し示すところは、
/js/pages/A/にあるファイルB.jsの350行目の1列目から無駄な空白があるから消せ」
ということです。

これらのエラーは地道に解消しても良いですが、あなたの見ているエラーの量を見てください。地獄ですよね。

でも安心してください、自動でこれらのエラーをほとんど解消してくれるESLintのコマンドがあります。

エラーを吐いているファイル名とそのディレクトリを確認して、コンソールで

// ESLintとPrettierの両方を実行する
$ eslint resources/js/pages/*/*.js --fix

を実行しましょう。
ファイル名はresources/js/pages/*/*.jsのようにワイルドカードを使用して指定できます。
これによってresources/js/pages/*/*.js に該当するファイルのエラーが自動で解消されていきます。

eslint ファイル名 --fixコマンドによってESLintとPrettierの両方が実行されることに注意してください
これはすでに上記で導入した eslint-config-prettierの恩恵を受けています。
ESLintとPrettierの併用に関する詳しい説明は以下の記事などがわかりやすいと思います。
(参考)ESLint - Prettier連携のやり方と仕組み

これでLaravelの*.jsファイルや*.vueファイルに対してESlintとPrettierを導入することができました。

どのようなエラーを吐いているかはあなたのプロジェクト次第です。

コンソールでのエラーを見ながら .eslintrc.jsファイルで整形方法を調整しましょう。特定の整形ルールをoffにすることもできますから、あなたの開発チームメンバーにとって最良の設定を記述し、幸せな開発サイクルを目指しましょう。

最後に

この設定を行ったことで、あなたのLaravelの*.jsファイルや*.vueファイルは全ての開発者間でフォーマットが統一されました。

今後の展望としては
1. git commitのときにESLintを自動で強制する
2. *.phpファイルに対しても何かしらのLintを施す

本記事ではこれらの設定はオーバーワークになりますので、別記事で紹介しようと思います。

長くなりましたが、ありがとうございました。

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

実際に運用しているサービスのLaravelのファイル構成を共有してみる。

Laravelを始めるにあたって、どんなファイル構成が正しいのかわからない
実際私たちも全然わからなかったので、今の運用形式を共有してみる。

だれかの助けになったり、指摘をいただけたらとても嬉しいと思います:sushi:

階層毎に h タグの階層をつけてます

app

アプリケーションファイル、基本的なアプリケーションコードはここに入れている
ほかにはKernel.phpという、最初に実行されうファイルがおいてある。

console

コンソールから利用するアプリケーションコードはここに入れている

Command

コンソールから利用するコマンドを入れている
日別実行、バッチファイルなどが入っている

うちのチームではECSのタスクのスケジューリングから
dockerコンテナ内でcronを設定するようにして、これらのファイルを実行している。

image.png

Exceptions

例外処理を入れるためのフォルダだと思われる。
しかし使っていない。
例外のハンドリングができていないのでいずれやりたい。

Helpers

グローバルなメソッドを定義している
どこでも 関数名() でその処理を呼び出せるので便利だが、グローバル変数環境がすごく汚染される
極力使わないようにしている

if (! function_exists('hoge')) {
  /**
    * hogeする
    *
    * @param Eroquent\Collection Hoge
    * @param Integer 取得したい hoge_id
    * @return String 内容
  */
  function hoge($hoge, $piyo_id) {
    return $hoge->hogeSections()->where('piyo_id', $piyo_id)->first();
  }
}

Http

ControllerやMiddlewware、Requests、ViewComposersが入っている。

Controller

いろんなControllerが入ってる

管理画面は /admin 以下にControllerをまとめている
そのほかにも /markdown や /auth などがある。

自由に使い分けて良いと思う。

Middleware

Controllerの前に実行するミドルウェアをたくさん入れているフォルダ

EncryptCookiesでCookieの暗号化
Authenticate、認証ミドルウェア
CheckForMaintenanceModeといったメンテナンス用ページ作成するときに使う
TrimStrings、getパラメータから特定のkeyを削除してくれる(password)など

そのほかに、サイト計測の値を操作したり、CSRFトークンチェックなどを実装して入れている

Requests

postされた値のバリデーションルールを定義しているファイルが配置されている

ViewComposers

viewで利用する共有処理をまとめておける。
グローバルなメソッドは定義せず、極力ここにまとめておきたい。

Jobs

非同期処理を行うことを前提としたコードを入れておく。
railsでいう sidekiq 的なものが内装されている。

Logging

ログフォーマットを独自のものに書き換えたりするときの処理を入れておく。
例えばjson形式で吐き出して、ログをS3に保存して、CloudWatchに送り、Athenaとかで解析するなどの使い道がある。

Mail

メールを送信するときの処理を書いておく

私たちはメール内容をDBに文字列で保存(ビジネスチームが編集するため)
送信処理はこのディレクトリのファイルに記述
送信先や送信タイミングは各Controllerに任せている。

Models

Laravelを学習し始めるとき、多くの開発者はmodelsディレクトリが存在しないことに戸惑います。しかし、意図的にこのディレクトリを用意していません。多くの別々の人達にとって、その意味合いは様々なため、"models"という言葉の定義は曖昧であることに私達は気づきました。

https://readouble.com/laravel/5.5/ja/structure.html

Laravel公式でも議題にしているが、どこに配置するのが良いのか議論した結果、私たちはapp配下に置いている
Rails を使ったサービスが多いので、 Rails と同じ配置ならわかりやすいだろうという判断だ。

私たちはRailsに慣れ親しんでいるので私たちの定義ではここに配置するのが良いだろうという判断なので、正解はないような気がしている。

Controllerと階層を合わせてhttp以下に入れるというのもありだが。
Modelはデータを管理する場所であって、あまりhttpとは関係ないなと思い外に出している。

Providers

正直全然使ったことないので、Laravel公式の文章を持ってくる
https://readouble.com/laravel/5.3/ja/providers.html

Laravelアプリケーション全体の起動処理における、初めの心臓部です。皆さんのアプリケーションと同じく、Laravelのコアサービス全部もサービスプロバイダを利用し、初期起動処理を行っています

色々なプロバイダがデフォルトで配置されているが

私たちのアプリケーションでは、routesファイルたちの読み込み順をRouteServiceProviderで変更している
通常のroutesファイルの処理を行う前に、redirect処理をしたかったからだ。

redirect.phpというファイルを routesディレクトリ以下に新規追加し、そちらを優先的に処理するよう記述している。

また、先ほどのviewComposerで作成したファイルをどのviewファイルで利用できるようにするかを定義し、読み込んでいるComposerServiceProviderというファイルもある。

Services

システム全体で利用する、他システムとの連携部の共通処理をまとめている。

私たちのアプリケーションではAWSへの画像アップロード処理、CSV作成処理、マークダウンをHTMLに変換するときの処理などを共通にしている。

詳しくはこの記事を読むとわかりやすい、おすすめ。
https://qiita.com/nunulk/items/6b7a7bbda17192f6b2f5

Bootstrap

あのデザインフレームワーク!と思う人も多いと思いますが違います。
アプリケーションを実行するときに最初に読み込まれるファイルです。

この子たちを呼び出したりしております。

  • httpのkernel
  • consoleのkernel
  • exceptionsのkernel

この記事がわかりやすいのでおすすめ。

Laravelのリクエスト開始からコントローラにたどり着くまで
https://qiita.com/takyam/items/c2d397dd486c047dcbb1#step-3-bootstrap

Config

基本的にLaravelに初めから入っている、認証機能、メール機能、非同期処理機能などの設定はここで設定が可能です。

便利ですね。

database

DB周りのファイルが入っている

factories

テストコードを実行するときに必要なサンプルデータを作る処理を入れておく

migrations

DB構造の変更を履歴を残して管理してくれているおなじみのディレクトリ。
DB変更のたびにファイルが増える。

seeds

アプリケーションを実行する上で必要となるマスターデータを入れるための処理を記述するファイルを配置する。
全部を記述する必要はないと考え

都道府県データ
性別データ

このようなマスターデータのみseedsでgit管理している。

public

静的なファイルを配置したいときに利用する
faviconを配置したり、robots.txtなどを配置している。

また、Laravelのアプリケーション動作時はこのディレクトリにコンパイルされたファイルたちが配置される。

CSS、JS

SCSSのコンパイル結果がここに保存されていく。
基本的にはgitignoreするべき。

fonts

こちらも同じくコンパイルされたものが入る
管理画面でfont-awesomeを利用しているのでそれはここに入っている。

images

基本的な画像はここに配置している。
フォルダ構成は極力viewファイルと合わせている

resorces

コンパイル前のjs、scss、viewファイルなどフロント周りのファイルが入っている

js

私たちのチームでは生jsを利用。
アプリケーションの形式上、jsを使う部分がほぼないため、フレームワークは導入せず。

フォルダとファイルの規則はシンプルで、viewファイルの構成に合わせることにしている。

生のjsで書いている、無論 jquery も使っていない。
必要十分で非常にやりやすい、チャンスがあればフレームワーク良い感じにしたい。

lang

多言語対応用のファイル、入社直後の中国人エンジニアが英語を日本語に翻訳してくれた。
それ以降特に問題なく使えている、すごい、ありがとう。

下層ディレクトリ

  • en
  • ja
バリデーション周りの和訳設定
    'accepted' => ':attributeを承認してください。',
    'active_url' => ':attributeが有効なURLではありません。',
    'after' => ':attributeには、:dateより後の日付を指定してください。',
    'after_or_equal' => ':attributeには、:date以前の日付を指定してください。',
    'alpha' => ':attributeはアルファベットのみがご利用できます。',

sass

cssはsass形式で書いている。
ディレクトリ構成はviewファイルと1:1対応するようにしている。

それ以外のファイルとしてはこれらを用意している

  • layouts
  • mixins
  • module
  • elements
  • variables

views

viewファイルです、デフォルトのこの配置のまま利用しています。

admin
front
lp

このようなディレクティブだけ切っております。

routes

ルーティング系ファイルですね

  • admin.php
  • web.php
  • redirect.php

おもに、この3つを使ってます。
adminとredirectは独自のファイルですね。

リダイレクトはもちろんngixでやったほうが早いのですが、メンテナンスコストなどを鑑みてこうしております。

storage

今日までこのファイルを認識しておりませんでした。

私たちは独自でS3との連携を行う処理を、Serviceディレクトリ以下に作りましたがこちらに作るのが本来正しいのかもしれません。

https://readouble.com/laravel/5.5/ja/filesystem.html

LaravelはFrank de Jongeさんが作成したありがたいほど素晴らしい、抽象ファイルシステムであるFlysystem PHPパッケージを提供しています
LaravelとFlysystemの統合によりローカルのファイルシステム、Amazon S3、Rackspaceクラウドストレージを操作できる、シンプルなドライバが提供できました。更に素晴らしいことにそれぞれのシステムに対し同じAPIを使用しているため、ストレージをとても簡単に変更できるのです。

tests

テストコードを配置してます。
FeatureテストとUnitテストの2種類ですね。

Featureテストではリクエストテストを最低限カバーし、Unitテストで重要な部分のみテストを書いております。
カバー率を上げていきたいところです。

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

【Laravel開発ノート】ドメインを設定するポイント

1.目的

ローカル環境で「http://192.168.10.10/」→「http://weibo.test」と表示したい。

2.設定のポイント

2.1 「.env」ファイル

APP_DEBUG=true
APP_URL=http://weibo.test

2.2 「homestead.yaml」ファイル

sites:
    - map: weibo.test # <--- ここ!
      to: /home/vagrant/code/Laravel/public 

2.3 「/etc/hosts」ファイル

$ vim /etc/hosts

最後の一行に以下の内容を追加

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

【Laravel】メール送信失敗時のログを残す

Laravel で複数のメールアドレスへの送信処理を書いた時、メール送信が失敗したアドレスの一覧を取得したかったが、ドキュメントには載っていなかったのでメモとして残しておく。

環境

Laravel 6.X

やり方

Mail::failures() で送信失敗メールアドレスが配列で取得できる。(参考
例えば、Mailable クラスの設定後、送信処理をコントローラに書く場合、下記のような感じ。

MailSendController.php
namespace App\Http\Controllers;

use App\Mail\SampleMailableClass;
use Illuminate\Support\Facades\Mail;
use Illuminate\Http\Request;
use Log;

class MailSendController extends Controller
{
    public function send()
    {
        $emails = ['hoge@example.com', 'huga@example.com']
        Mail::to($emails)->send(new SampleMailableClass());
        if (count(Mail::failures()) > 0) {
            Log::channel('slack')->warning(Mail::failures());
        };
    }
}

Slack へのログ投稿の方法は こちらの記事 に詳しく書かれていました。

参考

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

Laravel6でRedisとSSE(Server-Sent Events)を使った簡易チャットを作成してみた

私はLaravel初心者です。基本的にググりながら書いています。よろしくお願いします。:bow:

Laravelで簡単にチャットを作ってみようと思ったのですが、1秒ごとにDBにアクセスするようなのは避けたかったので、Redisでやってみるといいんじゃないかと思い、作成してみたので書かせていただきます。m(_ _)m

Redisを使ってチャットをするというやり方はこちらが参考です。
RedisとServer Sent EventでJavaScriptでチャットを作ってみた:電脳ヒッチハイクガイド:電脳空間カウボーイズZZ(電脳空間カウボーイズ)


このような簡単なチャットを作成します

FQ44bRqI04.gif


自動生成ファイルもありますが、自分が最終的に変更したのは↓です

https://github.com/okumurakengo/laravel-sse-chat/pull/1/files

1. Homesteadで環境構築しました

参考:Laravel Homestead - Laravel - The PHP Framework For Web Artisans

vagrant@homestead:~$ pwd
/home/vagrant

vagrant@homestead:~$ composer create-project --prefer-dist laravel/laravel code #laravelのプロジェクト作成
/* 省略 */

vagrant@homestead:~$ cd code #laravelのルートに移動

/home/vagrant/code/public の内容が読み込まれて、Laravelの最初のページを表示できました。
Laravelのバージョンは6.2です。

Screen Shot 2019-12-08 at 22.13.25.png

2. コントローラー、ビュー作成

2-1. ルートを定義する

web.php に1行追加

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

2-2. コントローラー作成

$ php artisan make:controller ChatController #app/Http/Controllers/ChatController.php が作成される

コントローラーで画面が表示されるように変更します

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

namespace App\Http\Controllers;

use Illuminate\View\View;
use Illuminate\Http\Request;

class ChatController extends Controller
{
    /**
     * チャット画面表示
     * 
     * @return View
     */
    public function index(): View
    {
        return view('chat');
    }
}

2-3. bladeファイル作成

chat.blade.phpを作成

resources/views/chat.blade.php
<!DOCTYPE html>
<meta charset=UTF-8>
<title>Document</title>

<h1>Simple Chat</h1>

これで /chat にアクセスしてhtmlを表示することができます

Screen Shot 2019-12-09 at 23.50.43.png

ここまでの変更分はこのようになりました

https://github.com/okumurakengo/laravel-sse-chat/commit/b4792d091f8279d00a403ea7303b2ede9ddd8f17

3. vuejsを使えるようにする

参考:JavaScript & CSS Scaffolding - Laravel - The PHP Framework For Web Artisans

3-1. vueの足場(雛形)を作成

$ composer require laravel/ui --dev #vueの足場(雛形)を作成するために、laravel/uiをインストールする
$ php artisan ui vue #vueの足場(雛形)を作成

php artisan ui vue 実行後は↓のような差分になりました

https://github.com/okumurakengo/laravel-sse-chat/commit/5c46d127f3fed00b252e32e75c4c8523ed0677ec

3-2. vueのコンポーネントを画面に表示する

自動生成された ExampleComponent.vue を表示してみます
app.jschat.blade.php を変更

resources/js/app.js
  const app = new Vue({
      el: "#app",
+     template: '<example-component />',
  });
resources/views/chat.blade.php
  <!DOCTYPE html>
  <meta charset=UTF-8>
  <title>Document</title>
+ <script src={{ mix('js/app.js') }} defer></script>

  <h1>Simple Chat</h1>
+ <div id=app></div>
$ yarn #ライブラリなどをインストール
$ yarn dev #js、cssをビルドしてpublic/配下に出力。yarn watchとしたら変更を監視してくれる

vueのコンポーネントを画面に表示することができました。

Screen Shot 2019-12-10 at 0.34.33.png

yarn devのビルドした出力結果も含まれていますが、ここで変更したコミットは以下です。
※今回は特に気にしませんでしたが、app.jsなどの自動生成ファイルは.gitignoreに設定しましょう

https://github.com/okumurakengo/laravel-sse-chat/commit/5ee0df386ceb298331d382ba2dafe3bc03afd7a9

4. チャットの画面を作成

app.jsApp.vueを読み込むように変更

resources/js/app.js
  Vue.component('example-component', require('./components/ExampleComponent.vue').default);
+ Vue.component('App', require('./components/App.vue').default);


  const app = new Vue({
      el: "#app",
-     template: '<example-component />',
+     template: '<App />',
  });

resources/js/components/App.vue を作成

resources/js/components/App.vue
<template>
    <div id="app" class="container">
        <div class="flex">
            <div class="users">
                <select v-model="selectUser">
                    <option v-for="user in users">
                        {{ user }}
                    </option>
                </select>
            </div>
            <div class="chat">
            </div>
        </div>
        <form>
            <input>
            <input type="submit" value="送信">
        </form>
    </div>
</template>

<script>
    const users = ['Bob', 'Alice', 'Carol'];
    export default {
        data() {
            return {
                users,
                selectUser: users[0],
            }
        },
    }
</script>

<style lang="scss" scoped>
#app {
    font-family: Verdana;

    &.container {
        width: 500px;
        padding: 10px;
        background: #eee;

        .users {
            width: 30%;
            height: 300px;
            border-right: 1px solid gray;
        }

        .chat {
            width: 70%;
            height: 300px;
            padding: 10px;
            overflow: scroll;

            p {
                margin: 0;
            }
        }
    }

    .flex {
        display: flex;
    }
}
</style>

Screen Shot 2019-12-10 at 23.50.42.png

ここでの変更、※public/js/app.jsyarn devyarn watchで生成されたファイル

https://github.com/okumurakengo/laravel-sse-chat/commit/85f2216270110b2075b26a248c45cf2794812747

5. テーブル、モデル作成

チャットの内容をMySQLにも保存します

5-1. MySQL接続の設定

参考
【Laravel Homestead - Laravel - The PHP Framework For Web Artisans】
https://laravel.com/docs/6.x/homestead#connecting-to-databases

user:homestead
password:secret
でMySQLに接続できるので、.envを変更します

.env
- DB_CONNECTION=mysql
- DB_HOST=127.0.0.1
- DB_PORT=3306
- DB_DATABASE=laravel
- DB_USERNAME=root
- DB_PASSWORD=
+ DB_CONNECTION=mysql
+ DB_HOST=127.0.0.1
+ DB_PORT=3306
+ DB_DATABASE=homestead
+ DB_USERNAME=homestead
+ DB_PASSWORD=secret

5-2. テーブル作成

migrationファイル作成

$ php artisan make:migration create_chat_table --table chat
Created Migration: 2019_12_11_145549_create_chat_table

以下のように変更

database/migrations/2019_12_11_145549_create_chat_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateChatTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('chats', function (Blueprint $table) {
            $table->increments('id');
            $table->string('user');
            $table->string('post');
            $table->timestamps();
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('chats');
    }
}

php artisan migrateを実行する。※チャットのテーブル以外にもデフォルトであるマイグレーションファイルも実行される

$ php artisan migrate
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (0.08 seconds)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table (0.12 seconds)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated:  2019_08_19_000000_create_failed_jobs_table (0.18 seconds)
Migrating: 2019_12_11_145549_create_chat_table
Migrated:  2019_12_11_145549_create_chat_table (0.2 seconds)

chatsテーブルができていればOK

Screen Shot 2019-12-12 at 0.24.38.png

5-3. モデル作成

$ php artisan make:model Chat

生成されたapp/Chat.phpを変更

app/Chat.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Chat extends Model
{
    protected $fillable = ['user', 'post'];
}

ここでの変更のコミットです

https://github.com/okumurakengo/laravel-sse-chat/commit/f8d4b20e68d10b3a7372165df761fef991466b73

6. チャットの登録をする

チャットの入力フォームからサブミットすると、ajaxでapiにデータを送るように変更します

resources/js/components/App.vue
 <template>
     <div id="app" class="container">
         <div class="flex">
             <div class="users">
                 <select v-model="selectUser">
                     <option v-for="user in users">
                         {{ user }}
                     </option>
                 </select>
             </div>
             <div class="chat">
             </div>
         </div>
-        <form>
-            <input>
-            <input type="submit" value="送信">
-        </form>
+        <form @submit.prevent="addPost">
+            <input v-model="textValue">
+            <input type="submit" value="送信">
+        </form>
     </div>
 </template>

 <script>
     const users = ['Bob', 'Alice', 'Carol'];
     export default {
         data() {
             return {
                 users,
                 selectUser: users[0],
+                textValue: '',
             }
         },
+        methods: {
+            async addPost() {
+                if (!this.textValue.trim()) {
+                    return
+                }
+                await axios.post('/api/chat/add', { user: this.selectUser, post: this.textValue })
+                this.textValue = ''
+            },
+        },
     }
 </script>

 // ...

api.phpに追加

routes/api.php
// ...

Route::post('/chat/add', 'Api\\ChatController@add');

Controllersフォルダに新たにApiフォルダを作成し、そこに新しくChatController.phpを作成します

addメソッドで、MySQLとRedisにデータを保存します。

app/Http/Controllers/Api/ChatController.php
<?php

namespace App\Http\Controllers\Api;

use App\Chat;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Redis;

class ChatController extends Controller
{
    /**
     * チャット保存
     * 
     * @param Request $request
     * @return JsonResponse
     */
    public function add(Request $request): JsonResponse
    {
        $chat = Chat::make([
            'user' => $request->get('user'), 
            'post' => $request->get('post'),
        ]);
        $chat->save();
        Redis::set('latest_created_at', $chat->created_at->toDateTimeString());
        return response()->json(['result' => 'ok']);
    }
}

この状態でチャットで送信してみると

Screen Shot 2019-12-13 at 0.41.36.png

データを保存することができました

Screen Shot 2019-12-13 at 0.42.15.png

ここでのコミットです

https://github.com/okumurakengo/laravel-sse-chat/commit/3002bf6db1c1f0fc23b6c4ef1f154e1d8d2a4b27

7. チャットの一覧取得

チャットで別の人が打ち込んだ内容を取得するためにServer Sent Eventsを使って取得します。

Server Sent Events(SSE)とはサーバーからプッシュ通知を受信するためのHTTP接続をしてくれるAPIです。
よくwebsocketと比較されます、websocketはブラウザとサーバーで双方向通信できるが、SSEはサーバーからブラウザへの一方向の通信を行います。

参考 : Server Sent Events using Laravel and Vue

created() でServer Sent Eventsを使ってHTTP接続を開き、接続しっぱなしにしてくれます。

resources/js/components/App.vue
 <template>
     <div id="app" class="container">
         <div class="flex">
             <div class="users">
                 <select v-model="selectUser">
                     <option v-for="user in users">
                         {{ user }}
                     </option>
                 </select>
             </div>
-            <div class="chat">
-            </div>
+            <div class="chat" ref="chat">
+                 <div v-for="({ user, post, created_at }) in posts">
+                    <p><strong>{{ user }}</strong>&nbsp;<small>{{ created_at }}</small></p>
+                    <p>{{ post }}</p>
+                    <hr>
+                </div>
+            </div>
         </div>
         <form @submit.prevent="addPost">
             <input v-model="textValue">
             <input type="submit" value="送信">
         </form>
     </div>
 </template>

 <script>
     const users = ['Bob', 'Alice', 'Carol'];
     export default {
         data() {
             return {
                 users,
                 selectUser: users[0],
                 textValue: '',
+                posts: [],
             }
         },
+        created() {
+            const es = new EventSource('/api/chat/event');
+            es.addEventListener('message', e => {
+                const { posts } = JSON.parse(e.data)
+                if (posts.length) {
+                    this.renderList(posts)
+                }
+            });
+        },
         methods: {
             async addPost() {
                 if (!this.textValue.trim()) {
                     return
                 }
                 await axios.post('/api/chat/add', { user: this.selectUser, post: this.textValue })
                 this.textValue = ''
             },
+            renderList(posts) {
+                this.posts = [...this.posts, ...posts]
+                // 下に追加したのでスクロールする
+                this.$nextTick(() => this.$refs.chat.scrollTop = this.$refs.chat.scrollHeight)
+            },
         },
     }
 </script>

 // ...

api.phpに追加

routes/api.php
  // ...

  Route::post('/chat/add', 'Api\\ChatController@add');
+ Route::get('/chat/event', 'Api\\ChatController@event');

Server Sent EventsでJSONを返すようにします

app/Http/Controllers/Api/ChatController.php
use App\Chat;
use Carbon\Carbon;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Redis;
use Symfony\Component\HttpFoundation\StreamedResponse;

// ...

    /**
     * チャット一覧取得
     * 
     * @return StreamedResponse
     */
    public function event(): StreamedResponse
    {
        // 最近のチャット5件と、一番最近のcreated_atを取得
        $chats = Chat::orderBy('created_at', 'desc')->limit(5)->get()->sortBy('created_at')->values();
        $tmpLatestCreatedAt = optional($chats->last())->created_at ?? Carbon::minValue();
        $response = new StreamedResponse(function() use ($chats, $tmpLatestCreatedAt) {
            printf("data: %s\n\n", json_encode(['posts' => $chats]));
            ob_flush();
            flush();
            while(true) {
                $latestCreatedAt = is_null(Redis::get('latest_created_at')) ? Carbon::minValue() : Carbon::parse(Redis::get('latest_created_at'));
                if ($latestCreatedAt->gt($tmpLatestCreatedAt)) {
                    // チャットに更新があった場合はテーブルから取得
                    $latestChats = Chat::where('created_at', '>', $tmpLatestCreatedAt)->orderBy('created_at', 'asc')->get();
                    $tmpLatestCreatedAt = $latestCreatedAt;
                }
                echo 'data: ' . json_encode(['posts' => $latestChats ?? []]) . "\n\n";
                ob_flush();
                flush();

                $latestChats = null;

                sleep(1);
            }
        });
        $response->headers->set('Content-Type', 'text/event-stream');
        $response->headers->set('X-Accel-Buffering', 'no');
        $response->headers->set('Cach-Control', 'no-cache');
        return $response;
    }

// ...

チャットの内容を更新に合わせてリアルタイムに取得できました。

A6kh3wvL29.gif

ここでの変更です

https://github.com/okumurakengo/laravel-sse-chat/commit/7515b4cd22c67d4a074166b51655688673b66668

8. 「Bobが入力中です」と表示させる

resources/js/components/App.vue
 <template>
     <div id="app" class="container">
         <div class="flex">
             <div class="users">
                 <select v-model="selectUser">
                     <option v-for="user in users">
                         {{ user }}
                     </option>
                 </select>
             </div>
             <div class="chat" ref="chat">
                 <div v-for="({ user, post, created_at }) in posts">
                     <p><strong>{{ user }}</strong>&nbsp;<small>{{ created_at }}</small></p>
                     <p>{{ post }}</p>
                     <hr>
                 </div>
             </div>
         </div>
         <form @submit.prevent="addPost">
-            <input v-model="textValue">
+            <input v-model="textValue" @keyup="typing">
             <input type="submit" value="送信">
+            {{ typingMessage }}
         </form>
     </div>
 </template>

 <script>
     const users = ['Bob', 'Alice', 'Carol'];
     export default {
         data() {
             return {
                 users,
                 selectUser: users[0],
                 textValue: '',
                 posts: [],
+                typingUsers: [],
             }
         },
         created() {
             const es = new EventSource('/api/chat/event');
             es.addEventListener('message', e => {
-                const { posts } = JSON.parse(e.data)
+                const { posts, typing_users: typingUsers = [] } = JSON.parse(e.data)
                 if (posts.length) {
                     this.renderList(posts)
                 }
+                if (!_.isEqual(this.typingUsers, typingUsers)) {
+                    this.typingUsers = typingUsers
+                }
             });
         },
         methods: {
             async addPost() {
                 if (!this.textValue.trim()) {
                     return
                 }
                 await axios.post('/api/chat/add', { user: this.selectUser, post: this.textValue })
                 this.textValue = ''
             },
             renderList(posts) {
                 this.posts = [...this.posts, ...posts]
                 // 下に追加したのでスクロールする
                 this.$nextTick(() => this.$refs.chat.scrollTop = this.$refs.chat.scrollHeight)
             },
+            typing: _.throttle(async function () {
+                await axios.post('/api/chat/typing', { user: this.selectUser })
+            }, 700),
         },
+        computed: {
+            typingMessage() {
+                const typingOtherUsers = this.typingUsers.filter(user => user !== this.selectUser)
+                if (typingOtherUsers.length === 0) {
+                    return ''
+                }
+                if (typingOtherUsers.length === 1) {
+                    return `${typingOtherUsers[0]}が入力しています`
+                }
+                if (typingOtherUsers.length > 1) {
+                    return '複数人が入力しています'
+                }
+            }
+        },
     }
 </script>

// ...

api.phpに追加

routes/api.php
  // ...

  Route::post('/chat/add', 'Api\\ChatController@add');
+ Route::post('/chat/typing', 'Api\\ChatController@typing');
  Route::get('/chat/event', 'Api\\ChatController@event');

入力中のユーザー情報を保存して、配列で返すように変更

app/Http/Controllers/Api/ChatController.php
+   /**
+    * 入力中の人の情報を保存
+    * 
+    * @param Request $request
+    * @return JsonResponse
+    */
+   public function typing(Request $request): JsonResponse
+   {
+       Redis::sadd('typing_users', $request->get('user'));
+       return response()->json(['result' => 'ok']);
+   }

    /**
     * チャット一覧取得
     * 
     * @return StreamedResponse
     */
    public function event(): StreamedResponse
    {
        // 最近のチャット5件と、一番最近のcreated_atを取得
        $chats = Chat::orderBy('created_at', 'desc')->limit(5)->get()->sortBy('created_at')->values();
        $tmpLatestCreatedAt = optional($chats->last())->created_at ?? Carbon::minValue();
        $response = new StreamedResponse(function() use ($chats, $tmpLatestCreatedAt) {
            echo 'data: ' . json_encode(['posts' => $chats]) . "\n\n";
            ob_flush();
            flush();
            while(true) {
                $latestCreatedAt = is_null(Redis::get('latest_created_at')) ? Carbon::minValue() : Carbon::parse(Redis::get('latest_created_at'));
+               $typingUsers = Redis::smembers('typing_users');
                if ($latestCreatedAt->gt($tmpLatestCreatedAt)) {
                    // チャットに更新があった場合はテーブルから取得
                    $latestChats = Chat::where('created_at', '>', $tmpLatestCreatedAt)->orderBy('created_at', 'asc')->get();
                    $tmpLatestCreatedAt = $latestCreatedAt;
                }
-               echo 'data: ' . json_encode(['posts' => $latestChats ?? []]) . "\n\n";
+               echo 'data: ' . json_encode(['posts' => $latestChats ?? [], 'typing_users' => $typingUsers]) . "\n\n";
                ob_flush();
                flush();

                $latestChats = null;
+               Redis::del('typing_users');

                sleep(1);
            }
        });
        $response->headers->set('Content-Type', 'text/event-stream');
        $response->headers->set('X-Accel-Buffering', 'no');
        $response->headers->set('Cach-Control', 'no-cache');
        return $response;
    }

入力中の人の情報を出せることができました

FQ44bRqI04.gif

ここでの変更です

https://github.com/okumurakengo/laravel-sse-chat/commit/4ff8bfeb455b19d22276c50b4689d7ee6db28c25


最後まで見ていただいてありがとうございましたm(_ _)m

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

AWS Coud9でphpMyAdmin画面へアクセスしたときに 404|not foud エラーを解決する方法

Cloud9で開発環境を整えて、時間がたってからなぜかphpMyAdminへアクセスできず、ログイン画面で見れなくなる症状に躓きました。

いくら調べても解決しなかったんですが、もしかしたら稀にいるかもしれないと思って記事を書くことにしました。

解決のために事前チェックその1

サーバーを起動しているか確認

サーバー起動コマンド
$ ^Cec2-user:~/environment/project1 $ php -S $IP:$PORT
PHP 7.3.11 Development Server started at Mon Nov 25 15:22:41 2019
Listening on http://127.0.0.1:8080
Document root is /home/ec2-user/environment/project1
Press Ctrl-C to quit.

下記のコマンドで起動したサーバーは phpMyAdminのページへアクセスしても 404|not foudとなります。

サーバー起動コマンド(1)
$ ec2-user:~/environment/project1  $ php artisan serve
Laravel development server started: http://127.0.0.1:8000
サーバー起動コマンド(2)
$ ec2-user:~/environment/project1  $ php artisan serve --port=8080
Laravel development server started: http://127.0.0.1:8080

事前チェックその2

Laravelのプロジェクト(ディレクトリ)内でmysqld を起動しているか確認

mysqld起動コマンド
$ sudo service mysqld start
ec2-user:~/environment/project1  $ sudo service mysqld start
Starting mysqld:                                           [  OK  ]
mysqldの状態確認コマンド
$ ec2-user:~/environment/project1  $ sudo service mysqld status
mysqld (pid  20873) is running...                              [  OK  ]

running...と表示されていれば、起動中です。

一応、mysqld 停止コマンドも明記しておきます。

mysqldの停止コマンド
$ ec2-user:~/environment/project1  $ sudo service mysqld stop
Stopping mysqld:                                           [  OK  ]

事前チェックその3

phpMyAdminのURLを "https://~~.vfs.cloud9.~~.amazonaws.com/phpMyAdmin/index.php" まで指定している

それでも解決しなかった場合、Cloud9上で作成した他のプロジェクト内でmysqldが起動している可能性があるので、それを停止します。

(アクセス権限に関しても調べて変更してみましたが、結果的に関係ありませんでした。)
linuxコマンドに詳しい方はもうわかると思うので、読み進める必要はないです。

念のためlinux初心者の方のために細かいコマンド実行の様子を記しておきます。

まずはディレクトリの移動

親ディレクトリ移動
$ ec2-user:~/environment/project1  $ cd ../

そこでlsコマンドを実行すると他にもプロジェクトがあることがわかります。

ec2-user:~/environment $ ls
project1  project2  README.md 

project1 のディレクトリから project2 (別のプロジェクト)のディレクトリを移動します。

親ディレクトリ移動
$ ec2-user:~/environment/  $ cd project2

その中で再度、mysqldの状態を確認してみると、、、

mysqldの状態確認コマンド
$ ec2-user:~/environment/project2 $ sudo service mysqld status
mysqld (pid  20873) is running...                              [  OK  ]

動いてる。。。。(このプロジェクト、ずっと触ってなかったのに、、)

それで、もしかしたらと思い停止コマンドを実行して停止し、
project1 の方のディレクトリに移ってから、mysqldを起動してみると

mysqldの停止コマンド
$ ec2-user:~/environment/project2 $ sudo service mysqld stop
Stopping mysqld:                                           [  OK  ]
$ ec2-user:~/environment/  $ cd ../project1
ec2-user:~/environment/project1 $ sudo service mysqld start
Starting mysqld:                                           [  OK  ]

無事表示されました!!

phpmyadmin.png

以上です。 うっかり他のプロジェクトでのmysqldを停止し損ねて、
別プロジェクトで使おうとしたときに起きた症状の対策方法でした。

AWSはまだ触り始めたばかりなので、わからないことだらけで苦戦していますが、
上記の方法以外で解決方法をしっていたり、設定方法をご存知の方がいらっしゃいましたら
ぜひコメントいただけるとありがたいです。

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