Laravel Ecommerce Tutorial: Part 5, Managing Brands
Given Ncube
Took me a while to make this tutorial (procrastination, I know, right?), but in the end I decided to just do it. Where do you get motivation to keep coding, sometimes I just don't feel like writing code, let me know your answers on Twitter @ncubegiven_.
Anyway...
In the previous tutorial, we added the ability to manage product categories and features like searching and filtering. Up to this point, we have added the ability to:
In almost every e-commerce site there's some concept of brands, for example, Nike, HP, or something like that. This will give the ability to group products by their brands, and that's what we'll implement in this post.
This is going to be very similar to the product categories we did in the last tutorial, just basic CRUD.
I know you're dying to write some code, so let's get started
We'll start by creating the model
php artisan make:model Brand -msf
This will create the model, a seeder, a migration, and a factory.
Let's define the migration.
Basically for a brand, we just need its name, a slug for friendly URLs, and probably the description, but it's not required
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::create('brands', static function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->string('slug')->unique();
$table->string('description')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::dropIfExists('brands');
}
};
And migrate the database
php artisan migrate
Next, let's configure our model. We will have to search this model with Laravel Scout, we'll configure that just like we did with categories in the previous post.
We also need the ability to generate slugs using Spatie Sluggable, we will configure that as well
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
use Spatie\Sluggable\HasSlug;
use Spatie\Sluggable\SlugOptions;
class Brand extends Model
{
use HasFactory;
use HasSlug;
use Searchable;
/**
* The attributes that should not be mass assignable
*
* @var array
*/
protected $guarded = [];
/**
* @return SlugOptions
*/
public function getSlugOptions(): SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('name')
->saveSlugsTo('slug');
}
/**
* Get the indexable data array for the model.
*
* @return array
*/
public function toSearchableArray(): array
{
return [
'name' => $this->name,
'description' => $this->description,
];
}
/**
* Scope a query to only include listings that are returned by scout
*
* @param Builder $query
* @param string $search
* @return Builder
*/
public function scopeWhereScout(Builder $query, string $search): Builder
{
return $query->whereIn(
'id',
self::search($search)
->get()
->pluck('id'),
);
}
}
and perhaps also configure the factory
<?php
namespace Database\Factories;
use App\Models\Brand;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Brand>
*/
class BrandFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => $this->faker->company(),
'description' => $this->faker->paragraphs(asText: true),
];
}
}
We also need to generate the authorization policies and permissions for this model
php artisan authorizer:policies:generate -m Brand --force
php artisan authorizer:permissions:generate -m Brand
Next, let's the create the controller to handle the brands
php artisan make:controller Admin\\BrandController --model=Brand --test -R
Add this to the constructor to authorize the controller
/**
* Create a new controller instance
*/
public function __construct()
{
$this->authorizeResource(Brand::class, 'brand');
}
Create the views for this resource
php artisan make:view admin.brands.create
php artisan make:view admin.brands.index
php artisan make:view admin.brands.edit
And then register the resource route inside the admin group
Route::resource('brands', BrandController::class);
This is the part where you start to write the tests for the controller, I've already written mine separately, but I won't be sharing them in this tutorial because that would make this post really long
So, first, we need the ability to create a brand, let's implement the create action of the brand controller and simply return the view with the form to create a brand
/**
* Show the form for creating a new resource.
*
* @return Renderable
*/
public function create(): Renderable
{
return view('admin.brands.create');
}
Add the following to the admin.brands.create
view to display the form to create a new brand
@extends('layouts.app')
@section('title')
Create Brand
@endsection
@section('content')
<section class="section">
<div class="section-header">
<h1>Brands</h1>
<div class="section-header-breadcrumb breadcrumb">
<div class="breadcrumb-item active"><a href="{{ route('admin.home.index') }}">Dashboard</a></div>
<div class="breadcrumb-item"><a href="{{ route('admin.brands.index') }}">Brands</a></div>
<div class="breadcrumb-item">Create Brand</div>
</div>
</div>
<div class="section-body">
<h2 class="section-title">Create Brand</h2>
<p class="section-lead mb-5">On this page you can create brand for your products.</p>
<form method="post"
action="{{ route('admin.brands.store') }}">
@csrf
<div class="row">
<div class="col-12 col-md-6 col-lg-6">
<p class="section-lead">Add basic information about the brand.</p>
</div>
<div class="col-12 col-md-6 col-lg-6">
<div class="card">
<div class="card-header">
<h4>Brand details</h4>
</div>
<div class="card-body">
<div class="form-group">
<label for="name">Name</label>
<input type="text"
name="name"
id="name"
class="form-control @error('name') is-invalid @enderror"
value="{{ old('name') }}">
@error('name')
<span class="invalid-feedback">
{{ $message }}
</span>
@enderror
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea name="description"
id="description"
rows="8"
class="form-control @error('description') is-invalid @enderror ">{{ old('description') }}</textarea>
@error('description')
<span class="invalid-feedback">
{{ $message }}
</span>
@enderror
</div>
<div class="form-group text-right">
<button type="submit"
class="btn btn-primary btn-lg">Create Brand</button>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</section>
@endsection
If you visit /admin/brands/create
you should be able to see a form to create a new brand.
However, submitting the form won't do anything, and that's what we'll implement next.
First, let's define the StoreBrandRequest
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreBrandRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return $this->user()->can('create brand');
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'name' => 'required|string|unique:brands|max:255',
'description' => 'sometimes|nullable|string',
];
}
}
The authorize method makes sure that only users with the permission to create brand
can submit this form.
Next, let's define the controller action to store the brand in the database
/**
* Store a newly created resource in storage.
*
* @param StoreBrandRequest $request
* @return RedirectResponse
*/
public function store(StoreBrandRequest $request): RedirectResponse
{
Brand::create($request->validated());
return to_route('admin.brands.index')->with(
'success',
'Brand was successfully created',
);
}
Now, if we submit the form, we should be able to create a new brand, but the action redirects to the brands index page and it's currently empty, let's tell Laravel to return the brands index view on this route.
While we're here we'll use spatie laravel query builder to add the ability to search for brands
/**
* Display a listing of the resource.
*
* @param Request $request
* @return Renderable
*/
public function index(Request $request): Renderable
{
$brands = QueryBuilder::for(Brand::class)
->allowedFilters([AllowedFilter::scope('search', 'whereScout')])
->paginate()
->appends($request->query());
return view('admin.brands.index', [
'brands' => $brands,
]);
}
In the admin.brands.index
view
@extends('layouts.app')
@section('title')
Brands
@endsection
@section('content')
<section class="section">
<div class="section-header">
<h1>Brands</h1>
<div class="section-header-button">
<a href="{{ route('admin.brands.create') }}"
class="btn btn-primary">Create Brand</a>
</div>
<div class="section-header-breadcrumb breadcrumb">
<div class="breadcrumb-item"><a href="{{ route('admin.home.index') }}">Dashboard</a></div>
<div class="breadcrumb-item"><a href="{{ route('admin.brands.index') }}">Brands</a></div>
<div class="breadcrumb-item active">All Brands</div>
</div>
</div>
<div class="section-body">
<h2 class="section-title">Brand</h2>
<p class="section-lead">
You can manage brands, such as editing, deleting and more.
</p>
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h4>Brands</h4>
</div>
<div class="card-body">
<div class="float-end">
<form>
<div class="d-flex">
<input type="text"
class="form-control w-full"
placeholder="Search"
{{ stimulus_controller('filter', [
'route' => 'admin.brands.index',
'filter' => 'search',
]) }}
{{ stimulus_action('filter', 'change', 'input') }}>
</div>
</form>
</div>
<div class="clearfix mb-3"></div>
<turbo-frame class='w-full'
id='categories'
target="_top"
{{ stimulus_controller('reload') }}
{{ stimulus_actions([
[
'reload' => ['filterChange', 'filter:change@document'],
],
[
'reload' => ['sortChange', 'sort:change@document'],
],
]) }}>
<div class="table-responsive">
<table class="table table-borderless">
<tr>
<th>Name</th>
<th>Description</th>
<th>Created At</th>
</tr>
@foreach ($brands as $brand)
<tr>
<td
{{ stimulus_controller('obliterate', ['url' => route('admin.brands.destroy', $brand)]) }}>
{{ Str::title($brand->name) }}
<div class="table-links">
<a class="btn btn-link"
href="{{ route('admin.brands.edit', $brand) }}">Edit</a>
<div class="bullet"></div>
<button {{ stimulus_action('obliterate', 'handle') }}
class="btn btn-link text-danger">Trash</button>
<form {{ stimulus_target('obliterate', 'form') }}
method="POST"
action="{{ route('admin.brands.destroy', $brand) }}">
@csrf
@method('DELETE')
</form>
</div>
</td>
<td>
{!! Str::limit($brand->description, 90) !!}
</td>
<td>{{ $brand->created_at->diffForHumans() }}</td>
</tr>
@endforeach
</table>
</div>
<div class="float-right">
<nav>
{{ $brands->links('vendor.pagination.bootstrap-5') }}
</nav>
</div>
</turbo-frame>
</div>
</div>
</div>
</div>
</div>
</section>
@endsection
Now if you visit /admin/brands
you should be able to see all the brands and be able to perform a search. Let's add the link to sidebar so we can easily navigate to brands
In the layouts.partials.sidebar
just below the categories link, add this
<li class="nav-item @if (Route::is('admin.brands.*')) active @endif">
<a href="{{ route('admin.brands.index') }}"
class="nav-link">
<i class="fas fa-hashtag"></i> <span>Brands</span>
</a>
</li>
At this point, we can create, list brands, let's add the ability to edit them.
To do that, let's edit the edit
action to return the edit view
/**
* Show the form for editing the specified resource.
*
* @param Brand $brand
* @return Renderable
*/
public function edit(Brand $brand): Renderable
{
return view('admin.brands.edit', [
'brand' => $brand,
]);
}
And the in the admin.brands.edit
let's add the following
@extends('layouts.app')
@section('title')
Edit Brand
@endsection
@section('content')
<section class="section">
<div class="section-header">
<h1>Brands</h1>
<div class="section-header-breadcrumb breadcrumb">
<div class="breadcrumb-item active"><a href="{{ route('admin.home.index') }}">Dashboard</a></div>
<div class="breadcrumb-item"><a href="{{ route('admin.brands.index') }}">Brands</a></div>
<div class="breadcrumb-item">Edit Brand</div>
</div>
</div>
<div class="section-body">
<h2 class="section-title">Edit Brand</h2>
<p class="section-lead mb-5">On this page you can edit the brand.</p>
<form method="post"
action="{{ route('admin.brands.update', $brand) }}">
@csrf
@method('PATCH')
<div class="row">
<div class="col-12 col-md-6 col-lg-6">
<p class="section-lead">Add basic information about the brand.</p>
</div>
<div class="col-12 col-md-6 col-lg-6">
<div class="card">
<div class="card-header">
<h4>Brand details</h4>
</div>
<div class="card-body">
<div class="form-group">
<label for="name">Name</label>
<input type="text"
name="name"
id="name"
class="form-control @error('name') is-invalid @enderror"
value="{{ old('name', $brand->name) }}">
@error('name')
<span class="invalid-feedback">
{{ $message }}
</span>
@enderror
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea name="description"
id="description"
rows="8"
class="form-control @error('description') is-invalid @enderror ">{{ old('description', $brand->description) }}</textarea>
@error('description')
<span class="invalid-feedback">
{{ $message }}
</span>
@enderror
</div>
<div class="form-group text-right">
<button type="submit"
class="btn btn-primary btn-lg">Update Brand</button>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</section>
@endsection
For the form submission to work, let's first define the UpdateBrandRequest
with some validations
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateBrandRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return $this->user()->can('update brand', $this->route('brand'));
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'name' => [
'required',
'string',
'max:255',
Rule::unique('brands')->ignore($this->route('brand')->id),
],
'description' => 'sometimes|nullable|string',
];
}
}
Finally, let's store the update the database
/**
* Update the specified resource in storage.
*
* @param UpdateBrandRequest $request
* @param Brand $brand
* @return RedirectResponse
*/
public function update(UpdateBrandRequest $request, Brand $brand): RedirectResponse
{
$brand->update($request->validated());
return to_route('admin.brands.index')->with(
'success',
'Brand was successfully updated',
);
}
Let's wrap this up by adding the ability to delete brands, we have already added the links in the view, let's simply implement the destroy action in the controller
/**
* Remove the specified resource from storage.
*
* @param Brand $brand
* @return RedirectResponse
*/
public function destroy(Brand $brand): RedirectResponse
{
$brand->delete();
return to_route('admin.brands.index')->with(
'success',
'Brand was successfully deleted',
);
}
With that, we should be able to fully manage brands in this ecommerce store, we added the ability to
- create brands
- list all created brands
- search available brands using scout
- edit brands
- delete brands
- of course authorize all this with spatie permissions and authorizer
We now have everything we need to add products in the store. In the upcoming tutorial, we will allow users to create products and assign them to different categories and brands.
In the meantime, make sure to subscribe to the newsletter and get notified when the next post is up.
[convertkit=2542481]