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.
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:
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!
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.'); }}
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: