見出し画像

inertia.js(react) + survey.js - 3: survey作成

さて、今、質問セットが定義できたので、これをcreate時に渡してセレクトボックスを作成してみよう

Create時にこの情報を渡す

<?php

namespace App\Http\Controllers;

use App\Models\Survey;
use App\Models\SurveyQuestion;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;

class SurveyController extends Controller
{

    /**
     * Show the form for creating a new resource.
     */
    public function create(): Response
    {
        $surveyQuestionSets = SurveyQuestion::pluck('name', 'id');
        return Inertia::render('Surveys/Create', [
            'surveyQuestionSets' => $surveyQuestionSets,
        ]);
    }

このようにしておく、これでviewからsurveyQuestionSets変数をループしてセレクトボックスを作れるようになるだろう

create view(コンポーネント)

resources/js/Pages/Surveys/Create.jsx

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import {
  Head, useForm, usePage,
} from '@inertiajs/react';
import { useLaravelReactI18n } from 'laravel-react-i18n';

import InputError from '@/Components/InputError';
import InputLabel from '@/Components/InputLabel';
import PrimaryButton from '@/Components/PrimaryButton';
import TextInput from '@/Components/TextInput';

export default function SurveyCreate({ auth, surveyQuestionSets }) {
  const { t } = useLaravelReactI18n();

  const {
    data, setData, post, errors, processing, recentlySuccessful,
  } = useForm({
    title: '',
    description: '',
    settings: '',
    survey_question_set_id: '',
  });


  const submit = (e) => {
    e.preventDefault();

    post(route('surveys.store'));
  };

  return (
    <AuthenticatedLayout
      user={auth.user}
      header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">{t('Create New Survey')}</h2>}
    >
      <Head title={t("Create New Survey")} />
      <div className="py-12">
        <div className="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
          <div className="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
            <div>
              <div>
                <InputLabel htmlFor="title" value={t('Title')} />

                <TextInput
                  id="title"
                  className="mt-1 block w-full"
                  value={data.title}
                  onChange={(e) => setData('title', e.target.value)}
                  onSubmit={submit}
                />

                <InputError className="mt-2" message={errors.title} />
              </div>

              <div className="mt-3">
                <InputLabel htmlFor="description" value={t('Description')} />

                <TextInput
                  id="description"
                  className="mt-1 block w-full"
                  value={data.description}
                  onChange={(e) => setData('description', e.target.value)}
                  onSubmit={submit}
                />

                <InputError className="mt-2" message={errors.description} />
              </div>

              <div className="mt-3">
                <InputLabel htmlFor="survey_question_set_id" value={t("Question Set")} />
                <select
                  id="survey_question_set_id"
                  className="mt-1 block w-full"
                  value={data.survey_question_set_id}
                  onChange={(e) => setData('survey_question_set_id', e.target.value)}
                >
                  <option value="">-- {t("Select a Question Set")} --</option>
                  {Object.entries(surveyQuestionSets).map(([key, value]) => (
                    <option key={key} value={key}>
                      {value}
                    </option>
                  ))}
                </select>
                <InputError className="mt-2" message={errors.survey_question_set_id} />
              </div>

              <form onSubmit={submit} className="mt-6 space-y-6">
                <div className="flex items-center gap-4">
                  <PrimaryButton disabled={processing}>{t('Save')}</PrimaryButton>
                </div>
              </form>
            </div>
          </div>
        </div>
      </div>
    </AuthenticatedLayout>

  );
}

これでok。翻訳は適当に与えてね! (lang/ja.json)

