[laravel][開発編]
いよいよ開発だーーー!!!!
アプリ名設定
envファイル
APP_NAME=uCRM
サイトのアイコン設定
(私の場合)画像格納場所
C:\xampp\htdocs\uCRM\public\images\site_icon.png
<template>
<div>
<img src="/images/site_icon.png">
</div>
</template>
変更後http://127.0.0.1:8000/loginにアクセスし、設定したロゴが表示されていれば完了
アイテムのテーブルを作成する
🫠ストレージリンクのコマンド
php artisan storage:link
★Udemy講座ではないメモ★
マイグレーションの作成
🫠ざっくり
マイグレーション:データベースのバージョン管理
🫠マイグレーションの生成
php artisan make:migration create_flights_table
🫠格納場所
Artisanコマンド
make:migration
を使用して、データマイグレーションを生成する
database/migrations
ディレクトリに配置されます。
Items下準備
🫠モデル作成
php artisan make:model Item -a
-a:all
▼以下を生成
- モデル
- ファクトリー
- マイグレーション
- シーダー
- リクエスト
- リソースコントローラー
- ポリシー
😎講座で紹介されているページ
テーブル
public function up()
{
Schema::create('items', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('memo')->nullable();
$table->integer('price');
$table->boolean('is_selling')->default(true);
$table->timestamps();
});
}
->nullable()
:nullを許容する
🫠DB反映&マイグレーションファイル反映
php artisan migrate
実行画面
モデルルーティング
🫠コード
use App\Http\Controllers\ItemController;
// 認証していたら表示
Route::resource('items', ItemController::class)->middleware(['auth','verified']);
🫠シェル
php artisan route:list
実行画面
ユーザーシーダー、ログイン後のロゴ調節
🫠シーディングとは
🫠先にダミーデータを作成する
<?php
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
class UserSeader extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
DB::table('users')->insert([
'name' => 'test',
'email' => 'test@test.com',
'password' => Hash::make('password123'),
]);
}
}
public function run()
{
$this->call([
UserSeeder::class,
]);
}
//抜粋
🫠シェルスクリプト
php artisan migrate:fresh --seed
シーダー作成の流れ
公式リファレンス
database/seeders
に任意ファイルを追加
①例:ItemSeeder.php
②ダミーデータを追記
<?php
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
class ItemSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
DB::table('items')->insert([
[
'name' => 'カット',
'memo' => 'シャンプー/ブロー込・ロング料金なし・amicoオリジナルの小顔カット',
'price' => 4700,
],
[
'name' => 'フルカラー',
'memo' => 'シャンプー/ブロー込・ロング料金なし・amicoオリジナルのカラー',
'price' => 7300,
],
[
'name' => 'コスメパーマ',
'memo' => 'シャンプー/ブロー込・ロング料金なし・amicoオリジナルのケアパーマ',
'price' => 9500,
],
]);
}
}
DatabaseSeeder.php
にクラスを追加
③<?php
namespace Database\Seeders;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use App\Models\Item;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*
* @return void
*/
public function run()
{
$this->call([
ItemSeeder::class
]);
}
}
artisan
コマンド実行
シーダクラス定義(講座では手動で作成しているためスキップ)
php artisan make:seeder UserSeeder
シーダの実行
php artisan migrate:fresh --seed
🫠VallidationErrorsコンポーネントがなくなった件の対策
ValidationErrorsファイルを追加する
<script setup>
import { computed } from 'vue';
const props = defineProps({
errors: Object
})
const hasErrors = computed(() => Object.keys(props.errors).length > 0);
</script>
<template>
<div v-if="hasErrors">
<div class="font-medium text-red-600">問題が発生しました。</div>
<ul class="mt-3 list-disc list-inside text-sm text-red-600">
<li v-for="(error, key) in props.errors" :key="key">{{ error }}</li>
</ul>
</div>
</template>
import文追加
<script setup>
import { reactive } from 'vue';
import { Inertia } from '@inertiajs/inertia'
import BreezeValidationErrors from '@/Components/ValidationErrors.vue'
//
defineProps({
errors: Object
})
const form = reactive({
title: null,
content: null
})
// フォームを入力したときの処理
const submitFunction = () => {
Inertia.post('/inertia', form)
}
</script>
以下略
バージョンエラーがでた😢
🫠DBからItemを取得
->get()
を忘れないように
SQL文だけでは確定されず取得できない
class ItemController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
//DBから取得
dd(Item::select('id','name','price','is_selling')->get());
return Inertia::render('Items/Index');
}
}
🫠実行結果
コレクション型でシーダーで設定した3件が受け取れていたらOK
🫠リターン追記
public function index()
{
// 方法1
// //DBから取得
// $items = Item::select('id','name','price','is_selling')->get();
// // 取得したデータを返す
// return Inertia::render('Items/Index',[
// 'items' => $items
// ]);
//方法2 実行速度を配慮し、行数が少ない方法でかく
return Inertia::render('Items/Index',[
'items' => Item::select('id','name','price','is_selling')->get()
]);
}
🫠全コード
コード
<?php
namespace App\Http\Controllers;
use App\Models\Item;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreItemRequest;
use App\Http\Requests\UpdateItemRequest;
use Inertia\Inertia;
class ItemController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
// 方法1
// //DBから取得
// $items = Item::select('id','name','price','is_selling')->get();
// // 取得したデータを返す
// return Inertia::render('Items/Index',[
// 'items' => $items
// ]);
//方法2 実行速度を配慮し、行数が少ない方法でかく
return Inertia::render('Items/Index',[
'items' => Item::select('id','name','price','is_selling')->get()
]);
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\StoreItemRequest $request
* @return \Illuminate\Http\Response
*/
public function store(StoreItemRequest $request)
{
//
}
/**
* Display the specified resource.
*
* @param \App\Models\Item $item
* @return \Illuminate\Http\Response
*/
public function show(Item $item)
{
//
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\Item $item
* @return \Illuminate\Http\Response
*/
public function edit(Item $item)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\UpdateItemRequest $request
* @param \App\Models\Item $item
* @return \Illuminate\Http\Response
*/
public function update(UpdateItemRequest $request, Item $item)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Item $item
* @return \Illuminate\Http\Response
*/
public function destroy(Item $item)
{
//
}
}
🫠Vueファイル
コントローラーから受け取る場合はdefineProps
を使う
<script setup>
defineProps({
items: Array
})
</script>
コード全文
<script setup>
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
import { Head } from "@inertiajs/vue3";
// コントローラーから受け取る場合は「defineProps」
defineProps({
items: Array,
});
</script>
<template>
<Head title="商品一覧" />
<AuthenticatedLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">商品一覧</h2>
</template>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
<section class="text-gray-600 body-font">
<div class="container px-5 py-8 mx-auto">
<div class="flex pl-4 mt-4 lg:w-2/3 w-full mx-auto">
<button
class="flex ml-auto text-white bg-green-500 border-0 py-2 px-6 focus:outline-none hover:bg-green-600 rounded"
>
Button
</button>
</div>
<div class="lg:w-2/3 w-full mx-auto overflow-auto">
<table
class="table-auto w-full text-left whitespace-no-wrap"
>
<thead>
<tr>
<th
class="px-4 py-3 title-font tracking-wider font-medium text-gray-900 text-sm bg-gray-100 rounded-tl rounded-bl"
>
ID
</th>
<th
class="px-4 py-3 title-font tracking-wider font-medium text-gray-900 text-sm bg-gray-100"
>
商品名
</th>
<th
class="px-4 py-3 title-font tracking-wider font-medium text-gray-900 text-sm bg-gray-100"
>
価格
</th>
<th
class="px-4 py-3 title-font tracking-wider font-medium text-gray-900 text-sm bg-gray-100"
>
ステータス
</th>
</tr>
</thead>
<tbody>
<tr v-for="item in items" :key="item.id">
<td class="px-4 py-3">{{ item.id }}</td>
<td class="px-4 py-3">{{ item.name }}</td>
<td class="px-4 py-3">
{{ item.price }}
</td>
<td class="px-4 py-3">
{{ item.is_selling }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>
Laravelのデバッグ
dd();
🫠ページ遷移できない問題
Udemy講座名:【Laravel】【Vue.js3】で【CRM(顧客管理システム)】をつくってみよう【Breeze(Inertia)】
環境:Windows11、XAMPP、WSL2利用
🫠解決までの道のり
((Udemyの講師に聞いても解決できず、会社の先輩を頼りました😥))
-
node.js
とnpm
のバージョンをvite
に合わせて調整
## 動画でdev用にinstallしているせいで--devオプションが必要でした
composer install --dev
# nvmのインストール(nodeとnpm管理してくれる
# &コマンド1つで簡単にバージョン切り替えが出来る
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash
# パス通す
source ~/.bashrc
source ~/.bash_profile
# node version14 インストール
nvm install 14
# ここ大事!!
npm run build
😎もう一度
npm run dev
npm run build
先輩社員から
viteをつかっているのでnpm run devではなく
npm run buildにすることでビルドファイルが生成されます。
なのでnpm run devより安定するかと思われます
🫠ステータスを見やすくする
v-if
を使うことで条件によって表示を変えられる
<tr v-for="item in items" :key="item.id">
<td class="border-b-2 border-gray-200 px-4 py-3">{{ item.name }}</td>
<td class="border-b-2 border-gray-200 px-4 py-3">
<td class="border-b-2 border-gray-200 px-4 py-3">{{ item.id }}</td>
{{ item.price }}
</td>
<td class="border-b-2 border-gray-200 px-4 py-3">
<span v-if="item.is_selling === 1">販売中</span>
<span v-if="item.is_selling === 0">停止中</span>
<!-- {{ item.is_selling }} -->
</td>
</tr>
🫠jsconfig編集
{
"compilerOptions": {
"jsx":"preserve",
"baseUrl": ".",
"paths": {
"@/*": ["resources/js/*"]
}
},
"exclude": ["node_modules", "public"]
}
🌎現時点のPages
│ ComponentTest.vue
│ Dashboard.vue
│ InertiaTest.vue
│ Welcome.vue
│
├─Auth
│ ConfirmPassword.vue
│ ForgotPassword.vue
│ Login.vue
│ Register.vue
│ ResetPassword.vue
│ VerifyEmail.vue
│
├─Inertia
│ Create.vue
│ Index.vue
│ Show.vue
│
├─Items
│ Create.vue
│ Index.vue
│
└─Profile
│ Edit.vue
│
└─Partials
DeleteUserForm.vue
UpdatePasswordForm.vue
UpdateProfileInformationForm.vue
🫠商品登録
商品登録へのリンク追加
<Link as="button" :href="route('items.create')" class="flex ml-auto text-white bg-green-500 border-0 py-2 px-6 focus:outline-none hover:bg-green-600 rounded">商品登録</Link>
商品登録画面作成
Dashboardをコピペし、一部編集する
<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import { Head } from '@inertiajs/vue3';
</script>
<template>
<Head title="商品一覧" />
<AuthenticatedLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">商品一覧</h2>
</template>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">You're logged in!</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>
``
🫠商品登録 フォーム作成
tailblocksのCONTACTを引用
フォーム入力以外のUIを消す
:::
class="w-full"
横幅いっぱいに広げる
class="w-1/2"
横幅の半分の長さに広げる
:::
引用部分
<template>
<Head title="商品一覧" />
<AuthenticatedLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">商品一覧</h2>
</template>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
<section class="text-gray-600 body-font relative">
<div class="container px-5 py-8 mx-auto">
<div class="lg:w-1/2 md:w-2/3 mx-auto">
<div class="flex flex-wrap -m-2">
<div class="p-2 w-1/2">
<div class="relative">
<label
for="name"
class="leading-7 text-sm text-gray-600"
>Name</label
>
<input
type="text"
id="name"
name="name"
class="w-full bg-gray-100 bg-opacity-50 rounded border border-gray-300 focus:border-green-500 focus:bg-white focus:ring-2 focus:ring-green-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
/>
</div>
</div>
<div class="p-2 w-1/2">
<div class="relative">
<label
for="email"
class="leading-7 text-sm text-gray-600"
>Email</label
>
<input
type="email"
id="email"
name="email"
class="w-full bg-gray-100 bg-opacity-50 rounded border border-gray-300 focus:border-green-500 focus:bg-white focus:ring-2 focus:ring-green-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
/>
</div>
</div>
<div class="p-2 w-full">
<div class="relative">
<label
for="message"
class="leading-7 text-sm text-gray-600"
>Message</label
>
<textarea
id="message"
name="message"
class="w-full bg-gray-100 bg-opacity-50 rounded border border-gray-300 focus:border-green-500 focus:bg-white focus:ring-2 focus:ring-green-200 h-32 text-base outline-none text-gray-700 py-1 px-3 resize-none leading-6 transition-colors duration-200 ease-in-out"
></textarea>
</div>
</div>
<div class="p-2 w-full">
<button
class="flex mx-auto text-white bg-green-500 border-0 py-2 px-8 focus:outline-none hover:bg-green-600 rounded text-lg"
>
Button
</button>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>
Inertia/Create.vue
から引用
script部分を<script setup>
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
import { Head } from "@inertiajs/vue3";
import { reactive } from 'vue';
import { Inertia } from '@inertiajs/inertia'
//
defineProps({
errors: Object
})
const form = reactive({
name: null,
memo: null,
price: null
})
// フォームを入力したときの処理
const storeItem = () => {
Inertia.post('/items', form)
}
</script>
🫠商品追加ページに合わせていく
タイトル
<script setup>
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
import { Head } from "@inertiajs/vue3";
import { reactive } from 'vue';
import { Inertia } from '@inertiajs/inertia'
//
defineProps({
errors: Object
})
const form = reactive({
name: null,
memo: null,
price: null
})
// フォームを入力したときの処理
const storeItem = () => {
Inertia.post('/items', form)
}
</script>
<template>
<Head title="商品一覧" />
<AuthenticatedLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">商品一覧</h2>
</template>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
<section class="text-gray-600 body-font relative">
<form @submit.prevent="storeItem">
<div class="container px-5 py-8 mx-auto">
<div class="lg:w-1/2 md:w-2/3 mx-auto">
<div class="flex flex-wrap -m-2">
<!-- name start -->
<div class="p-2 w-full">
<div class="relative">
<label
for="name"
class="leading-7 text-sm text-gray-600"
>商品名</label
>
<input
type="text"
id="name"
name="name"
v-model="form.name"
class="w-full bg-gray-100 bg-opacity-50 rounded border border-gray-300 focus:border-green-500 focus:bg-white focus:ring-2 focus:ring-green-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
/>
</div>
</div>
<!-- memo start -->
<div class="p-2 w-full">
<div class="relative">
<label
for="memo"
class="leading-7 text-sm text-gray-600"
>Message</label
>
<textarea
id="memo"
name="memo"
v-model="form.memo"
class="w-full bg-gray-100 bg-opacity-50 rounded border border-gray-300 focus:border-green-500 focus:bg-white focus:ring-2 focus:ring-green-200 h-32 text-base outline-none text-gray-700 py-1 px-3 resize-none leading-6 transition-colors duration-200 ease-in-out"
></textarea>
</div>
</div>
<!-- price start -->
<div class="p-2 w-full">
<div class="relative">
<label
for="price"
class="leading-7 text-sm text-gray-600"
>商品名</label
>
<input
type="number"
id="price"
name="price"
v-model="form.price"
class="w-full bg-gray-100 bg-opacity-50 rounded border border-gray-300 focus:border-green-500 focus:bg-white focus:ring-2 focus:ring-green-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
/>
</div>
</div>
<div class="p-2 w-full">
<button
class="flex mx-auto text-white bg-green-500 border-0 py-2 px-8 focus:outline-none hover:bg-green-600 rounded text-lg"
>
Button
</button>
</div>
</div>
</div>
</div>
</form>
</section>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>
関係性をメモ
🫠フォームのバリデーション
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreItemRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the バリデーション rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'name' => ['request', 'max:50'],
'memo' => ['request', 'max:255'],
'price' => ['required', 'numeric'],
];
}
}
🫠コンポーネントをつかう
computed()
Vueの機能
リアタイで検知、計算する
usePage()
Inertiaの機能
マニュアルはShared data
頭にuseがついているのは合成関数
てか合成関数ってなに?調べても理解できなかった))
🫠Itemsのフラッシュメッセージ作成中にエラー
$ npm run build
> @ build /mnt/c/xampp/htdocs/uCRM
> vite build
vite v4.5.0 building for production...
✓ 19 modules transformed.
✓ built in 6.13s
[vite:vue] [vue/compiler-sfc] Identifier 'FlashMessage' has already been declared. (5:7)
/mnt/c/xampp/htdocs/uCRM/resources/js/Pages/Items/Index.vue
3 | import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
4 | import { Head, Link } from "@inertiajs/vue3";
5 | import FlashMessage from "@/Components/FlashMessage.vue";
| ^
6 | // コントローラーから受け取る場合は「defineProps」
7 | defineProps({
file: /mnt/c/xampp/htdocs/uCRM/resources/js/Pages/Items/Index.vue:5:7
error during build:
SyntaxError: [vue/compiler-sfc] Identifier 'FlashMessage' has already been declared. (5:7)
🤔 resources\js\Pages\Items\Index.vue
がおかしいらしい
😥 importがおかしいということはresources\js\Components\FlashMessage.vue
がおかしいってこと?
🙄 講座見返したらv-if="$page.props.flash.status === 'success'"
がおかしかった。
<script setup></script>
<template>
<!-- フラッシュメッセージなので、リロードしたら消える -->
<div v-if="$page.props.flash.status === 'success'" class="bg-blue-300 text-white p-4">
{{ $page.props.flash.message }}
</div>
</template>
🫠 まだエラーがでる
$ npm run build
> @ build /mnt/c/xampp/htdocs/uCRM
> vite build
vite v4.5.0 building for production...
✓ 18 modules transformed.
✓ built in 6.42s
[vite:vue] [vue/compiler-sfc] Identifier 'FlashMessage' has already been declared. (5:7)
/mnt/c/xampp/htdocs/uCRM/resources/js/Pages/Items/Index.vue
3 | import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
4 | import { Head, Link } from "@inertiajs/vue3";
5 | import FlashMessage from "@/Components/FlashMessage.vue";
| ^
6 |
7 | // コントローラーから受け取る場合は「defineProps」
file: /mnt/c/xampp/htdocs/uCRM/resources/js/Pages/Items/Index.vue:5:7
error during build:
SyntaxError: [vue/compiler-sfc] Identifier 'FlashMessage' has already been declared. (5:7)
翻訳してみた
[vite:vue] [vue/compiler-sfc] 識別子「FlashMessage」はすでに宣言されています。 (5:7)
ビルド中のエラー:
SyntaxError: [vue/compiler-sfc] 識別子「FlashMessage」はすでに宣言されています。 (5:7)
🫠解決
最初から翻訳すればよかった)))
2回importで読み込んでいたのがいけなかった
<script setup>
import FlashMessage from "@/Components/FlashMessage.vue";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
import { Head, Link } from "@inertiajs/vue3";
import FlashMessage from "@/Components/FlashMessage.vue"
// コントローラーから受け取る場合は「defineProps」
defineProps({
items: Array,
});
</script>
<script setup>
import FlashMessage from "@/Components/FlashMessage.vue";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
import { Head, Link } from "@inertiajs/vue3";
// コントローラーから受け取る場合は「defineProps」
defineProps({
items: Array,
});
</script
🫠💄Vue3の色指定方法
自分で設定する方法
メモの改行を反映させたい!
🫠JSで改行するための関数をつくる
PHPにあるbl2br
メソッドをjsでつくる
※調べたらコピペできそうなものが沢山でてくる
const nl2br = (str) => {
var res = str.replace(/\r\n/g, "<br>");
res = res.replace(/(\n|\r)/g, "<br>");
return res;
}
export { nl2br }
🫠vueファイルでimportする
※追記した部分だけ載せてます
<script setup>
import { nl2br } from `@/common`
</script>
<template>
<div
id="memo" v-html="nl2br(item.memo)"
class="w-full bg-opacity-50 rounded border border-gray-300 focus:border-green-500 focus:bg-white focus:ring-2 focus:ring-green-200 h-32 text-base outline-none text-gray-700 py-1 px-3 resize-none leading-6 transition-colors duration-200 ease-in-out"></div>
</template>
🫠気を付けること
<div></div>
内にnl2br{{ item.memo }}
を書くと
タグが表示されてしまう。
🫠商品削除
まず、ルート情報を確認する
php artisan route:list
Show.vue
にボタン追加
<div class="mt-20 p-2 w-full">
<button @click="deleteItem(item.id)" class="flex mx-auto text-white bg-red-500 border-0 py-2 px-8 focus:outline-none hover:bg-red-600 rounded text-lg">削除する</button>
</div>
Show.vue
に削除関数追加(script)
// 削除
const deleteItem = id => {
Inertia.delete(route('items.destroy',{ item: id }),{
onBefore: () => confirm('本当に削除しますか')
})
}
全コード
<script setup>
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
import { Head,Link } from "@inertiajs/vue3";
import { nl2br } from "@/common";
import { Inertia } from '@inertiajs/inertia';
// コントローラーから受け取る場合は「defineProps」
defineProps({
item : Object
})
// 削除
const deleteItem = id => {
Inertia.delete(route('items.destroy',{ item: id }),{
onBefore: () => confirm('本当に削除しますか')
})
}
</script>
<template>
<Head title="商品詳細" />
<AuthenticatedLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">商品詳細</h2>
</template>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
<section class="text-gray-600 body-font relative">
<div class="container px-5 py-8 mx-auto">
<div class="lg:w-1/2 md:w-2/3 mx-auto">
<div class="flex flex-wrap -m-2">
<!-- name start -->
<div class="p-2 w-full">
<div class="relative">
<label
for="name"
class="leading-7 text-sm text-gray-600"
>商品名</label
>
<!-- divタグに変更 -->
<div
id="name"
class="w-full bg-opacity-50 rounded border border-gray-300 focus:border-green-500 focus:bg-white focus:ring-2 focus:ring-green-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
>{{ item.name }}</div>
</div>
</div>
<!-- memo start -->
<div class="p-2 w-full">
<div class="relative">
<label
for="memo"
class="leading-7 text-sm text-gray-600"
>メモ</label
>
<div
id="memo" v-html="nl2br(item.memo)"
class="w-full bg-opacity-50 rounded border border-gray-300 focus:border-green-500 focus:bg-white focus:ring-2 focus:ring-green-200 h-32 text-base outline-none text-gray-700 py-1 px-3 resize-none leading-6 transition-colors duration-200 ease-in-out"
></div>
</div>
</div>
<!-- price start -->
<div class="p-2 w-full">
<div class="relative">
<label
for="price"
class="leading-7 text-sm text-gray-600"
>商品価格</label
>
<div
id="price"
class="w-full bg-opacity-50 rounded border border-gray-300 focus:border-green-500 focus:bg-white focus:ring-2 focus:ring-green-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
>{{ item.price }}</div>
</div>
</div>
<!-- 販売中 -->
<div class="p-2 w-full">
<div class="relative">
<label
for="status"
class="leading-7 text-sm text-gray-600"
>商品価格</label
>
<div
id="status"
class="w-full bg-opacity-50 rounded border border-gray-300 focus:border-green-500 focus:bg-white focus:ring-2 focus:ring-green-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
> <span v-if="item.is_selling === 1">販売中</span>
<span v-if="item.is_selling === 0">停止中</span>
</div>
</div>
</div>
<div class="p-2 w-full">
<Link as="button" :href="route('items.edit',{ item: item.id })"
class="flex mx-auto text-white bg-green-500 border-0 py-2 px-8 focus:outline-none hover:bg-green-600 rounded text-lg"
>編集する</Link>
</div>
<div class="mt-20 p-2 w-full">
<button @click="deleteItem(item.id)" class="flex mx-auto text-white bg-red-500 border-0 py-2 px-8 focus:outline-none hover:bg-red-600 rounded text-lg"
>削除する</button>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>
フラッシュメッセージに追加
<!-- 削除メッセージ -->
<div v-if="$page.props.flash.status === 'denger'" class="bg-red-300 text-white p-4">
{{ $page.props.flash.message }}
</div>
php編集
public function destroy(Item $item)
{
$item->delete();
return to_route('items.index')
->with([
'message' => '削除しました',
'status' => 'denger'
]);
}
}
🫠実行結果
ポップアップがでる
フラッシュメッセージがでる
🫠顧客情報
🔧Customersの下準備
php artisan make:model Customer -a
- モデル
- ファクトリー
- マイグレーション
- シーダー
- リクエスト
- リソースコントローラ
- ポリシー
💾マイグレーションファイル
public function up()
{
Schema::create('customers', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('kana');
$table->string('tel')->unique();
$table->string('email');
$table->string('postcode');
$table->string('address');
$table->date('birthday')->nullable();
$table->tinyInteger('gender'); // 0男性, 1女性、2その他
$table->text('memo')->nullable();
$table->timestamps();
});
}
php artisan migrate
実行
customersテーブルが増えているか確認
ダミーデータの日本語対応
'faker_locale' => 'ja_JP',
ダミーデータ生成
public function definition()
{
return [
'name' => $this->faker->name,
'kana' => $this->faker->kanaName,
'tel' => $this->faker->phoneNumber,
'email' => $this->faker->email,
'postcode' => $this->faker->postcode,
'address' => $this->faker->address,
'birthday' => $this->faker->dateTime,
'gender' => $this->faker->numberBetween(0, 2),
'memo' => $this->faker->realText(50),
];
}
FakerとFactoryを使うと大量のデータを簡単に作れる
DB Seederにファクトリーを何件作るか。の設定をかく
public function run()
{
$this->call([
UserSeeder::class,
ItemSeeder::class
]);
\App\Models\Customer::factory(1000)->create();
// \App\Models\User::factory(10)->create();
// \App\Models\User::factory()->create([
// 'name' => 'Test User',
// 'email' => 'test@example.com',
// ]);
}
php artisan migrate:fresh --seed
を実行
ダミーデータが1000件入っていれば良き
🫠ダミーデータの修正
public function definition()
{
$tel = str_replace('-','', $this->faker->phoneNumber);
$address = mb_substr($this->faker->address, 9);
return [
'name' => $this->faker->name,
'kana' => $this->faker->kanaName,
'tel' => $tel,
'email' => $this->faker->email,
'postcode' => $this->faker->postcode,
'address' => $address,
'birthday' => $this->faker->dateTime,
'gender' => $this->faker->numberBetween(0, 2),
'memo' => $this->faker->realText(50),
];
}
🫠顧客一覧
ルーティング
※追記のみ
use App\Http\Controllers\CustomerController;
Route::resource('customer',CustomerController::class)->middleware(['auth','verified']);
php artisan route:list
を実行
customerが追加されているか確認
🫠コントローラー
※追記のみ
use Inertia\Inertia;
public function index()
{
return Inertia::render('Customers/Index',[
'customers' => Customer::select('id','name','kana','tel')->get()
]);
}
🫠vueファイル
コード
<script setup>
import FlashMessage from "@/Components/FlashMessage.vue";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
import { Head, Link } from "@inertiajs/vue3";
// コントローラーから受け取る場合は「defineProps」
defineProps({
customers: Array,
});
</script>
<template>
<Head title="顧客一覧" />
<AuthenticatedLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">顧客一覧</h2>
</template>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
<section class="text-gray-600 body-font">
<div class="container px-5 py-8 mx-auto">
<FlashMessage />
<div class="flex pl-4 mt-4 lg:w-2/3 w-full mx-auto">
<Link as="button" :href="route('customers.create')" class="flex ml-auto text-white bg-green-500 border-0 py-2 px-6 focus:outline-none hover:bg-green-600 rounded">顧客登録</Link>
</div>
<div class="lg:w-2/3 w-full mx-auto overflow-auto">
<table
class="table-auto w-full text-left whitespace-no-wrap"
>
<thead>
<tr>
<th
class="px-4 py-3 title-font tracking-wider font-medium text-gray-900 text-sm bg-gray-100 rounded-tl rounded-bl"
>
ID
</th>
<th
class="px-4 py-3 title-font tracking-wider font-medium text-gray-900 text-sm bg-gray-100"
>
氏名
</th>
<th
class="px-4 py-3 title-font tracking-wider font-medium text-gray-900 text-sm bg-gray-100"
>
カナ
</th>
<th
class="px-4 py-3 title-font tracking-wider font-medium text-gray-900 text-sm bg-gray-100"
>
電話番号
</th>
</tr>
</thead>
<tbody>
<tr v-for="customer in customers" :key="customer.id">
<td class="border-b-2 border-gray-200 px-4 py-3">{{ customer.id }}</td>
<td class="border-b-2 border-gray-200 px-4 py-3">{{ customer.name }}</td>
<td class="border-b-2 border-gray-200 px-4 py-3">{{ customer.kana }}</td>
<td class="border-b-2 border-gray-200 px-4 py-3">{{ customer.tel }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>
ナビゲーションメニューに追加
<NavLink :href="route('customers.index')" :active="route().current('customers.index')">顧客管理</NavLink>
~中略~
<ResponsiveNavLink :href="route('customers.index')" :active="route().current('customers.index')">顧客管理</ResponsiveNavLink>
🫠ページネーション
Inertiaでの実装は手間がかかる
Vuetifyに定義されている角丸classを使う
🫠モーダルウィンドウ
今回使うライブラリ
コマンドで実行
npm i micromodal@0.4.10 --save
package.jsonが追加されていたら良き
エラー地獄の記録
🫠購入履歴一覧
😎欲しい情報
- 購買ID
- 顧客名
- 合計金額
- ステータス
- 購入日
🫶サブクエリ
4つのテーブルをjoin
金額*合計=小計
を表示
🐲合計金額を表示
サブクエリで生成したテーブルをもとにgroup by
で購入毎の
合計金額を表示
📂グローバルスコープ用ファイル作成
php artisan make:scope Subtotal
⌚日時の表示変更
▼現在の日時はこんなかんじ
PHPの問題じゃなくて、console.log
で受け取った際にこの形に変換されちゃうらしい😥
const props = defineProps({
orders: Object
})
onMounted(() => {
console.log(props.orders.data)
})
ライブラリ
便利なライブラリ
JSで日付のフォーマットを整えるためのライブラリ
#インストール
npm i dayjs@1.11.5 --save
インストールができていたら↓が追加されているはず
"dependencies": {
"dayjs": "^1.11.5",
}
👽vueファイルにインポート
<script setup>
import dayjs from 'dayjs'
</script>
👽表示部分を編集
フォーマット参考元
{{ dayjs(order.created_at).format('YYYY-MM-DD HH:mm:ss')}}
解決!
データ分析の機能を追加していくぞー!!
🫠データ分析
CRM(顧客管理システムの1つ)
たくさんのデータを分析して資格化する
👻今回は
- 日別
- 月別
- 年別
- デシル分析
- RFM分析
をしていく
とりまいつものやつ
php artisan make:controller AnalysisController
web.php
use App\Http\Controllers\AnalysisController;
Route::get('analysis', [AnalysisController::class, 'index'])->name('analysis');
use Inertia\Inertia;
を読み込むのを忘れない
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Inertia\Inertia;
class AnalysisController extends Controller
{
public function index()
{
return Inertia::render('Analysis');
}
}
Inertia::render('Analysis')
❔何気に使っている(今回でいうこれ)Inertia::render
関数は第一引数に コンポーネント 、第二引数に プロパティ配列 渡す
未来の自分へ 開発した時のための備忘録
controllerがインポートされない問題
試したこと
composer dump-autoload
↑変わらなかった
😥実行結果にエラーがでるようになった
怪しそうなコードを触っていたら、やっと実行結果にエラーがでるようになった
Cannot declare class App\Http\Controllers\Api\AnalysisController, because the name is already in use
訳:名前はすでに使用されているため、クラス App\Http\Controllers\Api\AnalysisController を宣言できません
🫠解決
namespace名がかぶっていた
namespace App\Http\Controllers;
//↓間違い Api直下ファイルとかぶっていた
namespace App\Http\Controllers\Api;
namespace App\Http\Controllers\Api;
メモ
select history.id, history.customer_name, sum(history.subtotal) as
total, history.status, history.created_at
from (select purchases.id as id, item_purchase.id as pivot_id,
customers.name as customer_name,
items.price * item_purchase.quantity as subtotal,
items.name as item_name, items.price as item_price,
item_purchase.quantity, purchases.status, purchases.created_at,
purchases.updated_at
from purchases
left join item_purchase on purchases.id = item_purchase.purchase_id
left join items on item_purchase.item_id = items.id
left join customers on purchases.customer_id = customers.id
) as history
where created_at BETWEEN '2022-08-01' and '2022-08-11'
group by history.id
vue3-charts
インストールする
npm i vue-chart-3@3.1.8 chart.js@3.9.1
ファットコントローラーとは
Laravelなどのフレームワークで用いられているコントローラーに様々な処理を任せてしまい、1つのコントローラー内、1つのメソッド内の行数が多くなってしまって肥大化していることを指します。
https://techblog.styleedge.co.jp/entry/2022/02/28/164301
controllerはなるべつコードが少ないほうがいい!
デメリット
- どこに処理を記述したかが分かりにくく、コードを追いにくい
- 改修漏れが生じやすい
😥デシル分析の弱点
- 検索期間が長期間だと
過去は優良顧客だったけど現在は通っていないユーザーも含まれてしまう - 検索期間が短期間だと
得られるデータが少ない
定期的に購入する安定顧客が含まれず、一時的に大きな買い物をしたユーザーが優良と扱われる
📝RFM分析について
Recency 最新購入日
Frequency 購入回数
Monetary 購入金額合計
3つの軸に分ける事で、
1回のみ高額で購入したユーザー と
定期的に高額ではない商品を購入しているユー
ザーは それぞれ別のグループとして扱われる
🎒開発の流れ
- 購買ID毎にまとめる
- 会員毎にまとめて最終購入日、回数、合計
金額を取得 - RFMランクを仮設定する
- 会員毎のRFMランクを計算する
- ランク毎の数を計算する(3を再調整)
- RとFで2次元で表示してみる