Open18

【挑戦】Laravel 8 でEvernoteの様なメモアプリを作ってみる

DaiNakaDaiNaka

はじめに

作ってみたシリーズの第三弾となります。
自身のインプット&アウトプットの為に過程をここに収めていこうと思います。

第一弾のチャットアプリ

https://zenn.dev/dainaka/scraps/6eafb7c86dad16

第二弾の掲示板アプリ

https://zenn.dev/dainaka/scraps/14be8378905118

開発環境

  • XAMPP v3.3.0
  • composer 2.1.3
  • VS Code

利用言語

  • PHP
  • Laravel 8
  • tailwind

公開コード[GitHub]

https://github.com/DaiNaka1207/note-app

DaiNakaDaiNaka

企画

今回は少しお題が大げさになってしまいましたが、平たく言うとノートアプリを作っていこうと思います。
Evernoteと全く同じ様にはいかないですが、左ペインでフォルダ管理をして、右ペインでメモが書ける様な感じにしていけたらと考えております。

機能

  • ログイン機能がありユーザー毎の情報を表示する
  • ノートブックに複数のノートを入れて管理する事できる
  • ノートブックやノートは削除したり移動したりする事ができる
  • ノートは様々な条件で検索する事ができる
  • ノートはタグを付ける事ができる

少し欲張っている感じではありますが、ちょっとのレベルアップを考えるとこの位の機能は実装していけたらと思います。

DaiNakaDaiNaka

プロジェクトの作成

早速、プロジェクトを作成していきます。
laravel new note-app --git --branch="main"

次に、先程GitHub上に作成しておいたリポジトリと連携させていきます。
git remote add origin https://github.com/DaiNaka1207/note-app.git

最後に、GitHub上に初期状態をプッシュしていきます。
git push -u origin main

DaiNakaDaiNaka

環境設定

.env
- APP_NAME=laravel
+ APP_NAME=note-app
config/app.php
- 'timezone' => 'UTC',
+ 'timezone' => 'Asia/Tokyo',

- 'locale' => 'en',
+ 'locale' => 'ja',

- 'faker_locale' => 'en_US',
+ 'faker_locale' => 'ja_JP',

データベースの作成

.envファイル内のDB_DATABASE=bbs_appを参考にデータベースを作成しておきます。

Tailwindcssのインストール

今回も、前回同様にTailwindcssを使っていきたいと思いますので、SEの休日さんのページを参考にインストールしていきます。
https://uedive.net/2021/5608/laravel8x-tailwind/

この環境設定のところは、理解して自分自身で設定ができる様になると良いと思いますが、今回の趣旨からは外れてしまう為、深堀りはしないつもりです。
ただ、いつかこういった設定も自身でできる様になり、説明もできる様になりたいと思っています。

DaiNakaDaiNaka

ビューの作成とルーティング処理

ここではひとまずテスト的なビューを表示させるところを書いていきます。

ビューの作成

resources\views\dashboard.blade.phpを作成します。

dashboard.blade.php
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>{{ env('app_name') }}</title>
    
    <!-- Styles -->
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
    <script src="{{ asset('js/app.js') }}"></script>
</head>
<body>
    <p class="text-blue-500 bg-red-100">^-^)v Hello World !</p>
</body>
</html>

ルーティング処理

次にルーティングの設定を書いていきます。

web.php
- Route::get('/', function () {
-     return view('welcome');
- });
+ Route::redirect('/', '/dashboard');
+ Route::view('/dashboard', 'dashboard');

画面キャプチャ

http://localhost:8000

DaiNakaDaiNaka

画面構成

ここでは、画面構成を決めていく為に、テキストをベタ打ちで書いていきます。

dashboard.blade.php
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>{{ env('app_name') }}</title>
    
    <!-- Styles -->
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
    <script src="{{ asset('js/app.js') }}"></script>