     "Already registered?": "登録済みの方はこちら",
     "Are you sure you want to delete your account?": "アカウントを削除しますか?",
     "Action": "操作",
+    "Available Surveys": "利用可能なアンケート調査",
     "Cancel": "キャンセル",
     "Click here to re-send the verification email.": "確認メールの再送はこちら",
     "Confirm": "確認",
     "Confirm Password": "パスワード(確認用)",
     "Current Password": "現在のパスワード",
     "Create": "作成",
+    "Create New Survey": "新しいアンケート調査の作成",
     "Create New User": "新規ユーザー作成",
     "Created At": "作成日時",
     "Dashboard": "ダッシュボード",
@@ -55,6 +57,7 @@
     "Profile": "プロフィール",
     "Profile Information": "プロフィール情報",
     "Properties": "プロパティー",
+    "Question Set": "質問セット",
     "Regards": "よろしくお願いします",
     "Register": "アカウント作成",
     "Remember me": "ログイン状態を保持する",
@@ -68,6 +71,7 @@
     "Saved.": "保存が完了しました。",
     "Server Error": "サーバーエラー",
     "Service Unavailable": "サービスは利用できません",
+    "Select a Question Set": "質問セットを選択してください",
     "Showing": "表示中",
     "Surveys": "アンケート調査",
     "Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn't receive the email, we will gladly send you another.": "ご登録ありがとうございます。入力いただいたメールアドレス宛に確認メールを送信しました。メールをご確認いただき、メールに記載されたURLをクリックして登録を完了してください。メールが届いていない場合、メールを再送できます。",
@@ -77,6 +81,7 @@
     "This action is unauthorized.": "禁止されているアクションです",
     "This is a secure area of the application. Please confirm your password before continuing.": "ここはアプリケーションの安全な領域です。パスワードを入力して続行ください。",
     "This password reset link will expire in :count minutes.": "このパスワード再設定リンクの有効期限は:count分です。",
+    "Title": "タイトル",

store

とりあえず
app/Http/Controllers/SurveyController.php

    public function store(Request $request)
    {
        dd($request->all());
    }

としてデーターが正しく渡っていればok。渡ったデーターをDBに登録していくんだけど、質問のjsonデーターを展開して自身のDBに再度「エレメント」として取り込んでいく必要があったりする。

質問セットの展開

の前に場所がない

というわけでテーブルを設計していくぞ〜い

これは以下3つのモデルで構成される

  • SurveyPage

  • SurveyElement

  • SurveyElementChoice


これは上記の概念に基いている。Question1本1本はElementという単位で保存しているし、Choiceは多肢選択用でどうしても必要になってくる。
以下のようにモデルを作成。

% ./vendor/bin/sail artisan make:model SurveyPage -m

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

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

% ./vendor/bin/sail artisan make:model SurveyElement -m

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

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

% ./vendor/bin/sail artisan make:model SurveyElementChoice -m

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

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

SurveyPage

        Schema::create('survey_pages', function (Blueprint $table) {
            $table->id();
            $table->foreignId('survey_id')->constrained();
            $table->string('name')->nullable()->default('');
            $table->string('title')->nullable()->default('');
            $table->string('description')->default('');
            $table->timestamps();
        });

モデルも変更しておくよ〜

app/Models/SurveyPage.php

<?php

namespace App\Models;

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

class SurveyPage extends Model
{
    use HasFactory;

    protected $fillable = [
        'survey_page_id',
        'type',
        'name',
        'title',
        'description',
        'is_required',
    ];


    public function elements(): HasMany
    {
        return $this->hasMany(SurveyElement::class);
    }
}

まあ実はnameとかdescriptionとか入れてないんすけどね

SurveyElement

        Schema::create('survey_elements', function (Blueprint $table) {
            $table->id();
            $table->foreignId('survey_page_id')->constrained();
            $table->string('title');
            $table->boolean('is_required')->default(0);
            $table->enum('type', ['text', 'radiogroup', 'checkbox']);
            $table->timestamps();
        });

モデルも更新

<?php

namespace App\Models;

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

class SurveyElement extends Model
{
    use HasFactory;

    protected $fillable = [
        'survey_page_id',
        'type',
        'title',
        'is_required',
    ];

    public function choices(): HasMany
    {
        return $this->hasMany(SurveyElementChoice::class);
    }
}

SurveyChoice

        Schema::create('survey_element_choices', function (Blueprint $table) {
            $table->id();
            $table->foreignId('survey_element_id')->constrained();
            $table->string('choice');
            $table->timestamps();
        });

app/Models/SurveyElementChoice.php

<?php

namespace App\Models;

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

class SurveyElementChoice extends Model
{
    use HasFactory;

    protected $fillable = [
        'survey_element_id',
        'choice',
    ];

    public function SurveyElement(): BelongsTo
    {
        return $this->hasMany(SurveyElement::class);
    }
}

以下はchatgpt作成の関係図

[ surveys ]
   |
   | 1
   | n
[survey_pages] --1:n-- [survey_elements] --1:n-- [survey_element_choices]

再度storeを開発していく

さて、現状のstoreは

