- 概要
- 環境構築
- アプリケーションの作成
- データベースを扱う
- ログイン
- Laravel Mix
- メール送信
- キュー
- スケジューラー
- 画像のアップロード
- テスト
- デプロイ
- 認証
- 認証
- 検索
- スケジュール
- キュー
- メンテナンスモード
- 未整理メモ
概要
書籍「プロフェッショナルWebプログラミング Laravel」を参考にしつつ、Laravel9を検証
以下の公式ドキュメントも参考にしている
9.x Laravel
https://readouble.com/laravel/9.x/ja/
Laravel Sailについては、以下も参考になりそう
【Docker】Laravel Sailのインストールと使い方を確認 | アールエフェクト
https://reffect.co.jp/laravel/laravel-sail
Laravel9はLTSでは無くなった
リリースノート 9.x Laravel
https://readouble.com/laravel/9.x/ja/releases.html
Laravel9はLTSをやめた・・・? クライマー株式会社 | 福岡・東京
https://c-limber.co.jp/blog/2499
【Laravel9】いつまでも、あると思うな、LTS - Qiita
https://qiita.com/wadakatu/items/1038d5a020383e54ca5b
Laravelバージョン
https://laravelversions.com/ja
環境構築
■準備
\\wsl$\Ubuntu-20.04\home\refirio\docker\laravel\code
C:/windows/System32/drivers/etc/hosts
127.0.0.1 laravel.local
■インストール
インストール 9.x Laravel
https://readouble.com/laravel/9.x/ja/installation.html
Sailを使用する場合、以下のようなコマンドから始める
Sailを使用しない場合、これまでどおりcomposer create-projectから始めることもできる(詳細は上記のページを参照)
$ cd /home/refirio/docker/laravel/code
$ curl -s https://laravel.build/laravel9 | bash
curl によって必要なファイルががダウンロードされる
また、laravel9 が作成され、その中に必要なファイルも配置される
この時点で、laravel9 をGit管理対象にしておく
具体的にはSourcetreeで「Create」からフォルダを選択し、コミット対象のファイルは初期コミットとしてコミットしておく
以下を実行
$ cd laravel9
$ ./vendor/bin/sail up
15分ほどでイメージのダウンロードが完了した
引き続きDockerfileの内容により諸々が取得される
20ほどで起動した(-d を付けていないからバックグラウンドでは無い)
ブラウザから以下にアクセスして、Laravelの画面が表示された
http://localhost/
表示を確認出来たら、いったん Ctrl+C で終了する
■コマンドの調整
$ cd /home/refirio/docker/laravel/code/laravel9
$ sail
Command 'sail' not found
.profile の最後にaliasのための設定を追加
$ vi ~/.profile
コンソールを再起動し、パス指定なしでsailコマンドを実行できることを確認する
# set alias for sail
alias sail='[ -f sail ] && bash sail || bash vendor/bin/sail'
$ cd /home/refirio/docker/laravel/code/laravel9
$ sail
Laravel Sail
■起動と終了
以下でデーモンを起動できる
$ cd /home/refirio/docker/laravel/code/laravel9
$ sail up -d
以下にアクセスして表示を確認する
http://laravel.local/
以下のように操作できる
「sail shell」でシェルに入ることができるが、sailコマンド経由で直接操作することもできる
$ sail shell
$ sail php -v
$ sail composer -V
$ sail artisan -V
$ sail node -v
$ sail npm -v
$ sail mysql
$ exi
各バージョンは以下のようになっていた
$ sail php -v
PHP 8.1.27 (cli) (built: Dec 21 2023 20:19:54) (NTS)
$ sail composer -V
Composer version 2.7.2 2024-03-11 17:12:18
$ sail artisan -V
Laravel Framework 9.19.0
$ sail node -v
v18.20.0
$ sail npm -v
10.5.0
$ sail mysql
Server version: 8.0.29 MySQL Community Server - GPL
以下でデーモンを終了できる
$ sail down
■環境の調整
Sail環境の現状を確認する
タイムゾーンがUTCで、文字コードがlatin1になっている箇所があるので調整する
$ sail shell
$ date
Tue Apr 2 09:25:59 UTC 2024
$ exit
$ sail mysql
mysql> SELECT NOW();
+---------------------+
| NOW() |
+---------------------+
| 2024-04-02 09:28:17 |
+---------------------+
1 row in set (0.00 sec)
> SHOW VARIABLES LIKE '%char%';
+--------------------------+--------------------------------+
| Variable_name | Value |
+--------------------------+--------------------------------+
| character_set_client | latin1 |
| character_set_connection | latin1 |
| character_set_database | utf8mb4 |
| character_set_filesystem | binary |
| character_set_results | latin1 |
| character_set_server | utf8mb4 |
| character_set_system | utf8mb3 |
| character_sets_dir | /usr/share/mysql-8.0/charsets/ |
+--------------------------+--------------------------------+
8 rows in set (0.00 sec)
Sail環境をカスタマイズする
以下のコマンドを実行すると、dockerフォルダ内にDocker用のファイルが出力され、これを編集することでSail環境をカスタマイズできる
また docker-compose.yml も編集され、vendor/laravel/sail/runtimes/8.1 内ではなく docker/8.1 内のファイルが参照されるようになる
$ sail artisan sail:publish
以下のとおり、タイムゾーンと文字コードを変更する
docker/8.1/Dockerfile
ENV TZ=UTC
↓
ENV TZ='Asia/Tokyo'
docker/8.1/my.cnf(新規に作成)
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_bin
default-time-zone = 'Asia/Tokyo'
[client]
default-character-set = utf8mb4
docker-compose.yml
volumes:
- 'sail-mysql:/var/lib/mysql'
↓
volumes:
- 'sail-mysql:/var/lib/mysql'
- './docker/8.1/my.cnf:/etc/my.cnf'
Dockerfileを編集したので、ビルドし直す
$ sail down
$ sail build --no-cache
$ sail up -d
ビルド中に以下のエラーになった
E: Release file for http://security.ubuntu.com/ubuntu/dists/jammy-security/InRelease is not valid yet (invalid for another 3d 14h 39min 16s). Updates for this repository will not be applied.
PCとWSLの時刻差が大きいと発生するみたい
WSLの時刻を調整
$ date
Wed Apr 3 06:32:54 JST 2024
$ sudo hwclock --hctosys
$ date
Sat Apr 6 22:07:11 JST 2024
ビルド中に以下のエラーになった
npm ERR! code EBADENGINE
npm ERR! engine Unsupported engine
npm ERR! engine Not compatible with your version of node/npm: npm@10.5.0
npm ERR! notsup Not compatible with your version of node/npm: npm@10.5.0
npm ERR! notsup Required: {"node":"^18.17.0 || >=20.5.0"}
npm ERR! notsup Actual: {"npm":"8.19.4","node":"v16.20.2"}
nodeのバージョンは18もしくは20が必要となっているので、さらに以下のとおり調整して再度ビルドする
docker/8.1/Dockerfile
ARG NODE_VERSION=16
↓
ARG NODE_VERSION=18
完了したら、変更を確認する
タイムゾーンがJSTで、文字コードがutf8mb4になっていることを確認できる
$ sail shell
$ date
Sat Apr 6 22:24:09 JST 2024
$ exit
$ sail mysql
mysql> SELECT NOW();
+---------------------+
| NOW() |
+---------------------+
| 2024-04-06 22:24:38 |
+---------------------+
1 row in set (0.00 sec)
mysql> SHOW VARIABLES LIKE '%char%';
+--------------------------+--------------------------------+
| Variable_name | Value |
+--------------------------+--------------------------------+
| character_set_client | utf8mb4 |
| character_set_connection | utf8mb4 |
| character_set_database | utf8mb4 |
| character_set_filesystem | binary |
| character_set_results | utf8mb4 |
| character_set_server | utf8mb4 |
| character_set_system | utf8mb3 |
| character_sets_dir | /usr/share/mysql-8.0/charsets/ |
+--------------------------+--------------------------------+
8 rows in set (0.01 sec)
mysql> QUIT
アプリケーションの作成
■コントローラー作成の検証
$ sail artisan make:controller Sample/IndexController
以下のファイルが作成される
app/Http/Controllers/Sample/IndexController.php
<?php
namespace App\Http\Controllers\Sample;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class IndexController extends Controller
{
//
}
以下のように変更してみる
app/Http/Controllers/Sample/IndexController.php
<?php
namespace App\Http\Controllers\Sample;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class IndexController extends Controller
{
public function show()
{
return 'Hello';
}
public function showId($id)
{
return 'Hello ' . $id;
}
}
ルーティングも変更
routes/web.php
Route::get('/sample', [\App\Http\Controllers\Sample\IndexController::class, 'show']);
Route::get('/sample/{id}', [\App\Http\Controllers\Sample\IndexController::class, 'showId']);
以下にアクセスして表示を確認する
http://laravel.local/sample/
http://laravel.local/sample/10
■アプリケーションの作成
シングルアクションコントローラーを作成
$ sail artisan make:controller Article/IndexController --invokable
以下のファイルが作成される(「invoke」は、ここでは英語の「呼び出す」の意味で使われていると思われる)
app/Http/Controllers/Article/IndexController.php
<?php
namespace App\Http\Controllers\Article;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class IndexController extends Controller
{
/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function __invoke(Request $request)
{
//
}
}
以下のように変更してみる
app/Http/Controllers/Article/IndexController.php
<?php
namespace App\Http\Controllers\Article;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class IndexController extends Controller
{
/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function __invoke(Request $request)
{
return 'Article';
}
}
ルーティングも変更
routes/web.php
Route::get('/article', \App\Http\Controllers\Article\IndexController::class);
以下にアクセスして表示を確認する
http://laravel.local/article/
ビューに対応
app/Http/Controllers/Article/IndexController.php
public function __invoke(Request $request)
{
return view('article.index', [
'name' => 'Laravel',
'code' => '<script>console.log(\'Laravel\');</script>',
]);
}
resources/views/article/index.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Article</title>
</head>
<body>
<h1>Article</h1>
<p>{{ $name }}</p>
{!! $code !!}
</body>
</html>
以下にアクセスして表示を確認する
http://laravel.local/article/
データベースを扱う
■データベースの作成
$ sail mysql
mysql> SHOW DATABASES;
+--------------------+
| Database |
+--------------------+
| information_schema |
| laravel9 |
| testing |
+--------------------+
3 rows in set (0.00 sec)
mysql> USE laravel9;
mysql> SHOW TABLES;
Empty set (0.00 sec)
もともとデータベース「laravel9」が作成済みになっているので、これをそのまま利用する
■テーブルの作成
マイグレーションを作成
$ sail artisan make:migration create_articles_table
以下のファイルが作成される
database/migrations/2024_04_02_101902_create_articles_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('articles', function (Blueprint $table) {
$table->id();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('articles');
}
};
以下のとおり、マイグレーションを修正する(titleとcontentを追加する)
Schema::create('articles', function (Blueprint $table) {
$table->id();
$table->timestamps();
});
↓
Schema::create('articles', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->timestamps();
});
マイグレーションを実行する
$ sail artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_000000_create_users_table (57.38ms)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated: 2014_10_12_100000_create_password_resets_table (46.95ms)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated: 2019_08_19_000000_create_failed_jobs_table (50.15ms)
Migrating: 2019_12_14_000001_create_personal_access_tokens_table
Migrated: 2019_12_14_000001_create_personal_access_tokens_table (69.67ms)
Migrating: 2024_04_02_101902_create_articles_table
Migrated: 2024_04_02_101902_create_articles_table (25.10ms)
テーブルの作成を確認できる
$ sail mysql
mysql> USE laravel9;
mysql> SHOW TABLES;
+------------------------+
| Tables_in_laravel9 |
+------------------------+
| articles |
| failed_jobs |
| migrations |
| password_resets |
| personal_access_tokens |
| users |
+------------------------+
6 rows in set (0.00 sec)
mysql> SHOW COLUMNS FROM articles;
+------------+-----------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+-----------------+------+-----+---------+----------------+
| id | bigint unsigned | NO | PRI | NULL | auto_increment |
| title | varchar(255) | NO | | NULL | |
| content | text | NO | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+------------+-----------------+------+-----+---------+----------------+
5 rows in set (0.01 sec)
■データの作成
シーダーを作成
$ sail artisan make:seeder ArticlesSeeder
以下のファイルが作成される
database/seeders/ArticlesSeeder.php
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class ArticlesSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
//
}
}
以下のとおり編集する
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class ArticlesSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
DB::table('articles')->insert([
'title' => Str::random(20),
'content' => Str::random(200),
'created_at' => now(),
'updated_at' => null,
]);
}
}
以下のファイルを編集して、シーダーを呼び出す
database/seeders/DatabaseSeeder.php
// \App\Models\User::factory(10)->create();
$this->call([ArticlesSeeder::class]);
シーダーを実行する
$ sail artisan db:seed
Seeding: Database\Seeders\ArticlesSeeder
Seeded: Database\Seeders\ArticlesSeeder (15.77ms)
Database seeding completed successfully.
データの登録を確認できる
$ sail mysql
mysql> USE laravel9;
mysql> SELECT id, title FROM articles;
+----+----------------------+
| id | title |
+----+----------------------+
| 1 | bhkWrdUVSauoQh9h5lcX |
+----+----------------------+
1 row in set (0.00 sec)
確認できたら、いったんデータは削除しておく
mysql> TRUNCATE TABLE articles;
■モデルの作成
モデルを作成
$ sail artisan make:model Article
以下のファイルが作成される
app/Models/Article.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
use HasFactory;
}
■ファクトリーの作成
ファクトリーを作成
$ sail artisan make:factory ArticleFactory --model=Article
以下のファイルが作成される
database/factories/ArticleFactory.php
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Article>
*/
class ArticleFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition()
{
return [
//
];
}
}
以下のとおり編集する
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Article>
*/
class ArticleFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition()
{
return [
'title' => $this->faker->realText(20),
'content' => $this->faker->realText(200),
'created_at' => now(),
'updated_at' => null,
//'created_at' => $this->faker->datetime($max = 'now', $timezone = date_default_timezone_get()),
//'updated_at' => $this->faker->datetime($max = 'now', $timezone = date_default_timezone_get()),
];
}
}
日本語の設定に変更する
config/app.php
'faker_locale' => 'en_US',
↓
'faker_locale' => 'ja_JP',
以下のファイルを編集する
database/seeders/ArticlesSeeder.php
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
//use Illuminate\Support\Facades\DB;
//use Illuminate\Support\Str;
use App\Models\Article;
class ArticlesSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
//DB::table('articles')->insert([
// 'title' => Str::random(20),
// 'content' => Str::random(200),
// 'created_at' => now(),
// 'updated_at' => null,
//]);
Article::factory()->count(10)->create();
}
}
シーダーを実行する
$ sail artisan db:seed
Seeding: Database\Seeders\ArticlesSeeder
Seeded: Database\Seeders\ArticlesSeeder (171.08ms)
Database seeding completed successfully.
データの登録を確認できる
$ sail mysql
mysql> USE laravel9;
mysql> SELECT id, title, created_at, updated_at FROM articles;
+----+--------------------------------------------------------------+---------------------+------------+
| id | title | created_at | updated_at |
+----+--------------------------------------------------------------+---------------------+------------+
| 1 | かけにはないのが、ぱっと窓まどから私の。 | 2024-04-03 10:48:49 | NULL |
| 2 | かたあとだと思い出していると包つつまし。 | 2024-04-03 10:48:49 | NULL |
| 3 | のあるい実みもらだの今だってそのいらっ。 | 2024-04-03 10:48:49 | NULL |
| 4 | 覆ひおおきく天井てんじゅうに、一足さき。 | 2024-04-03 10:48:49 | NULL |
| 5 | ドをかすが少しおずおずしない水に落おと。 | 2024-04-03 10:48:49 | NULL |
| 6 | れたまって、まあ、すすんで、そのまって。 | 2024-04-03 10:48:49 | NULL |
| 7 | まにもついているんです」「お母さんも眼。 | 2024-04-03 10:48:49 | NULL |
| 8 | といつかまわりにいっぱんの方で、黒い洋。 | 2024-04-03 10:48:49 | NULL |
| 9 | ついてあらまあ、切符きっとみちがすと喧。 | 2024-04-03 10:48:49 | NULL |
| 10 | いせいよ光っていました。「ああ、孔雀く。 | 2024-04-03 10:48:49 | NULL |
+----+--------------------------------------------------------------+---------------------+------------+
10 rows in set (0.00 sec)
■記事の表示
以下のとおり編集する
app/Http/Controllers/Article/IndexController.php
<?php
namespace App\Http\Controllers\Article;
use App\Http\Controllers\Controller;
use App\Models\Article;
use Illuminate\Http\Request;
class IndexController extends Controller
{
/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function __invoke(Request $request)
{
$articles = Article::all();
return view('article.index', [
'articles' => $articles,
]);
}
}
resources/views/article/index.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Article</title>
</head>
<body>
<h1>Article</h1>
<ul>
@foreach ($articles as $article)
<li>{{ $article->id }} / {{ $article->title }}</li>
@endforeach
</ul>
</body>
</html>
以下にアクセスして表示を確認する
http://laravel.local/article/
■記事の投稿
リクエストを作成
$ sail artisan make:request Article/CreateRequest
以下のファイルが作成される
app/Http/Requests/Article/CreateRequest.php
<?php
namespace App\Http\Requests\Article;
use Illuminate\Foundation\Http\FormRequest;
class CreateRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return false;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
//
];
}
}
以下のとおり編集する
<?php
namespace App\Http\Requests\Article;
use Illuminate\Foundation\Http\FormRequest;
class CreateRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'title' => 'required|max:20',
'content' => 'required|max:200',
];
}
}
コントローラーを作成
$ sail artisan make:controller Article/CreateController --invokable
以下のファイルが作成される
app/Http/Controllers/Article/CreateController.php
<?php
namespace App\Http\Controllers\Article;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class CreateController extends Controller
{
/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function __invoke(Request $request)
{
//
}
}
以下のとおり編集する
<?php
namespace App\Http\Controllers\Article;
use App\Http\Controllers\Controller;
use App\Models\Article;
use App\Http\Requests\Article\CreateRequest;
class CreateController extends Controller
{
/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function __invoke(CreateRequest $request)
{
$article = new Article;
$article->title = $request->title;
$article->content = $request->content;
$article->save();
return redirect()->route('article.index')->with('message', '投稿しました。');
}
}
routes/web.php
Route::get('/article', \App\Http\Controllers\Article\IndexController::class);
↓
Route::get('/article', \App\Http\Controllers\Article\IndexController::class)->name('article.index');
Route::post('/article/create', \App\Http\Controllers\Article\CreateController::class)->name('article.create');
resources/views/article/index.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Article</title>
</head>
<body>
<h1>Article</h1>
@if (session('message'))
<p>{{ session('message') }}</p>
@endif
<form action="{{ route('article.create') }}" method="post">
@csrf
<dl>
<dt>タイトル</dt>
<dd>
<input type="text" name="title" value="{{ old('title', isset($article) ? $article->title : '') }}">
@error('title')
<p>{{ $message }}</p>
@enderror
</dd>
<dt>本文</dt>
<dd>
<textarea name="content" rows="5" cols="50">{{ old('content', isset($article) ? $article->content : '') }}</textarea>
@error('content')
<p>{{ $message }}</p>
@enderror
</dd>
</dl>
<p><button type="submit">投稿</button></p>
</form>
<ul>
@foreach ($articles as $article)
<li>{{ $article->id }} / {{ $article->title }}</li>
@endforeach
</ul>
</body>
</html>
config/app.php
'locale' => 'en',
'fallback_locale' => 'en',
↓
'locale' => 'ja',
'fallback_locale' => 'ja',
$ sail composer require laravel-lang/lang:~10.3
$ cp -R vendor/laravel-lang/lang/locales/ja lang/ja
lang/ja/validation.php
],
'attributes' => [
'title' => 'タイトル',
'content' => '本文',
],
];
以下にアクセスして表示を確認する
http://laravel.local/article/
■記事の編集
※コントローラーを Update 内に置くか否かは考えたい
※登録処理も含めて、この時点では authorize() を false にしておく方が自然か。検証したい
ファイルを作成
$ sail artisan make:request Article/UpdateRequest
$ sail artisan make:controller Article/EditController --invokable
$ sail artisan make:controller Article/UpdateController --invokable
以下のとおり編集する
app/Http/Requests/Article/UpdateRequest.php (変更箇所は CreateRequest.php と同じ)
<?php
namespace App\Http\Requests\Article;
use Illuminate\Foundation\Http\FormRequest;
class UpdateRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'title' => 'required|max:20',
'content' => 'required|max:200',
];
}
}
app/Http/Controllers/Article/EditController.php
<?php
namespace App\Http\Controllers\Article;
use App\Http\Controllers\Controller;
use App\Models\Article;
use Illuminate\Http\Request;
class EditController extends Controller
{
/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function __invoke(Request $request)
{
$id = (int) $request->route('id');
$article = Article::where('id', $id)->firstOrFail();
return view('article.edit', [
'article' => $article,
]);
}
}
app/Http/Controllers/Article/UpdateController.php
<?php
namespace App\Http\Controllers\Article;
use App\Http\Controllers\Controller;
use App\Models\Article;
use Illuminate\Http\Request;
class UpdateController extends Controller
{
/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function __invoke(Request $request)
{
$article = Article::where('id', $request->id)->firstOrFail();
$article->title = $request->title;
$article->content = $request->content;
$article->save();
return redirect()->route('article.edit', ['id' => $request->id])->with('message', '編集しました。');
}
}
resources/views/article/edit.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Article</title>
</head>
<body>
<h1>Article</h1>
<p><a href="{{ route('article.index') }}">戻る</a></p>
@if (session('message'))
<p>{{ session('message') }}</p>
@endif
<form action="{{ route('article.update', ['id' => $article->id]) }}" method="post">
@method('PUT')
@csrf
<dl>
<dt>タイトル</dt>
<dd>
<input type="text" name="title" value="{{ old('title', isset($article) ? $article->title : '') }}">
@error('title')
<p>{{ $message }}</p>
@enderror
</dd>
<dt>本文</dt>
<dd>
<textarea name="content" rows="5" cols="50">{{ old('content', isset($article) ? $article->content : '') }}</textarea>
@error('content')
<p>{{ $message }}</p>
@enderror
</dd>
</dl>
<p><button type="submit">編集</button></p>
</form>
</body>
</html>
resources/views/article/index.blade.php
<li>{{ $article->id }} / {{ $article->title }}</li>
↓
<li>{{ $article->id }} / {{ $article->title }} <a href="{{ route('article.edit', ['id' => $article->id]) }}">編集</a></li>
routes/web.php
Route::get('/article/edit/{id}', \App\Http\Controllers\Article\EditController::class)->name('article.edit')->where('id', '[0-9]+');
Route::put('/article/update/{id}', \App\Http\Controllers\Article\UpdateController::class)->name('article.update')->where('id', '[0-9]+');
以下にアクセスして表示を確認する
http://laravel.local/article/
■記事の削除
ファイルを作成
$ sail artisan make:controller Article/DeleteController --invokable
以下のとおり編集する
app/Http/Controllers/Article/DeleteController.php
<?php
namespace App\Http\Controllers\Article;
use App\Http\Controllers\Controller;
use App\Models\Article;
use Illuminate\Http\Request;
class DeleteController extends Controller
{
/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function __invoke(Request $request)
{
$id = (int) $request->route('id');
$article = Article::where('id', $id)->firstOrFail();
$article->delete();
return redirect()->route('article.index')->with('message', '削除しました。');
}
}
resources/views/article/edit.blade.php
<form action="{{ route('article.delete', ['id' => $article->id]) }}" method="post">
@method('DELETE')
@csrf
<p><button type="submit">削除</button></p>
</form>
routes/web.php
Route::delete('/article/delete/{id}', \App\Http\Controllers\Article\DeleteController::class)->name('article.delete');
ログイン
Laravel Breezeをインストールする
※「sail artisan breeze:install」によって routes/web.php の既存コードが消えてしまうので、バックアップを取っておいて復元する
$ sail composer require laravel/breeze --dev
$ sail artisan breeze:install
フロントエンドのコードビルドする
※実行しなければ、ログイン画面へアクセスしたときに「Vite manifest not found at: /var/www/html/public/build/manifest.json」のエラーになる
※「sail npm run dev」を実行すると、開発用にビルドされる?
画面に表示されるURLでアクセスしなおす必要がある?
$ sail npm install
$ sail npm run build
画面右上に、ログインとユーザ登録のためのリンクが追加される
ユーザ登録を行い、その情報でログインができることを確認しておく
★Lravelが書き出す auth.php のコードは、コントローラーに1つのメソッドしか持たない書き方では無い
それなら、自分で各コードもそれに寄せるか
以下のようにすると、ログインしていないと投稿できないようになる
Route::post('/article/create', \App\Http\Controllers\Article\CreateController::class)->name('article.create');
↓
Route::post('/article/create', \App\Http\Controllers\Article\CreateController::class)->middleware('auth')->name('article.create');
以下のようにすると、ログインしているか否かで画面の表示を切り替えられる
@auth
<p>ログインしています。</p>
@else
<p>ログインしていません。</p>
@endauth
Laravel Mix
環境を確認
$ sail node -v
v18.20.0
$ sail npm install
開発用にビルドする場合
$ sail npm run dev
公開用にビルドする場合
$ sail npm run build
Tailwind CSSを導入(Laravel Breezeを導入した場合、一緒に導入されている)
$ sail npm install -D tailwindcss
$ sail npx tailwindcss init
ヘッダタグ内に以下を記述すると、CSSを読み込める
@vite(['resources/css/app.css', 'resources/js/app.js'])
一例だがbody内の要素全体を以下で囲うと、画面の見た目が変化する
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100">
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white shadow-md overflow-hidden sm:rounded-lg">
</div>
</div>
メール送信
Laravel Sailには、MailHogが導入されている
mailhog:
image: 'mailhog/mailhog:latest'
ports:
- '${FORWARD_MAILHOG_PORT:-1025}:1025'
- '${FORWARD_MAILHOG_DASHBOARD_PORT:-8025}:8025'
networks:
- sail
.env でデフォルトの送信元アドレスを設定する
MAIL_FROM_ADDRESS="hello@example.com"
以下からメールを送信できる
http://laravel.local/forgot-password
以下にアクセスすると、受信したメールを確認できる
http://laravel.local:8025/
キュー
※未検証
以下でジョブを作成できる
$ sail artisan make:job SampleJob
.env でデータベースを指定する
QUEUE_CONNECTION=database
以下でテーブルを作成
$ sail artisan queue:table
$ sail artisan migrate
以下でジョブを実行
$ sail artisan queue:work
スケジューラー
※未検証
以下でコマンドを作成できる
$ sail artisan make:command SampleCommand
以下でコマンドを実行
$ sail artisan sample-command
app/Console/Kernel.php に登録し、以下でスケジューラーを実行
$ sail artisan schedule:run
画像のアップロード
※未検証
テスト
Laravel×テスト駆動開発で初テストを書いてみた - Qiita
https://qiita.com/makimoch/items/7388b17aa8c13f638e4c
$ sail test
デプロイ
サーバ要件やnginxの設定など、以下のページが参考になる
デプロイ 9.x Laravel
https://readouble.com/laravel/9.x/ja/deployment.html
認証
Laravel の認証・認可パッケージが多すぎてわけわからんので図にまとめた - Qiita
https://qiita.com/mpyw/items/c944d4fcbb45c1a3924c
自前で認証機能を実装する場合、
・初心者のポートフォリオ制作などでは Breeze が最適
・一般的な商用ユースケースには Fortify が最適
・JetStream は積極的に使うべきではないが、「管理画面用途であるため UI デザインは関心がない」かつ「Breeze では機能不足」というときには有用
らしい
それぞれ実際に試したい
認証
composerからAdminLTEをインストールできるみたい
管理画面は基本的にこれを使えば良さそう
Laravel-AdminLTEの導入手順 - Qiita
https://qiita.com/sasao3/items/bde9cc9b8336b7724002
【Laravel】AdminLTEを導入 | チグサウェブ
https://chigusa-web.com/blog/laravel-adminlte/
検索
Laravel9の話というわけでは無いが、以下は参考にできそう
Laravelでほんの少しハイレベルな検索機能を作ってみた。(初心者向け) - Qiita
https://qiita.com/howaito01/items/7c7ce20410b29337ac63
意外と簡単!Laravel で全文検索をつくる(Laravel Scout + Algolia) - console dot log
https://blog.capilano-fw.com/?p=3843
以下も参考にできるか
新刊『検索システム ― 実務者のための開発改善ガイドブック』のお知らせ - 技術書出版と販売のラムダノート
https://www.lambdanote.com/blogs/news/ir-system
スケジュール
タスクスケジュール 9.x Laravel
https://readouble.com/laravel/9.x/ja/scheduling.html
Laravel5のときの検証した内容が以下にある
Laravel.txt
以下のようにすると、フォアグラウンドで1分ごとにスケジュールが実行される
ローカルでの動作確認などに使える
$ php artisan schedule:work
キュー
キュー 9.x Laravel
https://readouble.com/laravel/9.x/ja/queues.html
SupervisorでLaravelのQueue worker管理 | ソフトウェア開発のギークフィード
https://www.geekfeed.co.jp/geekblog/laravel_queue_with_supervisor/
supervisorとLaravelのQueueを使ってみた - Qiita
https://qiita.com/kenta_kitagawa/items/c9144fe0e33e4e98d844
バックグラウンド処理のため、「php artisan queue:work」を使用することができる
永続的に実行させ続けるためには、Supervisorなどのプロセスモニタを使用して、キューワーカの実行が停止しないようにする必要がある
Supervisorについては、以下ファイルの「バックグラウンドで処理を実行する(Supervisor)」を参照
Dropbox\サーバ\Etcetera.txt
メンテナンスモード
【Laravel】メンテナンスモードが便利すぎるので広めたい!10分でマスターできます。 | Web Apps Labo
https://e-seventh.com/laravel8-maintenance-mode/
以下のコメントでメンテナンスモードになる
$ php artisan down
以下のコメントでメンテナンスモードが解除される
$ php artisan up
メンテナンス中は以下のファイルを作成することで、任意のメンテナンス画面を表示できる
resources/views/errors/503.blade.php
なお、メンテナンス中はCronによるスケジュールが動かなくなるので注意
以下のように、「スケジュールされたコマンドは無い」となる
$ php artisan schedule:run
No scheduled commands are ready to run.
未整理メモ
■Service層とRepository層
LaravelでService層とRepository層を取り入れる | システム開発部Blog
https://enjoyworks.jp/tech-blog/7743
・Service層を持つことで、クラスの責任範囲を分けることができる。業務ロジック単位で扱う
・Repository層を持つことで、処理の差し替えやテストを容易にする。データベーステーブル単位で扱う
Repositoryをモック化すると、テストを容易にできる
案件によっては、そんな思想で作られている
色々参考にしつつ、最初にそんなレールを敷いたが、案件規模によっては無駄に複雑ではある
単体テストなど厳密に書いているわけでは無いし
案件の規模に応じて「コントローラー → サービス → モデル」や「コントローラー → モデル」でも問題無いと思う
リポジトリパターンについては、以下なども参考になる
LaravelでRepositoryパターンを実装する-入門編-
https://www.ritolab.com/entry/165
Laravelアプリケーションでリポジトリパターンを使う方法
https://www.twilio.com/blog/repository-pattern-in-laravel-application-jp
「例えば、注文管理をサードパーティアプリケーションにアウトソースする場合」
で処理を差し替える場合などが大きなメリットだと思う
ただし現実的には「注文管理をサードパーティアプリケーションにアウトソースしますが、それに伴いビューとかCSSも変わります。新規の機能も追加します」とかはありそうなので、そんな単純にはいかないとは思う
また
「小規模のプロジェクトでは、このアプローチは多くの作業が必要で」
というデメリットもあるので、ケースバイケースではある
■サービスコンテナ
サービスコンテナの仕組みに乗っておけば、処理の差し替えは契約インターフェイスの紐付けを変えるだけで対応できる?ただし小規模アプリケーションの場合は、無駄に複雑になる感は否めないが
サービスプロバイダで処理の紐づけを変更できる
■リポジトリパターン考察
「例えば、注文管理をサードパーティアプリケーションにアウトソースする場合」
で処理を差し替える場合などが大きなメリットだと思う
とするなら、リポジトリにwhereを長々書くのは思想的におかしいかもしれない
とは言え、柔軟な検索をどう実現すべきか、そもそも汎用的な検索メソッドではなく、専用の検索メソッドを用意すべきなのかもしれない
interfaceの継承がアリなら、契約用のインターフェイスはすべて共通の親クラスを作るか
以下などによると、クラス設計的にはおかしなことは無さそう
【PHP】interfaceは継承・多重継承が可能。読み込みの順番もあり?【697日目】 - エンジニアのひよこ_level10
https://www.nyamucoro.com/entry/2019/09/11/012246
ただし以下によると、機械的にCRUDを実装するのは良くないらしい。とは言え、同じ内容のインターフェイスが量産されるのも微妙だが
Laravel におけるリポジトリ実装のポイント - Shin x Blog
https://blog.shin1x1.com/entry/laravel-repository
「3. 機械的に CRUD メソッドを実装しない」
「機械的に CRUD を付けるというのは、データベーステーブルに対応するリポジトリを実装するという発想から来るものでしょう。そうではなく、リポジトリでは、対象のドメインデータが永続化層にどのような操作が必要かを考えて、それのみを実装すると良いです。」
CRUDな契約をinterfaceで定義して、その実装を抽象クラスとして実装するのはどうか
そういう書き方が駄目なら、以前検証したとおりリポジトリごとにインターフェイスを作るか
画一的に書くべきで無いなら、参照登録編集削除crudもしくは参照専用master、とか
外部APIであっても、基本的にはそのどちらかになるはず
PHPにおけるインターフェースと抽象クラス、多重継承、トレイトの使い方:PHPオブジェクト指向プログラミング入門(5)(1/2 ページ) - @IT
https://atmarkit.itmedia.co.jp/ait/articles/1511/02/news016.html
運用時と開発時で、データの取得内容を差し替えられる
また、リポジトリとモデルは対になることが多いが、それは絶対ではない
常に別のリポジトリ経由でモデルを扱うことも考えられる
リポジトリパターンと Laravel アプリケーションでのディレクトリ構造 - Qiita
https://qiita.com/karayok/items/d7740ab2bd0adbab2e06
「AppServiceProvider で環境ごとに注入する Repository を変更します。」
「これにより、テスト環境のときはデータの取得方法を変更することができます。」
「Model と Repository は 1:1 とは限りません。」
■その他
Service層とRepository層を作る意味
具体例はLaravel6を参照
ただし後述しているように、Repository層は無くてもいいかもしれない
以下は公式の説明だが、あまり解りやすいとは思わない
サービスコンテナ 9.x Laravel
https://readouble.com/laravel/9.x/ja/container.html
【Laravel】サービスコンテナとは?2つの強力な武器を持ったインスタンス化マシーン。簡単に解説。 - Qiita
https://qiita.com/minato-naka/items/afa4b930a2afac23261b
Laravel6.txt
https://refirio.org/memos/technology/?file=Laravel6.txt
以前の勉強メモ
「サービスとリポジトリを作成」部分を参照
以下、Laravel経験者の方の意見
ORMではEloquentとCollectionを使っている
Eloquentの準備 9.x Laravel
https://readouble.com/laravel/9.x/ja/eloquent.html
コレクション 9.x Laravel
https://readouble.com/laravel/9.x/ja/collections.html
…がRepositoryがあることで、この恩恵を受けにくくなっている
Repositoryをしっかり設計して思想を統一することで改善できるかもしれないが、中規模での開発なら
「コントローラー → サービス → リポジトリ → モデル」
よりも
「コントローラー → サービス → モデル」
の方がいいのでは
非常に大規模な開発ならリポジトリ層が活きるかもしれないが、それならLaravelではなくSymfonyの方がいいかもしれない
Laravelは色々簡単にできるようになっているが、Laravelの恩恵を受けるなら、リポジトリパターンは向いていないのでは
検索の一部の処理だけ使っていたり、はある
「こういう条件ではこういうwhereを書く」のような分岐処理など
ただ、これはサービス層で吸収できるかもしれない
以下も参考になりそう
5年間 Laravel を使って辿り着いた,全然頑張らない「なんちゃってクリーンアーキテクチャ」という落としどころ
https://zenn.dev/mpyw/articles/ce7d09eb6d8117