- 投稿日:2021-12-05T23:52:06+09:00
Laravel Activity Logを使って簡単に変更履歴機能を実装しよう
はじめに 変更履歴の機能を実装するのって、考慮する事も多いし、大変ですよね? Laravel Activity Logを使うと、簡単に変更履歴の機能を実装できます。 この記事では、laravel-activitylogをどのように導入するか説明します。 やりたい事 データの作成・更新・削除のユーザの動作に対して履歴を残したい。 変更履歴データを画面に表示したい。 使うライブラリ laravel-activitylog https://github.com/spatie/laravel-activitylog 実装手順 1.下記のコマンドを実行する。 ライブラリのインストール $ composer require spatie/laravel-activitylog $ php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="activitylog-migrations" 2.マイグレーションの実行 migrateを実行してactivity_logテーブルを作成します。 <?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateActivityLogTable extends Migration { /** * Run the migrations. */ public function up() { Schema::connection(config('activitylog.database_connection'))->create(config('activitylog.table_name'), function (Blueprint $table) { $table->bigIncrements('id'); $table->string('log_name')->nullable(); $table->text('description'); $table->nullableMorphs('subject', 'subject'); $table->nullableMorphs('causer', 'causer'); $table->json('properties')->nullable(); $table->timestamps(); $table->index('log_name'); }); } /** * Reverse the migrations. */ public function down() { Schema::connection(config('activitylog.database_connection'))->dropIfExists(config('activitylog.table_name')); } } $ php artisan migrate 3.Modelの修正 useでLogsActivityを指定するのと、$logAttributesの配列に変更履歴に残したいカラムを指定します。 <?php /** * Class User * @package App\Entities\Models */ class User extends Authenticatable { + use LogsActivity; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ 'name', 'name_kana', 'tel', 'is_active', 'suspended_at', 'access_permission', ]; + /** + * @var array + */ + protected static $logAttributes = [ + 'name', + 'name_kana', + 'tel', + 'is_active', + 'suspended_at', + 'access_permission', + ]; 4.Controllerの修正 activity()で対象のModel指定します。 causedBy()で更新作業を実行するユーザを指定します。 log()でDBのdescriptionに入る内容を定義します。 <?php /** * Class UserProfilesController * @package App\Http\Controllers */ class UserProfilesController extends Controller { /** * @param ProfileUpdateRequest $request * @param User $user */ public function update(ProfileUpdateRequest $request, User $user) { $loginUser = auth()->user(); $params = $request->validated(); DB::transaction(function () use ($user, $params, $loginUser) { + activity('user') + ->causedBy($loginUser)->log("{$loginUser->name}さんが{$user->name}さんのユーザ詳細情報を更新"); + $this->updateUserProfile($user, $params); }); 登録されるデータの例 +----+----------+-------------------------------------+---------------------+------------+--------------+-----------+-------------+-------------------------------------+---------------------+---------------------+ | id | log_name | description | subject_id | subject_type | causer_id | causer_type | properties | created_at | updated_at | +----+----------+-----------------------------------------------------------------+------------+--------------+-----------+-------------+-------------------------------------+---------------------+---------------------+ | 1 | user | "鈴木さんがユーザ1さんのユーザ詳細情報を更新" | 1 | App\Entities\Models\User | 1 | App\Entities\Models\User | {"business_name":"Best Restaurant"} | 2021-08-04 14:58:06 | 2021-08-04 14:58:06 | +----+----------+-----------------------------------------------------------------+------------+--------------+-----------+-------------+-------------------------------------+---------------------+---------------------+ あとはDBに登録されたデータを取得して、表示させたい画面に表示させればOKです!
- 投稿日:2021-12-05T19:27:55+09:00
Laravel Eloquent Model Factory の今昔
この記事について Laravel Advent Calendar 2021の6日目の記事です。もうすぐ 9 がリリースされるタイミングではありますが、私自身が最近ようやく 8 を使い始めた関係で 8 関連の内容になっております。 さて、Laravel 8 で変わった Eloquent Model Factory ですが、Laravel 7 以前のバージョンからアップグレードするには厳しすぎるほどの大幅変更になっております。本記事では、これまでの方式と新しい方式がどう違うのか、かいつまんで解説します。最後のほうで Model Factory を旧来の方式で使い続けられるよう取り計らってくれるライブラリもご紹介します。 はじめに 概要 Model Factory はほぼ完成されたと思っていたので、正直、ここまで大幅な変更コストに見合ったベネフィットがあるとは思えないんですが、変わってしまったものは仕方がないと、割り切って追従していくしかありません。概念的にはそれほど大きな違いはなく、できることもそんなには増えていないので、これまでのメンタルモデルを微調整して臨めば、わりとすんなり使いこなせるのではないかという感じはします。 7 までの書き方を「旧方式」、8 以降の書き方を「新方式」と記述します。 環境 Laravel 7: 7.30.5 Laravel 8: 8.55.0 PHP: 8.0.13 参考 Model Factory の実用的(?)な使い方については下記の記事も参考にしてください。 Eloquent Model Factory を使ってテストデータを整備する - Qiita 宣言と利用 Faker を使いデフォルトで生成されるアトリビュートを定義しておき、クライアント側では個々のアトリビュートを記述せずにインスタンス(レコード)を生成できるようになっています。 旧方式 // database/factories/UserFactory.php /** @var \Illuminate\Database\Eloquent\Factory $factory */ $factory->define(User::class, function (Faker $faker) { return [ 'name' => $faker->name, // ... ]; }); // client factory(User::class)->create(); autoload は classmap 方式 したがって名前空間はなし クラスではない 呼び出し側で require されるため $factory が各ファクトリで共用される Faker をクロージャの引数で受け取る 使用する際はヘルパ関数を使う 新方式 // database/factories/UserFactory.php class UserFactory extends Factory { protected $model = User::class; public function definition() { return [ 'name' => $this->faker->name, // ... ]; } } // client User::factory()->create(); autoload は psr-4 方式 したがって名前空間あり クラスである Faker はプロパティに持っている 使用する際はモデルの静的関数を使う 違い 基本的には変わりはなく、宣言も利用の仕方も表現の違いでしかないです。詳しくは後述しますが、クラスになったことで表現の幅が広がっており、とりわけリレーションの生成がしやすくなっている感じはします。 ステート 状態に応じて定義するアトリビュートを上書きする機能です。例として、TODO管理アプリケーションにおけるタスクの状態(todo, doing, done, etc.)を指定するユースケースで比較します。status というアトリビュートに enum 的な値が入ります。まぁ、単一のアトリビュートであれば、生成時に指定してしまってもぜんぜんいいと思うので、より良いユースケースとしては、複数のアトリビュートが連動するとか、子のモデルも状態に応じて変化するとか、そういうケースのほうがいいでしょう。ここでは使い方がわかればいいのであまり適切でない例かもしれませんがご了承ください。 旧方式 // database/factories/TaskFactory.php $factory->state(Task::class, 'done', function () { return [ 'status' => 'done', ]; }); // client factory(Task::class)->states(['done'])->create(); ステートは文字列で表現 複数のステートを組み合わせる場合は配列の要素を増やす 新方式 // class TaskFactory public function done() { return $this->state(function () { return [ 'status' => 'done', ]; }); } // client Task::factory()->done()->create(); ステートは関数で表現 複数のステートを組み合わせる場合はメソッドチェーンで行う 関数なので引数を渡すことができる 3番目の変更はメリットがあるかもしれないですが、個人的にはこれまで特に不便に感じてなかったので、どうなんだろう、という気はしてます。 違い ステートが関数(メソッド)になったことで、自由度が増しています。後述する after コールバックを使わずに、ステートに応じたリレーションの設定もしやすくなっています。惜しむらくは、ボイラープレート $this->state(function () { ... }) を毎回書かなければならず、旧方式では単に配列を返せばよかったので、記述量は増えてしまっています。メソッドチェーンができるようになったメリットはあるので、悩ましいですね。 after コールバック インスタンス(レコード)が生成されるタイミングで自動的に呼ばれるコールバックを作成することで、暗黙的に追加でデータ操作(主に関連するテーブルのデータ生成)を行うことができます。afterCreating と afterMaking がありますが、利用頻度で言えば afterCreating のほうが多いと思うので、afterMaking は省略します。新方式では、新たに追加された has メソッドも取り上げます。例として、Task と HasMany な関係の Comment を一緒に生成するケースで比較してみましょう。 旧方式 // 1. デフォルトでリレーションを自動的に作成したいケース // TaskFactory.php $factory->afterCreating(Task::class, function (Task $task) { $task->comments()->save(factory(Comment::class)->make()); }); // client // 必ず Comment が作成される factory(Task::class)->create(); // 2. 動的にリレーションを作成したいケース // TaskFactory.php $factory->afterCreatingState(Task::class, 'hasComment', function (Task $task, Faker $faker) { $task->comments()->save(factory(Comment::class)->make([ 'user_id' => $task->created_by, 'body' => $faker->sentence, ])); }); // client // hasComment ステートのときだけ Comment が作成される factory(Task::class)->states(['hasComment'])->create(); // 複数件つくりたい場合はステートの識別子を件数分渡す factory(Task::class)->states(array_fill(0, 2, 'hasComment'))->create(); Factory 側で after コールバックを定義する必要がある ステートに応じてリレーションを作成したい場合は afterCreatingState を使う 複数件つくりたい場合はステート識別子を複数回渡す リレーション側のアトリビュートは上書きできない 新方式 // 1. デフォルトでリレーションを自動的に作成したいケース // TaskFactory.php public function configure() { return $this->hasComments(); // 以下と同等 // return $this->afterCreating(function (Task $task) { // $task->comments()->save(Comment::factory()->make()); // }); } // client // 必ず Comment が作成される Task::factory()->create(); // 2. 動的にリレーションを追加したいケース // Model 側に HasMany を返す comments メソッドが定義されていれば、別途コールバックを定義する必要はない // client Task::factory()->hasComments()->create(); // 複数件つくりたい場合は第1引数で件数を指定する(第2引数にはリレーション側のアトリビュートを指定可能) Task::factory()->hasComments(2, ['user_id' => $user->id])->create(); // アトリビュートはクロージャで定義することもできる Task::factory() ->hasComments(2, function (array $attributes, Task $task) { return [ 'user_id' => $task->created_by, 'body' => "{$attributes['body']} for $task->subject", ] }) ->create(); // 3. ステートに応じて動的にリレーションを追加したいケース // Task が「完了した(done)」場合は Comment が自動的に「解決(resolved)」になるものとします // TaskFactory.php public function done() { return $this ->state(function () { return [ 'status' => 'done', ]; }) ->has(Comment::factory()->resolved(), 'comments'); // 以下と同等 // ->afterCreating(function (Task $task) { // $task->comments()->save(Comment::factory()->resolved()->make()); // }); } // client Task::factory()->done()->create(); Model 側にリレーションメソッドが定義されていれば after コールバックを定義する必要はない ステートに応じてリレーションを作成したい場合はメソッドチェーンに has または has{Relation} で繋げるか、ステートメソッドの中にそのまま書けばいい 複数件つくりたい場合はメソッドチェーンを件数分繋げるか、件数を引数で渡す リレーション側のアトリビュートを上書きできる has{Relation} メソッドはマジックメソッドで、命名規約があります。 {Relation} の部分は、Model 側で定義されたリレーションメソッドをアッパーキャメルケースに変換した文字列(先頭を大文字)にしなければなりません。 新方式におけるリレーション生成の記述については Many to Many なテーブル構造のケースもありますが、基本的には同じなので省略します。 違い 新方式でリレーション(子)を生成できる has および has{Relation} メソッドが増えたことにより、after コールバックの利用頻度は下がるでしょう。クライアントから呼び出せるので子のステートを指定したりアトリビュートを上書きしたりできるようになったので自由度も増しています。自動テストで使用する際にはできるだけボイラープレートを排除したいので、 データセットの状態に応じて自動的にリレーションを生成できる機能は重宝しますが、状態が多くなるとけっきょくクライアント側で生成したほうが楽という側面もあったりするので(特に階層が深くなると新方式でも辛いと思います)、どれほど活用できるかはいかに状態の定義を明確にできるかにかかってる気はします。 親テーブルの外部キー指定 前項で HasOne、HasMany なリレーションのケースをみましたが、逆に親テーブルの外部キーを持っている(BelongsTo)ケースについてはどうでしょう?例として、Task の登録者(created_by: NOT NULL)および担当者(assigned_to: NULLABLE)に特定のユーザーを指定するケースで比較します。 旧方式 // TaskFactory.php $factory->define(Task::class, function (Faker $faker) { return [ // ... // 指定されていなければ生成するパターン 'created_by' => function () { return factory(User::class)->create(); }, // 必ず明示的に指定してほしいので null にしておくパターン 'assigned_to' => null, ]; }); // client $users = factory(User::class, 2)->create(); factory(Task::class)->create(['created_by' => $users[0], 'assigned_to' => $users[1]]); // User から Task へのリレーションメソッドが定義されていれば $users[1]->assignedTasks()->save(factory(Task::class)->make(['created_by' => $users[0]])); // もしくは $users[1]->assignedTasks()->create(factory(Task::class)->raw(['created_by' => $users[0]])); 基本的には create/make の際のアトリビュートで指定する リレーションメソッドが定義されていれば、save/create でひもづけすることもできる 新方式 // Task.php public function creator(): BelongsTo { return $this->belongsTo(User::class, 'created_by'); } public function assignee(): BelongsTo { return $this->belongsTo(User::class, 'assigned_to'); } // TaskFactory.php public function definition() { return [ // ... // 指定されていなければ生成するパターン 'created_by' => function () { return User::factory()->create(); }, // 必ず明示的に指定してほしいので null にしておくパターン 'assigned_to' => null, ]; } // client $users = User::factory(2)->create(); // create/make の引数で指定するパターン Task::factory()->create(['created_by' => $users[0], 'assigned_to' => $users[1]]); // 既存のインスタンスとひもづけるパターン Task::factory() ->for($users[0], 'created_by') ->for($users[1], 'assigned_to') ->create(); // または新たに親レコードを生成してひもづけるケース(has 同様リレーションメソッドの名前を使う) Task::factory() ->forCreator(['name' => 'creator']) ->forAssignee(['name' => 'assignee']) ->create(); // 以下と同等 Task::factory() ->for(User::factory()->create(['name' => 'creator']), 'creator') ->for(User::factory()->create(['name' => 'assignee']), 'assignee') ->create(); // User から Task へのリレーションメソッドが定義されていれば $users[1]->assignedTasks()->save(Task::factory()->make(['created_by' => $users[0]])); // もしくは $users[1]->assignedTasks()->create(Task::factory()->raw(['created_by' => $users[0]])); create/make の引数でも指定できるし、for / for{Relation} メソッドを使ってもいい リレーションメソッドが定義されていれば、save/create でひもづけすることもできる 違い 新方式でやり方が増えましたが、どちらもアトリビュートに直接指定する方法は使えるので、大差ないといえば大差ないですね。 新たに追加された for メソッドは、それほど使いたいシチュエーションがない気がします。親テーブルの外部キー指定は単に外部キーをセットするだけなので、アトリビュート内に直接指定で事足りますよね。同じことが実現できるやり方を複数用意しても選択するコストが増えるだけなので、基本的にはアトリビュートに直接指定方式を今後も使っていきたいと思います。 8 でも旧方式を使うためのパッケージ laravel/legacy-factories について 新規で 8 を使うのであれば新方式でいいと思うんですが、7 からのアップグレードする場合は、移行するコストをかけてまで得られるベネフィットがあるかというと微妙なので、laravel/legacy-factories を入れて旧方式を使い続けるという選択肢はありかな、と思います。ただ、いずれ旧方式が deprecated になりこのパッケージがメンテされなくなる可能性もなくはないので、緩やかに移行していく、というのが最適解な気がします(もちろん、時間とリソースが許すなら一気にやってしまうのもありですが)。 以下に、Model Factory 周りのアップグレードで最低限やっておく必要がある作業を列挙します。新旧方式は共存できるので、一気に移行できない場合は少しずつ移行していくといいでしょう。 1. 7 -> 8 のアップグレードが完了したら、laravel/legacy-factories を追加します # composer require laravel/legacy-factories 2. database/factories, database/seeds 周りを手動で書き換えていきます まずは composer.json の autoload を以下のように書き換えます。 json "autoload": { "psr-4": { - "App\\": "app/" + "App\\": "app/", + "Database\\Factories\\": "database/factories/", + "Database\\Seeders\\": "database/seeders/" }, "classmap": [ - "database/seeds", "database/factories" ] }, 一気に移行しない場合は classmap にある database/factories を残しておいてください。また、7.x だとシーダのディレクトリ名が seeds になっているので 8.x に合わせて seeders に変更します(seeds のままでも問題はありませんが)。 3. すべてのシーダクラスに名前空間を付与します <?php // 以下の行を追加 namespace Database\Seeders; class DatabaseSeeder extends Seeder 4. 移行したいファクトリをクラス化します(UserFactory を例に) before <?php /** @var \Illuminate\Database\Eloquent\Factory $factory */ use App\Models\User; use Faker\Generator as Faker; use Illuminate\Support\Str; $factory->define(User::class, function (Faker $faker) { return [ 'name' => $faker->name, 'email' => $faker->unique()->safeEmail, 'email_verified_at' => now(), 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 'remember_token' => Str::random(10), ]; }); after <?php namespace Database\Factories; use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Str; class UserFactory extends Factory { protected $model = User::class; public function definition() { return [ 'name' => $this->faker->name, 'email' => $this->faker->unique()->safeEmail, 'email_verified_at' => now(), 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 'remember_token' => Str::random(10), ]; } } 続いて Model 側で HasFactory を use します。 User.php // この行を追加 use Illuminate\Database\Eloquent\Factories\HasFactory; class User extends Model { use Notifiable; // この行を追加 use HasFactory; } ステート定義は省略しますが、いずれも機械的にできそうです。 最後に呼び出し側を変更します。 before factory(User::class)->create(); after User::factory()->create(); 5. 移行が完了したら composer.json の classmap を空にし、laravel/legacy-factories を削除します。 おまけ: Seeder::callWith 8.0 からシーダ実行時に引数を追加できるようになっていて、8.3 から新たに callWith というメソッドが追加になっています。 自動テスト用にシーダを使うときはマスターデータくらいしか使わないわけですが、ローカルで手動テストする際にはひととおりデータセットを用意するのが一般的と思います。特にユーザーは、ロールや状態ごとに特定のユーザーでログインすると動作確認がしやすいように複数用意しておくと便利なわけですが、シーダ間でデータの共有ができないため、都度取得してひもづけする必要がありました。 シーダにパラメータを渡せるようになったことで、以下のように書くことができます。 // DatabaseSeeder.php // call だと戻り値が取れないので run を直接実行する $users = (new UserSeeder())->run(); // 配列のキーが呼び出される側の変数名と一致していなければならないことに注意 $this->callWith(TaskSeeder::class, compact('users')); // UserSeeder.php public function run(): array { return [ '作成者' => User::factory()->create(['name' => 'テストユーザー1', 'email' => 'test1@hoge.test']), '担当者' => User::factory()->create(['name' => 'テストユーザー2', 'email' => 'test2@hoge.test']), '休眠中' => User::factory()->suspending()->create(), ]; } // TaskSeeder.php public function run(array $users): void { Task::factory() ->for($users['作成者'], 'creator') ->for($users['担当者'], 'assignee') ->has(Comment::factory(['user_id' => $users['作成者']]), 'comments') ->create(); } まぁ、あんまりパターンが多くなると収集つかなくなる恐れもありますが、手動テストで使用するユーザーを明確に識別できるメリットは大きいと思います。渡すパラメータも少ないほうがいいでしょう。基本的にシーダはテーブルごとに用意されますが(make:model -mfs したときとか)、集約ルートごとにまとめるとか(上の例でいえば CommentSeeder は用意せず TaskSeeder から生成する、とか)、シナリオごとにまとめてしまうとか、工夫は必要かもしれません。 とはいえ、ユーザー名やメールアドレスで識別容易にしておいて、その都度取得する方法でも十分明確になると思うので、この変更もそれほどインパクトはないですかね。 $creator = User::where('name', '作成者ユーザー')->first(); $assignee = User::where('name', '担当者ユーザー')->first(); Task::factory() ->for($creator, 'creator') ->for($assignee, 'assignee') ->has(Comment::factory(['user_id' => $creator]), 'comments') ->create(); おわりに とりあえずひととおりざっと Model Factory の新旧の差異をみてきましたが、やはり冒頭に書いたとおり変更コストに見合ったベネフィットは感じられませんでした。まだ見落としてる新機能もあるかもしれないので、もし、こんな便利な機能が追加されてるよ、という方がいたら、ぜひコメント欄にて教えてください
- 投稿日:2021-12-05T18:10:03+09:00
【備忘録】【Laravel8】ファン交流サイトを作ってみよう②
※この記事は、【備忘録】【Laravel8】ファン交流サイトを作ってみよう①の続きです。 投稿一覧ページに、新規登録、詳細、編集、削除ボタンをつける データの一覧という意味では前回のコードだけでもいいのですが、新規登録、詳細、編集、削除ボタンが一覧ページについていたほうが便利なので、用意しておきましょう。 post/index.blade.phpを更に編集し、下記のようにします。 ポイント idはvalue等ではなくurlに含んで送る(POSTの場合も)。 削除(destroy)は、リンク(GET)ではなくPOSTで処理する。idはurlで送る。 削除ボタンにbtn-destroyというクラスを追加。 また、restfulとかならhref="/users/{{$user->id}}/edit"とする方が適切ですが、わかりやすさ重視でいきます。 post/index.blade.php @extends('layouts.app') @section('content') <h1>一覧表示</h1> <!-- 新規投稿 --> <div class="row"> <div class="col-sm-12"> <a href="/post/create" class="btn btn-primary" style="margin:20px;">新規投稿</a> </div> </div> <!-- table --> <table class="table table-striped"> <!-- loop --> @foreach($posts as $post) <tr> <td>{{$post->id}}</td> <td>{{$post->title}}</td> <td>{{$post->body}}</td> <td><a href="/post/show/{{$post->id}}" class="btn btn-primary btn-sm">詳細</a></td> <td><a href="/post/edit/{{$post->id}}" class="btn btn-primary btn-sm">編集</a></td> <td> <form method="post" action="/post/destroy/{{$post->id}}"> <input type="hidden" name="_token" value="{{csrf_token()}}"> <input type="submit" value="削除" class="btn btn-danger btn-sm btn-destroy"> </form> </td> </tr> @endforeach </table> <!-- page control --> {!! $posts->render() !!} @endsection 一度見てみましょう。 こんな感じになっています。 各ボタンにマウスオーバーし、id等がちゃんとリンク先に渡されるようにURLが生成されているか確認してください。 これで、一覧表示機能は完成です。 postに編集、詳細、削除機能を追加 今の状態だとまだ一覧ページで編集、詳細、削除を押しても404エラーとなります。まだルーティング、コントローラーを設定していないからです。 まずはルーティングから 一気に追加してしまいましょう。 web.php Route::get('/post/show/{id}', [App\Http\Controllers\PostController::class, 'show'])->name('show'); Route::get('/post/edit/{id}', [App\Http\Controllers\PostController::class, 'edit'])->name('edit'); Route::post('/post/update/{id}', [App\Http\Controllers\PostController::class, 'update'])->name('update'); Route::post('/post/destroy/{id}', [App\Http\Controllers\PostController::class, 'destroy'])->name('destroy'); ここからは各機能ごとに追加していきます。 まずは詳細表示から。 詳細表示 コントローラー show()メソッドを定義します。 idでレコードを検索し、その結果をそのままviewに返します。 PostController.php //idを受け取る public function show($id) { //受け取ったidを元にpostテーブルからレコードを検索 $post = Post::find($id); //検索結果をビューに渡す return view('post.show')->with('post',$post); } ビュー post/show.blade.phpを作成し、編集していきます。 post/show.blade.php @extends('layouts.app') @section('content') <h1>詳細表示</h1> <div class="row"> <div class="col-sm-12"> <a href="/post/index" class="btn btn-primary" style="margin:20px;">一覧に戻る</a> </div> </div> <!-- table --> <table class="table table-striped"> <tr><td>ID</td><td>{{$post->id}}</tr> <tr><td>タイトル</td><td>{{$post->title}}</tr> <tr><td>本文</td><td>{{$post->body}}</tr> </table> @stop 難しいところは何もありません。見た目は、こんな感じ。 編集 次に、各レコードの編集機能を作ります。基本は、新規作成の応用です。 コントローラー PostController.php //idを受け取る public function edit($id) { //受け取ったidを元にpostテーブルからレコードを検索 $post = Post::find($id); //検索結果をビューに渡す return view('post.edit')->with('post',$post); } editアクションでは、受け取ったidを元にレコードを検索し、その情報をviewに返します。つまり、該当の情報をビュ一に表示させるだけの単純作業。 そして、update。受け取ったidを元にレコードを検索、更新し、一覧へリダイレクトさせています。 PostController.php //idを受け取る public function update(Request $request, $id) { //受け取ったidを元にpostテーブルからレコードを検索 $post = Post::find($id); //値を代入 $post->title = $request->title; $post->body = $request->body; //保存(更新) $post->save(); //リダイレクト return redirect()->to('/post/index'); } ビュ一 基本的に構成はcreateと同じなので、コピーしてedit.blade.phpとし、必要な箇所を編集するだけでOKです。 post/edit @extends('layouts.app') @section('content') <h1>投稿編集</h1> <div class="row"> <div class="col-sm-12"> <a href="/post/index" class="btn btn-primary" style="margin:20px;">一覧に戻る</a> </div> </div> <!-- form --> <form method="post" action="/post/update"> <div class="form-group"> <label>タイトル</label> <input type="string" name="title" value="{{ $post->title}}" class="form-control"> </div> <div class="form-group"> <label>本文</label> <input type="text" name="body" value="{{ $post->body}}" class="form-control"> </div> <input type="hidden" name="_token" value="{{csrf_token()}}"> <input type="submit" value="更新" class="btn btn-primary"> </form> @stop name=”~”の「~」の部分は基本的にはテーブルのカラムと同じにしておけば良いです。 value=”~”はテキストボックスに最初から表示される文字列を指定します。 「~」の部分に「{{ $post->title }}」などといれることでユーザの編集前の値を入れておくことができます。 ▼参考記事 削除 削除は全く難しくありません。強いて言うなら、一覧のところでformで削除処理を書くところくらいです。 コントローラー PostController.php //idを受け取る public function destroy($id) { ////受け取ったidを元に削除対象レコードを検索 $post = Post::find($id); //削除 $post->delete(); //リダイレクト return redirect()->to('/post/index'); } ビュ一 削除にはビューはありません。 これでPostのCRUD処理が完成しました! 次回の記事では、Userの処理を実装していきます。
- 投稿日:2021-12-05T16:16:58+09:00
Laravelで本番環境だけmigrateが反映されない
はじめに Laravelで本番環境だけmigrateが反映されていませんでした。 結論、以下のメッセージが表示されて、マイグレーションが実行されていなかったことが原因でした。 ************************************** * Application In Production! * ************************************** 背景 Laravel初心者です。 EC2-RDS環境へCodeDeployを使用してLaravelのアプリケーションをデプロイしているのですが、 dev,stgへのDB migrationはうまくいくのに、prodのみ実行されていませんでした。 CodeDeployより実行されるシェルの中にphp artisan migrateを記載していたのですが、 デプロイ時にはエラーにはなっておらずアプリケーションの反映もできているが、DBのみ変更が反映されていない状態となっていました。 EC2よりmigrateのステータスを確認すると以下のような感じでした。 (テーブル名は隠しています) $ php artisan migrate:status +------+----------------------------------------------------+-------+ | Ran? | Migration | Batch | +------+----------------------------------------------------+-------+ | Yes | 2021_06_11_114558_・・・ | 1 | | Yes | 2021_06_14_080808_・・・ | 1 | | Yes | 2021_07_28_120012_・・・ | 1 | | Yes | 2021_07_28_132034_・・・ | 1 | | Yes | 2021_07_29_035254_・・・ | 1 | | No | 2021_09_24_150711_・・・ | | | No | 2021_10_11_153540_・・・ | | | No | 2021_10_22_140438_・・・ | | | No | 2021_10_27_092010_・・・ | | | No | 2021_10_28_143847_・・・ | | +------+----------------------------------------------------+-------+ 本格的に解決に乗り出していなかったこともありますが、 原因がわからず解決するまでの間は、本番環境のみ直接DBへSQLを実行してDBの変更を反映していました。 原因 EC2へログインしCodeDeployのログを見てみると、本番環境だけど本当に実行していいの?という確認メッセージが出ているようでした。 cat /opt/codedeploy-agent/deployment-root/deployment-logs/codedeploy-agent-deployments.log 〜省略〜 ************************************** * Application In Production! * ************************************** Do you really wish to run this command? (yes/no) [no]: > Command Canceled! その確認に回答していないため、コマンドがキャンセルされてしまっていましたようです。 envがproductionの場合のみ確認メッセージが出るようで、devやstg環境では発生していなかったのもそのためです。 ※先に記載したmigrate:statusで7月まではうまくいっていた原因は、その期間はprodで実行していなかったためのようです。 解決策 CodeDeployから実行されるシェル内のmigrateコマンドにforceオプションを付けることで、 productionでも確認メッセージなしで実行することができました。 php artisan migrate --force その他対応 上記の通り本番環境のみ直接DBへSQLを実行していたため、そのままmigrateコマンドを実行することはできませんでした。 今までのmigration文のDB変更は済んでいるため、migrateを実行したことにする必要がありました。 migrateを実行したことにするためにはDBに作成されているmigrationsテーブルを変更することでできるようです。 (migrateの履歴を管理しているテーブル) stg環境のmigrationsテーブルと比較しながら以下のようなインサート文を実行しました。 idはオートインクリメント、batchは現在のbatch番号+1しています。(ロールバックに関わる?) insert into migrations (migration,batch) values('2021_09_24_150711_・・・','2'); insert into migrations (migration,batch) values('2021_10_11_153540_・・・','2'); insert into migrations (migration,batch) values('2021_10_22_140438_・・・','2'); insert into migrations (migration,batch) values('2021_10_27_092010_・・・','2'); insert into migrations (migration,batch) values('2021_10_28_143847_・・・','2'); 実行後EC2よりmigrateのステータスを確認すると、反映済みとなっているようでした。 $ php artisan migrate:status +------+----------------------------------------------------+-------+ | Ran? | Migration | Batch | +------+----------------------------------------------------+-------+ | Yes | 2021_06_14_080808_・・・ | 1 | | Yes | 2021_07_28_120012_・・・ | 1 | | Yes | 2021_07_28_132034_・・・ | 1 | | Yes | 2021_07_29_035254_・・・ | 1 | | Yes | 2021_09_24_150711_・・・ | 2 | | Yes | 2021_10_11_153540_・・・ | 2 | | Yes | 2021_10_22_140438_・・・ | 2 | | Yes | 2021_10_27_092010_・・・ | 2 | | Yes | 2021_10_28_143847_・・・ | 2 | +------+----------------------------------------------------+-------+ その後、CodeDeployを通した新しいmigrationの取り込みもうまくいきました。 $ php artisan migrate:status +------+----------------------------------------------------+-------+ | Ran? | Migration | Batch | +------+----------------------------------------------------+-------+ | Yes | 2021_06_14_080808_・・・ | 1 | | Yes | 2021_07_28_120012_・・・ | 1 | | Yes | 2021_07_28_132034_・・・ | 1 | | Yes | 2021_07_29_035254_・・・ | 1 | | Yes | 2021_09_24_150711_・・・ | 2 | | Yes | 2021_10_11_153540_・・・ | 2 | | Yes | 2021_10_22_140438_・・・ | 2 | | Yes | 2021_10_27_092010_・・・ | 2 | | Yes | 2021_10_28_143847_・・・ | 2 | | Yes | 2021_11_15_072434_・・・ | 3 | +------+----------------------------------------------------+-------+ おわりに productionの場合に挙動が変わる?コマンドって親切とは思いますが、こういったことが起こるんですね。 CodeDeployのログを確認する際にEC2に潜らなくては確認できないのは不便なので、CloudWatchに連携したいと思います。
- 投稿日:2021-12-05T14:31:23+09:00
【備忘録】【Laravel8】ファン交流サイトを作ってみる
Laravel学習中の者です。 実際にアプリを作りながら習得していくことを目的として、 ファン交流サイトを作ってみます。 完成したページのイメージ ER図 前提 このプログラムは下記環境で作成しています。 + MacOS + ターミナル + composer + php7.3以上 + MySQL が必要になります。 composerのインストール composerがインストールされていない場合、インストールが必要です。 ▼インストールはこちらから(公式ページ) https://getcomposer.org/doc/00-intro.md 公式だと難しいと思う方は「Mac composer インストール」で検索すると、インストール記事が見つかります。 ターミナルを開いて composer --version を実行してバージョン情報が表示されればOK。 php7.3 and MySQL MacにphpとMySQLをインストールするにはMAMPが最も簡単です。 MAMPはMac用のアプリで、PHP, MySQL, Apacheといった開発に必要なサーバー側のソフトウェアを一発でインストールできるスグレモノです。 Laravel8はPHP7.3以上でないと動かないのでPHPは7.3以上をインストールする必要があります。 MAMPをMacにインストールしたらターミナルから php -v を実行してPHPのバージョンを確認します。 ターミナル php -v PHP 7.4.25 (cli) (built: Oct 23 2021 15:38:15) ( NTS ) Copyright (c) The PHP Group Zend Engine v3.4.0, Copyright (c) Zend Technologies with Zend OPcache v7.4.25, Copyright (c), by Zend Technologies 基本設定 composerでプロジェクトを作成する それでは1からファン交流サイトを作っていきます! ホームディレクトリ直下に新規ディレクトリを作って移動します。 ターミナル cd ~ mkdir program cd program 以下のcomposerコマンドを実行します。 Laravelのバージョンは8.*の最新 プロジェクト名はfan_siteとしました。 ターミナル composer create-project laravel/laravel=8.* fan_site composerでプロジェクトが作成されたら、作成されたディレクトリでcomposer installを行いましょう。 ターミナル cd fan_site composer install ロケールをJPにする VScodeなどのエディタでconfig/app.phpファイルを変更します。 例: vi config/app.php 70行目近辺 'timezone' => 'Asia/Tokyo', 83行目近辺 'locale' => 'ja', ↑この設定をすることでエラーメッセージなどが日本語に切り替わります。 109行目近辺 'faker_locale' => 'ja_JP', ↑この設定をするとテスト用データを作成するときに人の名前を日本人っぽい名前にしてくれます。 MySQLでデータベースを作成する データベースはMySQLを使用します。 MySQLにコマンドラインでログインしてユーザーとデータベースを作成します。 MySQLに「fan_site」という名前のユーザーを作成し、fan_siteユーザーがアクセス出来る「fan_site」データベースを作成します。 使用するデータベースの作成方法はご自分の好みの方法で構いませんが、 ここではMAMPのphpMyAdminを使用していきます。 MAMPのユーザアカウント機能を使って、ユーザーとデータベースを一括で作成してしまいましょう。 入力項目は下記の通り。 ユーザ名 fan_site ホスト名 localhost [パスワードを生成する] ボタンをクリック(生成されたパスワードはどこかにメモしておく) 同名のデータベースを作成してすべての権限を与える。にチェックを入れる 一番下にある[実行] ボタンをクリックします。 .envを設定 .envファイルとはプロジェクトで使用する設定定義ファイルです。秘匿にすべきパスワード情報なども入っています。取り扱い要注意です。 ターミナル cp -p .env.example .env 上記を実行して.envを用意します。 以下の設定は例です。ご自分のMySQLに合わせて設定を行ってください。 .env 変更箇所のみ抜粋 APP_NAME=ファン交流サイト DB_CONNECTION=mysql DB_HOST=localhost DB_PORT=3306 DB_DATABASE=fan_site DB_USERNAME=fan_site DB_PASSWORD=#mypassword# key generate と laravel/uiのインストール Laravel8の認証機能を使うために以下のコマンドを実行します。 ターミナル php artisan key:generate composer require laravel/ui composer install php artisan ui vue --auth 試しに migration してみる 基本的なデータベース設定が済んだところでちゃんと接続できるか試してみましょう。 以下のコマンドを実行します。 ターミナル php artisan migrate すると、私の場合はこんなエラーが出ました。 エラーメッセージで検索して、こちらの記事を参考に進めたところ解消できました。 https://qiita.com/mineaki27th/items/5effcc2605fe90ff6997 修正した箇所 ①.env DB_PASSWORD="パスワード" ←ダブルクォーテーションでパスワードを囲む ②database.php mysqlのunix_socketの箇所を下記に変更 'unix_socket' => '/Applications/MAMP/tmp/mysql/mysql.sock', ③キャッシュクリア $ php artisan config:cache $ php artisan cache:clear その上で再度以下のコマンドを実行します。 ターミナル php artisan migrate 今度は成功しました! phpMyAdminを見てみると、usersテーブル等が作成されています。 データベースアクセスクラスの用意 artisan make:modelコマンドで、データベースにアクセスする用のクラスを作成していきます。 まずはPostsテーブルを作成 ターミナル php artisan make:model Post --migration 成功すると以下のような表示になります。 php artisan make:model Post --migration Model created successfully. Created Migration: 2021_11_24_020205_create_posts_table php artisan make:model Role --migrationを実行したときに以下のファイルが作られます。 database/migrations/#YYYY_MM_DD_NNNN#_create_posts_table.php app/Models/Post.php YYYY_MM_DD_NNNN#_create_posts_table.phpは、投稿テーブルpostsの定義ファイルです。 Post.phpはpostsテーブルにアクセスするためのEloquentモデルクラスファイルです。 EloquentとはLaravelのデータベースアクセスクラスです。 EloquentとはLaravelの一部で、データベースにアクセスする機能がまとまっているファイルです。データベースの1テーブルに対して1つのファイルを作ります。 postsテーブルの定義 マイグレーションファイルはartisanコマンドで作った直後にはidとtimestampsカラムしかありません。 postsテーブルにtitleとbodyカラムを追加します。 2021_11_24_020205_create_posts_table.php <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreatePostsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('posts', function (Blueprint $table) { $table->id(); $table->string('title', 8)->comment('タイトル'); // ← 追記 ********* $table->text('body')->comment('本文'); // ← 追記 ********* $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('posts'); } } $table->string('title', 8)->comment('タイトル'); の部分がカラムの定義になります。 ('title', 8)の「8」は●●●●を意味しています。 Blueprintクラスのインスタンス $tableに対して stringメソッドを呼んで文字列型のカラムを追加しています。 $table->integer('amount');のようにintegerメソッドを呼ぶと数値型を格納するカラムを追加出来ます。 他にもDateなど日付を扱う指定もできます。 テーブル定義が出来たところで migrate:refreshしてみる ターミナル php artisan migrate:refresh マイグレーションが実行されてテーブルが作成されました。 ファクトリの作成(割愛) ファクトリはテスト用にダミーデータを作る仕組みです。TDDでの開発が楽に出来るようになりますが、必須ではないので今回は割愛します。 シーダーの作成 seederというテスト用データ作成クラスを作ります。今回はシーダーを使って会員情報、投稿内容を作っていきます。 以下のコマンドを実行してシーダークラスを作成します。 php artisan make:seeder UsersTableSeeder php artisan make:seeder PostsTableSeeder UsersTableSeederの編集 UsersTableSeeder.php <?php namespace Database\Seeders; 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(['id' => 1, 'name' => '山田花子', 'email' => 'sute1@example.com', 'email_verified_at' => "2021-09-15 15:22:10", 'password' => bcrypt('password')]); DB::table('users')->insert(['id' => 2, 'name' => '畠山まさみ', 'email' => 'sute2@example.com', 'email_verified_at' => "2021-10-12 16:22:10", 'password' => bcrypt('password')]); DB::table('users')->insert(['id' => 3, 'name' => '伊藤順子', 'email' => 'sute3@example.com', 'email_verified_at' => "2021-11-14 17:22:10", 'password' => bcrypt('password')]); DB::table('users')->insert(['id' => 4, 'name' => '西岡京子', 'email' => 'sute4@example.com', 'email_verified_at' => "2021-11-22 18:22:10", 'password' => bcrypt('password')]); } } PostsTableSeederの編集 PostsTableSeeder.php <?php namespace Database\Seeders; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\DB; // ← 追記 ********* class PostsTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { // ↓ 追記 ********* DB::table('posts')->insert(['id'=>1,'title'=>'大橋担','body'=>'大橋くんのファンの方、お友達になりませんか?']); // ← 追記 ********* DB::table('posts')->insert(['id'=>2,'title'=>'高橋担','body'=>'高橋くんの魅力はあのクールな瞳!']); // ← 追記 ********* DB::table('posts')->insert(['id'=>3,'title'=>'西畑担','body'=>'西畑くんの努力が報われて良かった']); // ← 追記 ********* DB::table('posts')->insert(['id'=>4,'title'=>'大西担','body'=>'かわいい〜〜!']); // ← 追記 ********* } } デフォルトで定義されているDatabaseSeeder.phpに、実行するシーダーを登録 DatabaseSeederクラスのrun関数にある $this->callの引数に実行するシーダークラスを追記します。 それによりシードの実行順序を制御できます。 DatabaseSeeder.php <?php namespace Database\Seeders; use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { /** * Seed the application's database. * * @return void */ public function run() { // $this->call(UsersTableSeeder::class); $this->call([ UsersTableSeeder::class, PostsTableSeeder::class, ]); } } 試しにマイグレーション ターミナル php artisan migrate:refresh シーダーを実行してみる ターミナル php artisan db:seed phpMyAdminを確認すると、こんな感じでシーダーで設定したデータが登録されているはずです。 ブラウザで表示してみる ポート番号を指定できるので指定します。省略すると8000ポートが使われます。 ターミナル php artisan serve --port=9999 デフォルトのWelcome画面が表示されます。 Log inを押すと、Bootstrapが反映されておらず質素なページになっています。 なので反映させていきます。 下記コマンドを実施してください。 ターミナル npm install npm run dev //1回目 npm run dev //2回目 確認してみると、今度はちゃんと反映されました。 前もって作っておいたid 1番の人のemailとpasswordを入れて、ログインしてみると... ログインできました! 新規投稿ページの作成 まずはルーティング では、次に新規投稿ページを設定していきましょう。 まずはweb.phpを開き、こちらを追加します。 web.php Route::get('/post/create', [App\Http\Controllers\PostController::class, 'create'])->name('create'); localhost:9999/post/createにアクセスすると、PostControllerのcreateメソッドを呼び出す設定になります。 name('create')とすることで、任意のnameを設定できますが、これは後ほどコントローラーやビューで活かされます。 次はコントローラー PostController.phpファイルを作成します。 ターミナル php artisan make:controller PostController --resource --model=Post --resource オプションを入れることで PostControllerに予めCRUD処理を行う関数が作成されます。 --modelはこのコントローラーで操作するModelを指定します。投稿(Post)を扱うため Postモデルを指定します。 PostControllerを開いてみましょう。 下記のように記述されているはずです。 PostController.php <?php namespace App\Http\Controllers; use App\Models\Post; use Illuminate\Http\Request; class PostController extends Controller { /** * Display a listing of the resource. * * @return \Illuminate\Http\Response */ public function index() { // } /** * Show the form for creating a new resource. * * @return \Illuminate\Http\Response */ public function create() { // } /** * Store a newly created resource in storage. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function store(Request $request) { // } /** * Display the specified resource. * * @param \App\Models\Post $post * @return \Illuminate\Http\Response */ public function show(Post $post) { // } /** * Show the form for editing the specified resource. * * @param \App\Models\Post $post * @return \Illuminate\Http\Response */ public function edit(Post $post) { // } /** * Update the specified resource in storage. * * @param \Illuminate\Http\Request $request * @param \App\Models\Post $post * @return \Illuminate\Http\Response */ public function update(Request $request, Post $post) { // } /** * Remove the specified resource from storage. * * @param \App\Models\Post $post * @return \Illuminate\Http\Response */ public function destroy(Post $post) { // } } createメソッド編集 試しに下記のようにcreateメソッドの箇所を編集して、 PostController.php public function create() { { return '新規投稿ページです'; }; } localhost:9999/post/createにアクセスしてみてください。↓ 後ほど、入力フォームを追加していきます。 indexメソッド 次にindexメソッドを編集して、投稿が一覧表示されるようにしていきます。 まずはルーティングから web.php Route::get('/post/index', [App\Http\Controllers\PostController::class, 'index'])->name('index'); 次にコントローラー Postモデルを使いたいので、頭のほうで PostController.php use App\Model\Post; としておいてください。 PostController.php public function index() { $query = Post::query(); //全件取得 //$posts = $query->get(); //ページネーション $posts = $query->orderBy('id','desc')->paginate(10); //post/index.blade.phpに$postsという変数を渡す場合 return view('post/index', ['posts' => $posts]); } view(ビューファイル名,ビューに渡したいもの)とすることでコントローラからビューへ指示を出すことができます。 postsがビューで使うときの変数名、$postsはコントローラ内で定義した変数になります。 コントローラの中では、ビューで使う用の変数名に$をつけませんが、viewファイル内(bladeファイル)では変数の$をつけたものを使います。ややこしいので注意! 下記のような書き方も可能です。 PostController.php public function index() { $query = Post::query(); //全件取得 $posts = $query->get(); //ページネーション $posts = $query->orderBy('id','desc')->paginate(10); //post/index.blade.phpに$postsという変数を渡す場合 return view('post.index')->with('posts',$posts); } 一覧だけなら、posts=Post::all();とかでもいいのですが、ここではqueryオブジェクトを生成して対応しています(その理由は検索機能の実装等で便利だからですが、ここでは触れません)。 またせっかくなので、orderByで最新登録が先頭に来るようにしているのと、10行毎にページ処理をしています。 取得したデータはcompact()等で返してもいいのですが、わかりやすく-with()を利用しています。 view('view名')-with('viewでの変数名','実データ'); という形式になります。 ビューの編集 postディレクトリを作成して、その中にindexファイルを作成します。 下記のように編集してみましょう。 post/index.blade.php @extends('layouts.app') @section('content') <h1>一覧表示</h1> <table class="table table-striped"> @foreach($posts as $post) <tr> <td>{{$post->title}}</td> <td>{{$post->body}}</td> </tr> @endforeach </table> <!-- page control --> {!! $posts->render() !!} @endsection localhost:9999/post/indexを開いてみましょう。 このように、予めDBに登録しておいたpostデータが表示されました。 投稿の保存 ここからは、入力フォームの内容をDBに保存できるように実装していきたいと思います。 ▼参考記事 https://nodoame.net/archives/11612 まずはルーティングから web.php Route::post('/post/store', [App\Http\Controllers\PostController::class, 'store'])->name('store'); ※storeメソッドはPOST送信になります。 コントローラー createメソッドとstoreメソッドを使用することになりますが、storeメソッドの方は最も重要な箇所です。 まずはcreateメソッド。 PostController.php public function create() { //createに転送 return view('post.create'); } 基本的に、postディレクトリのcreate.blade.phpファイルに処理を転送しているだけになります。 そして、store。 createが投げてきた値を受け取り、DBに保存。そして、一覧表示へリダイレクトさせています。 PostController.php public function store(Request $request) { //postオブジェクト生成 $post = Post::create(); //値の登録。 //右側はviewのnameから持ってきたもの //左側はモデルのカラム名 $post->title = $request->title; $post->body = $request->body; //保存 $post->save(); //一覧にリダイレクト return redirect()->to('/post/index'); } 入力フォームを作って見た目を整える createアクション用のビューを作っていきます。 post/create.blade.phpを作成し、下記のように編集してみましょう。 post/create.blade.php @extends('layouts.app') @section('content') <h1>新規投稿</h1> <div class="row"> <div class="col-sm-12"> <a href="/post/index" class="btn btn-primary" style="margin:20px;">一覧に戻る</a> </div> </div> <!-- form --> <form method="post" action="/post/store"> <div class="form-group"> <label>タイトル</label> <input type="string" name="title" value="" class="form-control"> </div> <div class="form-group"> <label>本文</label> <input type="text" name="body" value="" class="form-control"> </div> <input type="hidden" name="_token" value="{{csrf_token()}}"> <input type="submit" value="投稿" class="btn btn-primary"> </form> @stop post先(action)はstore(/post/store)。methodはpost。 hiddenでLaravelでpostするときに原則必要となるcsrf_tokenを送っています。 ▼参考記事 動作確認として、入力フォームに値を入れて投稿してみます。 残念。エラーです。 解決するためには、DBの厳密モードを変更する必要があります。無効にするには、以下の手順に従ってください。 ①Config/database.phpを開きます ②find 'strict'値をtrueからfalseに変更して、再試行します これで無事にDBに入力フォームの値が保存されました。 その他学習したこと フォームから受け取った値を一度加工してからDBに格納するためにはEloquentが必要。 Eloquentとは DB のデータを操作する機能のこと。 ▼参考記事 https://laraweb.net/knowledge/2324/ DBに保存する方法として、 fillメソッドとsaveメソッドがありますが、 メソッドチェーンで連結しています。 fillメソッド 実行するSQL文のプレースホルダに値をバインドする処理 saveメソッド insertする処理 です。 プレースホルダとは 実際の内容を後から挿入するために、とりあえず仮に確保した場所のこと。 また、そのことを示す標識などのこと。 バインドとは 束縛(する)、拘束(する)、結びつける、関連付ける、などの意味を持つ英単語。 ITの分野では、何らかの要素やデータ、ファイルなどが相互に関連付けられている状態や、そのような状態を実現する機能などのことを指すことが多い。
- 投稿日:2021-12-05T14:31:23+09:00
【備忘録】【Laravel8】ファン交流サイトを作ってみよう①
Laravel学習中の者です。 実際にアプリを作りながら習得していくことを目的として、 ファン交流サイトを作ってみます。 ER図 未作成 前提 このプログラムは下記環境で作成しています。 + MacOS + ターミナル + composer + php7.3以上 + MySQL が必要になります。 composerのインストール composerがインストールされていない場合、インストールが必要です。 ▼インストールはこちらから(公式ページ) https://getcomposer.org/doc/00-intro.md 公式だと難しいと思う方は「Mac composer インストール」で検索すると、インストール記事が見つかります。 ターミナルを開いて composer --version を実行してバージョン情報が表示されればOK。 php7.3 and MySQL MacにphpとMySQLをインストールするにはMAMPが最も簡単です。 MAMPはMac用のアプリで、PHP, MySQL, Apacheといった開発に必要なサーバー側のソフトウェアを一発でインストールできるスグレモノです。 Laravel8はPHP7.3以上でないと動かないのでPHPは7.3以上をインストールする必要があります。 MAMPをMacにインストールしたらターミナルから php -v を実行してPHPのバージョンを確認します。 ターミナル php -v PHP 7.4.25 (cli) (built: Oct 23 2021 15:38:15) ( NTS ) Copyright (c) The PHP Group Zend Engine v3.4.0, Copyright (c) Zend Technologies with Zend OPcache v7.4.25, Copyright (c), by Zend Technologies 基本設定 composerでプロジェクトを作成する それでは1からファン交流サイトを作っていきます! ホームディレクトリ直下に新規ディレクトリを作って移動します。 ターミナル cd ~ mkdir program cd program 以下のcomposerコマンドを実行します。 Laravelのバージョンは8.*の最新 プロジェクト名はfan_siteとしました。 ターミナル composer create-project laravel/laravel=8.* fan_site composerでプロジェクトが作成されたら、作成されたディレクトリでcomposer installを行いましょう。 ターミナル cd fan_site composer install ロケールをJPにする VScodeなどのエディタでconfig/app.phpファイルを変更します。 例: vi config/app.php 70行目近辺 'timezone' => 'Asia/Tokyo', 83行目近辺 'locale' => 'ja', ↑この設定をすることでエラーメッセージなどが日本語に切り替わります。 109行目近辺 'faker_locale' => 'ja_JP', ↑この設定をするとテスト用データを作成するときに人の名前を日本人っぽい名前にしてくれます。 MySQLでデータベースを作成する データベースはMySQLを使用します。 MySQLにコマンドラインでログインしてユーザーとデータベースを作成します。 MySQLに「fan_site」という名前のユーザーを作成し、fan_siteユーザーがアクセス出来る「fan_site」データベースを作成します。 使用するデータベースの作成方法はご自分の好みの方法で構いませんが、 ここではMAMPのphpMyAdminを使用していきます。 MAMPのユーザアカウント機能を使って、ユーザーとデータベースを一括で作成してしまいましょう。 入力項目は下記の通り。 ユーザ名 fan_site ホスト名 localhost [パスワードを生成する] ボタンをクリック(生成されたパスワードはどこかにメモしておく) 同名のデータベースを作成してすべての権限を与える。にチェックを入れる 一番下にある[実行] ボタンをクリックします。 .envを設定 .envファイルとはプロジェクトで使用する設定定義ファイルです。秘匿にすべきパスワード情報なども入っています。取り扱い要注意です。 ターミナル cp -p .env.example .env 上記を実行して.envを用意します。 以下の設定は例です。ご自分のMySQLに合わせて設定を行ってください。 .env 変更箇所のみ抜粋 APP_NAME=ファン交流サイト DB_CONNECTION=mysql DB_HOST=localhost DB_PORT=3306 DB_DATABASE=fan_site DB_USERNAME=fan_site DB_PASSWORD=#mypassword# key generate と laravel/uiのインストール Laravel8の認証機能を使うために以下のコマンドを実行します。 ターミナル php artisan key:generate composer require laravel/ui composer install php artisan ui vue --auth 試しに migration してみる 基本的なデータベース設定が済んだところでちゃんと接続できるか試してみましょう。 以下のコマンドを実行します。 ターミナル php artisan migrate すると、私の場合はこんなエラーが出ました。 エラーメッセージで検索して、こちらの記事を参考に進めたところ解消できました。 https://qiita.com/mineaki27th/items/5effcc2605fe90ff6997 修正した箇所 ①.env DB_PASSWORD="パスワード" ←ダブルクォーテーションでパスワードを囲む ②database.php mysqlのunix_socketの箇所を下記に変更 'unix_socket' => '/Applications/MAMP/tmp/mysql/mysql.sock', ③キャッシュクリア $ php artisan config:cache $ php artisan cache:clear その上で再度以下のコマンドを実行します。 ターミナル php artisan migrate 今度は成功しました! phpMyAdminを見てみると、usersテーブル等が作成されています。 データベースアクセスクラスの用意 artisan make:modelコマンドで、データベースにアクセスする用のクラスを作成していきます。 まずはPostsテーブルを作成 ターミナル php artisan make:model Post --migration 成功すると以下のような表示になります。 php artisan make:model Post --migration Model created successfully. Created Migration: 2021_11_24_020205_create_posts_table php artisan make:model Role --migrationを実行したときに以下のファイルが作られます。 database/migrations/#YYYY_MM_DD_NNNN#_create_posts_table.php app/Models/Post.php YYYY_MM_DD_NNNN#_create_posts_table.phpは、投稿テーブルpostsの定義ファイルです。 Post.phpはpostsテーブルにアクセスするためのEloquentモデルクラスファイルです。 EloquentとはLaravelのデータベースアクセスクラスです。 EloquentとはLaravelの一部で、データベースにアクセスする機能がまとまっているファイルです。データベースの1テーブルに対して1つのファイルを作ります。 postsテーブルの定義 マイグレーションファイルはartisanコマンドで作った直後にはidとtimestampsカラムしかありません。 postsテーブルにtitleとbodyカラムを追加します。 2021_11_24_020205_create_posts_table.php <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreatePostsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('posts', function (Blueprint $table) { $table->id(); $table->string('title', 8)->comment('タイトル'); // ← 追記 ********* $table->text('body')->comment('本文'); // ← 追記 ********* $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('posts'); } } $table->string('title', 8)->comment('タイトル'); の部分がカラムの定義になります。 ('title', 8)の「8」は●●●●を意味しています。 Blueprintクラスのインスタンス $tableに対して stringメソッドを呼んで文字列型のカラムを追加しています。 $table->integer('amount');のようにintegerメソッドを呼ぶと数値型を格納するカラムを追加出来ます。 他にもDateなど日付を扱う指定もできます。 テーブル定義が出来たところで migrate:refreshしてみる ターミナル php artisan migrate:refresh マイグレーションが実行されてテーブルが作成されました。 ファクトリの作成(割愛) ファクトリはテスト用にダミーデータを作る仕組みです。TDDでの開発が楽に出来るようになりますが、必須ではないので今回は割愛します。 シーダーの作成 seederというテスト用データ作成クラスを作ります。今回はシーダーを使って会員情報、投稿内容を作っていきます。 以下のコマンドを実行してシーダークラスを作成します。 php artisan make:seeder UsersTableSeeder php artisan make:seeder PostsTableSeeder UsersTableSeederの編集 UsersTableSeeder.php <?php namespace Database\Seeders; 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(['id' => 1, 'name' => '山田花子', 'email' => 'sute1@example.com', 'email_verified_at' => "2021-09-15 15:22:10", 'password' => bcrypt('password')]); DB::table('users')->insert(['id' => 2, 'name' => '畠山まさみ', 'email' => 'sute2@example.com', 'email_verified_at' => "2021-10-12 16:22:10", 'password' => bcrypt('password')]); DB::table('users')->insert(['id' => 3, 'name' => '伊藤順子', 'email' => 'sute3@example.com', 'email_verified_at' => "2021-11-14 17:22:10", 'password' => bcrypt('password')]); DB::table('users')->insert(['id' => 4, 'name' => '西岡京子', 'email' => 'sute4@example.com', 'email_verified_at' => "2021-11-22 18:22:10", 'password' => bcrypt('password')]); } } PostsTableSeederの編集 PostsTableSeeder.php <?php namespace Database\Seeders; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\DB; // ← 追記 ********* class PostsTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { // ↓ 追記 ********* DB::table('posts')->insert(['id'=>1,'title'=>'大橋担','body'=>'お友達になりませんか?']); // ← 追記 ********* DB::table('posts')->insert(['id'=>2,'title'=>'高橋担','body'=>'語り合いましょう']); // ← 追記 ********* DB::table('posts')->insert(['id'=>3,'title'=>'西畑担','body'=>'お話ししましょ']); // ← 追記 ********* DB::table('posts')->insert(['id'=>4,'title'=>'大西担','body'=>'よろしくお願いします!']); // ← 追記 ********* } } デフォルトで定義されているDatabaseSeeder.phpに、実行するシーダーを登録 DatabaseSeederクラスのrun関数にある $this->callの引数に実行するシーダークラスを追記します。 それによりシードの実行順序を制御できます。 DatabaseSeeder.php <?php namespace Database\Seeders; use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { /** * Seed the application's database. * * @return void */ public function run() { // $this->call(UsersTableSeeder::class); $this->call([ UsersTableSeeder::class, PostsTableSeeder::class, ]); } } 試しにマイグレーション ターミナル php artisan migrate:refresh シーダーを実行してみる ターミナル php artisan db:seed phpMyAdminを確認すると、こんな感じでシーダーで設定したデータが登録されているはずです。 ブラウザで表示してみる ポート番号を指定できるので指定します。省略すると8000ポートが使われます。 ターミナル php artisan serve --port=9999 デフォルトのWelcome画面が表示されます。 Log inを押すと、Bootstrapが反映されておらず質素なページになっています。 なので反映させていきます。 下記コマンドを実施してください。 ターミナル npm install npm run dev //1回目 npm run dev //2回目 確認してみると、今度はちゃんと反映されました。 前もって作っておいたid 1番の人のemailとpasswordを入れて、ログインしてみると... ログインできました! 新規投稿ページの作成 まずはルーティング では、次に新規投稿ページを設定していきましょう。 まずはweb.phpを開き、こちらを追加します。 web.php Route::get('/post/create', [App\Http\Controllers\PostController::class, 'create'])->name('create'); localhost:9999/post/createにアクセスすると、PostControllerのcreateメソッドを呼び出す設定になります。 name('create')とすることで、任意のnameを設定できますが、これは後ほどコントローラーやビューで活かされます。 次はコントローラー PostController.phpファイルを作成します。 ターミナル php artisan make:controller PostController --resource --model=Post --resource オプションを入れることで PostControllerに予めCRUD処理を行う関数が作成されます。 --modelはこのコントローラーで操作するModelを指定します。投稿(Post)を扱うため Postモデルを指定します。 PostControllerを開いてみましょう。 下記のように記述されているはずです。 PostController.php <?php namespace App\Http\Controllers; use App\Models\Post; use Illuminate\Http\Request; class PostController extends Controller { /** * Display a listing of the resource. * * @return \Illuminate\Http\Response */ public function index() { // } /** * Show the form for creating a new resource. * * @return \Illuminate\Http\Response */ public function create() { // } /** * Store a newly created resource in storage. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function store(Request $request) { // } /** * Display the specified resource. * * @param \App\Models\Post $post * @return \Illuminate\Http\Response */ public function show(Post $post) { // } /** * Show the form for editing the specified resource. * * @param \App\Models\Post $post * @return \Illuminate\Http\Response */ public function edit(Post $post) { // } /** * Update the specified resource in storage. * * @param \Illuminate\Http\Request $request * @param \App\Models\Post $post * @return \Illuminate\Http\Response */ public function update(Request $request, Post $post) { // } /** * Remove the specified resource from storage. * * @param \App\Models\Post $post * @return \Illuminate\Http\Response */ public function destroy(Post $post) { // } } createメソッド編集 試しに下記のようにcreateメソッドの箇所を編集して、 PostController.php public function create() { { return '新規投稿ページです'; }; } localhost:9999/post/createにアクセスしてみてください。↓ 後ほど、入力フォームを追加していきます。 indexメソッド 次にindexメソッドを編集して、投稿が一覧表示されるようにしていきます。 まずはルーティングから web.php Route::get('/post/index', [App\Http\Controllers\PostController::class, 'index'])->name('index'); 次にコントローラー Postモデルを使いたいので、頭のほうで PostController.php use App\Model\Post; としておいてください。 PostController.php public function index() { $query = Post::query(); //全件取得 $posts = $query->get(); //ページネーション $posts = $query->orderBy('id','desc')->paginate(10); //post/index.blade.phpに$postsという変数を渡す場合 return view('post/index', ['posts' => $posts]); } view(ビューファイル名,ビューに渡したいもの)とすることでコントローラからビューへ指示を出すことができます。 postsがビューで使うときの変数名、$postsはコントローラ内で定義した変数になります。 コントローラの中では、ビューで使う用の変数名に$をつけませんが、viewファイル内(bladeファイル)では変数の$をつけたものを使います。ややこしいので注意! 下記のような書き方も可能です。 PostController.php public function index() { $query = Post::query(); //全件取得 $posts = $query->get(); //ページネーション $posts = $query->orderBy('id','desc')->paginate(10); //post/index.blade.phpに$postsという変数を渡す場合 return view('post.index')->with('posts',$posts); } 一覧だけなら、posts=Post::all();とかでもいいのですが、ここではqueryオブジェクトを生成して対応しています(その理由は検索機能の実装等で便利だからですが、ここでは触れません)。 またせっかくなので、orderByで最新登録が先頭に来るようにしているのと、10行毎にページ処理をしています。 取得したデータはcompact()等で返してもいいのですが、わかりやすく-with()を利用しています。 view('view名')-with('viewでの変数名','実データ'); という形式になります。 ビューの編集 postディレクトリを作成して、その中にindexファイルを作成します。 下記のように編集してみましょう。 post/index.blade.php @extends('layouts.app') @section('content') <h1>一覧表示</h1> <table class="table table-striped"> @foreach($posts as $post) <tr> <td>{{$post->title}}</td> <td>{{$post->body}}</td> </tr> @endforeach </table> <!-- page control --> {!! $posts->render() !!} @endsection localhost:9999/post/indexを開いてみましょう。 予めDBに登録しておいたpostデータが表示されたはずです。 投稿の保存 ここからは、入力フォームの内容をDBに保存できるように実装していきたいと思います。 ▼参考記事 https://nodoame.net/archives/11612 まずはルーティングから web.php Route::post('/post/store', [App\Http\Controllers\PostController::class, 'store'])->name('store'); ※storeメソッドはPOST送信になります。 コントローラー createメソッドとstoreメソッドを使用することになりますが、storeメソッドの方は最も重要な箇所です。 まずはcreateメソッド。 PostController.php public function create() { //createに転送 return view('post.create'); } 基本的に、postディレクトリのcreate.blade.phpファイルに処理を転送しているだけになります。 そして、store。 createが投げてきた値を受け取り、DBに保存。そして、一覧表示へリダイレクトさせています。 PostController.php public function store(Request $request) { //postオブジェクト生成 $post = Post::create(); //値の登録。 //右側はviewのnameから持ってきたもの //左側はモデルのカラム名 $post->title = $request->title; $post->body = $request->body; //保存 $post->save(); //一覧にリダイレクト return redirect()->to('/post/index'); } 入力フォームを作って見た目を整える createアクション用のビューを作っていきます。 post/create.blade.phpを作成し、下記のように編集してみましょう。 post/create.blade.php @extends('layouts.app') @section('content') <h1>新規投稿</h1> <div class="row"> <div class="col-sm-12"> <a href="/post/index" class="btn btn-primary" style="margin:20px;">一覧に戻る</a> </div> </div> <!-- form --> <form method="post" action="/post/store"> <div class="form-group"> <label>タイトル</label> <input type="string" name="title" value="" class="form-control"> </div> <div class="form-group"> <label>本文</label> <input type="text" name="body" value="" class="form-control"> </div> <input type="hidden" name="_token" value="{{csrf_token()}}"> <input type="submit" value="投稿" class="btn btn-primary"> </form> @stop post先(action)はstore(/post/store)。methodはpost。 hiddenでLaravelでpostするときに原則必要となるcsrf_tokenを送っています。 ▼参考記事 動作確認として、入力フォームに値を入れて投稿してみます。 残念。エラーです。 解決するためには、DBの厳密モードを変更する必要があります。無効にするには、以下の手順に従ってください。 ①Config/database.phpを開きます ②find 'strict'値をtrueからfalseに変更して、再試行します これで無事にDBに入力フォームの値が保存されました。 今回の記事ではリレーションの設定については割愛しました。 また別の記事にて投稿したいと思います。 その他学習したこと フォームから受け取った値を一度加工してからDBに格納するためにはEloquentが必要。 Eloquentとは DB のデータを操作する機能のこと。 ▼参考記事 https://laraweb.net/knowledge/2324/ DBに保存する方法として、 fillメソッドとsaveメソッドがありますが、 メソッドチェーンで連結しています。 fillメソッド 実行するSQL文のプレースホルダに値をバインドする処理 saveメソッド insertする処理 です。 プレースホルダとは 実際の内容を後から挿入するために、とりあえず仮に確保した場所のこと。 また、そのことを示す標識などのこと。 バインドとは 束縛(する)、拘束(する)、結びつける、関連付ける、などの意味を持つ英単語。 ITの分野では、何らかの要素やデータ、ファイルなどが相互に関連付けられている状態や、そのような状態を実現する機能などのことを指すことが多い。