
inertia.js(react) + survey.js - 7: 受験
保存用DBの作成
これはSurveyResponseに大元の回答データーを入れる事になる
% ./vendor/bin/sail artisan make:model SurveyResponse -m
Schema::create('survey_responses', function (Blueprint $table) {
$table->id();
$table->foreignId('survey_id')->constrained();
$table->foreignId('user_id')->constrained()->comment('回答者ID');
$table->timestamps();
});
このように、SurveyResponseはsurvey_idとuser_idを紐付けるため「だけ」に存在している。
では、早速保存してみよう
コントローラーの作成
SurveyResponseモデルを作ったならSurveyResponseControllerも作ればええやないかというんだけど、これはちょっと事情が異なっていて、親にSurveyを持ちたいのである。これはartisanのmake:modelではイマイチ対応できない
artisan make:model --help
Description:
Create a new Eloquent model class
Usage:
make:model [options] [--] <name>
Arguments:
name The name of the model
Options:
-a, --all Generate a migration, seeder, factory, policy, resource controller, and form request classes for the model
-c, --controller Create a new controller for the model
-f, --factory Create a new factory for the model
--force Create the class even if the model already exists
-m, --migration Create a new migration file for the model
--morph-pivot Indicates if the generated model should be a custom polymorphic intermediate table model
--policy Create a new policy for the model
-s, --seed Create a new seeder for the model
-p, --pivot Indicates if the generated model should be a custom intermediate table model
-r, --resource Indicates if the generated controller should be a resource controller
--api Indicates if the generated controller should be an API resource controller
-R, --requests Create new form request classes and use them in the resource controller
--test Generate an accompanying PHPUnit test for the Model
--pest Generate an accompanying Pest test for the Model
-h, --help Display help for the given command. When no command is given display help for the list command
-q, --quiet Do not output any message
-V, --version Display this application version
--ansi|--no-ansi Force (or disable --no-ansi) ANSI output
-n, --no-interaction Do not ask any interactive question
--env[=ENV] The environment the command should run under
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
従ってmake:controllerの方を見てみよう
% ./vendor/bin/sail artisan make:controller --help
Description:
Create a new controller class
Usage:
make:controller [options] [--] <name>
Arguments:
name The name of the controller
Options:
--api Exclude the create and edit methods from the controller
--type=TYPE Manually specify the controller stub file to use
--force Create the class even if the controller already exists
-i, --invokable Generate a single method, invokable controller class
-m, --model[=MODEL] Generate a resource controller for the given model
-p, --parent[=PARENT] Generate a nested resource controller class
-r, --resource Generate a resource controller class
-R, --requests Generate FormRequest classes for store and update
-s, --singleton Generate a singleton resource controller class
--creatable Indicate that a singleton resource should be creatable
--test Generate an accompanying PHPUnit test for the Controller
--pest Generate an accompanying Pest test for the Controller
-h, --help Display help for the given command. When no command is given display help for the list command
-q, --quiet Do not output any message
-V, --version Display this application version
--ansi|--no-ansi Force (or disable --no-ansi) ANSI output
-n, --no-interaction Do not ask any interactive question
--env[=ENV] The environment the command should run under
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
すると、このように--parentというのが見えるだろう、これを使う。
% ./vendor/bin/sail artisan make:controller SurveyResponseController -r -m SurveyResponse -p Survey
INFO Controller [app/Http/Controllers/SurveyResponseController.php] created successfully.
こんな感じで使うわけ
そうすると
<?php
namespace App\Http\Controllers;
use App\Models\Survey;
use App\Models\SurveyResponse;
use Illuminate\Http\Request;
class SurveyResponseController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Survey $survey)
{
//
}
/**
* Show the form for creating a new resource.
*/
public function create(Survey $survey)
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request, Survey $survey)
{
//
}
こんな感じで生成されてくる。
routeの設定
Route::get('/dashboard', [DashboardController::class, 'index'])
->middleware(['auth', 'verified'])
->name('dashboard');
Route::get('surveys/{survey}/take', [SurveyTakingController::class, 'show'])
->middleware(['auth', 'verified'])
->name('surveys.take');
Route::resource('surveys.responses', SurveyResponseController::class)
->middleware(['auth', 'verified']); // 追加
さて、保存する
resources/js/Pages/Surveys/Take.jsx
const surveyComplete = useCallback((sender) => {
// TODO
router.post(route('surveys.previewStore', surveyModel.id), sender.data);
}, []);
この部分を
const surveyComplete = useCallback((sender) => {
router.post(route('surveys.responses.store', surveyModel.id), sender.data);
}, []);
こうする
で
app/Http/Controllers/SurveyResponseController.php
public function store(Request $request, Survey $survey)
{
dd($request->all());
}
dd()で止めておく