</head>
<body class="bg-blue-100">
    <div class="flex h-screen">
        {{-- メニューエリア --}}
        <div class="bg-white rounded-lg w-80 m-5 p-3 shadow-lg">
            {{-- 新規作成ボタン --}}
            <form action="/" method="POST">
                @csrf
                <button class="block w-full bg-gray-500 text-white rounded font-bold text-xl mb-5" type="submit">+</button>
            </form>
            {{-- ノートブック1 --}}
            <div class="ml-3 mb-2">
                <h2 class="font-bold text-lg">Work</h2>
                <div class="flex flex-col ml-2">
                    <a class="truncate" href="/">Note1</a>
                    <a class="truncate" href="/">Note2</a>
                </div>
            </div>
            {{-- ノートブック2 --}}
            <div class="ml-3 mb-2">
                <h2 class="font-bold text-lg">Private</h2>
                <div class="flex flex-col ml-2">
                    <a class="truncate" href="/">Note1</a>
                    <a class="truncate" href="/">Note2</a>
                    <a class="truncate" href="/">Note3</a>
                </div>
            </div>
            {{-- ノートブック3 --}}
            <div class="ml-3 mb-2">
                <h2 class="font-bold text-lg">Family</h2>
                <div class="flex flex-col ml-2">
                    <a class="truncate" href="/">Note1</a>
                </div>
            </div>
        </div>

        {{-- テキストエリア --}}
        <div class="bg-white w-full rounded-lg my-5 mr-5 p-3 shadow-lg">
            <textarea class="w-full h-full p-3 resize-none outline-none" name="content" placeholder="Please input text content."></textarea>
        </div>
    </div>
</body>
</html>

見た目はこんな感じになりました。

DaiNakaDaiNaka

データベースの構成を検討

[notes]テーブル

カラム名 タイプ
id bigint(20)
user_id int(20)
note_title varchar(50)
created_at timestamp
update_at timestamp

[pages]テーブル

カラム名 タイプ
id bigint(20)
user_id varchar(20)
note_id int(20)
page_title varchar(50)
page_contents varchar(1000)
created_at timestamp
update_at timestamp
DaiNakaDaiNaka

テーブルの作成

データベースの構成を考えたら、マイグレーションファイルを作成してテーブルの作成を実行していきます。

[notes]テーブルの作成

コマンド:php artisan make:migration create_notes_table
生成場所:database\migrations\yyyy_mm_dd_hhmmss_create_notes_table.php

