見出し画像

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に反映できていないので次回はこのセットを含む要するに

これ

をちゃんと作っていく

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