surveyResponseを保存する
先にも書いたように、surveyResponseは単なる紐付けでしかない。
(再掲)
Schema::create('survey_responses', function (Blueprint $table) {
$table->id();
$table->foreignId('survey_id')->constrained();
$table->foreignId('user_id')->constrained()->comment('回答者ID');
$table->timestamps();
});
とりあえずシンプルに保存する
てかまあ、fillableつけて
app/Models/SurveyResponse.php
class SurveyResponse extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'survey_id',
];
}
これで保存する。まだトランザクションは閉じません
public function store(Request $request, Survey $survey)
{
DB::beginTransaction();
$surveyResponse = SurveyResponse::create([
'user_id' => $request->user()->id,
'survey_id' => $survey->id
]);
dd($request->all());
}
回答詳細テーブル
ヘビーになってきたけどここはページ分けたらわけわからんくなるので一気にやっちゃいますよ
これはsurveyのidに対してelement idと回答の値をjsonで挿入してしまう。
とりあえずモデル作る
% ./vendor/bin/sail artisan make:model SurveyResponseDetail -m
INFO Model [app/Models/SurveyResponseDetail.php] created successfully.
INFO Migration [database/migrations/2023_10_02_211235_create_survey_response_details_table.php] created successfully.
スキーマ作る
Schema::create('survey_response_details', function (Blueprint $table) {
$table->id();
$table->foreignId('survey_response_id')->constrained();
$table->foreignId('survey_element_id')->constrained();
$table->json('response_value')->nullable()->comment('回答の値');
$table->timestamps();
});
先程の保存を加工するのだが、まずモデルを
app/Models/SurveyResponseDetail.php
class SurveyResponseDetail extends Model
{
use HasFactory;
protected $casts = [
'response_value' => 'array',
];
protected $fillable = [
'survey_response_id',
'survey_element_id',
'response_value',
];
追加したらば
public function store(Request $request, Survey $survey)
{
DB::beginTransaction();
$surveyResponse = SurveyResponse::create([
'user_id' => $request->user()->id,
'survey_id' => $survey->id
]);
foreach ($request->all() as $elementId => $responseValue) {
if (!is_array($responseValue)) {
$responseValue = ['value' => $responseValue];
}
$data = [
'survey_response_id' => $surveyResponse->id,
'survey_element_id' => $elementId,
'response_value' => $responseValue,
];
SurveyResponseDetail::create($data);
}
DB::commit();
return redirect(route('dashboard'))
->with(['success' => '保存しました'])
;
}
となるわけだ
実際どういう風に保存されてるの?
このコードをおいかけるより保存結果を見た方がいいかも
= Illuminate\Database\Eloquent\Collection {#7502
all: [
App\Models\SurveyResponse {#7504
id: 1,
survey_id: 1,
user_id: 3,
created_at: "2023-10-02 21:20:21",
updated_at: "2023-10-02 21:20:21",
},
],
}
このように回答データーが一本保存され
> SurveyResponseDetail::all()
[!] Aliasing 'SurveyResponseDetail' to 'App\Models\SurveyResponseDetail' for this Tinker session.
= Illuminate\Database\Eloquent\Collection {#7511
all: [
App\Models\SurveyResponseDetail {#7509
id: 1,
survey_response_id: 1,
survey_element_id: 1,
response_value: "{"value": "ああ"}",
created_at: "2023-10-02 21:20:21",
updated_at: "2023-10-02 21:20:21",
},
App\Models\SurveyResponseDetail {#7508
id: 2,
survey_response_id: 1,
survey_element_id: 2,
response_value: "{"value": 1}",
created_at: "2023-10-02 21:20:21",
updated_at: "2023-10-02 21:20:21",
},
App\Models\SurveyResponseDetail {#7507
id: 3,
survey_response_id: 1,
survey_element_id: 3,
response_value: "{"value": "ああ"}",
created_at: "2023-10-02 21:20:21",
updated_at: "2023-10-02 21:20:21",
},
App\Models\SurveyResponseDetail {#7506
id: 4,
survey_response_id: 1,
survey_element_id: 4,
response_value: "[5, 6]",
created_at: "2023-10-02 21:20:21",
updated_at: "2023-10-02 21:20:21",
},
],
}
回答保存状態でtakeを変化させる
現在

もう覚えてないかもしれんけど回答を一度やったら二度受験させてはいけない仕様だったのだった。つまり、回答データーがあるときはTakeとかなっていてはいけないのである。
これはindexの
public function index(): Response
{
$surveys = Survey::latest()->get();
return Inertia::render('Dashboard', [
'surveys' => $surveys
]);
}
このザクっとしたやつを変更し、is_takenとかで検知できるようにする。このように取得したDBのcollectionに色付けるときは大抵mapする
の前にまずモデルを app/Models/Survey.php
public function responses(): HasMany
{
return $this->hasMany(SurveyResponse::class);
}
しておいて
app/Http/Controllers/DashboardController.php で
class DashboardController extends Controller
{
public function index(): Response
{
$surveys = Survey::latest()->get()->map(function ($survey) {
$response = $survey->responses()->where('user_id', auth()->id())->first();
$survey->response_id = $response ? $response->id : null;
return $survey;
});
return Inertia::render('Dashboard', [
'surveys' => $surveys
]);
}
}
このように、response_idのあるなしで検知するわけだ。
そしたらこれに基いてインタフェースを変更する
resources/js/Pages/Dashboard.jsx
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{surveys.map((survey, index) => (
<div key={index} className="bg-white rounded-lg shadow-lg p-4 border border-gray-300">
<div className="flex justify-between items-center mb-2">
<h4 className="text-lg font-semibold">
{survey.title}
</h4>
</div>
<p className="text-sm text-gray-700 mb-3">
{survey.description}
</p>
{survey.response_id ? (
<PrimaryButton href={route('surveys.responses.show', {survey: survey.id, response: survey.response_id})}>
<VscEye className="mr-2" /> {t('Show Details')}
</PrimaryButton>
) : (
<PrimaryButton href={route('surveys.take', survey.id)}>
<VscChecklist className="mr-2" /> {t('Take')}
</PrimaryButton>
)}
</div>
))}
</div>
みたいな的な。
surveys.responses.show
長くなってきたけど、もうやっちゃうよ。とりあえず分析じゃなくて回答結果を表示する
これは結局previewのstoreをやってたのとほとんど変わらない
なので、とりあえずそれをコピーしてみる。それはsurveys.showとかである
app/Http/Controllers/SurveyResponseController.php
public function show(Survey $survey, SurveyResponse $response, SurveyService $surveyService)
{
$pagesData = $surveyService->getSurveyData($survey);
$settings = $survey->settings;
$surveyData = [
'pages' => $pagesData,
];
if ($settings) {
$surveyData = array_merge($surveyData, $settings);
}
$surveyData = json_encode($surveyData, JSON_UNESCAPED_UNICODE);
/*
$responseCount = 0;
if ($previewData = $request->session()->get('preview_data')) {
$responseCount = count($previewData);
}
*/
$previewData = [];
return Inertia::render('SurveyResponses/Show', [
'surveyModel' => $survey,
'surveyData' => $surveyData,
'responseData' => $previewData,
'readOnly' => true,
]);
}
resources/js/Pages/SurveyResponses/Show.jsx
import React, { useCallback } from 'react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import 'survey-core/defaultV2.min.css';
import { Model } from 'survey-core';
import { Survey } from 'survey-react-ui';
import { Head, router } from '@inertiajs/react';
import { VscTrash } from 'react-icons/vsc';
import { useLaravelReactI18n } from 'laravel-react-i18n';
export default function SurveyShow({
auth, surveyModel, surveyData, responseData, readOnly,
}) {
const { t } = useLaravelReactI18n();
const survey = new Model(surveyData);
if (readOnly) {
survey.mode = 'display';
}
const surveyComplete = useCallback((sender) => {
router.post(route('surveys.previewStore', surveyModel.id), sender.data);
}, []);
survey.onComplete.add(surveyComplete);
survey.data = responseData;
return (
<AuthenticatedLayout
user={auth.user}
header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">{surveyModel.title}</h2>}
>
<Head title={surveyModel.title} />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<Survey model={survey} />
</div>
</div>
</AuthenticatedLayout>
);
}
これで回答データーなしのsurveyが表示される

ってことは
/*
$responseCount = 0;
if ($previewData = $request->session()->get('preview_data')) {
$responseCount = count($previewData);
}
*/
この辺を何とかして回答データーをとってきて詰めこめればクリアーであーる。
まず回答数を取る。今回は仕様上基本0か1のハズだ(変なデーターが紛れこまなければね)
$responseCount = $survey->responses()->where('user_id', auth()->id())->count();
dd($responseCount);
回答があるときの処理を書く
$responseCount = $survey->responses()->where('user_id', auth()->id())->count();
if ($responseCount) {
// 回答があるとき
}
の前に、リレーションをちゃんと定義しておく
app/Models/SurveyResponse.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class SurveyResponse extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'survey_id',
];
public function details(): HasMany
{
return $this->hasMany(SurveyResponseDetail::class);
}
}
そしたらば
$responseCount = $survey->responses()->where('user_id', auth()->id())->count();
if ($responseCount) {
$responseDetails = $survey->responses()
->with('details.element')
->where('user_id', auth()->id())
->first()
->details;
dd($responseDetails);
}
とすると
"id" => 1
"survey_response_id" => 1
"survey_element_id" => 1
"response_value" => "{"value": "ああ"}"
"created_at" => "2023-10-02 21:20:21"
"updated_at" => "2023-10-02 21:20:21"
このようなデーターが取れるはずだ。ここで重要なのはelement idに対してのresponse valueが正しく取得されているかどうかということです。よさそうであればこれをkey - value形式っぽい連想配列に変換する
$responseCount = $survey->responses()->where('user_id', auth()->id())->count();
if ($responseCount) {
$responseDetails = $survey->responses()
->with('details.element')
->where('user_id', auth()->id())
->first()
->details;
$surveyData = array_reduce($responseDetails->toArray(), function ($carry, $detail) {
$responseValue = $detail['response_value'];
// $responseValueが配列であり、'value'キーが存在する場合
if (is_array($responseValue) && isset($responseValue['value'])) {
$value = $responseValue['value']; // 'value'キーの値を取り出す
} else {
$value = $responseValue; // それ以外の場合、$responseValueをそのまま使用
}
$carry[$detail['survey_element_id']] = $value;
return $carry;
}, []);
dd($surveyData);
}
これは 「value: 」がある奴は単一の値とし、そうでないやつは配列と見做している。まあこの変の処理に関しては議論のあるところかもしれない、が、いずれにせよ
array:4 [▼ // app/Http/Controllers/SurveyResponseController.php:102
1 => "ああ"
2 => 1
3 => "ああ"
4 => array:2 [▼
0 => 5
1 => 6
]
]
となればok。このデーターを与えれば保存された回答データーが見られるようになる。