yyyy_mm_dd_hhmmss_create_notes_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateNotesTable extends Migration
{
    public function up()
    {
        Schema::create('notes', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('notes');
    }
}

[pages]テーブルの作成

コマンド:php artisan make:migration create_pages_table
生成場所:yyyy_mm_dd_hhmmss_create_pages_table.php

yyyy_mm_dd_hhmmss_create_pages_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePagesTable extends Migration
{
    public function up()
    {
        Schema::create('pages', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('pages');
    }
}

カラムの設定

それぞれのファイルを更新してカラムを設定していきます。

yyyy_mm_dd_hhmmss_create_notes_table.php
    public function up()
    {
        Schema::create('notes', function (Blueprint $table) {
-           $table->id();
+           $table->bigIncrements('id');
+           $table->integer('user_id');
+           $table->string('note_title', 50)->default('Title');
            $table->timestamps();
        });
    }
yyyy_mm_dd_hhmmss_create_pages_table.php
    public function up()
    {
        Schema::create('pages', function (Blueprint $table) {
-           $table->id();
+           $table->bigIncrements('id');
+           $table->integer('user_id');
+           $table->integer('note_id');
+           $table->string('page_title', 50)->default('Title');
+           $table->string('page_contents', 1000);
            $table->timestamps();
        });
    }

マイグレーション

カラムの設定が完了したら、マイグレーションを実行していきます。
コマンド:php artisan migrate

[notes]テーブル

[pages]テーブル

DaiNakaDaiNaka

コントローラーの作成

ここでは、データベースを扱うコントローラーを作成していきます。
コマンド:php artisan make:controller NoteController --resource
生成場所:app\Http\Controllers\NoteController.php

NoteController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class NoteController extends Controller
{
    public function index()
    {
        //
    }

    public function create()
    {
        //
    }

    public function store(Request $request)
    {
        //
    }

    public function show($id)
    {
        //
    }

    public function edit($id)
    {
        //
    }

    public function update(Request $request, $id)
    {
        //
    }

    public function destroy($id)
    {
        //
    }
}

コマンド:php artisan make:controller PageController --resource
生成場所:app\Http\Controllers\PageController.php

PageController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class PageController extends Controller
{
    public function index()
    {
        //
    }

    public function create()
    {
        //
    }

    public function store(Request $request)
    {
        //
    }

    public function show($id)
    {
        //
    }

    public function edit($id)
    {
        //
    }

    public function update(Request $request, $id)
    {
        //
    }

    public function destroy($id)
    {
        //
    }
}
DaiNakaDaiNaka

コントローラーを経由したビュー表示へ変更

これまではルーティング(web.php)でビューを表示する様にしていました。
ここからはコントローラーを経由したビュー表示へ切り替えていきます。

NoteController.php
    public function index()
    {
-       //
+       // ダッシュボードの表示
+       return view('dashboard');
    }
web.php
+ use App\Http\Controllers\NoteController;
- Route::redirect('/', '/dashboard');
- Route::view('/dashboard', 'dashboard');
+ Route::redirect('/', '/note');
+ Route::resource('/note', NoteController::class);
DaiNakaDaiNaka

モデルの作成

[テーブルの作成]のところで、モデルも一緒に作っている事がほとんどですが、今回は忘れていましたので、ここでモデルだけ作成していきたいと思います。

[Note]モデルの作成

コマンド:php artisan make:model Note
生成場所:app\Models\Note.php

Note.php
<?php

namespace App\Models;

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

class Note extends Model
{
    use HasFactory;
}

[Page]モデルの作成

コマンド:php artisan make:model Page
生成場所:app\Models\Page.php

Page.php
<?php

namespace App\Models;

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

class Page extends Model
{
    use HasFactory;
}
DaiNakaDaiNaka

データベースから情報を取得して表示

ここではベタ打ちしていたノートのタイトルやページのタイトルをデータベースから取得して表示させるところをやっていきます。

リレーション

[notes]テーブルと[pages]テーブルをリレーションして、ページの情報も取得できる様にしていきます。

Note.php
class Note extends Model
{
    use HasFactory;
+   public function pages()
+   {
+       return $this->hasMany(Page::class);
+   }
}

データベースからの取得

コントローラーでデータベースからの情報の取得を行い、ビューに渡していきます。

NoteController.php
    public function index()
    {
+       // データベースからノートの情報を取得して代入
+       $notes = Note::All();

        // ダッシュボードの表示
-       return view('dashboard');
+       return view('dashboard',compact('notes'));
    }

ビューへの反映

コントローラーから渡された情報を基にビューで表示していきます。

dashboard.blade.php
        {{-- メニューエリア --}}
        <div class="bg-white rounded-lg w-80 m-5 p-3 shadow-lg">
            {{-- 新規作成ボタン --}}
            <form action="/" method="POST">
                @csrf
                <button class="block w-full bg-gray-500 text-white rounded font-bold text-xl mb-5" type="submit">+</button>
            </form>
-           {{-- ノートブック1 --}}
+           {{-- ノートブック --}}
+           @foreach ($notes as $note)
            <div class="ml-3 mb-2">
-               <h2 class="font-bold text-lg">Work</h2>
+               <h2 class="font-bold text-lg">{{$note->note_title}}</h2>
                <div class="flex flex-col ml-2">
+                   @foreach ($note->pages as $page)
-                      <a class="truncate" href="/">Note1</a>
-                      <a class="truncate" href="/">Note2</a>
+                      <a class="truncate" href="/">{{$page->page_title}}</a>
+                   @endforeach
                </div>
            </div>
-           {{-- ノートブック2 --}}
-           <div class="ml-3 mb-2">
-               <h2 class="font-bold text-lg">Private</h2>
-               <div class="flex flex-col ml-2">
-                   <a class="truncate" href="/">Note1</a>
-                   <a class="truncate" href="/">Note2</a>
-                   <a class="truncate" href="/">Note3</a>
-               </div>
-           </div>
-           {{-- ノートブック3 --}}
-           <div class="ml-3 mb-2">
-               <h2 class="font-bold text-lg">Family</h2>
-               <div class="flex flex-col ml-2">
-                   <a class="truncate" href="/">Note1</a>
-               </div>
-           </div>
+           @endforeach
        </div>

実際の画面



DaiNakaDaiNaka

コンテンツの表示

ここでは、左側のページをクリックした時に、ページのコンテンツを表示させていきます。

ページタイトルをクリックした時のリンク先の変更

dashboard.blade.php
            {{-- ノートブック --}}
            @foreach ($notes as $note)
                <div class="ml-3 mb-2">
                    <h2 class="font-bold text-lg">{{$note->note_title}}</h2>
                    <div class="flex flex-col ml-2">
                        @foreach ($note->pages as $page)
-                           <a class="truncate" href="/">{{$page->page_title}}</a>
+                           <a class="truncate" href="../page/{{$page->id}}">{{$page->page_title}}</a>
                        @endforeach
                    </div>
                </div>
            @endforeach
        </div>

コントローラーにて対象ページの情報を取得

PageController.php
+ use App\Models\Note;
+ use App\Models\Page;

  public function show($id)
    {
+       // データベースからノートの情報を取得して代入
+       $notes = Note::all();

+       // データベースからページの情報を取得して代入
+       $contents = Page::find($id);

+       // ダッシュボードを表示
+       return view('dashboard',compact('notes','contents'));
    }

クリックした時の情報をビューに反映

dashboard.blade.php
        {{-- テキストエリア --}}
        <div class="bg-white w-full rounded-lg my-5 mr-5 p-3 shadow-lg">
-           <textarea class="w-full h-full p-3 resize-none outline-none" name="content" placeholder="Please input text content."></textarea>
+           <textarea class="w-full h-full p-3 resize-none outline-none" name="content" placeholder="Please input text content.">@isset($contents){{$contents->page_contents}}@endisset</textarea>
        </div>

リンク先へのルーティング

web.php
+ use App\Http\Controllers\PageController;
+ Route::resource('/page', PageController::class);

実際の画面

DaiNakaDaiNaka

画面構成の調整

これまでの画面構成では、下記の課題が発生してしまいました。

  • ページの新規作成ができない
  • ページの更新ができない

そこで、画面構成の少しだけ見直してみました。

dashboard.blade.php
<body class="bg-blue-100">
-   <div class="flex h-screen">
+   <div class="flex flex-col h-screen sm:flex-row">
        {{-- メニューエリア --}}
-       <div class="bg-white rounded-lg w-80 m-5 p-3 shadow-lg">
+       <div class="bg-white rounded-lg order-2 w-11/12 sm:w-80 mx-auto my-3 sm:m-5 p-3 shadow-lg">
-           {{-- 新規作成ボタン --}}
+           {{-- ノート新規作成ボタン --}}
            <form action="/" method="POST">
                @csrf
                <button class="block w-full bg-gray-500 text-white rounded font-bold text-xl mb-5" type="submit">+</button>
            </form>
            {{-- ノートブック --}}
            @foreach ($notes as $note)
                <div class="ml-3 mb-2">
                    <h2 class="font-bold text-lg">{{$note->note_title}}</h2>
                    <div class="flex flex-col ml-2">
                        @foreach ($note->pages as $page)
                        <a class="truncate" href="../page/{{$page->id}}">{{$page->page_title}}</a>
                        @endforeach
+                       {{-- ページ新規作成ボタン --}}
+                       <a class="truncate text-gray-300" href="/"><新規作成></a>
                    </div>
                </div>
            @endforeach
        </div>

+       {{-- ページエリア --}}
+       <div class="flex flex-col order-1 sm:order-2 w-11/12 sm:w-full my-3 sm:my-5 mx-auto sm:mr-5">
+           {{-- ボタンエリア --}}
+           <form class="mb-3" action="/" method="POST">
+               @csrf
+               <button class="bg-blue-500 text-white rounded text-sm font-bold px-3 py-1 shadow-lg" type="submit">更新</button>
+           </form>
            {{-- テキストエリア --}}
-       <div class="bg-white w-full rounded-lg my-5 mr-5 p-3 shadow-lg">
-           <textarea class="w-full h-full p-3 resize-none outline-none" name="content" placeholder="Please input text content.">@isset($contents){{$contents->page_contents}}@endisset</textarea>
            <textarea class="bg-white h-80 sm:h-full rounded-lg p-3 resize-none outline-none shadow-lg" name="content" placeholder="Please input text content.">@isset($contents){{$contents->page_contents}}@endisset</textarea>
        </div>
    </div>
</body>

変更点の説明

具体的には、下記の変更点を加えています。

  • ページの新規作成ボタンを各ノートの一番最下部に追加
  • 画面右側のテキストエリア部分に更新ボタンを追加
  • モバイル表示させた時に画面が左右ではなく上下になる様に修正

変更後の画面

DaiNakaDaiNaka

更新動作の処理

ここでは、更新ボタンを押下した時の処理を書いていきます。
まずはビューを編集して画面構成を変えていきます。

dashboard.blade.php
        {{-- ページエリア --}}
        <div class="flex flex-col order-1 sm:order-2 w-11/12 sm:w-full my-3 sm:my-5 mx-auto sm:mr-5">
            {{-- ボタンエリア --}}
-           <form class="mb-3" action="/" method="POST">
+           <div class="flex mb-3 items-center">
+               @isset($contents->id)
+                   <form action=" {{route('page.update', $contents->id)}} " method="POST" id="update_form">
                        @csrf
+                       @method('PATCH')
                        <button class="bg-blue-500 text-white rounded text-sm font-bold px-3 py-1 shadow-lg" type="submit">更新</button>
                    </form>
+               @endisset
+               @if(session('message')) <p class="ml-3">{{ session('message') }}</p> @endif
+           </div>
            {{-- テキストエリア --}}
-           <textarea class="bg-white h-80 sm:h-full rounded-lg p-3 resize-none outline-none shadow-lg" name="content" placeholder="Please input text content.">@isset($contents){{$contents->page_contents}}@endisset</textarea>
+           <textarea class="bg-white h-80 sm:h-full rounded-lg p-3 resize-none outline-none shadow-lg" name="content" placeholder="Please input text content." form="update_form">@isset($contents){{$contents->page_contents}}@endisset</textarea>
        </div>

変更点の説明

  • 更新ボタンを押下した後にメッセージを表示
  • 更新ボタンを押下した時のリンク先を設定

次にコントローラーを変更して更新ボタンをの処理を書いていきます。

PageController.php
    public function update(Request $request, $id)
    {
-       //
+       // データベースからページ情報を取得して代入
+       $page = Page::find($id);

+       // ページの内容を更新
+       $page->page_contents = $request->content;
+       $page->save();

+       // 元の画面を表示
+        return redirect()->route('page.show', compact('page'))->with('message', '更新しました。');
    }

※追加した内容はコメントの通りとなります。

実際の画面

テキストの内容を変更して、更新ボタンを押した直後の画面です。

DaiNakaDaiNaka

認証機能の構築(ここで失敗)

認証機能を先に構築すると作成段階では鬱陶しく感じる事を懸念して後回しにしておりましたが、後から追加しようと思うと上手くいきませんでした。
恐らく、ルーティングの部分が上手く構築できず失敗に終わってしまったので、改めて一から作成していこうと思います。
一旦経過はここまでにして、今後、続きや経過を書いていけたらと思います。

DaiNakaDaiNaka

再構築について

少し時間が掛かりましたが、これまでの経緯を確認しつつ、一から再構築しましたので再開したいと思います。
今回の事で、認証機能等、重要な部分は先に構築しておく必要がある事を理解した気がします。
また、再構築後の画面はこんか感じになりました。

右上の方を見ていただくと分かる様に、認証機能も備えています。

PC画面

モバイル画面

DaiNakaDaiNaka

反省

もう少し続けたかったのですが、どうにも気が進まなくなってしまったので、今回の挑戦はここまでにして、GitHub上にコードだけ残しておきたいと思います。
需要もないだろうし、いずれ消してしまうかもしれませんし、思い立って続きをやるかもしれません。

https://github.com/DaiNaka1207/note-app

今回の問題点

頭の中のロジックをコードに落とし込む作業が好きなので、どうしても設計部分を飛ばして進める傾向にありますが、無計画にいきなりコードを書くのは難しいなと実感しました。
ただ、プログラムを楽しむ事が出来なくなってしまうのは嫌なので、いったんは設計部分にこだわる事なく進めていこうと思います。

今後の進め方

大体の画面構成やデータベースとの繋がり部分位は考えてから進めても良いかなぁと思いました。
直ぐに全部を始めるのは自分に合わないので、データベースとの繋がりをしっかり考えるところから始めてみようと思いました。

少なからず記事にアクセスしていらっしゃる方が居るので、これまで記事に目を通していただき有難うございました。