survey.js アンケート項目の詳細保存と提出後の確認

現在のDB構造とSurveyResponseDetailの新設

今ちょっとsurveyの結果周りに関するtableを再度確認してみよう

survey_responses (回答親table)

        Schema::create('survey_responses', function (Blueprint $table) {
            $table->id();
            $table->foreignId('survey_id')->constrained();
            $table->foreignId('user_id')->constrained()->comment('回答者ID');
            $table->timestamps();
        });

survey_elements テーブル(質問テーブル)

        Schema::create('survey_elements', function (Blueprint $table) {
            $table->id();
            $table->foreignId('survey_id')->constrained();
            $table->string('title');
            $table->enum('type', ['text']);
            $table->timestamps();
        });

最後にこのtableだ、これで回答の詳細を保存する。ここではSurveyResponseDetailとする

% ./vendor/bin/sail artisan make:model SurveyResponseDetail -mrc

   INFO  Model [app/Models/SurveyResponseDetail.php] created successfully.

   INFO  Migration [database/migrations/2023_09_10_140923_create_survey_response_details_table.php] created successfully.

   INFO  Controller [app/Http/Controllers/SurveyResponseDetailController.php] created successfully.

migration

        Schema::create('survey_response_details', function (Blueprint $table) {
            $table->id();
            $table->foreignId('survey_response_id')->constrained();
            $table->foreignId('survey_element_id')->constrained();
            $table->text('response_value')->nullable()->comment('回答の値');
            $table->timestamps();
        });

現在の保存状況

現在はSurveyの送信により

