laravel によるカード型データベース

(2024-09-14)

laravel によるカード型データベースをレンタルサーバー上で動かしています。 私の個人的な情報や、ちょっとメモっておきたい情報を記録しておいて、キーワードで検索をかけることができるので、 どんなに情報が多くなっても自分の見たい情報にほぼ瞬時にアクセスできます。

このデータベースには画像も保存することができて、私のコロナ接種情報などを画像として保存してあります。

そこには私の住所・氏名が書いてありますが、割と強力なパスワードで保護されているので簡単には突破できません。 もし漏れたとしても、私の住所・氏名なんぞ誰も興味がないと思います。

最近、レンタルサーバーの独自ドメインが1年の期限を終えたので新しいドメインで登録したのですが、 以前から JavaScript と CSS に関してとてもアバウトな設定になっており、スマホで閲覧するとちょっと見にくい設定になっていました。

できれば livewire と tailwind を設定したいと思い挑戦してみました。

環境は linux mint 22 です。

プロジェクトの作成

composer create-project --prefer-dist laravel/laravel laramemo

移動して livewire のインストール。

cd laramemo
composer require livewire/livewire

tailwind のインストールと設定

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

tailwind.config.js の編集。

/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./resources/**/*.blade.php",
"./resources/**/*.js",
"./resources/**/*.vue",
],
theme: {
extend: {},
},
plugins: [],
}

resources/css/app.css の編集。

resources/css/app.css
@tailwind base;
@tailwind components;
@tailwind utilities;

サーバーを起動します。

npm run dev

.env の編集。

cd ~/laramemo
nano .env

データベースは mysql で。

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_memo
DB_USERNAME=root
DB_PASSWORD=user

モデルとマイグレーションの作成。

php artisan make:model Item -m

migration テーブルの編集

database/migrations/xxxxx_create_items_table.php
<?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('items', function (Blueprint $table) {
$table->id();
$table->string('category');
$table->longtext('comment');
$table->string('img_path')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('items');
}
};

マイグレーションの実行。

php artisan migrate

model の編集

app/Models/Item.php を編集します

app/Models/Item.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Livewire\Component;
class Item extends Model
{
protected $fillable = ['category','comment','img_path'];
}
class ItemDelete extends Component
{
public $itemId;
public function mount($id)
{
// URLから渡されたIDをプロパティに設定
$this->itemId = $id;
}
public function deleteItem()
{
// アイテムを削除
Item::destroy($this->itemId);
// セッションメッセージを設定
session()->flash('message', 'アイテムが削除されました。');
// 削除後、アイテムリストのページにリダイレクト
return redirect()->route('items.list'); // items.listはルーティングで設定する必要があります
}
public function render()
{
// 削除確認用のビューを表示
return view('livewire.item-delete');
}
}

Livewireコンポーネントの作成と設定

データの記録

Livewireコンポーネントの作成。

php artisan make:livewire ItemCreate

app/Livewire に ItemCreate.php が作成されるので以下のように編集。

app/Livewire/ItemCreate.php
<?php
namespace App\Livewire;
use Livewire\Component;
use Livewire\WithFileUploads; // トレイトをインポート
use App\Models\Item;
class ItemCreate extends Component
{
use WithFileUploads; // この行が必要
public $category;
public $comment;
public $img_path;
public function render()
{
return view('livewire.item-create')->layout('components.layouts.app');
}
public function saveItem()
{
$this->validate([
'category' => 'required',
'comment' => 'required',
'img_path' => 'nullable|image|max:1024', // 画像ファイルとしてバリデーション
]);
// 画像がある場合に保存
$stockdir = "uploads/".date("Y/m/d") ;
$imagePath = $this->img_path ? $this->img_path->store($stockdir, 'public') : null;
Item::create([
'category' => $this->category,
'comment' => $this->comment,
'img_path' => $imagePath,
]);
$this->reset('category', 'comment', 'img_path');
return redirect()->to('/');
}
}

全データ表示

php artisan make:livewire ItemList

app/Livewire/ItemList.php を編集します。

