www_blog

4月からのプログラマ

Laravelを使ってアンケート機能を作ってみた!!

目次

アンケート機能の内容

1つの投稿記事に対して、5つまでアンケートを設定可能。投稿のアンケートに回答することができ、回答された内容がデータベースに保存されます。

  • 完成イメージ

  • データベース保存イメージデータ

  • ER図

開発環境

  • macOS Ventura 13.0.1
  • PHP 8.1.8
  • Laravel 9.39.0
  • Bootstrap 5.0
  • MySQL 5.7.34

データベース周辺の作成

php artisan make:model Article -m
php artisan make:model Answer -m

database/migrations/****_**_**_******_create_articles_table.php

public function up()
{
    Schema::create('articles', function (Blueprint $table) {
      $table->id();
      $table->text('article_title');
      $table->text('article_detail')->nullable();
      $table->json('questionnaire_1')->nullable();
      $table->json('questionnaire_2')->nullable();
      $table->json('questionnaire_3')->nullable();
      $table->json('questionnaire_4')->nullable();
      $table->json('questionnaire_5')->nullable();
      $table->timestamps();
}); 

questionnaireカラムは、アンケートの「タイトル」「内容」「inputタイプ」が配列で保存されるため、jsonとしています。

database/migrations/****_**_**_******_create_answers_table.php

public function up()
{ Schema::create('answers', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('article_id')->nullable(); $table->foreign('article_id')->references('id')->on('articles')->onDelete('cascade'); $table->text('answer_1')->nullable(); $table->text('answer_2')->nullable(); $table->text('answer_3')->nullable(); $table->text('answer_4')->nullable(); $table->text('answer_5')->nullable(); $table->timestamps(); });

モデルの作成

app/Models/Article.php

class Article extends Model
{
    use HasFactory;

    // アンケート内容は、配列でデータベースに保存する
    protected $casts = [
       'questionnaire_1' => 'array',
       'questionnaire_2' => 'array',
       'questionnaire_3' => 'array',
       'questionnaire_4' => 'array',
       'questionnaire_5' => 'array'
   ];

    public function answers()
    {
        return $this->hasMany(Answer::class);
    }

    public function getQRepeatAttribute()
    {
        $blocks = collect();
        // 各アンケートを[collect]に入れる
        for ($i=1; $i <= 5; $i++) {
            $questionnaire = "questionnaire_".$i;
            // [type]と[title]が空ではない場合はcollectに代入する
            if (!empty($this->$questionnaire['q_type']) && !empty($this->$questionnaire['q_title'])) {
                ${'questionnaire_'.$i} =  $this->$questionnaire;
                $blocks = $blocks->merge([${'questionnaire_'.$i}]);
            }
        }
        return collect($blocks);
    }
}

bladeファイルでは、foreachだけで表示できるようにcollectで成形しています。

app/Models/Answer.php

class Answer extends Model
{
    use HasFactory;
    protected $fillable = ['article_id'];

    public function article()
    {
        return $this->belongsTo(Article::class);
    }

    public function getAnswerArrayAttribute()
    {
        for ($i=1; $i <= 5; $i++) {
            $answer = "answer_$i";
            if (!empty($this->$answer)) {
                $answer_array[] = explode(",",$this->$answer);
            }
        }
        return $answer_array;
    }
}

コントローラーの作成

app/Http/Controllers/ArticleController.php

class ArticleController extends Controller
{
    // 投稿記事作成画面
    public function index()
    {
        return view('article.index');
    }

    // 投稿記事詳細画面
    public function detail($detail)
    {
        $article = Article::find($detail);

        return view('article.detail', compact('article'));
    }

    // 投稿記事作成処理
    public function create(Request $request)
    {
        $article = new Article;
        $article->article_title = $request->article_title;
        $article->article_detail = $request->article_detail;

        for ($i=1; $i <= 5; $i++) {
        ${'questionnaire_'.$i} = array();
        ${'questionnaire_'.$i} = array(
            "q_type" => $request->input("q{$i}"),
            "q_title" => $request->input("q_title_{$i}"),
            "q_select" => $request->input("q_select_{$i}")
        );

            $questionnaire = "questionnaire_".$i;
            ${'questionnaire_'.$i}["q_select"] = explode(",", ${'questionnaire_'.$i}["q_select"]);
            $article->$questionnaire = ${'questionnaire_'.$i};
            !$request->input("q_title") ?: "questionnaire_".$i = $questionnaire;
        }

        $article->save();

        return redirect()->route('article.detail', ['detail' => $article->id])->with('flash_message', '投稿が完了しました。');
    }

    // アンケートの回答を保存する処理
    public function answer(Request $request, $article)
    {
        $answer = new Answer;

        for ($i=1; $i <= 5; $i++) {
            $answer_num = "answer_".$i;
            $request->$answer_num ? $answer_num = implode(",", $request->$answer_num) : $answer_num = null;
            $answer_str = "answer_".$i;
            $answer->$answer_str = $answer_num;
        }
        $answer->article_id = $article;
        $answer->save();

        return redirect()->route('article.detail', ['detail' => $article])->with('flash_message', 'アンケートを回答しました');
    }
}

投稿記事、アンケートの回答などの各処理をArticleControllerで実装しています。
未実装ですがアンケートの回答をCSVで出力する想定でしたので、アンケートの回答は、配列ではなく文字列で保存する実装としています。

ビューの作成

resources/views/index.blade.php

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
</head>
<body class="bg-dark">
<div class="container">
<div class="main container-fluid">
    <div class="row bg-light text-dark py-5">
        <div class="col-md-8 offset-md-2">
            <h2 class="fs-1 mb-5 text-center fw-bold">投稿入力フォーム</h2>
            <form method="post" action="{{ route('article.create') }}">
                @csrf
                <p>記事フォーム</p>
                <hr>
                <div class="mb-3">
                    <input type="text" class="form-control" name="article_title" placeholder="記事タイトル" value="" required>
                </div>
                <div class="mb-4">
                    <textarea class="form-control" name="article_detail" rows="5" placeholder="記事内容" required></textarea>
                </div>
                @for ($i = 1; $i < 6; $i++)
                <div class="questionnair-input{{$i}}" @if($i >= 2) style="display: none;"  @endif>
                    <p>アンケート内容記述フォーム{{$i}}</p>
                    <hr>
                    <div class="mb-3">
                        <input style="margin-right: 5px;" type="radio" id="radio{{$i}}" name="q{{$i}}" value="radio"><label style="margin-right: 20px;" for="radio{{$i}}">ラジオボタン</label>
                        <input style="margin-right: 5px;" type="radio" id="checkbox{{$i}}" name="q{{$i}}" value="checkbox"><label style="margin-right: 20px;" for="checkbox{{$i}}">チェックボックス</label>
                        <input style="margin-right: 5px;" type="radio" id="text{{$i}}" name="q{{$i}}" value="text"><label for="text{{$i}}">テキストフォーム</label>
                    </div>
                    <div class="mb-3">
                        <input type="text" class="form-control" name="q_title_{{$i}}" placeholder="アンケートタイトル" value="">
                    </div>
                    <div class="mb-3">
                        <input type="text" class="form-control" name="q_select_{{$i}}" placeholder="アンケート選択肢" value="">
                    </div>
                    @if ($i < 5)
                    <div class="text-left pb-2 mb-3 col-md-12">
                        <button type="button" class="btn btn-success add-button{{$i}}">追加</button>
                    </div>
                    @endif
                </div>
                @endfor
                <div class="text-center pt-4 col-md-6 offset-md-3">
                    <button type="submit" class="btn btn-primary">送信</button>
                </div>
            </form>
        </div>
    </div>
</div>
</div>
</body>
<script>
for (let i = 1; i < 5; i++) {
    document.querySelector('.add-button' + i).onclick = function() {
    var questionnair_from = document.getElementsByClassName('questionnair-input' + (i + 1));
        questionnair_from[0].style.display = "block";
    }
};
</script>
</html>


resources/views/article/index.blade.php

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
</head>
<body class="bg-dark">
<div class="container">
<div class="main container-fluid">
    <div class="row bg-light text-dark py-5">
        <div class="col-md-8 offset-md-2">
            <h2 class="fs-1 mb-5 text-center fw-bold">投稿記事</h2>
            <form method="post">
                @csrf
                <p>記事フォーム</p>
                <hr>
                <div class="mb-3">
                    <input type="text" class="form-control" name="article" placeholder="記事タイトル" value="">
                </div>
                <div class="mb-4">
                    <textarea class="form-control" name="お問い合わせ内容" rows="5" placeholder="記事内容"></textarea>
                </div>
                @for ($i = 1; $i < 6; $i++)
                <div class="questionnair-input{{$i}}" @if($i >= 2) style="display: none;"  @endif>
                    <p>アンケートフォーム{{$i}}</p>
                    <hr>
                    <div class="mb-3">
                        <input style="margin-right: 5px;" type="radio" id="radio{{$i}}" name="q{{$i}}" value="radio"><label style="margin-right: 20px;" for="radio{{$i}}">ラジオボタン</label>
                        <input style="margin-right: 5px;" type="radio" id="checkbox{{$i}}" name="q{{$i}}" value="checkbox"><label style="margin-right: 20px;" for="checkbox{{$i}}">チェックボックス</label>
                        <input style="margin-right: 5px;" type="radio" id="text{{$i}}" name="q{{$i}}" value="text"><label for="text{{$i}}">テキストフォーム</label>
                    </div>
                    <div class="mb-3">
                        <input type="text" class="form-control" name="article" placeholder="アンケートタイトル" value="">
                    </div>
                    <div class="mb-3">
                        <input type="text" class="form-control" name="article" placeholder="アンケート選択肢" value="">
                    </div>
                    @if ($i < 5)
                    <div class="text-left pb-2 mb-3 col-md-12">
                        <button type="button" class="btn btn-success add-button{{$i}}">追加</button>
                    </div>
                    @endif
                </div>
                @endfor
                <div class="text-center pt-4 col-md-6 offset-md-3">
                    <button type="submit" class="btn btn-primary">送信</button>
                </div>
            </form>
        </div>
    </div>
</div>
</div>
</body>
</html>


resources/views/article/detail.blade.php

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
<style type="text/css">

form {
    background-color: #fff;
    padding: 20px;
}
</style>
</head>
<body class="bg-dark">
<div class="container">
<div class="main container-fluid">
    <div class="bg-light text-dark py-5">
        <div class="col-md-8 offset-md-2">
            <h2 class="fs-1 mb-5 text-center fw-bold">投稿記事詳細</h2>
            @if (session('flash_message'))
                <div class="comment_bnr comment_submit alert-success">
                    <p>{{ session('flash_message') }}</p>
                </div>
            @endif
            <p>投稿記事</p>
            <hr>
            <p>{{ $article->article_title }}</p>
            <p>{{ $article->article_detail }}</p>

            @if ($article->q_repeat->isNotEmpty())
            <form method="post" action="{{route('article.answer', ['article' => $article])}}">
                @csrf
                <p>アンケートフォーム</p>
                <hr>
                @php
                    $counter = 1;
                @endphp

                @foreach ($article->q_repeat as $questionnaire)
                    <div class="mb-3">
                        <p>{{$questionnaire['q_title']}}</p>
                        @for ($i = 0; $i < count($questionnaire['q_select']); $i++)
                            <label>
                                <input type="{{$questionnaire['q_type']}}" name="answer_{{$counter}}[]" value="{{$questionnaire['q_select'][$i]}}">
                                {{$questionnaire['q_select'][$i]}}
                            </label>
                        @endfor
                    </div>
                    <hr>
                @php
                    $counter ++;
                @endphp
                @endforeach
                <div class="text-center pt-4 col-md-6 offset-md-3">
                    <a href="{{route('index')}}"><button type="button" class="btn btn-success">
                        投稿フォームへ戻る</button></a>
                </div>
                <div class="text-center pt-4 col-md-6 offset-md-3">
                    <button type="submit" class="btn btn-primary">送信</button>
                </div>
            </form>
            @endif
        </div>
    </div>
</div>
</div>
</body>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script>
// フラッシュメッセージのfadeout
$(function(){
    setTimeout("$('.comment_submit').fadeOut('slow')", 3000);
});
</script>
</html>

ルートの作成

resources/views/article/detail.blade.php

Route::view('/', 'index')->name('index');
Route::group(['prefix' => 'article'], function() {
    Route::get('/{detail}', [ArticleController::class, 'detail'])->name('article.detail');
    Route::post('/create', [ArticleController::class, 'create'])->name('article.create');
    Route::post('/{article}/answer', [ArticleController::class, 'answer'])->name('article.answer');
});

終わりに

今回は、同様のアンケート機能を実務で、実装したのでアウトプットと復習の意味を込めて記事を書かせていただきました。まだまだ、わからないところだらけでうまく実装できていない箇所もあると思いますが、参考になればと思います!