export default function SurveyShow({ auth, surveyModel, surveyData }) {
    const survey = new Model(surveyData)
    const surveyComplete = useCallback((sender) => {
        router.post(route('surveys.responses.store', surveyModel.id), sender.data)
    }, []);
    survey.onComplete.add(surveyComplete);

このようになっており、送信先は

        router.post(route('surveys.responses.store', surveyModel.id), sender.data)

であった

    public function store(Request $request, Survey $survey): RedirectResponse
    {
        SurveyResponse::create([
            'user_id' => auth()->id(),
            'survey_id' => $survey->id
        ]);
        return redirect(route('surveys.index'))
            ->with(['success' => '保存しました(実際は保存できていない)'])
        ;
    }

これはコードを見てわかるように送信された値を一切受信していない($request変数を何も触ってない)ので、一度これをdumpしてみよう

    public function store(Request $request, Survey $survey): RedirectResponse
    {
        dd($request->all());
        SurveyResponse::create([
            'user_id' => auth()->id(),
            'survey_id' => $survey->id
        ]);
        return redirect(route('surveys.index'))
            ->with(['success' => '保存しました(実際は保存できていない)'])
        ;
    }


とまあ、このようにelement IDをキーにして送信してくるので、これを何とかする。こういう時はまずtransactionをカマす

use Illuminate\Support\Facades\DB;
// ...
    public function store(Request $request, Survey $survey): RedirectResponse
    {
        DB::beginTransaction();
        $surveyResponse = SurveyResponse::create([
            'user_id' => auth()->id(),
            'survey_id' => $survey->id
        ]);
        dd($surveyResponse);
        // DB::commit();
        exit;

        return redirect(route('surveys.index'))
            ->with(['success' => '保存しました(実際は保存できていない)'])
        ;
    }

そうすると親テーブル$surveyResponseの状態が取れる

これに子テーブルをぶら下げて作っていく

use App\Models\SurveyResponseDetail;
//
    public function store(Request $request, Survey $survey): RedirectResponse
    {
        DB::beginTransaction();
        $surveyResponse = SurveyResponse::create([
            'user_id' => auth()->id(),
            'survey_id' => $survey->id
        ]);
        foreach ($request->all() as $elementId => $responseValue) {
            $data = [
                'survey_response_id' => $surveyResponse->id,
                'survey_element_id'  => $elementId,
                'response_value'     => $responseValue,
            ];
            SurveyResponseDetail::create($data);
        }
        // DB::commit();
        exit;

この場合fillableの更新も必要である

app/Models/SurveyResponseDetail.php

class SurveyResponseDetail extends Model
{
    use HasFactory;

    protected $fillable = [
        'survey_response_id',
        'survey_element_id',
        'response_value',
    ];
}

全てが整ったらコミットまで発動する

    public function store(Request $request, Survey $survey): RedirectResponse
    {
        DB::beginTransaction();
        $surveyResponse = SurveyResponse::create([
            'user_id' => auth()->id(),
            'survey_id' => $survey->id
        ]);
        foreach ($request->all() as $elementId => $responseValue) {
            $data = [
                'survey_response_id' => $surveyResponse->id,
                'survey_element_id'  => $elementId,
                'response_value'     => $responseValue,
            ];
            SurveyResponseDetail::create($data);
        }
        DB::commit();

        return redirect(route('surveys.index'))
            ->with(['success' => '保存しました'])
        ;
    }

DBを確認する

> SurveyResponse::all()
[!] Aliasing 'SurveyResponse' to 'App\Models\SurveyResponse' for this Tinker session.
= Illuminate\Database\Eloquent\Collection {#7264
    all: [
      App\Models\SurveyResponse {#7266
        id: 5,
        survey_id: 1,
        user_id: 1,
        created_at: "2023-09-11 10:13:25",
        updated_at: "2023-09-11 10:13:25",
      },
    ],
  }

> SurveyResponseDetail::all()
[!] Aliasing 'SurveyResponseDetail' to 'App\Models\SurveyResponseDetail' for this Tinker session.
= Illuminate\Database\Eloquent\Collection {#7276
    all: [
      App\Models\SurveyResponseDetail {#7273
        id: 3,
        survey_response_id: 5,
        survey_element_id: 1,
        response_value: "やまだ",
        created_at: "2023-09-11 10:13:25",
        updated_at: "2023-09-11 10:13:25",
      },
      App\Models\SurveyResponseDetail {#7272
        id: 4,
        survey_response_id: 5,
        survey_element_id: 2,
        response_value: "たろう",
        created_at: "2023-09-11 10:13:25",
        updated_at: "2023-09-11 10:13:25",
      },
    ],
  }

まあ、大体良さそうだ

view your response画面

今、回答があると

このようになるが、実際には再受験モードになってしまい、具合がよくない。これを改造する。

app/Http/Controllers/SurveyController.php

    public function show(Survey $survey)
    {
        $elements = $survey->elements()->get()->map(function ($element) {
            return [
                'name'  => (string)$element->id,
                'title' => $element->title,
                'type'  => $element->type,
            ];
        })->toArray();
        $surveyData = [
            'showQuestionNumbers' => 'off',
            'completedHtml' => '',
            'elements'      => $elements,
        ];

        return Inertia::render('Surveys/Show', [
            'surveyModel' => $survey,
            'surveyData'  => $surveyData
        ]);
    }

今、これで行っているのでそもそも結果がある時と無いときで分岐が必要である。ということだから、結果の数を取得してみよう

        $count = $survey->responses()->where('user_id', auth()->id())->count();
        dd($count);

結果が保存されていれば通常1が出力されてくるだろう。ここでは仕様として複数回のSurveyを考えていないからとりあえずこのcountがあった時は仕様をチェンジする

    public function show(Survey $survey)
    {
        $responseCount = $survey->responses()->where('user_id', auth()->id())->count();

        $elements = $survey->elements()->get()->map(function ($element) {
            return [
                'name'  => (string)$element->id,
                'title' => $element->title,
                'type'  => $element->type,
            ];
        })->toArray();
        $surveyData = [
            'showQuestionNumbers' => 'off',
            'completedHtml'       => '',
            'elements'            => $elements,
        ];

        return Inertia::render('Surveys/Show', [
            'surveyModel' => $survey,
            'surveyData'  => $surveyData,
            'readOnly'    => $responseCount ? true : false,
        ]);
    }

resources/js/Pages/Surveys/Show.jsx で readOnlyフラグを受け取り

export default function SurveyShow({ auth, surveyModel, surveyData, readOnly }) {
    const survey = new Model(surveyData)
    if (readOnly) {
        survey.mode = "display";
    }
    const surveyComplete = useCallback((sender) => {
        router.post(route('surveys.responses.store', surveyModel.id), sender.data )
    }, []);
    survey.onComplete.add(surveyComplete);

そうすると、まず回答があったときは回答できなくなる

displayモード

で、それぞれの回答に回答内容を記載する必要があるの

export default function SurveyShow({ auth, surveyModel, surveyData, readOnly }) {
    const survey = new Model(surveyData)
    if (readOnly) {
        survey.mode = "display";
    }
    const surveyComplete = useCallback((sender) => {
        router.post(route('surveys.responses.store', surveyModel.id), sender.data )
    }, []);
    survey.onComplete.add(surveyComplete);
    survey.data = {
        '1': 'a',
        '2': 'b',
    }

たとえばこうすると

ウッ、、微妙。SurveyDataというのは使われているから、これだとちょっとよくない

ってわけでリファクタリングする。surveyDataをまずsurveySettingとかに直す

        $surveySetting = [
            'showQuestionNumbers' => 'off',
            'completedHtml'       => '',
            'elements'            => $elements,
        ];

        return Inertia::render('Surveys/Show', [
            'surveyModel'   => $survey,
            'surveySetting' => $surveySetting,
            'readOnly'      => $responseCount ? true : false,
        ]);

viewの方

export default function SurveyShow({ auth, surveyModel, surveySetting, readOnly }) {
    const survey = new Model(surveySetting)
    if (readOnly) {
        survey.mode = "display";
    }
    const surveyComplete = useCallback((sender) => {
        router.post(route('surveys.responses.store', surveyModel.id), sender.data )
    }, []);
    survey.onComplete.add(surveyComplete);
    survey.data = {
        '1': 'a',
        '2': 'b',
    }

で、survey.dataには実データーを挿入する

リレーションの整理

ここで一度正しくリレーションを定義しよう

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);
    }
}

detailsで取得できるように

app/Models/SurveyResponseDetail.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class SurveyResponseDetail extends Model
{
    use HasFactory;

    protected $fillable = [
        'survey_response_id',
        'survey_element_id',
        'response_value',
    ];

    public function element(): BelongsTo
    {
        return $this->belongsTo(SurveyElement::class, 'survey_element_id');
    }
}

こちらはelementで取得できるように

した後ベタなコードを書いてみる

    public function show(Survey $survey)
    {
        $responseCount = $survey->responses()->where('user_id', auth()->id())->count();
        $surveyData = null;
        if ($responseCount) {
            $surveyData = [];
            $tmp = $survey->responses()->with('details.element')->where('user_id', auth()->id())->first();
            foreach ($tmp->details as $detail) {
               $surveyData[$detail->survey_element_id] = $detail->response_value;
            }
            unset($tmp);
        }

        $elements = $survey->elements()->get()->map(function ($element) {
            return [
                'name'  => (string)$element->id,
                'title' => $element->title,
                'type'  => $element->type,
            ];
        })->toArray();
        $surveySetting = [
            'showQuestionNumbers' => 'off',
            'completedHtml'       => '',
            'elements'            => $elements,
        ];

        return Inertia::render('Surveys/Show', [
            'surveyModel'   => $survey,
            'surveySetting' => $surveySetting,
            'surveyData'    => $surveyData,
            'readOnly'      => $responseCount ? true : false,
        ]);
    }

最後にviewを整理する

resources/js/Pages/Surveys/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';


export default function SurveyShow({ auth, surveyModel, surveySetting, surveyData, readOnly }) {
    const survey = new Model(surveySetting)
    if (readOnly) {
        survey.mode = "display";
    }
    const surveyComplete = useCallback((sender) => {
        router.post(route('surveys.responses.store', surveyModel.id), sender.data )
    }, []);
    survey.onComplete.add(surveyComplete);
    survey.data = surveyData;

    return (
        <AuthenticatedLayout
            user={auth.user}
            header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Survey</h2>}
        >
            <Head title="Survey.js" />

            <div className="py-12">
                <div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
                    <Survey model={survey} />;
                </div>
            </div>
        </AuthenticatedLayout>
    );
}


まとめ

実に基本的な事はできるようになった。ただ、今seedから作ってる部分もまあまああり、それはたとえばsurveyの本体などである


たとえばこのAvailable Surveys自体を増やしたり、設定を編集したりという事も考えられる(とはいえ、問題データーまで追加削除可能にするとまあまあ面倒くさい)。あるいはユーザーを複数人作って結果をviewしたい人がいるかもしれない。この辺はプロジェクトに依存するが、次回はまあ適当に増やしてみるとかするか?

あるいは、うーん、別の事をするかもしれない、まあ、まだ決めてないという所である。てかこのシリーズは全体的に推敲が必要だね。行き当たりばったりで作っていいアプリじゃないねw

この記事が気に入ったらサポートをしてみませんか?