エンジニアになるためにはブラインドタッチのスピードが大事となる。
プログラミングの学習をしていく上でも学習効率に関わり、仕事の上では生産性に関わっていく。
しかし、タイピングを学べるソフトやサービスはいくつもあるが、そもそも
「プログラミング言語を書く」
という目的のもとでタイピングが出来ていなければならない。
「タイピングが早い」
という事が
「プログラミング言語を書く事が早い」
という事に100%寄与しているわけではない。
なぜなら、
日本語のキータイピングが早い人にプログラミング独特の英語の文法を書かせても同じスピードが出せないからだ。
※本来ならここで統計やら数値的な根拠があるといい(但し、数値がなければジャッジが出来ないというのもビジネスではアウトだ)
そのため、
「プログラミング言語を書きながらタイピングを学ぶべき」だ。
そこで、
「プログラミング独特の言語を学びつつタイピングが練習できるサービス」
があったらどうだろうか?
※競合サービスを一覧にする
※ポジショニングマップを使う
ユーザーがそれぞれプログラミング言語のカテゴリごとにタイピング練習を登録し、ユーザーが登録されたタイピング練習を行ってタイピングスコアを計測できる。
タイピングスコアは下記に従い速度(WPM)と正確性で計測を行う。
練習は1分とする。
https://gtdwmse.com/typing_speed/
・ユーザー登録
・ログイン
・ログアウト
・タイピング練習登録
・タイピング練習登録一覧
・タイピング練習編集
・タイピング練習削除
・タイピング一覧
・タイピング練習
・マイページ
データベースにデータを保存する必要があるので、そのための「テーブル」をまず作りましょう。
まず、保存するデータはどんな種類のものが必要でしょうか?
今回だと
1. ユーザーの情報
2. タイピング練習問題の情報
この2つですね。
「どんな情報を管理する必要があるか」
「このサービスに登場する物はなんだろうか?」
と考えるとわかりやすいですね。
今回なら「人(ユーザー)」と「タイピング練習問題」という2つの物が登場します。
なので、作るテーブルとしては
1. usersテーブル
2. drillsテーブル
ということになります。
ただし、実際にはWEBサービス部でもやったように
「正規化」
をしていくと
1. usersテーブル
2. drillsテーブル(練習情報)
3. problemsテーブル(問題情報)
4. categoryテーブル(カテゴリー情報)
というように
1つの「練習」の中に複数の「問題」がある。
という親子関係の状態になるので、テーブルがそれぞれ分かれます。
また、問題に紐づくカテゴリーも別のテーブルで管理した方がいいですね。
ただ、今回はそこは最後の宿題にするのでテーブルは簡単に
1. usersテーブル
2. drillsテーブル
の2つで作っていきましょう。
テーブル名:Users
| 名前 | カラム名 | データ型 | 制約 |
|---|---|---|---|
| ユーザーID(PK) | id | Integer | PRIMAY、NOT NULL |
| ユーザー名 | name | String | NOT NULL |
| メールアドレス | String | UNIQUE、NOT NULL | |
| メールアドレス確認日時※ | email_verified_at | Timestamp | |
| パスワード※ | password | String | NOT NULL |
| ログイン保持トークン※ | remember_token | String | |
| 作成日時 | created_at | Datetime | |
| 更新日時 | updated_at | Timestamp |
※email_verified_atはLaravelのEmail認証の機能を使う際に必要なカラム
※remember_tokenはLaravelのログイン保持の機能を使う際に必要なカラム
※passwordはLaravelのデフォルトの機能では、60文字以上は必要になる
テーブル名:Drills
| 名前 | カラム名 | データ型 | 制約 |
|---|---|---|---|
| 練習ID(PK) | id | Integer | PRIMAY、NOT NULL |
| タイトル | title | String | NOT NULL |
| カテゴリ名 | category_name | String | NOT NULL |
| 問題1 | problem0 | String | |
| 問題2 | problem1 | String | |
| 問題3 | problem2 | String | |
| 問題4 | problem3 | String | |
| 問題5 | problem4 | String | |
| 問題6 | problem5 | String | |
| 問題7 | problem6 | String | |
| 問題8 | problem7 | String | |
| 問題9 | problem8 | String | |
| 問題10 | problem9 | String | |
| 作成日時 | created_at | Datetime | |
| 更新日時 | updated_at | Timestamp |
Problemsテーブルがある場合
テーブル名:Drills
| 名前 | カラム名 | データ型 | 制約 |
|---|---|---|---|
| 練習ID(PK) | id | Integer | PRIMAY、NOT NULL |
| タイトル | title | String | NOT NULL |
| カテゴリ名 | category_name | Integer | NOT NULL |
| 作成日時 | created_at | Datetime | |
| 更新日時 | updated_at | Timestamp |
テーブル名:Problems
| 名前 | カラム名 | データ型 | 制約 |
|---|---|---|---|
| 問題ID(PK) | id | Integer | PRIMAY、NOT NULL |
| 問題 | description | String | NOT NULL |
| 練習ID(FK) | drill_id | Integer | |
| 作成日時 | created_at | Datetime | |
| 更新日時 | updated_at | Timestamp |
テーブルを作るには「CUI環境でSQLを打つ」「phpMyAdminなど管理画面からGUIで行う」という方法がありますが、
最近では、フレームワークにある「マイグレーション」という機能でテーブルが作成できます。
マイグレーションとは、データベースへの変更をフレームワーク上で管理(ファイルとして)しておく機能です。
ソースとしてバージョンを管理することで、DBに手を加えて何か不具合が起こったりした時でも前日のDBの状態にすぐ戻せたり、何ヶ月前のデータベースの状態に戻すこともできます。
phpMyAdminで手でDBを変更していると「間違えた」といった時でも、前の状態を記憶して覚えていないと戻せません。(実際にはDBのカラム追加や削除など何か変更を加える前には、必ずDBの情報をエクスポート(ダンプファイルと呼ぶ)して取っておくのが一般的です)
php artisan make:migration {テーブル名}_table
database/migrations配下にファイルが作られます。
実用としては、
php artisan make:migration {何をする}_{テーブル名}_table
という形式でマイグレーションファイル名はテーブル名に対して何をするのかという名前にするとファイル名から推測しやすくなります。
実際にDBにテーブルを新規追加するなら--createというオプションをつけてあげ、
その後に=で繋ぎ、新規作成するテーブル名をつけます。
php artisan make:migration create_users_table --create=users
下記コマンドでマイグレーションファイルが作れますが、ただし、Laravelでは既に用意されています。
php artisan make:migration create_users_table --create=users
認証に関する設定ファイルは、config/auth.phpに用意してあります。
また、デフォルトでappディレクトリーの中の、App/Userモデルを読み込むようになっています。
また、マイグレーションファイルも最初から2つ用意されているので、そのまま使うことが出来ます。
2014_10_12_000000_create_users_table.php
2014_10_12_100000_create_password_resets_table.php
今回はそのまま使うのでそのままに。(もちろん、自分で新たに作っても良いし、デフォルトのを修正してもいいですね)
次にdrillsテーブルを作成するマイグレーションファイルを作成しましょう。
php artisan make:migration create_drills_table --create=drills
そのファイルにカラムを追加していきましょう。
追加したいカラムとその書き方はドキュメント参照してください。
https://readouble.com/laravel/5.5/ja/migrations.html
今回は、titleカラムとcategory_nameカラムを追加します。
<?php
class CreateDrillsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('drills', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('title');
$table->string('category_name');
$table->string('problem0');
$table->string('problem1');
$table->string('problem2');
$table->string('problem3');
$table->string('problem4');
$table->string('problem5');
$table->string('problem6');
$table->string('problem7');
$table->string('problem8');
$table->string('problem9');
$table->timestamps();
});
}
マイグレーションファイルには必ずup()とdown()があります。
up()には、DBに変更を加えたい処理内容を記述しておきます。
down()には、「ロールバック」した場合のために「変更したDBから元に戻す処理」を記述しておきます。
php artisan migrate
mysqlに接続したら、下記で使うDBを選択します。
USE laravel_sample;
これで、テーブル一覧を表示する。
SHOW TABLES;
4つ作られていればOK。
+--------------------------+
| Tables_in_laravel_sample |
+--------------------------+
| drills |
| migrations |
| password_resets |
| users |
+--------------------------+
4 rows in set (0.00 sec)
※テーブルのカラムも一覧で見る場合には、
DESCRIBE テーブル名;
マイグレーションを使うことで、SQL文を作ることもなく、管理画面をひたすら操作することもなく、
「マイグレーションファイルを作り、マイグレートコマンドを実行するだけ」でテーブル作成やカラム変更などDB操作が行えます。
https://readouble.com/laravel/5.6/ja/migrations.html
「ロールバック」とは、DBへの変更した処理を「戻す」ことを言います。
実際の現場でもよく使われ、新たに機能を追加するなど開発をする際にDBのカラムなりを変更する事が多々あります。
その際に「不具合」が出てしまう事があるため、その場合に「ロールバック」をして正常だった状態までDBの状態を元に戻すことがあります。
最後に「一度に」実行したマイグレーションをまとめて元に戻せます。
php artisan migrate:rollback
下記は5マイグレーション前に戻す場合
(マイグレーションコマンド1回実行毎に1マイグレーション。1回のマイグレーションの実行時にマイグレーションファイルが複数あっても、1マイグレーションとカウントされます)
php artisan migrate:rollback --step=5
※必ずマイグレーションファイルを作ったら、ロールバックできるか確認すること!!
DBのテーブルが準備できたところで、
ユーザー登録やログイン機能、ログアウト機能を作っていきましょう。
FW部でもやったようにそれぞれ、コントローラーとビューを作る必要がありますが、
Laravelではユーザー管理の機能のためのコントローラーやビューは
コマンド1つで作れてしまいます。
php artisan make:auth
作ると下記のファイルが自動生成されます。
resources/views/home.blade.php
resources/views/layouts/app.blade.php
resources/views/auth/
/passwords
- email.blade.php
- reset.blade.php
- login.blade.php
- register.blade.php
- verify.blade.php
app/Http/Controllers/HomeController.php
app/Http/Controllers/Auth/
- ForgotPasswordController.php
- LoginController.php
- RegisterController.php
- ResetPasswordController.php
- VerificationController.php
また、web.phpにもルーティングが2つ追加されます。
<?php
Route::get('/', function () {
return view('welcome');
});
Auth::routes();
Route::get('/home', 'HomeController@index')->name('home');
Auth::routes();では、vendor/laravel/framework/src/Illuminate/Routing/Router.phpで定義されたルーティングが読み込まれています。
(ルーティングに関してはまた次で詳しく説明します。)
画面右上にもLOGINとREGISTERというリンクが自動で作られていますね。
自動でこんな画面が作られています。
| URI(エンドポイント) | HTTPメソッド | アクション名 | 機能名 | 役割 |
|---|---|---|---|---|
| /home | GET | HomeController@index | マイページ表示 | マイページ画面を表示する |
| /register | GET | RegisterController@showRegistrationForm | ユーザー登録表示 | ユーザー登録画面を表示する |
| /register | POST | RegisterController@register | ユーザー登録 | ユーザー登録をする |
| /login | GET | LoginController@showLoginForm | ログイン表示 | ログイン画面を表示する |
| /login | POST | LoginController@login | ログイン | ログインする |
| /logout | POST | LoginController@logout | ログアウト | ログアウトする |
| /password/reset | GET | ForgotPasswordController@showLinkRequestForm | パスワードリマインダー画面 | パスワードリマインダー画面を表示する |
| /password/email | POST | ForgotPasswordController@sendResetLinkEmail | パスワードリマインド | パスワードを再発行する |
| /password/reset/{token} | GET | ForgotPasswordController@showResetForm | パスワード再入力画面 | パスワード再入力画面を表示する |
| /password/reset | POST | ForgotPasswordController@reset | パスワード更新 | パスワードを更新する |
| /email/verify | GET | VerificationController@show | Eメール認証画面 | Eメール認証画面を表示する |
| /email/verify/{id} | GET | VerificationController@verify | Eメール認証 | Eメール認証する |
| /email/resend | GET | VerificationController@resend | Eメール認証メール再送信 | Eメール認証メールを再送信する |
【Eメール認証画面】
※Eメール認証機能を使うには別途カラムが必要だったり、設定が必要になる
もう、これだけで機能一覧のうち
・ユーザー登録 *済み
・ログイン *済み
・ログアウト *済み
・タイピング練習登録
・タイピング練習登録一覧
・タイピング練習編集
・タイピング練習削除
・タイピング一覧
・タイピング練習
・マイページ
が終わってしまいました。
さらにはパスワードリマインダーも作られたわけですね。
どんなサービスでも最低限この機能は必要なことがほぼなので、とても便利ですね。
LaravelにはDBのテーブルにダミーデータ(とりあえず動かしたい時に入れておく適当なデータ)を入れる専用の機能があります。
ダミーデータは、例えば「ログイン機能」を作る際にまだ「ユーザー登録機能」といったデータを登録する機能が存在しない場合に「うまく動くか」といったテストをする際に使えます。
データベースに接続して直接SQLを叩いてレコードを追加してもいいが、そういったダミーデータは色々なテストでもあとあと使えるため、残しておくためにもseederという機能を使って「ファイル」として残しておくと便利です。
(マイグレーションファイルはDBのテーブルやカラムの変更を記録しておくファイルですが、それのデータ版がseederです)
seederファイルは下記コマンドで作れます。
php artisan make:seeder {適当な名前}
実務的には、usersテーブルにダミーデータを追加したいなら、下記のような名前にします。
php artisan make:seeder UsersTableSeeder
/database/seeds配下にUsersTableSeeder.phpが出来上がるので、
下記のように修正しましょう。
<?php
use Carbon\Carbon;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class UsersTableSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
DB::table('users')->insert([
'name' => 'test taro',
'email' => 'test@example.com',
'password' => bcrypt('password'),
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
]);
}
}
seederファイルを修正したら、下記を実行すれば、usersテーブルにseederで定義したダミーデータが作られます。
php artisan db:seed --class=UsersTableSeeder
LaravelではBladeというテンプレートエンジンを使っています。
Bladeテンプレートの拡張子には.blade.phpをつけます。
まず、テンプレートエンジンを使わずに普通に書いたコードをみてください。
<body>
<?php
$val = 1;
?>
<?php if ($val === 1): ?>
<div>処理1がtrueの場合にこれが表示</div>
<?php elseif ($val === 2): ?>
<div>elseifがtrueの場合にこれが表示</div>
<?php endif; ?>
<?php if(empty($val)): ?>
<div>emptyがtrueならこれが表示</div>
<?php endif; ?>
<?php if(isset($val)): ?>
<div>issetがtrueならこれが表示</div>
<?php endif; ?>
<?php if ($val === 1){ ?>
<?php if(empty($val)){ ?>
<div>emptyがtrueならこれが表示</div>
<?php if(isset($val)){ ?>
<div>issetがtrueならこれが表示</div>
<?php } ?>
<?php } ?>
<?php }elseif ($val === 2){ ?>
<div>elseifがtrueの場合にこれが表示</div>
<?php } ?>
<?php
$items = ['あいうえお', 'かきくけこ', 'さしすせそ'];
?>
<?php foreach ($items as $item): ?>
<li><?php echo $item; ?></li>
<?php endforeach; ?>
</body>
次にBladeテンプレートを使って同じ処理のhtmlを書いた場合です。
<body>
@php
$val = 1;
@endphp
@if ($val === 1)
<div>処理1がtrueの場合にこれが表示</div>
@elseif ($val === 2)
<div>elseifがtrueの場合にこれが表示</div>
@endif
@empty($val)
<div>emptyがtrueならこれが表示</div>
@endempty
@isset($val)
<div>issetがtrueならこれが表示</div>
@endisset
@if ($val === 1)
@empty($val)
<div>emptyがtrueならこれが表示</div>
@isset($val)
<div>issetがtrueならこれが表示</div>
@endisset
@endempty
@elseif ($val === 2)
<div>elseifがtrueの場合にこれが表示</div>
@endif
@php
$items = ['あいうえお', 'かきくけこ', 'さしすせそ'];
@endphp
@foreach ($items as $item)
<li>{{ $item }}</li>
@endforeach
</body>
だいぶスッキリしませんか?
<?phpで書く場合、入れ子が増えるほどごちゃごちゃしてきますね。
何より毎回phpの開始タグと閉じタグを書くのも面倒です。
こういったようにBladeを使えば可読性高いコードを書くことができます。
ちなみにBladeテンプレートは単体でも使えるため、composerなどでダウンロードして単体で使用することができます。
Laravelを使わないと使えないというわけではないので
「FWを使って作るまでもないシステムだけどphpコードは見やすくしときたい」
という場合に単体で使うことができます。
親子関係を作るために@include、@yield、@sectionが用意されています。
resources/views/layouts/welcome.blade.php
<html>
<head>
<title>Laravel | @yield('title', 'Home')</title>
</head>
<body>
@section('sidebar')
<p>メインのサイドバー(共通部分)</p>
@show
<div id='container'>
@yield('content')
</div>
@section('footer')
<script src="app.js"></script>
@show
</body>
</html>
resources/views/child.blade.php
@extends('layouts.parent')
@section('title', 'マイページ')
@section('sidebar')
@parent
<p>メインのサイドバー(共通部分)に追加される個別の部分</p>
@endsection
@section('content')
<p>メインコンテンツ</p>
@endsection
@section('footer')
@parent
<script src="dashboard.js"></script>
@endsection
親テンプレートを継承するものです。
@yieldは継承ができません。
デフォルト値の設定は出来ます。
共通化せずにコンテンツview毎にダイナミックに変化するものやページ毎に設定するようなものは@yieldを使いましょう。(それぞれのページのタイトル、メインコンテンツなど)
@section()〜@endsection は継承が出来ます。
@parentと書けば、親テンプレートの@section()の内容が展開されます。(継承される)
scriptタグの追加記述の他、サイドバーをページによって出し分けや追加等をしたい場合に使えますね。
@sectionの閉じタグは、親テンプレートの場合は@showで終わり、 子テンプレートの場合は@endsectionもしくは@stopで終わるので注意しましょう。
ヘッダーやフッターなどただテンプレートを読み込みたいだけだったり、
子テンプレートに値を渡したいのならサブビューを使いましょう。
resources/views/child.blade.php
@extends('layouts.parent')
@section('title', 'マイページ')
@section('sidebar')
@parent
<p>メインのサイドバー(共通部分)に追加される個別の部分</p>
@endsection
@section('content')
<p>メインコンテンツ</p>
@include('common.error', ['text' => 'エラー'])
@endsection
@section('footer')
@parent
<script src="dashboard.js"></script>
@endsection
resources/views/common/error.blade.php
<div class="alert alert-error">
{{ $text }}
...
</div>
@includeは継承ができず、デフォルト値の設定もできません。
ただ、唯一、子テンプレに変数を渡すことが出来ます。
他にも@componentというコンポーネントを作るためのものもあります。
https://readouble.com/laravel/5.8/ja/blade.html
エラーメッセージであったり「コンポーネント」としてhtmlをパーツとして切り出して管理することができます。
(javascriptにもFWがあり、htmlをコンポーネントとして切り出す機能があります。詳しくはvue.jsなどを学んでみてください)
Laravelの5.6からはBladeテンプレートが簡素に書けるようになっています。
5.5まで
{{ csrf_field() }}
5.6以降
@csrf
また、@errorといったものも使えるようになりました。
5.5まで
@if($errors->has('username'))
<div class="error">
<p>{{ $errors->first('username') }}</p>
</div>
@endif
5.6以降
@error('username')
<div class="error">
<p>{{ $message }}</p>
</div>
@enderror
FWでは自動でphpのhtmlspecialchars関数が実行されエスケープされるため、エスケープして表示したくない時には下記のように記述します。
{!! $val !!}
機能一覧のうち下記の機能をこの練習コントローラーで行います
・ユーザー登録 *済み
・ログイン *済み
・ログアウト *済み
・タイピング練習登録(練習コントローラー)
・タイピング練習登録一覧(練習コントローラー)
・タイピング練習編集(練習コントローラー)
・タイピング練習削除(練習コントローラー)
・タイピング一覧(練習コントローラー)
・タイピング練習(練習コントローラー)
・マイページ
さらに「マイページ」には自分が登録した練習の一覧を表示させちゃいましょう。
なので、タイピング練習登録一覧はマイページの機能にしてしたいと思います。
・ユーザー登録 *済み
・ログイン *済み
・ログアウト *済み
・タイピング練習登録(練習コントローラー)
・タイピング練習登録一覧(練習コントローラー)|マイページ
・タイピング練習編集(練習コントローラー)
・タイピング練習削除(練習コントローラー)
・タイピング一覧(練習コントローラー)
・タイピング練習(練習コントローラー)
Laravelは他のFWでも同じように1画面1アクションになります。
なので、コントローラーに各機能で表示させる画面表示用のアクションや「登録処理」「編集(更新)処理」用のアクションを作ってあげます。
まずは、必要なアクションを一覧に書き出してみましょう。
DrillsController
| URL(URI) | HTTPメソッド | アクション名 | 機能名 | 役割 |
|---|---|---|---|---|
| /drills | GET | index | タイピング練習一覧表示画面 | 全ての練習を表示する |
| /mypage | GET | mypage | マイページ(タイピング練習登録一覧表示画面) | 該当ユーザーの練習を表示する |
| /drills/new | GET | new | タイピング練習登録画面 | 新規登録画面を表示する |
| /drills | POST | create | タイピング練習登録 | 新規登録する |
| /drills/1 | GET | show | タイピング練習 | 該当の練習(詳細画面)を表示する |
| /drills/1/edit | GET | edit | タイピング練習編集画面 | 該当の練習の編集画面を表示 |
| /drills/1 | POST | update | 更新 | 該当の練習を更新する |
| /drills/1/delete | POST | destroy | 削除 | 該当の練習を削除する |
URLとHTTPメソッドから、そのリクエストを処理するコントローラとアクションに振り分ける。(ルーティング)
php artisan make:controller コントローラー名
※コントローラー名ははアッパーキャメルケースで○○Controllerという命名規則(複数形にするのが慣習)
今回は
php artisan make:controller DrillsController
app/Http/Controllersの中に自動で下記のようなファイルが作られます。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class DrillsController extends Controller
{
//
}
まずは、そこに練習登録画面を表示させるアクションを作りましょう。
主なFWと同じく、Laravelも1アクション1ビューになります。
今回のアクションの内容はただビューを表示させる処理のみです。
<?php
...
class DrillsController extends Controller
{
public function new()
{
return view('drills.new');
}
}
ビューを表示するには
return view('ビュー名');
とすることで、resources/views配下のビューファイルが読み込まれます。
ディレクトリを切っているなら、.で繋いで指定をします。
resources/views配下にビューファイルを作成しましょう。
今回、コントローラーではdrills.newと指定しているので、それと同じ場所に同じ名前のビューファイルを作れば読み込まれます。
formのname属性はレコード名と同じにしておくと余計な処理を書かずに楽にDBへ保存できます。
resources/views/drills/new.blade.php
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Drill Register') }}</div>
<div class="card-body">
<form method="POST" action="{{ route('drills.new') }}">
@csrf
<div class="form-group row">
<label for="title" class="col-md-4 col-form-label text-md-right">{{ __('Title') }}</label>
<div class="col-md-6">
<input id="title" type="text" class="form-control @error('title') is-invalid @enderror" name="title" value="{{ old('title') }}" autocomplete="title" autofocus>
@error('title')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row">
<label for="category_name" class="col-md-4 col-form-label text-md-right">{{ __('Category') }}</label>
<div class="col-md-6">
<input id="category_name" type="text" class="form-control @error('category_name') is-invalid @enderror" name="category_name" value="{{ old('category_name') }}" autocomplete="category_name" autofocus>
@error('category_name')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row">
<label for="problem0" class="col-md-4 col-form-label text-md-right">{{ __('Problem').'1' }}</label>
<div class="col-md-6">
<input id="problem0" type="text" class="form-control @error('problem0') is-invalid @enderror" name="problem0" value="{{ old('problem0') }}" autocomplete="problem0" autofocus>
@error('problem0')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row">
<label for="problem1" class="col-md-4 col-form-label text-md-right">{{ __('Problem').'2' }}</label>
<div class="col-md-6">
<input id="problem1" type="text" class="form-control @error('problem1') is-invalid @enderror" name="problem1" value="{{ old('problem1') }}" autocomplete="problem1" autofocus>
@error('problem1')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row">
<label for="problem2" class="col-md-4 col-form-label text-md-right">{{ __('Problem').'3' }}</label>
<div class="col-md-6">
<input id="problem2" type="text" class="form-control @error('problem2') is-invalid @enderror" name="problem2" value="{{ old('problem2') }}" autocomplete="problem0" autofocus>
@error('problem2')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row mb-0">
<div class="col-md-6 offset-md-4">
<button type="submit" class="btn btn-primary">
{{ __('Register') }}
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
1個1個書くのは面倒なので、ループさせてinputタグを生成しましょう。
@for ($i = 1; $i <= 10; $i++)
<div class="form-group row">
<label for="problem0" class="col-md-4 col-form-label text-md-right">{{ __('Problem').$i }}</label>
<div class="col-md-6">
<input id="problem{{$i - 1}}" type="text" class="form-control @error('problem'.($i - 1)) is-invalid @enderror" name="problem{{$i - 1}}" value="{{ $drill['problem'.($i - 1)] }}" autocomplete="problem{{$i - 1}}" autofocus>
@error('problem'.($i - 1))
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
@endfor
画面を表示させるには、URLとアクションとを対応させる必要があります。
それが「ルーティング」でしたね。
Laravelではルーティングはroutes/web.phpで設定します。
Route::get('/drills/new', 'DrillsController@new')->name('drills.new');
これで
http://localhost:8888/drills/new
のURLで画面が表示されます。
文字が全て英語で表示されてますね。
これを「日本語表記」で表示されるようにしましょう。
多くのFWには「多言語化対応」がしやすいようになっていて、Laravelでもそういったものがあります。(i18nといったライブラリが有名です。バックエンドだけでなくフロントエンドのjsにもあります。)
多言語化させるためにはhtml上で文字を出力する際に__()囲う必要があります。
タイトルならこんな感じですね。
{{ __('Title') }}
囲う文字は英語表記でもいいですし、日本語表記でも構いません。
最終的に英語と日本語とを紐づける設定ファイルを作ればどちらにも対応させられます。デフォルトの表記をどれにしたいか。だけですね。
https://readouble.com/laravel/5.4/ja/localization.html
多言語対応させる設定ファイルはresources/lang配下にファイルを設置します。phpファイルでやる方法とjsonファイルで設定する方法がありますが、jsonファイルを使ってみましょう。
resources/lang/ja.jsonを作り、下記のような設定をかきます。
{
"Drill Register": "練習登録",
"Title": "タイトル",
"Category": "カテゴリー",
"Problem": "問題"
}
アプリケーションのデフォルト言語はconfig/app.php設定ファイルで指定します。
fallback_localeでは翻訳できるものが翻訳ファイルになかった時にデフォルトで表示させる言語を指定できます。
...
'locale' => 'ja',
...
'fallback_locale' => 'ja',
入力フォーム画面は表示できたので、今度は入力されたものが送信された時にDBへ保存をする処理を実装しましょう。
登録処理用のcreateアクションを作成し、まずは入力値に問題がないかバリデーションを実装しましょう。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class DrillsController extends Controller
{
public function new()
{
return view('drills.new');
}
public function create(Request $request)
{
$request->validate([
'title' => 'required|string|max:255',
'category_name' => 'required|string|max:255',
'problem0' => 'required|string|max:255',
'problem1' => 'string|max:255',
'problem2' => 'string|max:255',
'problem3' => 'string|max:255',
'problem4' => 'string|max:255',
'problem5' => 'string|max:255',
'problem6' => 'string|max:255',
'problem7' => 'string|max:255',
'problem8' => 'string|max:255',
'problem9' => 'string|max:255',
]);
}
}
アクションの引数にリクエスト、レスポンスをセットしておけば、自動でそこにリクエストインスタンスとレスポンスインスタンスが入ってきます。
その他、バリデーション項目
https://readouble.com/laravel/5.6/ja/validation.html
これでpost送信した際に自動でバリデーションがされます。
Route::post('/drills/new', 'DrillsController@create');
バリデーションエラー時のエラーメッセージも多言語化対応と同じように翻訳できるので、ググってみましょう。
データベース操作方法には
1. DBクラスを使う方法
2. クエリビルダーを使う方法
3. ORM(Object-relational mapping、O/RM、ORM)を使う方法
があります。
ここでは、現在では一般的なORMを使っていきましょう。
LaravelではORMには、「Eloquent ORM」というものを使っています。
モデルはappディレクトリ配下に置きましょう。(composer.jsonで設定すれば、好きな場所に置くことができます)
modelの作成はartisanで出来ます。
モデルの命名規則は単数形でアッパーキャメルケースです。
php artisan make:model モデル名
今回の場合は練習モデルなので
php artisan make:model Drill
となります。
また、マイグレーションファイルと一緒にモデルも作成したい場合は、--migrationオプションをつけましょう。
php artisan make:model Drill --migration
省略して-mと書く事もできます。
php artisan make:model Drill -m
https://readouble.com/laravel/5.6/ja/eloquent.html
コマンドを叩くと下記のようなモデルのファイルが生成されます。
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Drill extends Model
{
//
}
自動的にモデル名の複数形のテーブル名と対応します。
もし、テーブル名がモデル名(複数形)ではない場合は下記のように$tableプロパティで設定できます。
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Drill extends Model
{
protected $table = 'my_drills';
}
<?php
...
class Drill extends Model
{
// DBで間違っても変更させたくないカラム(ユーザーID、管理者権限など)にはロックをかけておく事ができる
// ロックの掛け方にはfillableかguardedの2種類がある。
// どちらかしか指定できない
// モデルがその属性以外を持たなくなる(fillメソッドに対応しやすいが、カラムが増えるほど足していく必要あり)
protected $fillable = ['title', 'category_name', 'problem0', 'problem1', 'problem2', 'problem3', 'problem4', 'problem5', 'problem6', 'problem7', 'problem8', 'problem9'];
// モデルからその属性が取り除かれる(カラムが増えてもほとんど変更しなくて良い)
// protected $guarded = ['id'];
}
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Drill;
class DrillsController extends Controller
{
public function new()
{
return view('drills.new');
}
public function create(Request $request)
{
$request->validate([
'title' => 'required|string|max:255',
'category_name' => 'required|string|max:255',
'problem0' => 'required|string|max:255',
'problem1' => 'string|max:255',
'problem2' => 'string|max:255',
'problem3' => 'string|max:255',
'problem4' => 'string|max:255',
'problem5' => 'string|max:255',
'problem6' => 'string|max:255',
'problem7' => 'string|max:255',
'problem8' => 'string|max:255',
'problem9' => 'string|max:255',
]);
// モデルを使って、DBに登録する値をセット
$drill = new Drill;
// 1つ1つ入れるか
// $drill->title = $request->title;
// $drill->category_name = $request->category_name;
// $drill->save();
// fillを使って一気にいれるか
// $fillableを使っていないと変なデータが入り込んだ場合に勝手にDBが更新されてしまうので注意!
$drill->fill($request->all())->save();
// リダイレクトする
// その時にsessionフラッシュにメッセージを入れる
return redirect('/drills/new')->with('flash_message', __('Registered.'));
}
}
メッセージを表示させる処理を共通のビューに入れる。
app.blade.php
...
<body>
<div id="app">
...
<!-- フラッシュメッセージ -->
@if (session('flash_message'))
<div class="alert alert-primary text-center" role="alert">
{{ session('flash_message') }}
</div>
@endif
<main class="py-4">
@yield('content')
</main>
</div>
</body>
</html>
実際に登録をしてみましょう。
このままだと下記のようなエラーが出ます。
SQLSTATE[HY000]: General error: 1364 Field 'problem3' doesn't have a default value (SQL: insert into `drills` (`title`, `category_name`, `problem0`, `problem1`, `problem2`, `updated_at`, `created_at`) values (あああああああああ, JavaScript, if($val == '1'){, for($i = 1; $i < 10; $++){, function sampleFunc(){, 2019-05-30 03:48:04, 2019-05-30 03:48:04))
SQLが失敗していますね。
メッセージを読むと「problem3」カラムのデフォルト値が設定されていないよ。と出ています。
なぜこうなるかというと「NOT NULL」制約がDBのテーブル作成時にデフォルトでついてしまっているからです。
本当はマイグレーションファイルで「NULLを許容する」という風にしておく必要がありました。
なので、カラムの制限を変更しましょう。
1度既に作ったカラムの名前や各種制限を変更するのもマイグレーションを作成します。
ただし、カラムを変更する前にcomposer.jsonファイルでdoctrine/dbalを追加する必要があります。
(Doctrine DBALライブラリーというのは、現在のカラムの状態を決め、指定されたカラムに対する修正を行うSQLクエリを生成するために使用している。)
https://readouble.com/laravel/5.6/ja/migrations.html
下記コマンドで追加できる。
composer require doctrine/dbal
まず、カラム変更用のマイグレーションファイルを作成しましょう。
php artisan make:migration change_problem_not_null_to_null_on_drills_table --table=drills
次にマイグレーションファイルを下記のように修正します。
<?php
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class ChangeProblemNotNullToNullOnDrillsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('drills', function (Blueprint $table) {
$table->text('problem1')->nullable()->change();
$table->text('problem2')->nullable()->change();
$table->text('problem3')->nullable()->change();
$table->text('problem4')->nullable()->change();
$table->text('problem5')->nullable()->change();
$table->text('problem6')->nullable()->change();
$table->text('problem7')->nullable()->change();
$table->text('problem8')->nullable()->change();
$table->text('problem9')->nullable()->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('drills', function (Blueprint $table) {
$table->text('problem1')->nullable(false)->change();
$table->text('problem2')->nullable(false)->change();
$table->text('problem3')->nullable(false)->change();
$table->text('problem4')->nullable(false)->change();
$table->text('problem5')->nullable(false)->change();
$table->text('problem6')->nullable(false)->change();
$table->text('problem7')->nullable(false)->change();
$table->text('problem8')->nullable(false)->change();
$table->text('problem9')->nullable(false)->change();
});
}
}
migrateの実行
php artisan migrate
これで登録ができましたね。
ORMは「SQLを書かなくていいもの」です。
オブジェクトを扱うようにDB操作が行える機能になります。
今までは「サーバーサイド言語」も覚え、「SQL言語」も覚える。
という2つの言語を覚える必要がありましたが、
このORMという機能を使うことで、「サーバーサイド言語」のオブジェクトを扱っているように「SQLを書くことなく」DB操作が行える。というメリットがあります。
//DB接続関数
function dbConnect(){
//DBへの接続準備
$dsn = 'mysql:dbname=freemarket;host=localhost;charset=utf8';
$user = 'root';
$password = 'root';
$options = array(
// SQL実行失敗時にはエラーコードのみ設定
PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT,
// デフォルトフェッチモードを連想配列形式に設定
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
// バッファードクエリを使う(一度に結果セットをすべて取得し、サーバー負荷を軽減)
// SELECTで得た結果に対してもrowCountメソッドを使えるようにする
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
);
// PDOオブジェクト生成(DBへ接続)
$dbh = new PDO($dsn, $user, $password, $options);
return $dbh;
}
//SQL実行関数
function queryPost($dbh, $sql, $data){
//クエリー作成
$stmt = $dbh->prepare($sql);
//プレースホルダに値をセットし、SQL文を実行
$stmt->execute($data);
return $stmt;
}
//例外処理
try {
// DBへ接続
$dbh = dbConnect();
// SQL文作成
$sql = 'INSERT INTO users (email,password,login_time,create_date) VALUES(:email,:pass,:login_time,:create_date)';
$data = array(':email' => $email, ':pass' => password_hash($pass, PASSWORD_DEFAULT),
':login_time' => date('Y-m-d H:i:s'),
':create_date' => date('Y-m-d H:i:s'));
// クエリ実行
queryPost($dbh, $sql, $data);
header("Location:mypage.html"); //マイページへ
} catch (Exception $e) {
error_log('エラー発生:' . $e->getMessage());
$err_msg['common'] = MSG07;
}
↓
$drill = new Drill;
$drill->fill($request->all())->save();
ただし、実際は「SQLを覚えなくていい」わけではなく、
大規模になるほど複雑なDB操作が求められるため、そうなってくるとSQLを書かなければ難しい部分もまだあり、直接DB内に入ってSQLを操作する事も実務では多くあるため、必ず書けるようになっておく必要があります。
機能一覧のうち「タイピング一覧」機能を作りましょう。
・ユーザー登録 *済み
・ログイン *済み
・ログアウト *済み
・タイピング練習登録(練習コントローラー) *済み
・タイピング練習登録一覧(練習コントローラー)
・タイピング練習編集(練習コントローラー)
・タイピング練習削除(練習コントローラー)
☆タイピング一覧(練習コントローラー)
・タイピング練習(練習コントローラー)
・マイページ
<?php
// 全件取得
$drills = Drill::all();
foreach ($drills as $drill) {
echo $drill->title;
}
// 主キーで指定したモデル取得
$drills = Drill::find(1);
// クエリ条件にマッチした最初のレコード取得
$drills = Drill::where('title', 'aiueo')->first();
DrillsControllerにアクションを追加しましょう。
<?php
...
class DrillsController extends Controller
{
public function index(){
$drills = Drill::all();
return view('drills.index', ['drills' => $drills]);
}
...
view関数で指定したものに合わせたビューを作成しましょう。
今回ならviews/drills/index.blade.phpですね。
@extends('layouts.app')
@section('content')
<div class="container">
<h2>{{ __('Drill List') }}</h2>
<div class="row">
@foreach ($drills as $drill)
<div class="col-sm-6">
<div class="card">
<div class="card-body">
<h3 class="card-title">{{ $drill->title }}</h3>
<a href="#" class="btn btn-primary">{{ __('Go Practice') }}</a>
</div>
</div>
</div>
@endforeach
</div>
</div>
@endsection
※まだリンク先を指定していないので飛べません。
※まだ「自分が登録したものだけ」を一覧表示出来ていません。
※まだページネーションはありません。
<?php
...
Route::get('/drills', 'DrillsController@index')->name('drills');
アクションを1個しか実装しないのであれば、シングルアクションコントローラーを使うとわかりやすいです。
シングルアクションコントローラーを使えば、ルーティングはコントローラ名のみでよくなります。
class DrillsController extends Controller
{
function __invoke(){
//
}
}
Route::get('/diaries', 'DiariesController');
Route::get('/drills/{id}/edit', 'DrillsController@edit')->name('drills.edit');
このように{}で適当な名前をつけて囲っておくことで、その箇所のURLの値をコントローラー側で取得できるようになります。(取得方法はあとで)
パラメータには必須パラメータと任意パラメータがあります。
必須の場合
Route::get('/drills/{id}/edit', 'DrillsController@edit')->name('drills.edit');
任意の場合
Route::get('/drills/{id?}/edit', 'DrillsController@edit')->name('drills.edit');
※任意パラメータの場合はアクションの引数にデフォルト値を指定しておく必要があります。
<?php
...
class DrillsController extends Controller
{
public function edit($id){
// GETパラメータが数字かどうかをチェックする
// 事前にチェックしておくことでDBへの無駄なアクセスが減らせる(WEBサーバーへのアクセスのみで済む)
if(!ctype_digit($id)){
return redirect('/drills/new')->with('flash_message', __('Invalid operation was performed.'));
}
$drill = Drill::find($id);
return view('drills.edit', ['drill' => $drill]);
}
...
ja.json
"Invalid operation was performed.": "不正な操作が行われました"
編集画面は登録画面のを流用しましょう。
タイトルとformの飛び先を変えただけですね。
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Drill Updater') }}</div>
<div class="card-body">
<form method="POST" action="{{ route('drills.update',$drill->id ) }}">
@csrf
<div class="form-group row">
<label for="title" class="col-md-4 col-form-label text-md-right">{{ __('Title') }}</label>
<div class="col-md-6">
<input id="title" type="text" class="form-control @error('title') is-invalid @enderror" name="title" value="{{ $drill->title }}" autocomplete="title" autofocus>
@error('title')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row">
<label for="category_name" class="col-md-4 col-form-label text-md-right">{{ __('Category') }}</label>
<div class="col-md-6">
<input id="category_name" type="text" class="form-control @error('category_name') is-invalid @enderror" name="category_name" value="{{ $drill->category_name }}" autocomplete="category_name" autofocus>
@error('category_name')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
@for ($i = 1; $i <= 10; $i++)
<div class="form-group row">
<label for="problem0" class="col-md-4 col-form-label text-md-right">{{ __('Problem').$i }}</label>
<div class="col-md-6">
<input id="problem{{$i - 1}}" type="text" class="form-control @error('problem'.($i - 1)) is-invalid @enderror" name="problem{{$i - 1}}" value="{{ $drill['problem'.($i - 1)] }}" autocomplete="problem{{$i - 1}}" autofocus>
@error('problem'.($i - 1))
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
@endfor
<div class="form-group row mb-0">
<div class="col-md-6 offset-md-4">
<button type="submit" class="btn btn-primary">
{{ __('Register') }}
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
Route::post('/drills/{id}/edit', 'DrillsController@update')->name('drills.update');
public function update(Request $request, $id)
{
// GETパラメータが数字かどうかをチェックする
if(!ctype_digit($id)){
return redirect('/drills/new')->with('flash_message', __('Invalid operation was performed.'));
}
$drill = Drill::find($id);
$drill->fill($request->all())->save();
return redirect('/drills')->with('flash_message', __('Registered.'));
}
index.blade.phpのaタグにリンク先を入れましょう。
<a href="{{ route('drills.edit',$drill->id ) }}" class="btn btn-primary">{{ __('Go Practice') }}</a>
Route::post('/drills/{id}/delete', 'DrillsController@destroy')->name('drills.delete');
<?php
...
public function destroy($id)
{
// GETパラメータが数字かどうかをチェックする
if(!ctype_digit($id)){
return redirect('/drills/new')->with('flash_message', __('Invalid operation was performed.'));
}
// $drill = Drill::find($id);
// $drill->delete();
// こう書いた方がスマート
Drill::find($id)->delete();
return redirect('/drills')->with('flash_message', __('Deleted.'));
}
...
"Deleted.": "削除しました"
...
<form action="{{ route('drills.delete',$drill->id ) }}" method="post" class="d-inline">
@csrf
<button class="btn btn-danger" onclick='return confirm("削除しますか?");'>{{ __('Go Delete') }}</button>
</form>
...
Route::get('/drills/{id}', 'DrillsController@show')->name('drills.show');
public function show($id)
{
// GETパラメータが数字かどうかをチェックする
if(!ctype_digit($id)){
return redirect('/drills/new')->with('flash_message', __('Invalid operation was performed.'));
}
$drill = Drill::find($id);
return view('drills.show', ['drill' => $drill]);
}
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Practice').'「'.$drill->title.'」' }}</div>
<div class="card-body text-center">
<p>{{ $drill->problem0 }}</p>
<p>{{ $drill->problem1 }}</p>
<p>{{ $drill->problem2 }}</p>
</div>
</div>
</div>
</div>
</div>
@endsection
<a href="{{ route('drills.show',$drill->id ) }}" class="btn btn-primary">{{ __('Go Practice') }}</a>
LaravelではVue.js
LaravelではすぐにVueが使えるようpackage.jsonに既に記述されています。
なので、
npm i
でvue.jsなどその他もろもろのライブラリをインストールしましょう。
jsはデフォルトではresources/js/app.jsで管理しています。
ここにも、デフォルトでexample-componentを読み込むよう記述があります。
今回は、このコンポーネントをそのまま使ってしまいましょう。
デフォルトでは、id属性がappのタグ内でVue.jsで操作が行えるようになっていて、example-componentを読み込むようになっているので、それに対応するようにhtmlタグを書きましょう。
<div class="card-body text-center">
<p>{{ $drill->problem0 }}</p>
<p>{{ $drill->problem1 }}</p>
<p>{{ $drill->problem2 }}</p>
<div id="app">
<!-- デフォルトだとこの中ではvue.jsが有効 -->
<!-- example-component はLaravelに入っているサンプルのコンポーネント -->
<example-component></example-component>
</div>
</div>
example-componentというタグを記述することで、それに対応するvueファイルが自動で読み込まれます。
Laravelでは、resources/js/components/ExampleComponent.vueというファイルで既に用意されています。
その中にはhtmlと共にjsの処理を書くことができます。
まずは、コンポーネントに合わせてshow.blade.htmlを修正し、コンポーネント内で使いたいデータを渡してあげましょう。
@extends('layouts.app')
@section('content')
<div id="app">
<!-- デフォルトだとこの中ではvue.jsが有効 -->
<!-- example-component はLaravelに入っているサンプルのコンポーネント -->
<example-component title="{{ __('Practice').'「'.$drill->title.'」' }}" :drill="{{$drill}}"></example-component>
</div>
@endsection
その後にコンポーネントを修正します。
<template>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ title }} <span class="badge badge-success">{{ categoryName }}</span></div>
<div class="card-body text-center drill-body">
<button class="btn btn-primary " @click="doDrill" v-if="!isStarted">
START
</button>
<p v-if="isCountDown" style="font-size: 100px;">{{countDownNum}}</p>
<template v-if="isStarted && !isCountDown && !isEnd">
<p>{{timerNum}}</p>
<span v-for="(word, index) in problemWords" :class="{'text-primary': index < currentWordNum}">{{word}}</span>
</template>
<template v-if="isEnd">
<p>あなたのスコア</p>
<p>{{typingScore}}</p>
</template>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import keyCodeMap from '../master/keymap'
export default {
props: ['title', 'drill', 'categoryName'],
data: function() {
return {
countDownNum: 3, // カウントダウン用
timerNum: 30, // タイマー
missNum: 0, // ミス数
wpm: 0, // WPM
isStarted: false,
isEnd: false,
isCountDown: false,
currentWordNum: 0, // 現在回答中の文字数目
currentProblemNum: 0, // 現在の問題番号
}
},
computed: {
// 問題テキスト
problemText: function () {
return this.drill['problem' + this.currentProblemNum]
},
// 問題テキスト(配列形式)
problemWords: function () {
return Array.from(this.drill['problem' + this.currentProblemNum])
},
// 問題の解答キーコード配列
problemKeyCodes: function () {
if(!Array.from(this.drill['problem' + this.currentProblemNum]).length){
return null
}
// テキストから問題のキーコード配列を生成
let problemKeyCodes = []
console.log(Array.from(this.drill['problem' + this.currentProblemNum]))
Array.from(this.drill['problem' + this.currentProblemNum]).forEach((text) => {
$.each(keyCodeMap, (keyText, keyCode) => {
if(text === keyText){
problemKeyCodes.push(keyCode);
}
})
})
console.log(problemKeyCodes)
return problemKeyCodes
},
// 問題の文字数
totalWordNum: function () {
return this.problemKeyCodes.length
},
// タイピングスコア
typingScore: function () {
return (this.wpm * 2) * (1 - this.missNum / (this.wpm * 2))
}
},
methods: {
doDrill: function(){
this.isStarted = true
this.countDown()
},
countDown: function () {
// 効果音読み込み
const countSound = new Audio('../sounds/Countdown01-5.mp3')
const startSound = new Audio('../sounds/Countdown01-6.mp3')
this.isCountDown = true
this.soundPlay(countSound)
let timer = window.setInterval(() => {
this.countDownNum -= 1
if(this.countDownNum <= 0){
this.isCountDown = false
this.soundPlay(startSound)
window.clearInterval(timer)
this.countTimer()
this.showFirstProblem()
return
}
countSound.currentTime = 0
countSound.play()
}, 1000)
},
showFirstProblem: function () {
// 効果音読み込み
const okSound = new Audio('../sounds/punch-middle2.mp3')
const ngSound = new Audio('../sounds/sword-clash4.mp3')
const nextSound = new Audio('../sounds/punch-high2.mp3')
// 入力イベント時に入力キーと解答キーをチェック
$(window).on('keypress', e => {
console.log(e.which)
if(e.which === this.problemKeyCodes[this.currentWordNum]){
console.log('正解!!')
this.soundPlay(okSound)
++this.currentWordNum
++this.wpm
console.log('現在回答の文字数目:' + this.currentWordNum)
// 全文字正解終わったら、次の問題へ
if(this.totalWordNum === this.currentWordNum){
console.log('次の問題へ!')
++this.currentProblemNum
this.currentWordNum = 0
this.soundPlay(nextSound)
}
}else{
console.log('不正解です。。。。')
this.soundPlay(ngSound)
++this.missNum
console.log('現在回答の文字数目:' + this.currentWordNum)
}
})
},
soundPlay: function(sound){
sound.currentTime = 0
sound.play()
},
countTimer: function () {
const endSound = new Audio('../sounds/gong-played2.mp3')
let timer = window.setInterval(() => {
this.timerNum -= 1
if(this.timerNum <= 0){
this.isEnd = true
window.clearInterval(timer)
endSound.play()
}
}, 1000)
}
}
}
</script>
resources/js/master/keymap.js
export default {
"0":48,
"1":49,
"2":50,
"3":51,
"4":52,
"5":53,
"6":54,
"7":55,
"8":56,
"9":57,
"A":65,
"B":66,
"C":67,
"D":68,
"E":69,
"F":70,
"G":71,
"H":72,
"I":73,
"J":74,
"K":75,
"L":76,
"M":77,
"N":78,
"O":79,
"P":80,
"Q":81,
"R":82,
"S":83,
"T":84,
"U":85,
"V":86,
"W":87,
"X":88,
"Y":89,
"Z":90,
"a":97,
"b":98,
"c":99,
"d":100,
"e":101,
"f":102,
"g":103,
"h":104,
"i":105,
"j":106,
"k":107,
"l":108,
"m":109,
"n":110,
"o":111,
"p":112,
"q":113,
"r":114,
"s":115,
"t":116,
"u":117,
"v":118,
"w":119,
"x":120,
"y":121,
"z":122,
"@":64,
"?":63,
".":46,
",":44,
"$":36,
"&":38,
"#":35,
"!":33,
"'":39,
"\"":34,
":":58,
";":59,
"<":60,
">":62,
"/":47,
"{":123,
"}":125,
"[":91,
"]":93,
"=":61,
"-":45,
"(":40,
")":41,
"\\":92,
"+":43,
"_":95
}
効果音も適当なものをpublic配下に起きましょう。
今回は、
public/sounds配下に効果音の音源ファイルをおいています。
通常、vueファイルをjsファイルに変換(コンパイル)したり、sassをcssにコンパイルするためには、そういった「タスク」をgulpなど「タスクランナー」を使用して(package.jsonに直接書くことも出来る)コンパイルするためのコードを書いた上で、コンパイルのコマンドを実行する必要がありました。
また、タスクランナーの書き方を覚えるのも面倒です。
ですが、Laravelには「Laravel Mix」というライブラリが標準で入っていて、そういったコードを完結に書くことができます。
また、そういった基本的な設定コードはデフォルトで記述されています。
なので、あとは、下記コマンドでコンパイルするだけで済んでしまうわけです。
// 全タスク実行
npm run dev
// 全タスク実行を実行し、出力を圧縮
npm run production
// ファイルの変更を監視し、自動でタスクを実行する
npm run watch
タスクはwebpack.mix.jsに記述されている。
const mix = require('laravel-mix');
...
mix.js('resources/js/app.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css');
https://readouble.com/laravel/5.5/ja/mix.html
ちなみにログインしているかどうかやログインしているユーザーの情報取得はAuthファサードというクラスを使います。
use Illuminate\Support\Facades\Auth;
if (Auth::check()) {
// 現在認証されているユーザーの取得
$user = Auth::user();
// 現在認証されているユーザーのID取得
$id = Auth::id();
// ログアウト
Auth::logout();
}
こういった「〜ファサード」というものがLaravelではよく出てきます。「ファサード(Facade)」というのは「建物の正面」といった意味の英語で、
「アプリの正面に立ってまず処理を行う役割のクラス」
という意味合いで作られたクラスになります。
このあたりはLaravelの設計指針(概念、考え方)といったものなので、PHP言語にそういった機能があるわけではありません。
あくまで設計をしていく中で
「こういった役割のあるクラス達があるといいよね」
ということでLaravelで作られているクラスになります。
ファサードの役割は
「表に立って受け付けることで、内部の複雑な処理を隠す。(反対に呼び出す方は、内部で何をやっているかや仕組みを気にしなくて済む)」
というものです。
ファサードを簡単に例えるなら
「受付」
ですね。
例えば、Authファサードであれば、
「認証系の受け付け係」
です。
呼び出す側(受付にお願いする側)では、
Auth::user();
と呼ぶだけ(受付にお願いするだけ)で、ログインしているユーザー情報が取得出来ますね。
ですが、実際、その中では今までWEBサービス部で書いたような泥臭い何行にも渡る処理(それもまた色々なクラスがあって、それを読んだりしている)があるわけです。
ですが、
「そんな事は気にせず使える。」
それがファサードなわけですね。
「便利な執事」
のようなものですね。
「じぃや。あれをちょうだいな。」
と呼ぶだけで、やってくれるわけです。
なので、ファサードとは
「単なるクラス」
であって、
「クラスの中でも認証機能、Cookie機能、Session機能といった色々な便利な各機能ごとにまとめたもの」
というわけです。
https://readouble.com/laravel/5.8/ja/facades.html
ビューで表示させるために通常のMVCのFWでは、そのロジックをビューやコントローラー(主にコントローラー)に書いていましたが、そうすると処理が多くなるほど読みにくいコードになっていきます。
ビューにはほとんどロジックは書かずにただ「変数の中身を表示するだけ」にした方がコードが読みやすいですし、コントローラーも今まではビューに表示するために
というためのコードを毎回書いていましたが、
毎回お決まりのロジックなのであれば、書かずにだけにしたいところです。
Laravelでは、こういった悩みを解決してくれるのが「ビューコンポーザー」という機能です。
例えば、下記のようなブログの一覧と詳細画面があったとするとそこには「サイドバー」が大抵あり、どちらのアクションでも同じ情報を取得して表示する処理を書かなければいけません。
<?php
class BlogController extends Controller
{
public function index()
{
// メインコンテンツ
$posts = Post::all();
// 人気記事
$ranking_posts = Post::lanking();
// 最近の投稿
$latest_posts = Post::latest(5);
// カテゴリ
$categories = Category::all();
foreach($categories as $key => $category){
$category->count = Category::where('id', $category->id)->count();
$categories->put($key, $category);
}
return view("blog.index", compact('posts', 'ranking_posts', 'latest_posts', 'categories'));
}
public function show($post_id)
{
// メインコンテンツ
$post = Post::find($post_id);
// 人気記事
$ranking_posts = Post::lanking();
// 最近の投稿
$latest_posts = Post::latest(5);
// カテゴリ
$categories = Category::all();
foreach($categories as $key => $category){
$category->count = Category::where('id', $category->id)->count();
$categories->put($key, $category);
}
return view("blog.show", compact('post', 'ranking_posts', 'latest_posts', 'categories'));
}
}
これだと冗長的(毎回同じものを書いている=まとめられる=後々で修正が入った場合に全ての箇所を修正しなければいけない。)なので、
どっかにまとめておければいいですね。
そういった用途で使えるのが「ビューコンポーザー」です。
機能といっても、設計思想でしかなく、
「コントローラーやビューに今まで書いていた処理を別のファイルにまとめときましょうね。」
というだけのことです。
今回の場合だと「ログインしているユーザーの情報」はDrillsControllerのどのアクションでも使うことになります。
その場合、
$user = \Auth::user();
if ($user) {
echo "Hello $user->name";
}
のようなユーザー情報取得処理を毎回アクションに書くのは冗長なため、ビューコンポーザーに移してしまいましょう。
ビューコンポーザーの作り方は3STEPです。
1. サービスプロバイダクラスの作成
2. ビューコンポーザクラスの作成とcomposeメソッドの実装
3. サービスプロバイダクラスのbootメソッドでビューコンポーザクラスを利用する
ServiceProviderクラスはartisanコマンドで簡単に作れます。
「サービスプロバイダ」というのは「アプリケーションの準備段階で何かしらの処理や設定を行う」ため用のクラスとして用意されているものです。
名前はなんでも大丈夫ですが、今回は「コンポーザー」を提供するために「サービスプロバイダ」を作るので、「ComposerServiceProvider」としましょう。
(実際の実務でもそういった名前でつけられることがほとんどです)
php artisan make:provider ComposerServiceProvider
すると
app/Providers/ComposerServiceProvider.php
というファイルが出来上がります。
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class ComposerServiceProvider extends ServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
//
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
//
}
}
サービスプロバイダクラスは「ServiceProvider」というクラスを継承することで使えるようになります。
ビューコンポーザを使うには「boot()」内に処理を書いていくことになります。
ビューコンポーザークラスはapp/Http内のどこに置いても大丈夫です。
今回はHttp配下のViewComposersというディレクトリを作って、そこにはUserComposerを作りましょう。
コンポーザーは特に継承もしない普通のクラスです。
作り方は
<?php
namespace App\Http\ViewComposers;
use Illuminate\Contracts\View\View;
class UserComposer {
public function compose(View $view)
{
$view->with('user', 'ユーザー情報を第二引数へ');
}
}
となります。
今回の場合だとユーザーコンポーザーは
<?php
namespace App\Http\ViewComposers;
use Illuminate\Contracts\View\View;
use Illuminate\Contracts\Auth\Guard;
class UserComposer {
protected $auth;
public function __construct(Guard $auth)
{
$this->auth = $auth;
}
public function compose(View $view)
{
// userという変数を使えるようにし、その中に$this->auth->user()という値を詰めてビューに渡す。という定義の仕方になります
$view->with('user', $this->auth->user());
}
}
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Http\ViewComposers\UserComposer;
use Illuminate\Support\Facades\View;
class ComposerServiceProvider extends ServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
//
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
// 連想配列で渡します
// キーにコンポーザーを指定し、値にビューを指定します(ワイルドカードも使えます)
// この場合、layoutsディレクトリ配下のビューテンプレートが読み込まれた場合にUserComposerを読み込む(=$userが作られる)という設定の仕方になります
View::composers([
UserComposer::class => 'layouts.*'
]);
}
}
※コンポーザーでは、ビューで常に行われる処理だけを書きましょう
config/app.phpのprovidersにComposerServiceProviderを追加することで、ComposerServiceProviderをLaravelが自動で読み込んで実行できるようになります。
'providers' => [
...
App\Providers\ComposerServiceProvider::class,
],
app.blade.phpにヘッダーメニューが指定されているので、そこにemailを表示してみましょう。
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
{{ Auth::user()->name }} <span class="caret"></span>
</a>
となっているのを
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
{{$user->email}} <span class="caret"></span>
</a>
としてみるとコントローラーで定義していない$userが使えるようになっていますね。
ちなみにただユーザー情報を表示するだけならAuth::user()を使えばいいわけですが、ビュー側にロジック処理を書いていくのはどんどんコードが見通しづらくなります。
ビューは「ただコントローラーから受け取った値を表示するだけ」にし、ビューとコントローラーの役割を分けてあげたほうがコードが推測しやすくなりますね。
「恐らくコントローラーに書いてあるだろう」と推測して調べまくったけど実はビューにゴリゴリ書いてあった。なんて事になります。
それよりも、「ロジックに関してならコントローラーを見ればいい。」とルールが決まっていれば、分かりやすいですね。
(もちろん、今回のようにユーザー情報だけでしか全く使わないのであれば、わざわざビューコンポーザーなんてまどろっこしいものを作るよりは開発スピードもコードの見通しも楽です)
LaravelはデフォルトでSQLのログを出力してくれないので、デバッグ時に困ることがよくあります。そんな時は、
app/Providers/AppServiceProvider.phpに下記を追記しましょう。
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
# 商用環境以外だった場合、SQLログを出力させます
if (config('app.env') !== 'production') {
\DB::listen(function ($query) {
\Log::info("Query Time:{$query->time}s] $query->sql");
});
}
}
...
}
こういったように「○○サービスプロバイダ」と呼ばれるクラスがLaravelにはいくつもあります。
ファサードのようにLaravelの設計の中でついている「ある役割をもったクラスたち」のことを「サービスプロバイダー」と呼んでいます。
Laravelはサービス毎に「初期処理を定義」し、「実行」する仕組みを持っています。
その仕組や、「実際に初期処理の実装を行うクラス」のことをサービスプロバイダーと言います。
リクエストが来るたびにconfig/app.phpのproviders配列で定義されたプロバイダーが読み込まれます。(実際は、その各プロバイダーが使われる時に読み込まれます)
https://readouble.com/laravel/5.8/ja/providers.html
もっと細かく言うとサービスプロバイダーは「Service Container(サービスコンテナ)」と呼ばれるものにクラスを登録する機能を持ったものです。
(サービスコンテナは、いわゆる「DIコンテナ」と呼ばれているものです)
※サービスコンテナは「クラス間の依存性を管理する為の仕組み」で、詳しくは割愛します。
リクエストが来るたびにルーティングで設定したアクションが実行されますが、そのアクションが実行される前に読み込まれて初期処理を行う役割が「サービスプロバイダー」です。
サービスプロバイダーにはファサードと同じように役割ごとに色々なサービスプロバイダーがあり、パスワードの暗号化など暗号化サービスを提供sるEncryptionServiceProviderなどがあります。
※ちなみにファサードは「簡単にサービスコンテナに設定されたクラスを使える機能」なので、
ってことですね。
register()では、サービスコンテナへの登録を実装します。
それ以外の処理を行ってはいけません。
register() メソッドの定義は必須ですが、サービスコンテナへの登録が必要なければ、空のメソッドで構いません。
boot() では、そのサービス固有の初期処理を自由に実装できます。
サービス固有の初期処理が必要なければ、bootメソッドが無くても構いません。
boot()は、他の全てのサービスプロバイダーのregister()の実行を終えてから呼び出されます。
SQLログを取るようにした上で、app.blade.phpをちょっと変更してみましょう。
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
{{$user->email}} <span class="caret"></span>
</a>
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
{{Auth::user()->email}} <span class="caret"></span>
</a>
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
{{Auth::user()->name}} <span class="caret"></span>
</a>
いくつ書いてもDBアクセスは一回ですね。
ちなみにビューコンポーザーを使わなくても、Authファサードを使わなくても必ず1回アクセスされます。
なぜなら、ユーザー情報はどこかしこでも必ず内部で使うからですね。さらにその取得した情報がビューコンポーザーやAuthファサードでも使い回されているわけです。
使いまわせるものを1リクエスト(1画面)の中で同じ情報をDBから取ってきてしまっていては無駄ですし、ユーザーが多くなればなるほどDBへ負荷がかかることになるので注意する必要がありますが、LaravelなどFWではそのあたりもほとんど気にせずに作れてしまうわけです。
タイピングの一覧は出来ましたが、「自分が登録したものだけ」という一覧ではないので、それをマイページに表示させていきましょう。
しかし、そもそも「どのユーザーの問題か」という情報はテーブル上ではわかりません。
なので、テーブルにカラムを追加する必要があります。
php artisan make:migration add_user_id_to_drills --table=drills
既存のテーブルに変更を加える場合には、--create オプションではなく、--table オプションを使って、テーブル名を指定します。
出来たマイグレーションファイルを下記のように修正します。
レコードを残しておくとuser_idのないものが
public function up()
{
Schema::table('drills', function (Blueprint $table) {
DB::statement('DELETE FROM drills');
$table->unsignedBigInteger('user_id');
$table->foreign('user_id')->references('id')->on('users');
});
}
public function down()
{
Schema::table('drills', function (Blueprint $table) {
// 外部キー付きのカラムを削除するにはまず必ず外部キー制約を外す必要があります
$table->dropForeign(['user_id']);
$table->dropColumn('user_id');
});
}
あとはいつも通り実行するだけです。
php artisan migrate
ロールバックを確認することも忘れないようにしましょう。
またindexが貼られているかは
show index from drills;
で確認ができます。
「ログイン中のユーザーの登録した練習だけ」を取得したい場合、「テーブルの結合(JOIN)」をしたSQLを実行するんでしたね。
しかし、ORMを使う場合、そういったSQLを書く代わりに
「モデル同士のリレーション(関係性)」
を貼ることで、自動的にテーブル結合したSQLを作って実行できます。
モデル内に「テーブルとの関係性」を定義してあげるのです。
いわゆる
「1対1」「1対多」「多対多」と呼ばれているものです。
今回のようにユーザーに対して、多くの練習が紐づく場合は「1対多」になりますね。
https://readouble.com/laravel/5.8/ja/eloquent-relationships.html
その場合、まずUserモデルには下記のような設定をします。
<?php
...
class User extends Authenticatable
{
use Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'email', 'password',
];
...
public function drills()
{
return $this->hasMany('App\Drill');
}
}
function名はなんでもいいですが、慣習としてモデル名にします。このfunctionがモデルから使えるようになるので、ユーザーモデルから見てdrillは複数あるので、複数形にしておくとわかりやすいですね。
そして、hasManyメソッドで引数にモデルをAppからパス指定します。
hasManyは「多くを持つ」って意味ですね。
これで、アクション内で
$drills = User::find(1)->drills;
foreach ($drills as $drill) {
//
}
といったようにあるユーザーの練習を全て取得することができます。
反対に「ある練習のユーザー情報」を取得したい場合は、
Drill.phpに下記の設定を追記します。
<?php
...
public function user()
{
return $this->belongsTo('App\User');
}
これも、function名はなんでもいいですが、慣習としてモデル名にします。このfunctionがモデルから使えるようになるので、ユーザーは常に一人なので、単数形にしておきましょう。
そして、belongsToメソッドで引数にモデルをAppからパス指定します。
belongsToは「属する」って意味ですね。
練習情報はユーザー情報に「属している」という関係性を定義してあげるわけです。
これで、アクション内で
$drill = Drill::find(1);
echo $drill->user->email;
というように自動的にユーザー情報も取得されています。
アクシションでログインしている自分のIDを元にレコードを取得します。
ログインしているユーザーのレコードを取るのは、Authファサードからuserモデルが取得できるため、そこからリレーションを張っているdrillsモデルを操作できます。
$drills = Auth::user()->drills()->get();
なので、DrillsControllerに下記アクションを追加しましょう。
use Illuminate\Support\Facades\Auth;
...
public function mypage(){
$drills = Auth::user()->drills()->get();
return view('drills.mypage', compact('drills'));
}
そして、ルーティングも1行追記します。
Route::get('/mypage', 'DrillsController@mypage')->name('drills.mypage');
viewはmypage.blade.phpとしてindexのものをそのままコピペして使い回しましょう。
さらに「練習を登録する時」にも「どのユーザーの練習か」という情報を追加した上で登録させる必要があるので、createメソッドを編集します。
public function create(Request $request)
{
...
$drill = new Drill;
Auth::user()->drills()->save($drill->fill($request->all()));
...
}
あとは、ログインしたユーザーで再度練習を登録し、mypageを表示してみれば、そのユーザーの練習だけが表示されるようになります。
(2人以上のユーザーを作って試してみましょう。)
DrillsControllerのそれぞれのアクションも同じように変更しましょう。
public function edit($id){
...
// $drill = Drill::find($id);
$drill = Auth::user()->drills()->find($id);
return view('drills.edit', ['drill' => $drill]);
}
同様に「そのユーザーの練習に対して」CRUD処理を行なっているアクションは全て直していきましょう。
現状では、ログインをしていなくてもマイページや登録など各種機能にアクセスできてしまいますね。
なので、ログインしていないと見れないようにしましょう。
コントローラーの各アクションで「ログインしているかどうか」の判定を入れてもいいですが、冗長ですね。
そんな時Laravelではミドルウェアという機能が用意されています。
Laravelでは各コントローラーのaction実行前や実行後に何らかの処理をさせたい場合(ログインしているかを確かめ、ログインしていなければそのactionは実行しないようにするなど)のためにミドルウェアという機能が用意されています。
ミドルウェアを使うことで、FuelPHPのbefore_actionのようなことが出来ます。
ミドルウェアはアクション(エンドポイント)ごとに設定できる、ローカルミドルウェアとアプリ全体で設定できるグローバルミドルウェアがあります。
ローカルミドルウェアとするにしろ、グローバルミドルウェアにするにしろ、作るものは同じです。
まずは、そのミドルウェアを作りましょう。
php artisan make:middleware CheckLoggedIn
これで、app/Http/Middlewareの中にCheckLoggedInというミドルウェアクラスが作られます。
そこに下記のように記述します。
<?php
namespace App\Http\Middleware;
use Closure;
class CheckLoggedIn
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if (!Auth::check()) {
return redirect('login');
}
return $next($request);
}
}
もし、action実行後(リクエスト処理後)に何らかの処理をしたい場合は下記のようにします。
<?php
namespace App\Http\Middleware;
use Closure;
class CheckLoggedIn
{
public function handle($request, Closure $next)
{
$response = $next($request);
if (!Auth::check()) {
return redirect('login');
}
return $response;
}
}
あるミドルウェアをアプリケーションの全HTTPリクエストで実行したい場合は、app/Http/Kernel.phpクラスの$middlewareプロパティへ追加します。
特定のルートのみに対しミドルウェアを指定したい場合は、先ずapp/Http/Kernel.phpファイルでミドルウェアの短縮キーを登録します。
protected $routeMiddleware = [
...
'check' => \App\Http\Middleware\CheckLoggedIn::class,
];
あとはweb.phpで、それぞれの設定したいルートに対してミドルウェアを設定します。
Route::get('/mypage', 'DrillsController@mypage')->name('drills.mypage')->middleware('check');
認証系のミドルウェアは実はもうデフォルトで存在するので、ルーティングで設定をするだけで終わりになります。
Route::group(['middleware' => 'auth'], function() {
Route::get('/drills/new', 'DrillsController@new')->name('drills.new');
Route::post('/drills/new', 'DrillsController@create');
Route::get('/drills', 'DrillsController@index')->name('drills');
Route::get('/drills/{id}/edit', 'DrillsController@edit')->name('drills.edit');
Route::post('/drills/{id}/edit', 'DrillsController@update')->name('drills.update');
Route::post('/drills/{id}/delete', 'DrillsController@destroy')->name('drills.delete');
Route::get('/drills/{id}', 'DrillsController@show')->name('drills.show');
});
@componentを使って、各パーツ(コンポーネント)に分けて、読み込むようにしてみましょう。
バリデーションをコントローラ内に書くとかなり肥大化するので、laravelでは専用の「フォームリクエスト」というところに記載していきましょう。
フォームリクエストは
make:request フォームリクエスト名で作れます。
リクエストクラスは、Requests ディレクトリ内に入ってきます。
アクションで渡す引数にそのリクエストクラスを指定しましょう。
putやpatchはform内に下記を追記でできます。
@method('PUT')
※Ruby on Railsのようにaタグにそのままメソッドを追加できないので、必ずformタグを使う必要があります