見出し画像

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

このように、SurveyResponsesurvey_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。このデーターを与えれば保存された回答データーが見られるようになる。


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