app/Livewire/ItemList.php
<?php
namespace App\Livewire;
use Livewire\Component;
use App\Models\Item;
class ItemList extends Component
{
public $items;
public function mount()
{
$this->items = Item::orderBy('id', 'desc')->get();
}
public function deleteItem($id)
{
Item::destroy($id);
$this->items = Item::all();
return redirect()->to('/');
}
public function updateItem($id)
{
return redirect()->route('items.edit', ['id' => $id]);
}
public function render()
{
return view('livewire.item-list', ['items' => $this->items]);
}
}

キーワード検索

php artisan make:livewire ItemSearch

app/Livewire/ItemSearch.php を編集します。

app/Livewire/ItemSearch.php
<?php
namespace App\Livewire;
use Livewire\Component;
use App\Models\Item;
class ItemSearch extends Component
{
public $keyword;
public $items = [];
public function search()
{
if ($this->keyword) {
$this->items = Item::where('comment', 'LIKE', "%{$this->keyword}%")
->orderBy('id', 'desc')
->get();
}
}
public function deleteItem($id)
{
Item::destroy($id);
$this->items = Item::all();
return redirect()->to('/');
}
public function updateItem($id)
{
return redirect()->route('items.edit', ['id' => $id]);
}
public function render()
{
return view('livewire.item-search', ['items' => $this->items]);
}
}

カテゴリ検索

php artisan make:livewire ItemCategory

app/Livewire/ItemCategory.php を編集します。

app/Livewire/ItemCategory.php
<?php
namespace App\Livewire;
use Livewire\Component;
use App\Models\Item;
class ItemCategory extends Component
{
public $keyword;
public $items = [];
public function category()
{
if ($this->keyword) {
$this->items = Item::where('category', $this->keyword)
->orderBy('id', 'desc')
->get();
}
}
public function deleteItem($id)
{
Item::destroy($id);
$this->items = Item::all();
return redirect()->to('/');
}
public function updateItem($id)
{
return redirect()->route('items.edit', ['id' => $id]);
}
public function render()
{
return view('livewire.item-category', ['items' => $this->items]);
}
}

データの編集

php artisan make:livewire ItemEdit

app/Livewire/ItemEdit.php を編集します。

app/Livewire/ItemEdit.php
<?php
namespace App\Livewire;
use Livewire\Component;
use App\Models\Item;
class ItemEdit extends Component
{
public $itemId;
public $category;
public $comment;
public function mount($id)
{
// Load item data
$item = Item::find($id);
$this->itemId = $item->id;
$this->category = $item->category;
$this->comment = $item->comment;
}
public function updateItem()
{
// Update the item in the database
$item = Item::find($this->itemId);
$item->category = $this->category;
$item->comment = $this->comment;
$item->save();
// Redirect back to item list
return redirect()->route('items.list');
}
public function render()
{
return view('livewire.item-edit');
}
}

view の設定

livewire コンポーネントに対する blade を編集します。

item-create.blade.php を以下のように編集。

resources/views/livewire/item-create.blade.php
<div>
<h1 class="text-xl bg-blue-50 border border-stone-400 px-5 py-2 m-3">データを記録する</h1>
<form wire:submit.prevent="saveItem" enctype="multipart/form-data">
<div class="inline-block w-28 text-base px-5 py-2">カテゴリ</div>
<select wire:model="category" class="inline-block w-48 text-base bg-slate-50 border border-stone-400 px-2 py-1">
<option value="">以下から選んでください</option>
<option value="UC">UC</option>
<option value="Crohn">Crohn</option>
<option value="CF">CF</option>
<option value="HBV">HBV</option>
<option value="HCV">HCV</option>
<option value="その他の肝障害">その他の肝障害</option>
<option value="ERCP">ERCP</option>
<option value="その他の消化器疾患">その他の消化器疾患</option>
<option value="その他の医学関連">その他の医学関連</option>
<option value="グルメ">グルメ</option>
<option value="コンピュータ">コンピュータ</option>
<option value="マネー">マネー</option>
<option value="車">車</option>
<option value="etc">etc</option>
</select><br>
@error('category') <span>{{ $message }}</span> @enderror
<div class="inline-block w-28 text-base px-5 py-2 align-top">comment</div>
<textarea wire:model="comment" class="inline-block w-5/6 text-base bg-slate-50 border border-stone-400 px-2 py-1">
</textarea><br>
@error('comment') <span>{{ $message }}</span> @enderror
<div class="inline-block w-28 text-base px-5 py-2 align-top">画像</div>
<input type="file" wire:model="img_path" class="inline-block w-96 mt-2"><br>
@error('img_path') <span>{{ $message }}</span> @enderror
<button type="submit" class="inline-block w-24 bg-blue-900 text-white px-2 py-1 m-5">保存</button>
</form>
</div>

