Laravel Ecommerce Tutorial: Part 6.3, Editing Products
Given Ncube
In the last post, we added the ability to list all products, search with Scout and Spatie Query Builder, the ability to delete products. This part 6.3 of the on building an ecommerce website in Laravel from start to deployment.
In this post we will continue to add the ability to edit products.
Let's dive in
Head over to Admin\ProductController
and edit the edit
action and tell laravel to return the edit page
/**
* Show the form for editing the specified resource.
*
* @param Product $product
* @return Renderable
*/
public function edit(Product $product)
{
$categories = Category::all();
return view('admin.products.edit', [
'product' => $product,
'categories' => $categories
]);
}
Then let's edit the admin.products.edit
view, the markup will be the same as the create
view except this time we pre populate the fields and submit the form to the update action
Add the following snippet to your edit view
@extends('layouts.app')
@section('title')
Edit Product
@endsection
@section('content')
<section class="section">
<div class="section-header">
<div class="section-header-back">
<a href="{{ route('admin.products.index') }}"
class="btn btn-icon"><i class="fas fa-arrow-left"></i></a>
</div>
<h1>
Edit Product
</h1>
<div class="section-header-breadcrumb">
<div class="breadcrumb-item active"><a href="#">Dashboard</a></div>
<div class="breadcrumb-item"><a href="{{ route('admin.products.index') }}">Products</a></div>
<div class="breadcrumb-item">
Edit Product
</div>
</div>
</div>
<div class="section-body">
<h2 class="section-title">
Edit Product
</h2>
<p class="section-lead">
On this page you can edit a product and fill in all fields.
</p>
<div class="container">
<div class="row">
<div class="col-12 col-md-7">
<div class="card rounded-lg">
<div class="card-header">
<h4>Basic Info</h4>
</div>
<div class="card-body">
<form class=""
action="{{ route('admin.products.update', $product) }}"
method="post"
id="storeProduct">
@csrf
@method('PATCH')
<div class="form-group mb-3">
<label class="col-form-label"
for='name'>Name</label>
<input type="text"
name="name"
id='name'
class="form-control @error('name') is-invalid @enderror"
value="{{ old('name', $product->name) }}">
@error('name')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
<div class="form-group mb-3">
<label for='description'
class="col-form-label">
Description
</label>
<textarea name="description"
id='description'
rows="8"
cols="80"
{{ stimulus_controller('ckeditor') }}>{{ old('description', $product->description) }}</textarea>
@error('description')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
</form>
</div>
</div>
<div class="card rounded-lg">
<div class="card-header">
<h4>Media</h4>
</div>
<div class="card-body">
<div class="form-group"
data-controller="filepond"
data-filepond-process-value="{{ route('admin.images.store') }}"
data-filepond-restore-value="{{ route('admin.images.show') }}"
data-filepond-revert-value="{{ route('admin.images.destroy') }}"
data-filepond-current-value="{{ $product->fetchAllMedia()->push(...old('images', [])) }}">
<input type="file"
data-filepond-target="input">
<template data-filepond-target="template">
<input data-filepond-target="upload"
type="hidden"
name="NAME"
form="storeProduct"
value="VALUE">
</template>
</div>
</div>
</div>
<div class="card rounded-lg">
<div class="card-header">
<h4>Pricing</h4>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="form-group col-md-6">
<label class="form-label"
for='price'>Price</label>
<div class="input-group">
<div class="input-group-text">
$
</div>
<input form="storeProduct"
type="text"
name="price"
id='price'
placeholder="0.00"
class="form-control @error('price') is-invalid @enderror"
value="{{ old('price', $product->price) }}">
@error('price')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
</div>
<div class="form-group col-md-6">
<label class="form-label"
for='discounted_price'>Discounted price</label>
<div class="input-group">
<div class="input-group-text">
$
</div>
<input form="storeProduct"
type="text"
name="discounted_price"
id='discounted_price'
placeholder="0.00"
class="form-control @error('compare_price') is-invalid @enderror"
value="{{ old('compare_price', $product->discounted_price) }}">
@error('compare_price')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
</div>
</div>
<div class="form-group">
<label class="form-label"
for='cost'>Cost per item</label>
<div class="input-group">
<div class="input-group-text">
$
</div>
<input form="storeProduct"
type="text"
name="cost"
id='cost'
placeholder="0.00"
class="form-control @error('cost') is-invalid @enderror"
value="{{ old('cost', $product->cost) }}">
@error('cost')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
<span class="text-sm text-secondary d-block mt-2">Customers won't see this</span>
</div>
</div>
</div>
<div class="card rounded-lg"
data-controller="inventory">
<div class="card-header">
<h4>Inventory</h4>
</div>
<div class="card-body">
<div class="form-group">
<label class="form-label"
for='sku'>SKU</label>
<input id='sku'
form="storeProduct"
type="text"
name="sku"
class="form-control @error('sku') is-invalid @enderror"
value="{{ old('sku', $product->sku) }}">
@error('sku')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
<div class="form-group mb-3">
<label class="custom-switch pl-0">
<input form="storeProduct"
checked
type="checkbox"
@checked(old('track_quantity', $product->track_quantity))
name="track_quantity"
data-action="input->inventory#toggle"
class="custom-switch-input @error('track_quantity')
is-invalid
@enderror">
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description">Track quantity</span>
</label>
@error('track_quantity')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
<div class="form-group"
data-inventory-target="checkbox">
<label class="custom-switch pl-0">
<input form="storeProduct"
@checked(old('sell_out_of_stock', $product->sell_out_of_stock))
type="checkbox"
name="sell_out_of_stock"
class="custom-switch-input @error('sell_out_of_stock')
is-invalid
@enderror">
<span class="custom-switch-indicator"></span>
<span class="custom-switch-description">Continue selling when out of stock</span>
</label>
@error('sell_out_of_stock')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
<div class="form-group"
data-inventory-target="quantity">
<label class="form-label"
for='quantity'>Quantity</label>
<input form="storeProduct"
type="text"
name="quantity"
id='quantity'
class="form-control @error('quantity') is-invalid @enderror"
value="{{ old('quantity', $product->quantity) }}">
@error('quantity')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
</div>
</div>
<div class="card rounded-lg">
<div class="card-header">
<h4>Variants</h4>
</div>
<div class="card-body">
{{-- Product variation fields here, color and stuff --}}
</div>
</div>
<div class="card rounded-lg">
<div class="card-header">
<h4>Search Engine Optimization</h4>
</div>
<div class="card-body">
{{-- SEO Fields here --}}
</div>
</div>
</div>
<div class="col-12 col-md-5">
<div class="card rounded-lg">
<div class="card-header">
<h4>Product status</h4>
</div>
<div class="card-body">
<div class="form-group">
<select form="storeProduct"
name="status"
class="form-select @error('status') is-invalid @enderror">
<option value="draft"
@selected(old('status', $product->status) == 'draft')>Draft</option>
<option value="review"
@selected(old('status', $product->status) == 'review')>Review</option>
<option value="active"
@selected(old('status', $product->status) == 'active')>Active</option>
</select>
@error('status')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
</div>
</div>
<div class="card rounded-lg">
<div class="card-header">
<h4>Product organization</h4>
</div>
<div class="card-body">
<div class="form-group">
<label class="form-label"
for='category_id'>Category</label>
<select form="storeProduct"
name="category_id"
id='category_id'
class="form-select @error('category_id') is-invalid @enderror">
@foreach ($categories as $category)
@if ($category->id == old('category_id', $product->category->id) || strtolower($category->name) == 'default')
<option selected
value="{{ $category->id }}">{{ $category->name }}</option>
@else
<option value="{{ $category->id }}">{{ $category->name }}</option>
@endif
@endforeach
</select>
@error('category_id')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="form-group text-right">
<input type="submit"
class="btn btn-primary btn-lg"
value="Save"
form="storeProduct">
</div>
</div>
</div>
</div>
</div>
</section>
@endsection
Next, we edit the UpdateProductRequest
to add some validation and authorizations to the request before it reaches the controller
Edit the UpdateProductRequest
to the following
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateProductRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return $this->user()->can('update', $this->route('product'));
}
/**
* Prepare input for validation
*
* @return void
*/
protected function prepareForValidation(): void
{
$this->merge([
'track_quantity' =>
$this->has('track_quantity') &&
$this->input('track_quantity') == 'on',
'sell_out_of_stock' =>
$this->has('sell_out_of_stock') &&
$this->input('sell_out_of_stock') == 'on',
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'description' => 'required|string',
'sku' =>
'sometimes|nullable|string|unique:products,sku,' .
$this->route('product')->id,
'track_quantity' => 'sometimes|nullable|boolean',
'quantity' => 'required_if:track_quantity,true|nullable|int',
'sell_out_of_stock' => 'required_if:track_quantity,true|boolean',
'category_id' => 'required|int|exists:categories,id',
'price' => 'required|numeric|min:0',
'cost' => 'sometimes|nullable|numeric',
'discounted_price' => 'sometimes|nullable|numeric',
'status' => 'required|string|in:active,draft,review',
'images' => 'sometimes|nullable|array',
'images.*' => 'string',
];
}
}
So on the authorize method we check if the user authorized to update products, we don't anyone just products if they are not authorized.
The prepareForValidation
method simply turns the input checks ("on") into real booleans and then your everyday validation on the rules method
If validation passes let's make the actual updating in the Admin\ProdocutController
.
Edit the update
method and put the following snippet
/**
* Update the specified resource in storage.
*
* @param UpdateProductRequest $request
* @param Product $product
* @return RedirectResponse
* @throws Exception
*/
public function update(UpdateProductRequest $request, Product $product)
{
$product->update($request->safe()->except(['images']));
collect($request->validated('images'))->each(function ($image) use (
$product,
) {
$product->attachMedia(new File(storage_path('app/' . $image)));
Storage::delete($image);
});
return to_route('admin.products.index')->with(
'success',
'Product was successfully updated',
);
}
First we get the validated fields from the request except the images key and pass that to the update method of the product model
If there were images uploaded with this request we loop through all of them and then attach them to this product
Lastly, we return the user to the products index page with a toast message.
This is all we need to be able to edit products and in the upcoming tutorials we will add the ability to create product variations such as color, size etc
As I was using the products section of the ecommerce website I noticed a few issues if a user doesn't include optional fields which we will rectify in the next post
To make sure you don't miss the next post in this series subscribe to the newsletter and get notified when it comes out
Like before, Happy coding!