Back to Course |
Laravel Vue Inertia: Food Ordering Project Step-By-Step

Products Create/Edit/Delete

Time to allow vendors to manage Products. This will be another CRUD, very similar to the Categories, so I will not stop too much. I will mostly just show you the code.

We already have the Controller and Routes from the previous lesson:

routes/vendor.php:

use App\Http\Controllers\Vendor\ProductController;
 
Route::group([
'prefix' => 'vendor',
'as' => 'vendor.',
'middleware' => ['auth'],
], function () {
// ...
 
Route::resource('products', ProductController::class);
});

Now, let's fill in that Controller with actual logic.


Product Create Form

app/Http/Controllers/Vendor/ProductController.php:

namespace App\Http\Controllers\Vendor;
 
use App\Http\Controllers\Controller;
use App\Models\Category;
use Inertia\Inertia;
use Inertia\Response;
 
class ProductController extends Controller
{
public function create(): Response
{
return Inertia::render('Vendor/Products/Create', [
'categories' => Category::all(['id', 'name']),
'category_id' => request('category_id'),
]);
}
}

As you can see, we're passing all the categories for the dropdown and also the active category.

In the Menu.vue, we will pass that active category as a parameter:

resources/js/Pages/Vendor/Menu.vue:

<div v-for="category in categories" ...>
 
// ...
 
<div>
<Link
class="btn btn-secondary btn-sm"
:href="route('vendor.products.create', { category_id: category.id })"
>
Add Product to {{ category.name }}
</Link>
</div>
</div>

Here's how the Create form will look like in the Vue:

resources/js/Pages/Vendor/Products/Create.vue:

<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'
import { Head, useForm } from '@inertiajs/vue3'
import InputError from '@/Components/InputError.vue'
import InputLabel from '@/Components/InputLabel.vue'
import PrimaryButton from '@/Components/PrimaryButton.vue'
import TextInput from '@/Components/TextInput.vue'
import SelectInput from '@/Components/SelectInput.vue'
const props = defineProps({
categories: {
type: Array
},
category_id: {
type: String
}
})
const form = useForm({
category_id: props.category_id,
name: '',
price: ''
})
const submit = () => {
form.post(route('vendor.products.store'))
}
</script>
 
<template>
<Head title="Add New Product" />
 
<AuthenticatedLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">Add New Product</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 overflow-x-scroll">
<div class="p-6 text-gray-900 overflow-x-scroll">
<form @submit.prevent="submit" class="flex flex-col gap-4">
<div class="form-group">
<InputLabel for="category_id" value="Category" />
<SelectInput
id="category"
v-model="form.category_id"
:options="categories"
option-value="id"
option-label="name"
:default-option="{
id: '',
name: 'Product Category'
}"
:disabled="form.processing"
/>
<InputError :message="form.errors.category_id" />
</div>
 
<div class="form-group">
<InputLabel for="name" value="Name" />
<TextInput
id="name"
type="text"
v-model="form.name"
:disabled="form.processing"
/>
<InputError :message="form.errors.name" />
</div>
 
<div class="form-group">
<InputLabel for="price" value="Price" />
<TextInput
id="name"
type="number"
step="0.01"
v-model="form.price"
:disabled="form.processing"
/>
<InputError :message="form.errors.price" />
</div>
 
<div>
<PrimaryButton :disabled="form.processing"> Create New Product </PrimaryButton>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>

Visual result:


Save New Product

To store the result, we generate the Form Request class first:

php artisan make:request Vendor/StoreProductRequest

This is the content:

app/Http/Requests/Vendor/StoreProductRequest.php:

namespace App\Http\Requests\Vendor;
 
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
 
class StoreProductRequest extends FormRequest
{
public function authorize(): bool
{
return Gate::allows('product.create');
}
 
public function rules(): array
{
return [
'category_id' => ['required', 'exists:categories,id'],
'name' => ['required', 'string', 'max:255'],
'price' => ['required', 'numeric'],
];
}
 
public function prepareForValidation()
{
$this->merge([
'price' => (int) ($this->price * 100),
]);
}
}

As you can see, we're multiplying the price by 100 to save the data in cents in the database. You can read more about it in this tutorial: Dealing With Money in Laravel/PHP: Best Practices

And now, we can use that Form Request class in the Controller, save a new Product, and redirect back to the Menu:

app/Http/Controllers/Vendor/ProductController.php:

use App\Http\Requests\Vendor\StoreProductRequest;
use Illuminate\Http\RedirectResponse;
 
class ProductController extends Controller
{
// ...
 
public function store(StoreProductRequest $request): RedirectResponse
{
Product::create($request->validated());
 
return to_route('vendor.menu')
->withStatus('Product created successfully.');
}
}