resources/views/livewire/item-list.blade.php を編集します。

resources/views/livewire/item-list.blade.php
<div>
<h1 class="text-xl bg-blue-50 border border-stone-400 px-5 py-2 m-3">データ一覧</h1>
<div class="m-3">
<table class="table-auto w-full border-collapse border border-stone-300">
<thead>
<tr class="bg-blue-950 text-white">
<th class="border border-stone-300 p-2 w-32">date</th>
<th class="border border-stone-300 p-2 w-48">category</th>
<th class="border border-stone-300 p-2">comment</th>
<th class="border border-stone-300 p-2 w-16">image</th>
<th class="border border-stone-300 p-2 w-16">削除</th>
<th class="border border-stone-300 p-2 w-16">訂正</th>
</tr>
</thead>
<tbody>
@foreach ($items as $item)
<tr class="border-b border-stone-300">
<td class="border border-stone-300 p-2">{{ $item->created_at->toDateString() }}</td>
<td class="border border-stone-300 p-2">{{ $item->category }}</td>
<td class="border border-stone-300 p-2 max-w-32 break-words">{!! nl2br(e($item->comment)) !!}</td>
<td class="border border-stone-300 p-2">
@if($item->img_path)
<a href="{{ asset('storage/' . $item->img_path) }}">
<img src="{{ asset('storage/' . $item->img_path) }}" width="30">
</a>
@endif
</td>
<td class="border border-stone-300 p-2 hover:bg-orange-100">
<button wire:click="deleteItem({{ $item->id }})">削除</button>
</td>
<td class="border border-stone-300 p-2 hover:bg-blue-100">
<button wire:click="updateItem({{ $item->id }})">訂正</button>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>

resources/views/livewire/item-search.blade.php を編集します。

resources/views/livewire/item-search.blade.php
<div>
<h1 class="text-xl bg-blue-50 border border-stone-400 px-5 py-2 m-3">キーワード検索</h1>
<div><input type="text" wire:model="keyword" placeholder="検索キーワード" class="bg-slate-50 border border-stone-400 px-2 py-1 mx-5"></div>
<button wire:click="search" class="inline-block w-24 bg-blue-900 text-white px-2 py-1 m-5">検索</button>
<div class="m-3">
<table class="table-auto w-full border-collapse border border-stone-300">
<thead>
<tr class="bg-blue-950 text-white">
<th class="border border-stone-300 p-2 w-32">date</th>
<th class="border border-stone-300 p-2 w-48">category</th>
<th class="border border-stone-300 p-2">comment</th>
<th class="border border-stone-300 p-2 w-16">image</th>
<th class="border border-stone-300 p-2 w-16">削除</th>
<th class="border border-stone-300 p-2 w-16">訂正</th>
</tr>
</thead>
<tbody>
@foreach ($items as $item)
<tr class="border-b border-stone-300">
<td class="border border-stone-300 p-2">{{ $item->created_at->toDateString() }}</td>
<td class="border border-stone-300 p-2">{{ $item->category }}</td>
<td class="border border-stone-300 p-2 max-w-32 break-words">{!! nl2br(e($item->comment)) !!}</td>
<td class="border border-stone-300 p-2">
@if($item->img_path)
<a href="{{ asset('storage/' . $item->img_path) }}">
<img src="{{ asset('storage/' . $item->img_path) }}" width="30">
</a>
@endif
</td>
<td class="border border-stone-300 p-2 hover:bg-orange-100">
<button wire:click="deleteItem({{ $item->id }})">削除</button>
</td>
<td class="border border-stone-300 p-2 hover:bg-blue-100">
<button wire:click="updateItem({{ $item->id }})">訂正</button>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>

resources/views/livewire/item-category.blade.php を編集します。

resources/views/livewire/item-category.blade.php
<div>
<h1 class="text-xl bg-blue-50 border border-stone-400 px-5 py-2 m-3">カテゴリ検索</h1>
<div class="inline-block w-28 text-base px-5 py-2">カテゴリ</div>
<select wire:model="keyword" class="inline-block w-48 text-base bg-slate-50 border border-stone-400 px-2 py-1">
<option value="">以下から選んで下さい</option>
<option value="UC">UC</option>
<option value="Crohn">Crohn</option>
<option value="CF">CF</option>
<option value="HBV">HBV</option>
<option value="HCV">HCV</option>
<option value="その他の肝障害">その他の肝障害</option>
<option value="ERCP">ERCP</option>
<option value="その他の消化器疾患">その他の消化器疾患</option>
<option value="その他の医学関連">その他の医学関連</option>
<option value="グルメ">グルメ</option>
<option value="コンピュータ">コンピュータ</option>
<option value="マネー">マネー</option>
<option value="車"></option>
<option value="etc">etc</option>
</select><br>
<button wire:click="category" class="inline-block w-24 bg-blue-900 text-white px-2 py-1 m-5">検索</button>
<div class="m-3">
<table class="table-auto w-full border-collapse border border-stone-300">
<thead>
<tr class="bg-blue-950 text-white">
<th class="border border-stone-300 p-2 w-32">date</th>
<th class="border border-stone-300 p-2 w-48">category</th>
<th class="border border-stone-300 p-2">comment</th>
<th class="border border-stone-300 p-2 w-16">image</th>
<th class="border border-stone-300 p-2 w-16">削除</th>
<th class="border border-stone-300 p-2 w-16">訂正</th>
</tr>
</thead>
<tbody>
@foreach ($items as $item)
<tr class="border-b border-stone-300">
<td class="border border-stone-300 p-2">{{ $item->created_at->toDateString() }}</td>
<td class="border border-stone-300 p-2">{{ $item->category }}</td>
<td class="border border-stone-300 p-2 max-w-32 break-words">{!! nl2br(e($item->comment)) !!}</td>
<td class="border border-stone-300 p-2">
@if($item->img_path)
<a href="{{ asset('storage/' . $item->img_path) }}">
<img src="{{ asset('storage/' . $item->img_path) }}" width="30">
</a>
@endif
</td>
<td class="border border-stone-300 p-2 hover:bg-orange-100">
<button wire:click="deleteItem({{ $item->id }})">削除</button>
</td>
<td class="border border-stone-300 p-2 hover:bg-blue-100">
<button wire:click="updateItem({{ $item->id }})">訂正</button>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>

resources/views/livewire/item-edit.blade.php を編集します。

resources/views/livewire/item-edit.blade.php
<div>
<h1 class="text-xl bg-blue-50 border border-stone-400 px-5 py-2 m-3">データ編集</h1>
<form wire:submit.prevent="updateItem">
<div class="inline-block w-28 text-base px-5 py-2">カテゴリ</div>
<select wire:model="category" id="category" class="inline-block w-48 text-base bg-slate-50 border border-stone-400 px-2 py-1">
<option value="">以下から選んでください</option>
<option value="UC">UC</option>
<option value="Crohn">Crohn</option>
<option value="CF">CF</option>
<option value="HBV">HBV</option>
<option value="HCV">HCV</option>
<option value="その他の肝障害">その他の肝障害</option>
<option value="ERCP">ERCP</option>
<option value="その他の消化器疾患">その他の消化器疾患</option>
<option value="その他の医学関連">その他の医学関連</option>
<option value="グルメ">グルメ</option>
<option value="コンピュータ">コンピュータ</option>
<option value="マネー">マネー</option>
<option value="車"></option>
<option value="etc">etc</option>
</select><br>
<div class="inline-block w-28 text-base px-5 py-2 align-top">comment</div>
<textarea wire:model="comment" id="comment" class="inline-block w-5/6 text-base bg-slate-50 border border-stone-400 px-2 py-1">
</textarea><br>
<button type="submit" class="inline-block w-24 bg-blue-900 text-white px-2 py-1 m-5">更新</button>
</form>
</div>

resources/views/welcome.blade.php の編集。

resources/views/welcome.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@vite('resources/css/app.css')
<title>Top Page</title>
</head>
<body>
<h1 class="text-xl bg-blue-50 border border-stone-400 px-5 py-2 m-3">Menu</h1>
<ul class="list-disc leading-10 mx-10 px-2">
<li class="text-blue-800 hover:text-orange-800 delay-150"><a href="{{ route('item.create') }}">データの記録</a></li>
<li class="text-blue-800 hover:text-orange-800 delay-150"><a href="{{ route('items.list') }}">データ一覧</a></li>
<li class="text-blue-800 hover:text-orange-800 delay-150"><a href="{{ route('items.search') }}">キーワード検索</a></li>
<li class="text-blue-800 hover:text-orange-800 delay-150"><a href="{{ route('items.category') }}">カテゴリ検索</a></li>
</ul>
</body>
</html>

resources/views/components/layouts/app.blade.php を新規作成します。

resources/views/components/layouts/app.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@vite('resources/css/app.css')
<title>laravel</title>
</head>
<body>
{{ $slot }}
</body>
</html>

画像閲覧のためのリンク

cd ~/laramemo
php artisan storage:link

ルーティング

routes/web.php
<?php
use App\Livewire\ItemList;
use App\Livewire\ItemCreate;
use App\Livewire\ItemSearch;
use App\Livewire\ItemDelete;
use App\Livewire\ItemCategory;
use App\Livewire\ItemEdit;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
})->name('home');
Route::get('/item-create', ItemCreate::class)->name('item.create');
Route::get('/items-list', ItemList::class)->name('items.list');
Route::get('/items-search', ItemSearch::class)->name('items.search');
Route::get('/items-category', ItemCategory::class)->name('items.category');
Route::get('/items/edit/{id}', ItemEdit::class)->name('items.edit');

サーバーを起動してデータを保存したり、編集・削除します。

cd ~/laramemo
php artisan serve

npm の方はビルドしてしまえばサーバーとして立ち上げておく必要はないようです。

cd ~/laramemo
npm run build

Breeze の導入

レンタルサーバー上にこのアプリを置いておくので、当然ながらアクセス制限が必要です。

Breeze をインストールします。

composer require laravel/breeze --dev
php artisan breeze:install

オプションがよくわかりませんが、最初のオプションは「Livewire (Volt Class API) with Alpine」で最後のオプションは「Pest」にしました。 今のところ意味は不明です。

サーバーを立ち上げると、


右上に、Register と Login が表示されます。

Register で登録するとログインできるようになります。


dashboard を書き換えます。

<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('Menu') }}
</h2>
<ul class="list-disc leading-10 mx-10 px-2">
<li class="text-blue-800 hover:text-orange-800 delay-150"><a href="{{ route('item.create') }}">データの記録</a></li>
<li class="text-blue-800 hover:text-orange-800 delay-150"><a href="{{ route('items.list') }}">データ一覧</a></li>
<li class="text-blue-800 hover:text-orange-800 delay-150"><a href="{{ route('items.category') }}">カテゴリ検索</a></li>
<li class="text-blue-800 hover:text-orange-800 delay-150"><a href="{{ route('items.search') }}">キーワード検索</a></li>
</ul>
</x-slot>
</x-app-layout>

routes/web.php を書き換えます。

<?php
use Illuminate\Support\Facades\Route;
use App\Livewire\ItemList;
use App\Livewire\ItemCreate;
use App\Livewire\ItemSearch;
use App\Livewire\ItemDelete;
use App\Livewire\ItemCategory;
use App\Livewire\ItemEdit;
Route::middleware(['auth'])->group(function () {
Route::view('dashboard', 'dashboard')->middleware('verified')->name('dashboard');
Route::view('profile', 'profile')->name('profile');
Route::get('/item-create', ItemCreate::class)->name('item.create');
Route::get('/items-list', ItemList::class)->name('items.list');
Route::get('/items-search', ItemSearch::class)->name('items.search');
Route::get('/items-category', ItemCategory::class)->name('items.category');
Route::get('/items/edit/{id}', ItemEdit::class)->name('items.edit');
});
Route::view('/', 'welcome');
require __DIR__.'/auth.php';

そうすると、ログインするとメニューが表示されます。


データ処理をした後に、dashboard に redirect したい場合は、

public function deleteItem($id)
{
Item::destroy($id);
$this->items = Item::all();
return redirect()->to('dashboard');
}

register.blade.php の削除

resources/views/livewire/pages/auth/register.blade.php が残っていると、勝手に登録されてしまうので削除します。