inertia.js(react) + survey.js - 2: surveyのcrud的な
まずsurvey(の枠)を作るところから初める
これは先述の通り、管理者のみが実行できるとのことで作らないといけない。これは前端のstarter作成のときに/adminプレフィックスを付けた場合はミドルウェアを通じてroleがadminのものしか通過できないようになっている。
なお、/adminプレフィックスがついたルートだけ出力すると
GET|HEAD admin/users ................................... users.index › UserController@index
POST admin/users ................................... users.store › UserController@store
GET|HEAD admin/users/create .......................... users.create › UserController@create
GET|HEAD admin/users/{user} .............................. users.show › UserController@show
PUT|PATCH admin/users/{user} .......................... users.update › UserController@update
DELETE admin/users/{user} ........................ users.destroy › UserController@destroy
GET|HEAD admin/users/{user}/activity-log users.activity-log › UserController@showActivityL…
GET|HEAD admin/users/{user}/edit ......................... users.edit › UserController@edit
Showing [8] routes
というわけで、今のところユーザーの管理のみやっているので、これにsurveyの管理を増やしてみよう。
routeを作る
今、大まかな部分はこうなっている
use App\Http\Controllers\SurveyController;
Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
// Admin route
Route::prefix('admin')->middleware(['auth', EnsureUserIsAdmin::class])->group(function () {
Route::resource('users', UserController::class);
Route::get('users/{user}/activity-log', [UserController::class, 'showActivityLog'])->name('users.activity-log');
});
});
従ってここにsurveyへのリソースルートを作成しsurveyの「追加、更新、(削除)、編集」を可能とする、ただ、削除に関しては色々と考えないといけないから後まわしにしますけどね、っていう。
Route::prefix('admin')->middleware(['auth', EnsureUserIsAdmin::class])->group(function () {
Route::resource('users', UserController::class);
Route::get('users/{user}/activity-log', [UserController::class, 'showActivityLog'])->name('users.activity-log');
Route::resource('surveys', SurveyController::class); // これ
});
いずれにしたって、surveysへのリソースルートを一行追加するだけで完了する。
survey管理のメニューを作成する
ダッシュボードとユーザーのメニューが見えるので、ここにSurveyを増やして導線を作成していく。これは resources/js/Layouts/AuthenticatedLayout.jsx でやっている。アイコンはちょっと適当ではあるがとりあえず
import {
VscChevronDown,
VscMenu,
VscClose,
VscDashboard,
VscOrganization,
VscNote,
VscIndent,
VscOutput, // これ
} from 'react-icons/vsc';
これを追加した、でresources/js/Layouts/AuthenticatedLayout.jsx に
{auth.isAdmin && (
<>
<NavLink href={route('users.index')} active={route().current('users.*')}>
<VscOrganization className="mr-2" />
{t('Users')}
</NavLink>
<NavLink href={route('surveys.index')} active={route().current('surveys.*')}>
<VscOutput className="mr-2" />
{t('Surveys')}
</NavLink>
</>
)}
この辺りに追記していくわけだね。
<div className="pt-2 pb-3 space-y-1">
<ResponsiveNavLink href={route('dashboard')} active={route().current('dashboard')}>
<VscDashboard className="mr-2" />
{t('Dashboard')}
</ResponsiveNavLink>
{auth.isAdmin && (
<>
<ResponsiveNavLink href={route('users.index')} active={route().current('users.*')}>
<VscOrganization className="mr-2" />
{t('Users')}
</ResponsiveNavLink>
<ResponsiveNavLink href={route('surveys.index')} active={route().current('surveys.*')}>
<VscOutput className="mr-2" />
{t('Surveys')}
</ResponsiveNavLink>
</>
)}
</div>
下はモバイルビューというかsmall device用である。なお、<>と</>はこのパターンではまあ何となく必要なものだと思っておいてok。ここでSurveysの翻訳をどうするかという問題があるけど、まっとりあえずここは「アンケート調査」とでもしますか。
lang/ja.json
"Surveys": "アンケート調査",
完成したメニューがこちら
surveys.index
ここではsurvey一覧を表示する。
app/Http/Controllers/SurveyController.php
<?php
namespace App\Http\Controllers;
use App\Models\Survey;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class SurveyController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(): Response
{
$surveys = Survey::latest()->get();
$userId = auth()->id();
return Inertia::render('Surveys/Index', ['surveys' => $surveys]);
}
jsxでは単純にCreate New Surveyというボタンだけ配置している
resources/js/Pages/Surveys/Index.jsx
import React, { useCallback } from 'react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, Link } from '@inertiajs/react';
import { useLaravelReactI18n } from 'laravel-react-i18n';
export default function SurveyIndex({ auth, surveys }) {
const { t } = useLaravelReactI18n();
return (
<AuthenticatedLayout
user={auth.user}
header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">{t("Surveys")}</h2>}
>
<Head title="Survey.js" />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="bg-white p-6 rounded shadow-md max-w-7xl mx-auto">
<div className="flex justify-between items-center mb-4">
<h3 className="text-2xl font-semibold">
{t("Available Surveys")}
</h3>
<Link href={route("surveys.create")}>
<button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
{t("Create New Survey")}
</button>
</Link>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
);
}
surveys.createの構想
そしたらばsurveyの新規作成UIを作っていこう。これは以下のようなインターフェイスを想定するわけだ。イメージがわかないなら何となくのモックアップを作ってもいいのかもしれない
ここで重要なのは質問セットをセレクトボックスで選択できるようになっていることである。とはいえセレクトボックスのデーターは今は何もないので事前に作っておかないといけない。これに関してはこのプログラムではUIを供えないので、seederで用意する必要があるという事になる。
質問データベース的なやつの定義
これは実はもう考えられてあり、survey_questionsというテーブルで行っている。従って以下のようにモデルを作成する。これに関してはコントローラーを持たないので以下のようにして生成する。
% ./vendor/bin/sail artisan make:model SurveyQuestion -mfs
INFO Model [app/Models/SurveyQuestion.php] created successfully.
INFO Factory [database/factories/SurveyQuestionFactory.php] created successfully.
INFO Migration [database/migrations/2023_10_01_174142_create_survey_questions_table.php] created successfully.
INFO Seeder [database/seeders/SurveyQuestionSeeder.php] created successfully.
-mfsは
-m マイグレーションも作る
-f ファクトリーも作る
-s seedも作る
という意味になる。
survey_questionsの定義
そしたらmigrationをセットしていく
public function up(): void
{
Schema::create('survey_questions', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->json('question_data');
$table->timestamps();
});
}
これくらいで質問セットは達成できるだろう。まあ単純に名前とjsonの2本だけ。
seed
これはローカルに配置したjsonを読み込んで取り込むという作業になる。
database/seeders/SurveyQuestionSeeder.php
public function run(): void
{
SurveyQuestion::factory()->create();
$files = File::glob(database_path('seeders/data/*.json'));
foreach ($files as $file) {
// ...
}
こんな感じでseeders/data/*.json をglobしてこれを取り込むような仕様としよう。
質問セット
というわけでこれはseeders/data/*.json からインポートする事を想定する。以下のようなjsonである
{
"title": "年次調査 2023",
"pages": [
{
"name": "基本情報",
"elements": [
{
"type": "text",
"name": "fullName",
"title": "お名前を教えてください。",
"isRequired": true
},
{
"type": "radiogroup",
"name": "gender",
"title": "性別は?",
"choices": ["男", "女", "その他"],
"isRequired": true
}
]
},
{
"name": "職業と収入",
"elements": [
{
"type": "text",
"name": "occupation",
"title": "現在の職業は何ですか?",
"isRequired": false
},
{
"type": "checkbox",
"name": "incomeSources",
"title": "収入源は何ですか?(複数選択可)",
"choices": ["給与", "投資", "ビジネス", "その他"],
"isRequired": false
}
]
}
]
}
たとえば上記の問題セットを database/seeders/data/survey_questions.json に配置する。
モデルの設定
json型を使う場合はarray castしておく。
app/Models/SurveyQuestion.php
class SurveyQuestion extends Model
{
use HasFactory;
protected $casts = [
'question_data' => 'array',
];
}
seederを呼び出せるようにする
database/seeders/DatabaseSeeder.php
ここからcallする
<?php
namespace Database\Seeders;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;
use App\Models\User;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*/
public function run(): void
{
// パーミッションの作成
$editArticlesPermission = Permission::create(['name' => 'edit articles']);
// ロールの作成
$adminRole = Role::create(['name' => 'admin']);
// ロールにパーミッションを付与
$adminRole->givePermissionTo($editArticlesPermission);
// admin1 ユーザーの作成
$admin1 = User::factory()->create([
'name' => 'Admin 1',
'email' => 'admin1@example.com',
'password' => bcrypt('password'),
]);
// admin2 ユーザーの作成
$admin2 = User::factory()->create([
'name' => 'Admin 2',
'email' => 'admin2@example.com',
'password' => bcrypt('password'),
]);
// ユーザーにadminロールを割り当て
$admin1->assignRole('admin');
$admin2->assignRole('admin');
// User::factory(50)->create();
$this->call([
SurveyQuestionSeeder::class, // <- これ
]);
}
}
これでSurveyQuestionSeederがcallできるようになった
% ./vendor/bin/sail artisan migrate:fresh --seed
Dropping all tables ................................................................................................................... 171ms DONE
INFO Preparing database.
Creating migration table ............................................................................................................... 30ms DONE
INFO Running migrations.
2014_10_12_000000_create_users_table ................................................................................................... 57ms DONE
2014_10_12_100000_create_password_reset_tokens_table ................................................................................... 74ms DONE
2019_08_19_000000_create_failed_jobs_table ............................................................................................. 53ms DONE
2019_12_14_000001_create_personal_access_tokens_table .................................................................................. 81ms DONE
2023_09_18_101626_create_permission_tables ............................................................................................ 716ms DONE
2023_09_21_024514_create_activity_log_table ............................................................................................ 93ms DONE
2023_09_21_024515_add_event_column_to_activity_log_table ............................................................................... 25ms DONE
2023_09_21_024516_add_batch_uuid_column_to_activity_log_table .......................................................................... 31ms DONE
2023_09_29_135347_create_surveys_table ................................................................................................. 31ms DONE
2023_10_01_174142_create_survey_questions_table ........................................................................................ 26ms DONE
INFO Seeding database.
Database\Seeders\SurveyQuestionSeeder .................................................................................................... RUNNING
Database\Seeders\SurveyQuestionSeeder .............................................................................................. 10.62 ms DONE
factoryを用意する
seedを呼びこむ場合は大抵factoryを整理しておいた方が後々効いてくるからちゃんとfactoryを作っておく。
database/factories/SurveyQuestionFactory.php
public function definition(): array
{
return [
'name' => fake()->word,
'question_data' => [],
];
}
ゆってもこれだけだけど…
factoryを使って適当にseedする
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use App\Models\SurveyQuestion;
class SurveyQuestionSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
SurveyQuestion::factory()->create();
}
}
このようにしてこのseedを呼びこむと…
% ./vendor/bin/sail artisan tinker
SurveySurveyPsy Shell v0.11.21 (PHP 8.2.10 — cli) by Justin Hileman
> SurveyQuestion::all()
[!] Aliasing 'SurveyQuestion' to 'App\Models\SurveyQuestion' for this Tinker session.
= Illuminate\Database\Eloquent\Collection {#7489
all: [
App\Models\SurveyQuestion {#7491
id: 1,
name: "amet",
question_data: "[]",
created_at: "2023-10-01 18:04:40",
updated_at: "2023-10-01 18:04:40",
},
],
}
このように適当なデーターでSurveyQuestionが作成されてしまう。ここで一気に修正。
database/seeders/SurveyQuestionSeeder.php
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\File;
use App\Models\SurveyQuestion;
class SurveyQuestionSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$files = File::glob(database_path('seeders/data/*.json'));
foreach ($files as $file) {
$jsonString = File::get($file);
$questionData = json_decode($jsonString, true);
$name = $questionData['title'] ?? null;
if (!$name) {
$name = '質問セット_'. now()->format('YmdHis');
}
SurveyQuestion::factory()->create([
'name' => $name,
'question_data' => $questionData,
]);
}
}
}
nameに関してはjsonのtitleを見てるんだけど
{
"title": "年次調査 2023",
...
これなら年次調査 2023ね。これが無いときは適当なのをつめこんでいる
$name = '質問セット_'. now()->format('YmdHis');
最終確認
seedしなおして
% ./vendor/bin/sail artisan migrate:fresh --seed
Dropping all tables ................................................................................................................... 165ms DONE
INFO Preparing database.
Creating migration table ............................................................................................................... 28ms DONE
INFO Running migrations.
2014_10_12_000000_create_users_table ................................................................................................... 47ms DONE
2014_10_12_100000_create_password_reset_tokens_table ................................................................................... 64ms DONE
2019_08_19_000000_create_failed_jobs_table ............................................................................................ 123ms DONE
2019_12_14_000001_create_personal_access_tokens_table .................................................................................. 70ms DONE
2023_09_18_101626_create_permission_tables ............................................................................................ 744ms DONE
2023_09_21_024514_create_activity_log_table ........................................................................................... 104ms DONE
2023_09_21_024515_add_event_column_to_activity_log_table ............................................................................... 25ms DONE
2023_09_21_024516_add_batch_uuid_column_to_activity_log_table .......................................................................... 22ms DONE
2023_09_29_135347_create_surveys_table ................................................................................................. 25ms DONE
2023_10_01_174142_create_survey_questions_table ........................................................................................ 28ms DONE
INFO Seeding database.
Database\Seeders\SurveyQuestionSeeder .................................................................................................... RUNNING
Database\Seeders\SurveyQuestionSeeder .............................................................................................. 10.59 ms DONE
中を確認する
> SurveyQuestion::all()
[!] Aliasing 'SurveyQuestion' to 'App\Models\SurveyQuestion' for this Tinker session.
= Illuminate\Database\Eloquent\Collection {#7489
all: [
App\Models\SurveyQuestion {#7491
id: 1,
name: "年次調査 2023",
question_data: "{"pages": [{"name": "基本情報", "elements": [{"name": "fullName", "type": "text", "title": "お名前を教えてください。", "isRequired": true}, {"name": "gender", "type": "radiogroup", "title": "性別は?", "choices": ["男", "女", "その他"], "isRequired": true}]}, {"name": "職業と収入", "elements": [{"name": "occupation", "type": "text", "title": "現在の職業は何ですか?", "isRequired": false}, {"name": "incomeSources", "type": "checkbox", "title": "収入源は何ですか?(複数選択可)", "choices": ["給与", "投資", "ビジネス", "その他"], "isRequired": false}]}], "title": "年次調査 2023"}",
created_at: "2023-10-01 18:16:54",
updated_at: "2023-10-01 18:16:54",
},
],
}
よさそうである。
今回のまとめ
とりあえずcreateの準備と質問セットのseedを用意できた。とはいったものの、まったくweb UIに反映できていないので次回はこのセットを含む要するに
をちゃんと作っていく