Good, our new product is saved!


Edit Product

Let's add a button to Edit products to our list of products in the Menu component.

resources/js/Pages/Vendor/Menu.vue:

<div v-for="product in category.products" ...>
<div class="flex gap-4">
<Link
:href="route('vendor.products.edit', product)"
class="btn btn-secondary btn-sm"
>
Edit
</Link>
</div>
</div>

Then we fill in the edit() method of the Controller:

app/Http/Controllers/Vendor/ProductController.php:

use App\Models\Product;
 
class ProductController extends Controller
{
// ...
 
public function edit(Product $product)
{
return Inertia::render('Vendor/Products/Edit', [
'categories' => Category::get(['id', 'name']),
'product' => $product,
]);
}
}

Finally, the Edit component. It will be very similar to other Edit forms in the previous lessons.

resources/js/Pages/Vendor/Products/Edit.vue:

<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'
import { Head, useForm } from '@inertiajs/vue3'
import InputError from '@/Components/InputError.vue'
import InputLabel from '@/Components/InputLabel.vue'
import PrimaryButton from '@/Components/PrimaryButton.vue'
import TextInput from '@/Components/TextInput.vue'
import SelectInput from '@/Components/SelectInput.vue'
const props = defineProps({
product: {
type: Object
},
categories: {
type: Array
}
})
const form = useForm({
category_id: props.product.category_id,
name: props.product.name,
price: (props.product.price / 100).toFixed(2)
})
const submit = () => {
form.patch(route('vendor.products.update', props.product))
}
</script>
 
<template>
<Head :title="'Edit ' + product.name" />
 
<AuthenticatedLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ 'Edit ' + product.name }}
</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 overflow-x-scroll">
<div class="p-6 text-gray-900 overflow-x-scroll">
<form @submit.prevent="submit" class="flex flex-col gap-4">
<div class="form-group">
<InputLabel for="category_id" value="Category" />
<SelectInput
id="category"
v-model="form.category_id"
:options="categories"
option-value="id"
option-label="name"
:default-option="{
id: '',
name: 'Product Category'
}"
:disabled="form.processing"
/>
<InputError :message="form.errors.category_id" />
</div>
 
<div class="form-group">
<InputLabel for="name" value="Name" />
<TextInput
id="name"
type="text"
v-model="form.name"
:disabled="form.processing"
/>
<InputError :message="form.errors.name" />
</div>
 
<div class="form-group">
<InputLabel for="price" value="Price" />
<TextInput
id="name"
type="number"
step="0.01"
v-model="form.price"
:disabled="form.processing"
/>
<InputError :message="form.errors.price" />
</div>
 
<div>
<PrimaryButton :disabled="form.processing"> Update Product </PrimaryButton>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>

Now, to update the Product, we generate another validation Form Request class.

php artisan make:request Vendor/UpdateProductRequest

This is the content:

namespace App\Http\Requests\Vendor;
 
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
 
class UpdateProductRequest extends FormRequest
{
public function authorize(): bool
{
return Gate::allows('product.update');
}
 
public function rules(): array
{
return [
'category_id' => ['required', 'exists:categories,id'],
'name' => ['required', 'string', 'max:255'],
'price' => ['required', 'numeric'],
];
}
 
public function prepareForValidation()
{
$this->merge([
'price' => (int) ($this->price * 100),
]);
}
}

And now, we can use that class to update the Product and redirect back to the list:

app/Http/Controllers/Vendor/ProductController.php:

use App\Http\Requests\Vendor\UpdateProductRequest;
 
class ProductController extends Controller
{
// ...
 
public function update(UpdateProductRequest $request, Product $product)
{
$product->update($request->validated());
 
return to_route('vendor.menu')
->withStatus('Product updated successfully.');
}
}

Delete Products

The final step for Products CRUD is the Delete button. Let's add it:

resources/js/Pages/Vendor/Menu.vue:

<Link
:href="route('vendor.products.edit', product)"
class="btn btn-secondary btn-sm"
>
Edit
</Link>
<Link
:href="route('vendor.products.destroy', product)"
class="btn btn-danger btn-sm"
method="delete"
as="button"
>
Delete
</Link>

Finally, we fill in the code for the Controller's destroy() method.

app/Http/Controllers/Vendor/ProductController.php:

class ProductController extends Controller
{
// ...
 
public function destroy(Product $product)
{
$product->delete();
 
return to_route('vendor.menu')
->withStatus('Product deleted successfully.');
}
}

And that's it. Now we have complete both CRUDs for the restaurant menu!

Here's how it looks: