#概要
Laravelを初めて触ってみたので、簡単なチュートリアルを参考にしながら、メール通知付き問い合わせフォーム機能を作ってみた!
#前準備
##日本語化
###日本語の言語ファイルを追加
https://github.com/caouecs/Laravel-lang/tree/master/src/ja のファイルを resources/lang の下に追加。
###localeをjaに変更
// 'locale' => 'en',
'locale' => 'ja',
###timezoneをAsia/Tokyoに変更
// 'timezone' => 'UTC',
'timezone' => 'Asia/Tokyo',
##Laravel Collectiveの設定
###インストール
$ composer require "laravelcollective/html":"^5.4.0"
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
- Installing laravelcollective/html (v5.4.8): Downloading (100%)
Writing lock file
Generating optimized autoload files
> Illuminate\Foundation\ComposerScripts::postUpdate
> php artisan optimize
Generating optimized class loader
The compiled services file has been removed.
###設定
<?php
namespace App\Services\Html;
class HtmlBuilder extends \Collective\Html\HtmlBuilder
{
//
}
<?php
namespace App\Services\Html;
class FormBuilder extends \Collective\Html\FormBuilder
{
/**
* Get the action for a "route" option.
*
* @param array|string $options
*
* @return string
*/
protected function getRouteAction($options)
{
if (is_array($options)) {
return call_user_func_array([$this->url, 'route'], $options);
}
return $this->url->route($options);
}
}
<?php
namespace App\Services\Html;
use Collective\Html\HtmlServiceProvider;
class ServiceProvider extends HtmlServiceProvider
{
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->registerHtmlBuilder();
$this->registerFormBuilder();
$this->app->alias('html', HtmlBuilder::class);
$this->app->alias('form', FormBuilder::class);
}
/**
* Register the HTML builder instance.
*
* @return void
*/
protected function registerHtmlBuilder()
{
$this->app->singleton('html', function ($app) {
return new HtmlBuilder($app['url'], $app['view']);
});
}
/**
* Register the form builder instance.
*
* @return void
*/
protected function registerFormBuilder()
{
$this->app->singleton('form', function ($app) {
$form = new FormBuilder($app['html'], $app['url'], $app['view'], $app['session.store']->getToken());
return $form->setSessionStore($app['session.store']);
});
}
/**
* Get the services provided by the provider.
*
* @return array
*/
public function provides()
{
return [
'html',
'form',
HtmlBuilder::class,
FormBuilder::class,
];
}
}
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
+ App\Services\Html\ServiceProvider::class,
'URL' => Illuminate\Support\Facades\URL::class,
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,
+ 'Form' => Collective\Html\FormFacade::class,
+ 'Html' => Collective\Html\HtmlFacade::class,
###インストール
$ composer require prettus/l5-repository
Using version ^2.6 for prettus/l5-repository
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
- Installing prettus/laravel-validation (1.1.4): Downloading (100%)
- Installing prettus/l5-repository (2.6.20): Downloading (100%)
prettus/l5-repository suggests installing league/fractal (Required to use the Fractal Presenter (0.12.*).)
prettus/l5-repository suggests installing robclancy/presenter (Required to use the Presenter Model (1.3.*))
Writing lock file
Generating optimized autoload files
> Illuminate\Foundation\ComposerScripts::postUpdate
> php artisan optimize
Generating optimized class loader
The compiled services file has been removed.
$ composer require prettus/laravel-validation
Using version ^1.1 for prettus/laravel-validation
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Nothing to install or update
Writing lock file
Generating optimized autoload files
> Illuminate\Foundation\ComposerScripts::postUpdate
> php artisan optimize
Generating optimized class loader
The compiled services file has been removed.
$ composer require league/fractal
Using version ^0.16.0 for league/fractal
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
- Installing league/fractal (0.16.0): Downloading (100%)
league/fractal suggests installing pagerfanta/pagerfanta (Pagerfanta Paginator)
league/fractal suggests installing zendframework/zend-paginator (Zend Framework Paginator)
Writing lock file
Generating optimized autoload files
> Illuminate\Foundation\ComposerScripts::postUpdate
> php artisan optimize
Generating optimized class loader
The compiled services file has been removed.
###設定
Prettus\Repository\Providers\RepositoryServiceProvider::classはprovidersの最後に追加しないとおかしな動作をするらしい。
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
+ App\Providers\RepositoryServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Services\Html\ServiceProvider::class,
+ Prettus\Repository\Providers\RepositoryServiceProvider::class,
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class RepositoryServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* @return void
*/
public function boot()
{
//
}
/**
* Register the application services.
*
* @return void
*/
public function register()
{
$repositories = [
// \App\Repositories\UserRepository::class,
];
foreach ($repositories as $repository) {
$this->app->bind($repository, $repository.'Eloquent');
}
}
}
$ php artisan vendor:publish
Copied Directory [/vendor/laravel/framework/src/Illuminate/Notifications/resources/views] To [/resources/views/vendor/notifications]
Copied Directory [/vendor/laravel/framework/src/Illuminate/Pagination/resources/views] To [/resources/views/vendor/pagination]
Copied File [/vendor/prettus/l5-repository/src/resources/config/repository.php] To [/config/repository.php]
Copied Directory [/vendor/laravel/framework/src/Illuminate/Mail/resources/views] To [/resources/views/vendor/mail]
Publishing complete.
#雛形作成
artisanで雛形を作成してみる。
php artisan make:entity Contact
に関しては適当にyes/noしてしまったので、あとで見直したい。
$ php artisan make:controller --resource ContactController
Controller created successfully.
$ php artisan make:request ContactRequest
Request created successfully.
$ php artisan make:entity Contact
Would you like to create a Presenter? [y|N] (yes/no) [no]:
> no
Would you like to create a Validator? [y|N] (yes/no) [no]:
> N
Would you like to create a Controller? [y|N] (yes/no) [no]:
> y
Request created successfully.
Request created successfully.
Controller created successfully.
Repository created successfully.
Bindings created successfully.
$ php artisan make:migration create_contacts_table
Created Migration: 2017_08_17_160250_create_contacts_table
#Database Migration
マイグレーションファイルを一部修正する。
<?php
use Illuminate\Database\Migrations\Migration;
class CreateContactsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('contacts', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('email');
$table->string('subject');
$table->text('content');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('contacts');
}
}
$ php artisan migrate
Migrating: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_000000_create_users_table
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated: 2014_10_12_100000_create_password_resets_table
Migrating: 2017_08_17_155444_create_contacts_table
Migrated: 2017_08_17_155444_create_contacts_table
作成したDBを確認してみる。
$ mysql
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| homestead |
| mysql |
| performance_schema |
| sys |
+--------------------+
5 rows in set (0.01 sec)
show tables;
+---------------------+
| Tables_in_homestead |
+---------------------+
| contacts |
| migrations |
| password_resets |
| users |
+---------------------+
4 rows in set (0.01 sec)
mysql> DESCRIBE contacts;
+------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| name | varchar(255) | NO | | NULL | |
| email | varchar(255) | NO | | NULL | |
| subject | varchar(255) | NO | | NULL | |
| content | text | NO | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+------------+------------------+------+-----+---------+----------------+
7 rows in set (0.00 sec)
どうやら設定として必要らしいので設定をしておく。
<?php
namespace App\Entities;
use Illuminate\Database\Eloquent\Model;
use Prettus\Repository\Contracts\Transformable;
use Prettus\Repository\Traits\TransformableTrait;
class Contact extends Model implements Transformable
{
use TransformableTrait;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name',
'email',
'subject',
'content',
];
}
#ルーティング
indexとstoreを使うためにonlyで指定をかけている。
<?php
Route::resource('contacts', 'ContactController', ['only' => ['index', 'store']]);
#バリデーション
Form Request Validationを使うとバリデーションがコントローラから切り出せるのですっきりする。
###お問い合わせ画面のRequest
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ContactRequest extends FormRequest
{
use ConfirmRequestTrait;
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
// return false;
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required',
'email' => 'required|email',
'subject' => 'required',
'content' => 'required',
];
}
/**
* Set custom messages for validator errors.
*
* @return array
*/
public function messages()
{
return [
//
];
}
/**
* Set custom attributes for validator errors.
*
* @return array
*/
public function attributes()
{
return [
'name' => 'お名前',
'email' => 'メールアドレス',
'subject' => '件名',
'content' => '内容',
];
}
}
use ConfirmRequestTraitを追加することで、確認画面付きのフォームになる。authorizeメソッドの返り値をtrueにしておかないと、403エラーになるので注意。
###確認画面の共通処理
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\Validator;
trait ConfirmRequestTrait
{
/**
* Set custom messages for validator errors.
*
* @param \Illuminate\Contracts\Validation\Factory $factory
*
* @return \Illuminate\Contracts\Validation\Validator
*/
public function validator($factory)
{
// 値検証前の処理
if (method_exists($this, 'beforeValidate')) {
$this->beforeValidate();
}
// 確認画面用フラグのバリデーションを追加
$rules = array_merge($this->rules(), [
'confirming' => 'required|accepted',
]);
$validator = $factory->make(
$this->all(),
$rules,
$this->messages(),
$this->attributes()
);
$validator->after(function ($validator) {
$failed = $validator->failed();
// 確認画面用フラグのバリデーションを除外
unset($failed['confirming']);
// 確認画面用フラグ以外にエラーが無い場合は確認画面を表示
if (count($failed) === 0) {
$this->merge(['confirming' => 'true']);
}
// 値検証後の処理
if (method_exists($this, 'afterValidate')) {
$this->afterValidate($validator);
}
});
return $validator;
}
/**
* Format the errors from the given Validator instance.
*
* @param \Illuminate\Contracts\Validation\Validator $validator
*
* @return array
*/
protected function formatErrors(Validator $validator)
{
$errors = parent::formatErrors($validator);
// 確認画面用フラグのエラーメッセージを削除
unset($errors['confirming']);
return $errors;
}
}
#Repository Service Provider
make:entityで生成されたContactRepositoryを登録する。
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class RepositoryServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* @return void
*/
public function boot()
{
//
}
/**
* Register the application services.
*
* @return void
*/
public function register()
{
$repositories = [
\App\Repositories\ContactRepository::class,
];
foreach ($repositories as $repository) {
$this->app->bind($repository, $repository.'Eloquent');
}
}
}
#Blade Templates
###共通部分
<!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">
<title>@yield('title') - App Name</title>
@section('styles')
<link rel="stylesheet" href="{{ elixir('css/app.css') }}">
@show
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body class="@yield('body-class')">
<div class="container">
@yield('content')
</div>
@section('scripts')
<script src="{{ elixir('js/main.js') }}"></script>
@show
</body>
</html>
###フォーム画面
@extends('layouts.master')
@section('title', 'お問い合わせ')
@section('content')
@if(count($errors) > 0)
<div class="alert alert-danger">
<ul>
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<div class="panel panel-default">
<div class="panel-heading">
<h1 class="panel-title">お問い合わせ</h1>
</div>
<div class="panel-body">
{!! Form::open(['route' => ['contacts.store'], 'method' => 'post']) !!}
<input type="hidden" name="confirming" value="{{ old('confirming', 'false') }}">
<div class="form-group required {{ $errors->has('name') ? 'has-error' : '' }}">
<label class="control-label" for="name">お名前</label>
@if(old('confirming', 'false') === 'false')
<input type="text" class="form-control" name="name" value="{{ old('name') }}">
@else
<p class="form-control-static">{{ old('name') }}</p>
<input type="hidden" name="name" value="{{ old('name') }}">
@endif
@if($errors->has('name'))
<p class="help-block">{{ $errors->first('name') }}</p>
@endif
</div>
<div class="form-group required {{ $errors->has('email') ? 'has-error' : '' }}">
<label class="control-label" for="email">メールアドレス</label>
@if(old('confirming', 'false') === 'false')
<input type="email" class="form-control" name="email" value="{{ old('email') }}">
@else
<p class="form-control-static">{{ old('email') }}</p>
<input type="hidden" name="email" value="{{ old('email') }}">
@endif
@if($errors->has('email'))
<p class="help-block">{{ $errors->first('email') }}</p>
@endif
</div>
<div class="form-group required {{ $errors->has('subject') ? 'has-error' : '' }}">
<label class="control-label" for="subject">件名</label>
@if(old('confirming', 'false') === 'false')
<input type="text" class="form-control" name="subject" value="{{ old('subject') }}">
@else
<p class="form-control-static">{{ old('subject') }}</p>
<input type="hidden" name="subject" value="{{ old('subject') }}">
@endif
@if($errors->has('subject'))
<p class="help-block">{{ $errors->first('subject') }}</p>
@endif
</div>
<div class="form-group required {{ $errors->has('content') ? 'has-error' : '' }}">
<label class="control-label" for="content">内容</label>
@if(old('confirming', 'false') === 'false')
<textarea type="text" class="form-control" name="content" rows="10">{{
old('content')
}}</textarea>
@else
<p class="form-control-static">{!! nl2br(e(old('content'))) !!}</p>
<input type="hidden" name="content" value="{{ old('content') }}">
@endif
@if($errors->has('content'))
<p class="help-block">{{ $errors->first('content') }}</p>
@endif
</div>
<div class="form-group text-center">
@if(old('confirming', 'false') === 'false')
<button type="submit" class="btn btn-primary">確認</button>
@else
<button type="submit" name="action" value="post" class="btn btn-primary">送信</button>
<button type="submit" name="action" value="back" class="btn btn-default">戻る</button>
@endif
</div>
{!! Form::close() !!}
</div>
</div>
@endsection
###投稿完了画面
@extends('layouts.master')
@section('title', 'お問い合わせ')
@section('content')
<div class="panel panel-default">
<div class="panel-heading">
<h1 class="panel-title">お問い合わせ</h1>
</div>
<div class="panel-body">
<div class="well well-lg text-center">お問い合わせありがとうございました</div>
</div>
</div>
@endsection
#コントローラ
storeメソッドの引数にContactRequestを指定するとForm Request Validationで値の検証ができるので、そのままデータベースに突っ込んでもOK。
<?php
namespace App\Http\Controllers;
use App\Http\Requests\ContactRequest;
use App\Repositories\ContactRepository;
class ContactController extends Controller
{
/**
* @var ContactRepository
*/
protected $contacts;
/**
* コンストラクタ
*
* @param ContactRepository $contacts
*/
public function __construct(ContactRepository $contacts)
{
$this->contacts = $contacts;
}
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('contacts.index');
}
/**
* Store a newly created resource in storage.
*
* @param ContactRequest $request
*
* @return \Illuminate\Http\Response
*/
public function store(ContactRequest $request)
{
// 確認画面で戻るボタンが押された場合
if ($request->get('action') === 'back') {
// 入力画面へ戻る
return redirect()
->route('contacts.index')
->withInput($request->except(['action', 'confirming']));
}
// データベースに登録
$this->contacts->create($request->all());
// ブラウザリロード等での二重送信防止
$request->session()->regenerateToken();
// 完了画面を表示
return view('contacts.thanks');
}
}
###ルーティングのエラー
作成したものを確認するために一度http://homestead.app/contacts
にアクセスしてみる。そんなページないぞと怒られる。
laravel5.2を題材にしたチュートリアルを参照したため、5.4でrouteを設定するファイルの場所が違った。下記の場所に書いてみる。
<?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');
});
Route::resource('contacts', 'ContactController', ['only' => ['index', 'store']]);
###ContactRepositoryを読み込めてないエラー
違うエラーに遭遇した。
app/Providers/RepositoryServiceProvider.php
でContactRepositoryをうまく呼び出せていなかった。
public function register()
{
$repositories = [
// \App\Repositories\UserRepository::class,
\App\Repositories\ContactRepository::class,
];
foreach ($repositories as $repository) {
$this->app->bind($repository, $repository.'Eloquent');
}
}
###Call to undefined method Illuminate\Session\Store::getToken()
エラー内容が変わる。contacts/index.blade.phpで定義されてないメソッドが呼ばれていると怒られる。
これも5.2から5.4へのUpgradeの関係みたい。getToken()をtoken()に変更する。
Call to undefined method Illuminate\Session\Store::getToken()
protected function registerFormBuilder()
{
$this->app->singleton('form', function ($app) {
$form = new FormBuilder($app['html'], $app['url'], $app['view'], $app['session.store']->token());
return $form->setSessionStore($app['session.store']);
});
}
###js/main.jsがないと怒られる
main.jsを読み込んでいる部分をコメントアウトする。そもそもLaravel Elixirを入れてないので、エラーが出て当たり前。
<!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">
<title>@yield('title') - App Name</title>
@section('styles')
<link rel="stylesheet" href="{{ elixir('css/app.css') }}">
@show
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body class="@yield('body-class')">
<div class="container">
@yield('content')
</div>
@section('scripts')
{{-- <script src="{{ elixir('js/main.js') }}"></script> --}}
@show
</body>
</html>
表示された!
###お問い合わせページ
###確認画面から戻った画面
#データが入っているか確認
確かに先ほど入れたデータが入っている!
mysql> select * from contacts;
+----+------+---------------+---------+---------------+---------------------+---------------------+
| id | name | email | subject | content | created_at | updated_at |
+----+------+---------------+---------+---------------+---------------------+---------------------+
| 1 | test | test@test.com | test | testです。 | 2017-08-17 19:08:56 | 2017-08-17 19:08:56 |
+----+------+---------------+---------+---------------+---------------------+---------------------+
#メール通知機能を実装
###ContactResponseクラスを生成
artisan(アルチザン)コマンドからContactResponseクラスを生成する。
$ php artisan make:mail ContactResponse
生成直後のクラス名。
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
class Greet extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this->view('view.name');
}
}
###Mailableを編集する
お問い合わせ内容を適当にviewに渡します。
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
class ContactResponse extends Mailable
{
use Queueable, SerializesModels;
protected $options;
protected $content;
// protected $attach;
public function __construct($options, $content /*, $attach */)
{
$this->options = $options;
$this->content = $content;
// $this->attach = $attach;
}
public function build()
{
return $this->subject('お問い合わせありがとうございました。')
// ->attachData($this->attach['binary'], $this->attach['file_name']);
->view('emails.contact')
->with([
'options' => $this->options,
'content' => $this->content,
]);
}
}
###テンプレート
問い合わせてきた人の名前とか件名や内容を表示してます。
Mailテストくんからのご挨拶です。<br>
{{ $options['name'] }}さん、お問い合わせありがとうございます。<br>
<br>
以下がお問い合わせ内容になります。<br>
-------------------------------------------------------------<br>
件名:{{ $options['subject'] }}<br>
<br>
内容:<br>
{{ $content }}<br>
##メール送信
ContactControllerでお問い合わせがきたら、自動でメールを返信するようにします。
<?php
namespace App\Http\Controllers;
use App\Http\Requests\ContactRequest;
use App\Repositories\ContactRepository;
class ContactController extends Controller
{
...
public function store(ContactRequest $request)
{
// 確認画面で戻るボタンが押された場合
if ($request->get('action') === 'back') {
// 入力画面へ戻る
return redirect()
->route('contacts.index')
->withInput($request->except(['action', 'confirming']));
}
// データベースに登録
$this->contacts->create($request->all());
// ブラウザリロード等での二重送信防止
$request->session()->regenerateToken();
// 返答メールの送信
$email = $request->email;
$options = array('name' => $request->name, 'subject' => $request->subject);
$content = $request->content;
\Mail::to($email)->send(new \App\Mail\ContactResponse($options, $content /* , $attach */));
// 完了画面を表示
return view('contacts.thanks');
}
}
メールが来ていることを確認して終了!
こんな感じで表示されればOKです。
#感想
Laravel 5で確認画面付き問い合わせフォームを作るの記事がかなりうまくできていて、お問い合わせフォームの流れを作るまでは結構サクサクいった。(もちろん5.2→5.4でいろいろ修正することはあったけど。)それ以上にメールの実装に時間がかかってしまった。まだまだなのでこれを発展させていきたい。
#参照記事
Laravel 5で確認画面付き問い合わせフォームを作る
Laravel 5.4 通知
Markdown メール通知を作成する
[Laravel]Mailableで楽勝なメール処理