【挑戦】Laravel 11 がリリースされたのでメモアプリを作ってみる
はじめに
今回の作ってみるシリーズは、Laravel 11がリリースされたので、基本機能などを触れつつメモアプリの作成に再挑戦していきたいと思います。
アーカイブ
開発環境
- Docker 4.27.1
- VS Code
利用言語
- PHP PHP 8.3.3
- Laravel 11.0.0
公開コード[GitHub]
注意事項
ここで説明しているコマンドプロンプトで扱うコマンドですが、エイリアスを設定しておりXAMPPの時と同じようなコマンドで実行できるようにしています。
エラーが吐かれる場合には、php
をvendor/laravel/sail/bin/sail
に置き換えて実行してみてください。
例:php artisan route:list
→ vendor/laravel/sail/bin/sail artisan route:list
Laravel 11のインストール
~\Project\memo-app
にLaravel 11をインストールしたいと思います。
Project
がカレントディレクトリの状態でcurl -s https://laravel.build/memo-app | bash
を実行します。
~ここで数十分は経過~
初起動
いったん、localhost
へアクセスしてみるもエラーが発生してダメなようす。
環境設定
ひとまず.env
ファイルの中身を確認してみるとDB_DATABASE
がコメントアウトしている
調査開始
調査するにも何から調べたら良いのか・・・🤔
- コメントアウトを解除して起動 🤔変わらず
-
mysql
でデータベースにアクセスができるか確認 😀アクセスできた -
show databases;
を実行してデータベースmemo_app
があるか確認 🤔出来ていない
アプローチを変更して、.env
のとおりにデータベースが作成されない方法を調査したところ、下記のサイトを発見してコメントアウトを解除した後にビルドを開始。
sail down -v
でいったんコンテナを削除
sail up -d --build
でビルドし直し
起動してみた結果、ここまで表示された😀
php artisan migrate
を実行してデータベースを構築
再度、localhost
へアクセスして起動したところ、やっとできた!!
データベースを使わない人はどうするのだろう・・・🤔
機能説明
- ユーザー毎にメモが残せる
- メモを一覧で表示することができる(ダッシュボード)
- メモを登録することができる
- メモを更新することができる
- メモを削除することができる
- メモを検索することができる
- メモをPDFにして保存することができる
作っていく手順
- ログイン認証機能の追加(Jetstream:ダッシュボードも付属)
- ダッシュボードにメニューを追加
- メモの登録画面の作成
- メモの一覧表示機能の追加
- メモの編集機能の追加
- メモの削除機能の追加
- メモのPDF保存機能の追加
ログイン認証機能の追加
ログイン認証にはJetstream
を使っていきたいと思います。
上記ページのInstallation
ページを参考にして進めます。
コマンドプロンプトにてプロジェクトフォルダ~\Project\memo-app
をカレントディレクトリにして、下記のコマンドを順に実行します。
Jetstreamインストール手順
composer require laravel/jetstream
php artisan jetstream:install livewire
npm install
npm run build
2を実行した時に選択肢が表示されたらYes
を選択することで、認証関係のデータベースがマイグレーションされます。
ユーザー登録を試行
トップページの右上にLog in
とRegister
が表示される
Register
に遷移すると下記の画面が表示されるので、必要事項を入力
ダッシュボードはこんな感じ
ダッシュボードにメニューを追加
Jetstream
にTailwind CSS
が使われているため、Prelineというプラグインを使っていきます。
Laravelへのインストール手順
import defaultTheme from 'tailwindcss/defaultTheme';
import forms from '@tailwindcss/forms';
import typography from '@tailwindcss/typography';
/** @type {import('tailwindcss').Config} */
export default {
content: [
'./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
'./vendor/laravel/jetstream/**/*.blade.php',
'./storage/framework/views/*.php',
'./resources/views/**/*.blade.php',
+ 'node_modules/preline/dist/*.js',
],
theme: {
extend: {
fontFamily: {
sans: ['Figtree', ...defaultTheme.fontFamily.sans],
},
},
},
plugins: [
forms,
typography,
+ require('preline/plugin'),
],
};
import './bootstrap';
+ import 'preline';
ダッシュボードに新規作成ボタンを追加
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Dashboard') }}
</h2>
</x-slot>
- <div class="py-12">
- <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
+ <div class="mt-2 max-w-7xl mx-auto sm:px-6 lg:px-8">
- <div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
- <x-welcome />
- </div>
+ <button type="button" class="py-3 px-4 inline-flex items-center gap-x-2 text-sm font-semibold rounded-lg border border-transparent bg-blue-100 text-blue-800 hover:bg-blue-200 disabled:opacity-50 disabled:pointer-events-none dark:hover:bg-blue-900 dark:text-blue-400 dark:focus:outline-none dark:focus:ring-1 dark:focus:ring-gray-600">
+ 新規作成
+ </button>
- </div>
</div>
</x-app-layout>
画面キャプチャ
こんな感じになります。
メモの登録画面の作成
画面を作っていくにあたり、メモのデータベースを考える必要があるので、ここで書き出しておきます。
大したものは用意していません😄
+------------+-----------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+-----------------+------+-----+---------+----------------+
| id | bigint unsigned | NO | PRI | NULL | auto_increment |
| user_id | int | NO | | NULL | |
| subject | varchar(100) | NO | | NULL | |
| content | text | YES | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
| deleted_at | timestamp | YES | | NULL | |
+------------+-----------------+------+-----+---------+----------------+
モデルファイルを生成
モデルファイルと合わせて、コントローラー、マイグレーションのファイルも作成しています。
また、モデルにはソフトデリートの機能を設定していきます。
コマンドプロンプトにてphp artisan make:model Memo -crm
を実行します。
後ろの方に記載されている-crm
にコントローラーやマイグレーションのファイルも作成することを指定しています。ご参考はLaravel公式をご覧ください。
モデルファイルにソフトデリートを記述
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\SoftDeletes;
class Memo extends Model
{
use HasFactory;
+ use SoftDeletes;
}
マイグレーションを記述
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('memos', function (Blueprint $table) {
$table->id();
+ $table->integer('user_id');
+ $table->string('subject', 100);
+ $table->text('content')->nullable();
$table->timestamps();
+ $table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('memos');
}
};
登録画面への遷移
まずは、ダッシュボードにある新規作成
ボタンから画面遷移ができるようにします。
MemoController
はリソースで出力しているため、基本的なCRUDが含まれているので、ここでは、memo.create
に遷移するようにします。
<div class="mt-2 max-w-7xl mx-auto sm:px-6 lg:px-8">
- <button type="button" class="py-3 px-4 inline-flex items-center gap-x-2 text-sm font-semibold rounded-lg border border-transparent bg-blue-100 text-blue-800 hover:bg-blue-200 disabled:opacity-50 disabled:pointer-events-none dark:hover:bg-blue-900 dark:text-blue-400 dark:focus:outline-none dark:focus:ring-1 dark:focus:ring-gray-600">新規作成</button>
+ <button type="button" onclick="location.href='{{route('memo.create')}}'" class="py-3 px-4 inline-flex items-center gap-x-2 text-sm font-semibold rounded-lg border border-transparent bg-blue-100 text-blue-800 hover:bg-blue-200 disabled:opacity-50 disabled:pointer-events-none dark:hover:bg-blue-900 dark:text-blue-400 dark:focus:outline-none dark:focus:ring-1 dark:focus:ring-gray-600">新規作成</button>
</div>
次にルーティングの設定を行います。
ここでもリソースでルーティングの設定を行います。
<?php
use Illuminate\Support\Facades\Route;
+use App\Http\Controllers\MemoController;
Route::get('/', function () {
return view('welcome');
});
+Route::resource('memo', MemoController::class);
Route::middleware([
'auth:sanctum',
config('jetstream.auth_session'),
'verified',
])->group(function () {
Route::get('/dashboard', function () {
return view('dashboard');
})->name('dashboard');
});
エラーが起きないようにコマンドプロンプトにてphp artisan route:cache
を行っておきましょう。
コントローラーの修正
新規作成
ボタンが押されたらcreate
が実行されるようになっているので、ユーザーデータを取得して画面を表示するようにします。
- //
+ public function create()
+ {
+ $user = auth()->user();
+ return view('memo.index', compact('user'));
+ }
登録画面の作成
views\memo\index.blade.php
ファイルを作成します。
そして、下記のとおり記述します。
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Dashboard') }}
</h2>
</x-slot>
<form action="{{route('memo.store')}}" method="post" class="mt-5 max-w-7xl mx-auto sm:px-6 lg:px-8">
@csrf
<input type="hidden" name="user_id" value="{{$user->id}}">
<input type="text" name="subject" class="py-3 px-4 mb-3 block w-full border-gray-200 rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-slate-900 dark:border-gray-700 dark:text-gray-400 dark:focus:ring-gray-600" placeholder="タイトル" autofocus required>
<div class="relative">
<textarea name="content" class="py-3 px-4 block w-full h-48 border-gray-200 rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-slate-900 dark:border-gray-700 dark:text-gray-400 dark:focus:ring-gray-600" rows="3" placeholder="本文"></textarea>
</div>
<div class="text-right">
<button type="button" onclick="location.href='{{route('dashboard')}}'" class="py-3 px-4 mr-3 inline-flex items-center gap-x-2 text-sm font-semibold rounded-lg border border-transparent bg-yellow-100 text-yellow-800 hover:bg-yellow-200 disabled:opacity-50 disabled:pointer-events-none dark:hover:bg-yellow-900 dark:text-yellow-500 dark:hover:text-yellow-400">戻る</button>
<button type="submit" class="py-3 px-4 mt-3 inline-flex items-center gap-x-2 text-sm font-semibold rounded-lg border border-transparent bg-teal-100 text-teal-800 hover:bg-teal-200 disabled:opacity-50 disabled:pointer-events-none dark:hover:bg-teal-900 dark:text-teal-500 dark:hover:text-teal-400">送信</button>
</div>
</form>
</x-app-layout>
戻る
が押されるとダッシュボードに戻るようにしています。
送信
が押されるとデータベースへ登録するようにします。
データベースへの登録
送信
が押されるとstore
が実行されるようになっているので、フォームに入力されたものをデータベースへ登録していきます。
public function store(Request $request)
{
- //
+ $memo = new Memo;
+ $memo->fill($request->all())->save();
+ return redirect(route('dashboard'))->with('message', 'メモが登録されました。');
}
最後に登録したことをメッセージとしてダッシュボードに表示するようにします。
<div class="mt-2 max-w-7xl mx-auto sm:px-6 lg:px-8">
+ @if (session('message'))
+ <div class="mt-2 mb-2 bg-teal-100 border border-teal-200 text-sm text-teal-800 rounded-lg p-4 dark:bg-teal-800/10 dark:border-teal-900 dark:text-teal-500" role="alert">
+ <span class="font-bold">Info</span> {{ session('message') }}
+ </div>
+ @endif
<button type="button" onclick="location.href='{{route('memo.create')}}'" class="py-3 px-4 inline-flex items-center gap-x-2 text-sm font-semibold rounded-lg border border-transparent bg-blue-100 text-blue-800 hover:bg-blue-200 disabled:opacity-50 disabled:pointer-events-none dark:hover:bg-blue-900 dark:text-blue-400 dark:focus:outline-none dark:focus:ring-1 dark:focus:ring-gray-600">新規作成</button>
</div>
登録後の画面
登録後はこんな感じでメッセージが表示されます。
メモの一覧表示機能の追加
ここでは、ダッシュボード画面が表示された時に、メモの一覧が表示されるようにしていきたいと思います。
ルーティングの編集
今のままだとダッシュボードを表示するだけの状態のため、/dashboard
にリンクするとmemo.index
にリダイレクトするようにします。
Route::middleware([
'auth:sanctum',
config('jetstream.auth_session'),
'verified',
])->group(function () {
Route::get('/dashboard', function () {
- return view('dashboard');
+ return redirect(route('memo.index'));
})->name('dashboard');
});
コントローラーの編集
コントローラーを通してメモ一覧をデータベースから取得した後、ビューを表示させるようにします。
public function index()
{
- //
+ $memos = Memo::all();
+ return view('dashboard', compact('memos'));
}
モデルの編集
モデルを通してメモ一覧を取得する時に認証しているユーザーのデータのみを取得してくるようにします。この処理をしないと、他のユーザーの情報が取得できるようになってしまいます。
ここでは、memo
モデルにグローバルスコープを設定して制御しています。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
+use Illuminate\Database\Eloquent\Builder;
class Memo extends Model
{
use HasFactory;
use SoftDeletes;
protected $fillable = ['user_id', 'subject', 'content'];
+ protected static function booted(): void
+ {
+ static::addGlobalScope('auth', function (Builder $builder) {
+ $builder->where('user_id', auth()->id());
+ });
+ }
}
リストデザイン
リストのデザインはこのサンプル(Preline)を利用します。
右上のCopy
からHTMLをコピーすることができます。
ダッシュボードにリストを表示
新規作成
ボタンの下あたりに貼り付けます。
このままだとデータベースから取得してきたデータを表示できないので、下記の通り変更します。
コード真ん中付近にある@foreach
と@endforeach
で囲まれているところが編集しているところで、データの数だけループしている部分です。
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Dashboard') }}
</h2>
</x-slot>
<div class="mt-2 max-w-7xl mx-auto sm:px-6 lg:px-8">
@if (session('message'))
<div class="mt-2 mb-2 bg-teal-100 border border-teal-200 text-sm text-teal-800 rounded-lg p-4 dark:bg-teal-800/10 dark:border-teal-900 dark:text-teal-500" role="alert">
<span class="font-bold">Info</span> {{ session('message') }}
</div>
@endif
<button type="button" onclick="location.href='{{route('memo.create')}}'" class="py-3 px-4 inline-flex items-center gap-x-2 text-sm font-semibold rounded-lg border border-transparent bg-blue-100 text-blue-800 hover:bg-blue-200 disabled:opacity-50 disabled:pointer-events-none dark:hover:bg-blue-900 dark:text-blue-400 dark:focus:outline-none dark:focus:ring-1 dark:focus:ring-gray-600">新規作成</button>
+ <div class="flex flex-col mt-2">
+ <div class="-m-1.5 overflow-x-auto">
+ <div class="p-1.5 min-w-full inline-block align-middle">
+ <div class="border rounded-lg divide-y divide-gray-200 dark:border-neutral-700 dark:divide-neutral-700">
+ <div class="py-3 px-4">
+ <div class="relative max-w-xs">
+ <label class="sr-only">Search</label>
+ <input type="text" name="hs-table-with-pagination-search" id="hs-table-with-pagination-search" class="py-2 px-3 ps-9 block w-full border-gray-200 shadow-sm rounded-lg text-sm focus:z-10 focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 dark:placeholder-neutral-500 dark:focus:ring-neutral-600" placeholder="アイテムを検索する">
+ <div class="absolute inset-y-0 start-0 flex items-center pointer-events-none ps-3">
+ <svg class="size-4 text-gray-400 dark:text-neutral-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+ <circle cx="11" cy="11" r="8"></circle>
+ <path d="m21 21-4.3-4.3"></path>
+ </svg>
+ </div>
+ </div>
+ </div>
+ <div class="overflow-hidden">
+ <table class="min-w-full divide-y divide-gray-200 dark:divide-neutral-700">
+ <thead class="bg-gray-50 dark:bg-neutral-700">
+ <tr>
+ <th scope="col" class="px-6 py-3 text-start text-xs font-medium text-gray-500 uppercase dark:text-neutral-500">件名</th>
+ <th scope="col" class="px-6 py-3 text-start text-xs font-medium text-gray-500 uppercase dark:text-neutral-500">更新日時</th>
+ <th scope="col" class="px-6 py-3 text-end text-xs font-medium text-gray-500 uppercase dark:text-neutral-500">アクション</th>
+ </tr>
+ </thead>
+ <tbody class="divide-y divide-gray-200 dark:divide-neutral-700">
+ @foreach($memos as $memo)
+ <tr>
+ <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-neutral-200">{{$memo->subject}}</td>
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800 dark:text-neutral-200">{{$memo->updated_at}}</td>
+ <td class="px-6 py-4 whitespace-nowrap text-end text-sm font-medium"><button type="button" class="inline-flex items-center gap-x-2 text-sm font-semibold rounded-lg border border-transparent text-blue-600 hover:text-blue-800 disabled:opacity-50 disabled:pointer-events-none dark:text-blue-500 dark:hover:text-blue-400">消去</button></td>
+ </tr>
+ @endforeach
+ </tbody>
+ </table>
+ </div>
+ <div class="py-1 px-4">
+ <nav class="flex items-center space-x-1">
+ <button type="button" class="p-2.5 min-w-[40px] inline-flex justify-center items-center gap-x-2 text-sm rounded-full text-gray-800 hover:bg-gray-100 disabled:opacity-50 disabled:pointer-events-none dark:text-white dark:hover:bg-white/10">
+ <span aria-hidden="true">«</span>
+ <span class="sr-only">Previous</span>
+ </button>
+ <button type="button" class="min-w-[40px] flex justify-center items-center text-gray-800 hover:bg-gray-100 py-2.5 text-sm rounded-full disabled:opacity-50 disabled:pointer-events-none dark:text-white dark:hover:bg-white/10" aria-current="page">1</button>
+ <button type="button" class="min-w-[40px] flex justify-center items-center text-gray-800 hover:bg-gray-100 py-2.5 text-sm rounded-full disabled:opacity-50 disabled:pointer-events-none dark:text-white dark:hover:bg-white/10">2</button>
+ <button type="button" class="min-w-[40px] flex justify-center items-center text-gray-800 hover:bg-gray-100 py-2.5 text-sm rounded-full disabled:opacity-50 disabled:pointer-events-none dark:text-white dark:hover:bg-white/10">3</button>
+ <button type="button" class="p-2.5 min-w-[40px] inline-flex justify-center items-center gap-x-2 text-sm rounded-full text-gray-800 hover:bg-gray-100 disabled:opacity-50 disabled:pointer-events-none dark:text-white dark:hover:bg-white/10">
+ <span class="sr-only">Next</span>
+ <span aria-hidden="true">»</span>
+ </button>
+ </nav>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
</div>
</x-app-layout>
画面キャプチャ
こんな感じになります。
検索窓や消去ボタンはまだ機能していませんので、クリックしても何も起きません。