【挑戦】Laravel 8 でEvernoteの様なメモアプリを作ってみる
はじめに
作ってみたシリーズの第三弾となります。
自身のインプット&アウトプットの為に過程をここに収めていこうと思います。
アーカイブ
No | 記事 | デモサイト |
---|---|---|
1 | 【挑戦】Laravel 8 でLINEの様なチャットサービスを作ってみた | https://chat-app.dev-labo.net/ |
2 | 【挑戦】Laravel 8 で簡易的な掲示板を作ってみた | https://bbs-app.dev-labo.net/ |
開発環境
- XAMPP v3.3.0
- composer 2.1.3
- VS Code
利用言語
- PHP
- Laravel 8
- tailwind
公開コード[GitHub]
企画
今回は少しお題が大げさになってしまいましたが、平たく言うとノートアプリを作っていこうと思います。
Evernoteと全く同じ様にはいかないですが、左ペインでフォルダ管理をして、右ペインでメモが書ける様な感じにしていけたらと考えております。
機能
- ログイン機能がありユーザー毎の情報を表示する
- ノートブックに複数のノートを入れて管理する事できる
- ノートブックやノートは削除したり移動したりする事ができる
- ノートは様々な条件で検索する事ができる
- ノートはタグを付ける事ができる
少し欲張っている感じではありますが、ちょっとのレベルアップを考えるとこの位の機能は実装していけたらと思います。
プロジェクトの作成
早速、プロジェクトを作成していきます。
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
環境設定
- APP_NAME=laravel
+ APP_NAME=note-app
- '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の休日さんのページを参考にインストールしていきます。
この環境設定のところは、理解して自分自身で設定ができる様になると良いと思いますが、今回の趣旨からは外れてしまう為、深堀りはしないつもりです。
ただ、いつかこういった設定も自身でできる様になり、説明もできる様になりたいと思っています。
ビューの作成とルーティング処理
ここではひとまずテスト的なビューを表示させるところを書いていきます。
ビューの作成
resources\views\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>
ルーティング処理
次にルーティングの設定を書いていきます。
- Route::get('/', function () {
- return view('welcome');
- });
+ Route::redirect('/', '/dashboard');
+ Route::view('/dashboard', 'dashboard');
画面キャプチャ
http://localhost:8000
画面構成
ここでは、画面構成を決めていく為に、テキストをベタ打ちで書いていきます。
<!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>
見た目はこんな感じになりました。
データベースの構成を検討
[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 |
テーブルの作成
データベースの構成を考えたら、マイグレーションファイルを作成してテーブルの作成を実行していきます。
[notes]テーブルの作成
コマンド:php artisan make:migration create_notes_table
生成場所:database\migrations\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
<?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');
}
}
カラムの設定
それぞれのファイルを更新してカラムを設定していきます。
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();
});
}
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]テーブル
コントローラーの作成
ここでは、データベースを扱うコントローラーを作成していきます。
コマンド:php artisan make:controller NoteController --resource
生成場所:app\Http\Controllers\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
<?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)
{
//
}
}
コントローラーを経由したビュー表示へ変更
これまではルーティング(web.php)でビューを表示する様にしていました。
ここからはコントローラーを経由したビュー表示へ切り替えていきます。
public function index()
{
- //
+ // ダッシュボードの表示
+ return view('dashboard');
}
+ use App\Http\Controllers\NoteController;
- Route::redirect('/', '/dashboard');
- Route::view('/dashboard', 'dashboard');
+ Route::redirect('/', '/note');
+ Route::resource('/note', NoteController::class);
モデルの作成
[テーブルの作成]のところで、モデルも一緒に作っている事がほとんどですが、今回は忘れていましたので、ここでモデルだけ作成していきたいと思います。
[Note]モデルの作成
コマンド:php artisan make:model Note
生成場所:app\Models\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
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Page extends Model
{
use HasFactory;
}
データベースから情報を取得して表示
ここではベタ打ちしていたノートのタイトルやページのタイトルをデータベースから取得して表示させるところをやっていきます。
リレーション
[notes]テーブルと[pages]テーブルをリレーションして、ページの情報も取得できる様にしていきます。
class Note extends Model
{
use HasFactory;
+ public function pages()
+ {
+ return $this->hasMany(Page::class);
+ }
}
データベースからの取得
コントローラーでデータベースからの情報の取得を行い、ビューに渡していきます。
public function index()
{
+ // データベースからノートの情報を取得して代入
+ $notes = Note::All();
// ダッシュボードの表示
- return view('dashboard');
+ return view('dashboard',compact('notes'));
}
ビューへの反映
コントローラーから渡された情報を基にビューで表示していきます。
{{-- メニューエリア --}}
<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>
実際の画面
コンテンツの表示
ここでは、左側のページをクリックした時に、ページのコンテンツを表示させていきます。
ページタイトルをクリックした時のリンク先の変更
{{-- ノートブック --}}
@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>
コントローラーにて対象ページの情報を取得
+ 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'));
}
クリックした時の情報をビューに反映
{{-- テキストエリア --}}
<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>
リンク先へのルーティング
+ use App\Http\Controllers\PageController;
+ Route::resource('/page', PageController::class);
実際の画面
画面構成の調整
これまでの画面構成では、下記の課題が発生してしまいました。
- ページの新規作成ができない
- ページの更新ができない
そこで、画面構成の少しだけ見直してみました。
<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>
変更点の説明
具体的には、下記の変更点を加えています。
- ページの新規作成ボタンを各ノートの一番最下部に追加
- 画面右側のテキストエリア部分に更新ボタンを追加
- モバイル表示させた時に画面が左右ではなく上下になる様に修正
変更後の画面
更新動作の処理
ここでは、更新ボタンを押下した時の処理を書いていきます。
まずはビューを編集して画面構成を変えていきます。
{{-- ページエリア --}}
<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>
変更点の説明
- 更新ボタンを押下した後にメッセージを表示
- 更新ボタンを押下した時のリンク先を設定
次にコントローラーを変更して更新ボタンをの処理を書いていきます。
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', '更新しました。');
}
※追加した内容はコメントの通りとなります。
実際の画面
テキストの内容を変更して、更新ボタンを押した直後の画面です。
認証機能の構築(ここで失敗)
認証機能を先に構築すると作成段階では鬱陶しく感じる事を懸念して後回しにしておりましたが、後から追加しようと思うと上手くいきませんでした。
恐らく、ルーティングの部分が上手く構築できず失敗に終わってしまったので、改めて一から作成していこうと思います。
一旦経過はここまでにして、今後、続きや経過を書いていけたらと思います。
再構築について
少し時間が掛かりましたが、これまでの経緯を確認しつつ、一から再構築しましたので再開したいと思います。
今回の事で、認証機能等、重要な部分は先に構築しておく必要がある事を理解した気がします。
また、再構築後の画面はこんか感じになりました。
右上の方を見ていただくと分かる様に、認証機能も備えています。
PC画面
モバイル画面
反省
もう少し続けたかったのですが、どうにも気が進まなくなってしまったので、今回の挑戦はここまでにして、GitHub上にコードだけ残しておきたいと思います。
需要もないだろうし、いずれ消してしまうかもしれませんし、思い立って続きをやるかもしれません。
今回の問題点
頭の中のロジックをコードに落とし込む作業が好きなので、どうしても設計部分を飛ばして進める傾向にありますが、無計画にいきなりコードを書くのは難しいなと実感しました。
ただ、プログラムを楽しむ事が出来なくなってしまうのは嫌なので、いったんは設計部分にこだわる事なく進めていこうと思います。
今後の進め方
大体の画面構成やデータベースとの繋がり部分位は考えてから進めても良いかなぁと思いました。
直ぐに全部を始めるのは自分に合わないので、データベースとの繋がりをしっかり考えるところから始めてみようと思いました。
少なからず記事にアクセスしていらっしゃる方が居るので、これまで記事に目を通していただき有難うございました。