    public function store(Request $request)
    {
        dd($request->all());
    }

このようにリクエストダンプしているだけであった。

試しに適当に送信すると、以下のようなデーターになる

array:4 [▼ // app/Http/Controllers/SurveyController.php:46
  "title" => "てすと"
  "description" => "memo"
  "settings" => null
  "survey_question_set_id" => "1"
]

冒頭でモデルのuseが必要なのでばばっと登録しておこう

<?php

namespace App\Http\Controllers;

use App\Models\Survey;
use App\Models\SurveyQuestion;
use App\Models\SurveyPage;
use App\Models\SurveyElement;
use App\Models\SurveyElementChoice;

survey_question_set_idからsurvey_questionを取得する

ここでsurvey_question_set_id = 1とか取れているので、この情報を元に質問セットを取得する

namespace App\Http\Controllers;

use App\Models\Survey;
use App\Models\SurveyQuestion;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;

class SurveyController extends Controller
{

    public function store(Request $request)
    {
        $data = $request->all();
        $data['settings'] = [];
        // TODO: 本来はvalidationをかける
        $surveyQuestionSetId = $request->survey_question_set_id;
        $surveyStructure = SurveyQuestion::findOrFail($surveyQuestionSetId)->question_data;
        dd($surveyStructure);

単純にfindOrFailして当該質問セットのrowを取ってきたのちにquestion_dataを取る。これはjsonが入っていたものだけどarray castにより

このような「php配列」のに変換されていることがdumpで確認できる。これをsurveyに登録するにあたってモデルを更新しなくてはならない。webUIから更新するタイプはfillableが必須である。

app/Models/Survey.php

<?php

namespace App\Models;

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

class Survey extends Model
{
    use HasFactory;

    protected $fillable = [
        'title',
        'description',
    ];

}

本当はsettings周りも一気にやりたいところだけど、わけがわからなくなってくるかもなのでちょっとずつ、ちょっとずつ。

<?php

namespace App\Http\Controllers;

use App\Models\Survey;
use App\Models\SurveyQuestion;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;

use Illuminate\Support\Facades\DB;

class SurveyController extends Controller
    public function store(Request $request)
    {
        $data = $request->all();
        $data['settings'] = [];
        // TODO: 本来はvalidationをかける
        $surveyQuestionSetId = $request->survey_question_set_id;
        $surveyStructure = SurveyQuestion::find($surveyQuestionSetId)->question_data;

        DB::beginTransaction();
        $survey = Survey::create($data);
        // DB::commit();

これで、titleとdescriptionは登録されるのだが、問題はここではない。

survey pageの登録

一番外側のpageを登録していく

    public function store(Request $request)
    {
        $data = $request->all();
        $data['settings'] = [];
        // TODO: 本来はvalidationをかける
        $surveyQuestionSetId = $request->survey_question_set_id;
        $surveyStructure = SurveyQuestion::find($surveyQuestionSetId)->question_data;

        DB::beginTransaction();
        $survey = Survey::create($data);
        foreach ($surveyStructure['pages'] as $page) {
            $data = [
                'name'      => $page['name'],
                'survey_id' => $survey['id'],
            ];
            dump($data);
            $surveyPage = SurveyPage::create($data);
        }
        exit;

この辺は常に

この配列を意識しといてください。要するに 

  • 基本情報

  • 職業と収入

の段が登録された。

elementsの登録

elementsはpageの下に位置するよね。だからpageを作成した後じゃないと作成できませんよっと。

        DB::beginTransaction();
        $survey = Survey::create($data);
        foreach ($surveyStructure['pages'] as $page) {
            $data = [
                'name'      => $page['name'],
                'survey_id' => $survey['id'],
            ];
            $surveyPage = SurveyPage::create($data);

            foreach ($page['elements'] as $element) {
                $data                 = [
                    'survey_page_id' => $surveyPage->id,
                    'type'           => $element['type'],
                    'title'          => $element['title'],
                    'is_required'    => $element['isRequired'] ?? false,
                ];
                dump($data);
                $surveyElement = SurveyElement::create($data);
            }
        }
        exit;

要するに「お名前を…」みたいな奴が登録されていく。以下はdumpである。

choiceの登録

もう一歩!

    public function store(Request $request)
    {
        $data = $request->all();
        $data['settings'] = [];
        // TODO: 本来はvalidationをかける
        $surveyQuestionSetId = $request->survey_question_set_id;
        $surveyStructure = SurveyQuestion::find($surveyQuestionSetId)->question_data;

        DB::beginTransaction();
        $survey = Survey::create($data);
        foreach ($surveyStructure['pages'] as $page) {
            $data = [
                'name'      => $page['name'],
                'survey_id' => $survey['id'],
            ];
            $surveyPage = SurveyPage::create($data);

            foreach ($page['elements'] as $element) {
                $data                 = [
                    'survey_page_id' => $surveyPage->id,
                    'type'           => $element['type'],
                    'title'          => $element['title'],
                    'is_required'    => $element['isRequired'] ?? false,
                ];
                $surveyElement = SurveyElement::create($data);

                if ($choices = $element['choices'] ?? null) {
                    foreach ($choices as $choice) {
                        $data = [
                            'survey_element_id' => $surveyElement->id,
                            'choice'            => $choice,
                        ];
                        dump($data);
                        SurveyElementChoice::create($data);
                    }
                }
            }
        }
        exit;
        DB::commit();
        return redirect(route('surveys.index'))
            ->with(['success' => __('New Survey Created')])
        ;
    }

これはradioとかcheckboxで使われる枝ね。

保存

ここまでよさそうであればcommitして実際に保存してゆく。

最終のコード

<?php

namespace App\Http\Controllers;

use App\Models\Survey;
use App\Models\SurveyQuestion;
use App\Models\SurveyPage;
use App\Models\SurveyElement;
use App\Models\SurveyElementChoice;

use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Http\Request;

use Inertia\Inertia;
use Inertia\Response;

class SurveyController extends Controller
  
    public function store(Request $request): RedirectResponse
    {
        $data = $request->all();
        $data['settings'] = [];
        // TODO: 本来はvalidationをかける
        $surveyQuestionSetId = $request->survey_question_set_id;
        $surveyStructure = SurveyQuestion::find($surveyQuestionSetId)->question_data;

        DB::beginTransaction();
        $survey = Survey::create($data);
        foreach ($surveyStructure['pages'] as $page) {
            $data = [
                'name'      => $page['name'],
                'survey_id' => $survey['id'],
            ];
            $surveyPage = SurveyPage::create($data);

            foreach ($page['elements'] as $element) {
                $data                 = [
                    'survey_page_id' => $surveyPage->id,
                    'type'           => $element['type'],
                    'title'          => $element['title'],
                    'is_required'    => $element['isRequired'] ?? false,
                ];
                $surveyElement = SurveyElement::create($data);

                if ($choices = $element['choices'] ?? null) {
                    foreach ($choices as $choice) {
                        $data = [
                            'survey_element_id' => $surveyElement->id,
                            'choice'            => $choice,
                        ];
                        SurveyElementChoice::create($data);
                    }
                }
            }
        }
        DB::commit();
        return redirect(route('surveys.index'))
            ->with(['success' => __('New Survey Created')])
        ;
    }
保存されたといっても取り出しを書いてないので何もでてはこないがぬ

作成されたsurveyを表示

つわけで作成されて何も出てこないのもアレなので取り出しておく。

resources/js/Pages/Surveys/Index.jsx

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head } from '@inertiajs/react';

import PrimaryButton from '@/Components/PrimaryButton';
import SecondaryButton from '@/Components/SecondaryButton';

import { useLaravelReactI18n } from 'laravel-react-i18n';
import { useConfiguredDayjs } from '@/hooks/useConfiguredDayjs';
import {
  VscSettingsGear,
  VscOpenPreview,
} from 'react-icons/vsc';

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>
              <PrimaryButton href={route('surveys.create')}>
                {t('Create New Survey')}
              </PrimaryButton>
            </div>

            <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>

                    <SecondaryButton href={route('surveys.edit', survey.id)}>
                      <VscSettingsGear className="mr-2" />
                      {' '}
                      {t('Settings')}
                    </SecondaryButton>
                  </div>

                  <p className="text-sm text-gray-700 mb-3">
                    {survey.description}
                  </p>

                  <PrimaryButton href={route('surveys.show', survey.id)}>
                    <VscOpenPreview className="mr-2" />
                    {' '}
                    {t('Preview')}
                  </PrimaryButton>
                </div>
              ))}
            </div>
          </div>
        </div>
      </div>
    </AuthenticatedLayout>
  );
}

ちょい洗練させました


いいなと思ったら応援しよう!