Given Ncube
Posted on August 10, 2023
I know in the last post I promised in this post we will be building the home page of our e commerce site but I realized we need to give the user the ability to configure things like site name, socials, currency code, etc.
So in this post we will be using a package, spatie/laravel-settings to store settings in the database retrieve them later to perform custom logic based on the configured value.
To begin let's install the package with composer
composer require spatie/laravel-settings
Publish the assets and migrate the database
http://192.168.1.1/pub/login.htm php artisan vendor:publish --provider="Spatie\LaravelSettings\LaravelSettingsServiceProvider" --tag="migrations"
php artisan migrate
So here's what we're going to build,
- A settings index page, showing links to specific settings categories
- General Settings page for, well, general settings like store name, address and what not
- Social Settings page for social media links
- Legal Settings to add things like terms of service and privacy policy, return policy
- Misc settings for settings we won't be able to fit into these categories
All Settings
Let's start by creating a controller
php artisan make:controller Admin\\SettingsController --test
This controller will just have an index action that http://192.168.1.1/pub/login.htmjust shows the settings index page with links to other settings
Open the newly created controller and add the following snippet
<?php
namespace App\Http\Controllers;
use Illuminate\Contracts\Support\Renderable;
use Illuminate\Http\Request;
class SettingsController extends Controller
{
/**
* The settings index page
*
* @return Renderable
*/
public function index(): Renderable
{
return view('admin.settings.index');
}
}
Notice we don't have the admin.settings.index
view yet, let's add that by running the following artisan command
php artisan make:view admin.settings.index -e layouts.app
Open the newly created view and paste the following snippet
@extends('layouts.app')
@section('title')
Settings
@endsection
@section('content')
<section class="section">
<div class="section-header">
<h1>Settings</h1>
<div class="section-header-breadcrumb">
<div class="breadcrumb-item active"><a href="#">Dashboard</a></div>
<div class="breadcrumb-item">Settings</div>
</div>
</div>
<div class="section-body">
<h2 class="section-title">Overview</h2>
<p class="section-lead">
Organize and adjust all settings about this site.
</p>
<div class="container">
<div class="row">
<div class="col-lg-6">
<div class="card card-large-icons">
<div class="card-icon bg-primary text-white">
<i class="fas fa-cog"></i>
</div>
<div class="card-body">
<h4>General</h4>
<p>General settings such as, site title, site description, address and so on.</p>
<a href="#" class="card-cta">Change Setting <i class="fas fa-chevron-right"></i></a>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card card-large-icons">
<div class="card-icon bg-primary text-white">
<i class="fas fa-search"></i>
</div>
<div class="card-body">
<h4>Legal</h4>
<p>Legal settings such as, terms of service and privacy policy.</p>
<a href="#" class="card-cta">Change Setting <i class="fas fa-chevron-right"></i></a>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card card-large-icons">
<div class="card-icon bg-primary text-white">
<i class="fas fa-mobile"></i>
</div>
<div class="card-body">
<h4>Social</h4>
<p>Social networks settings, your Twitter, Facebook and IG handles.</p>
<a href="#" class="card-cta">Change Setting <i class="fas fa-chevron-right"></i></a>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card card-large-icons">
<div class="card-icon bg-primary text-white">
<i class="fas fa-stopwatch"></i>
</div>
<div class="card-body">
<h4>Misc</h4>
<p>Settings about miscellaneous things you might prefer.</p>
<a href="#" class="card-cta text-primary">Change Setting <i class="fas fa-chevron-right"></i></a>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
@endsection
We will replace the "#" links with routes to respective settings when we create them.
Lastly, let's register the route in the admin group
Route::get('/settings', [SettingsController::class, 'index'])->name(
'settings.index',
);
Now if you navigate to that route in your browser your settings index page should look something like the image below
Let's add the link to settings on the sidebar, add this snippet to layouts.partials.sidebar
<li class="nav-item @if (Route::is('admin.settings.*')) active @endif">
<a href='{{ route('admin.settings.index') }}' class='nav-link'>
<i class='fas fa-wrench'></i> <span>Settings</span>
</a>
</li>
Let's move on to general settings
General Settings
Let's start by giving the user the ability to manage general settings, create the settings class by running the following artisan command
php artisan make:setting GeneralSettings --group=general
Open the recently created setting in App\Settings\GeneralSettings
and add the following code
<?php
namespace App\Settings;
use Spatie\LaravelSettings\Settings;
class GeneralSettings extends Settings
{
/**
* @var string The name of the site
*/
public string $site_name;
/**
* @var string The contact email of the site
*/
public string $contact_email;
/**
* @var string The sender email of the site
*/
public string $sender_email;
/**
* @var string The legal name of the site
*/
public string $legal_name;
/**
* @var string The phone number of the site
*/
public string $phone;
/**
* @var string The address of the site
*/
public string $address;
/**
* @var string The city of the site
*/
public string $city;
/**
* @var string The country of the site
*/
public string $country;
/**
* @var string The currency of the site
*/
public string $currency;
/**
* @var string The currency symbol of the site
*/
public string $currency_symbol;
/**
* @var string The analytics code of the site
*/
public string $google_analytics_code;
/**
* @var bool The status of the site
*/
public bool $active;
public static function group(): string
{
return 'general';
}
}
Now generate the migration for the settings with
php artisan make:settings-migration CreateGeneralSettings
Open the recently created migration and add the following code
<?php
use Spatie\LaravelSettings\Migrations\SettingsMigration;
return new class extends SettingsMigration
{
public function up(): void
{
$this->migrator->add('general.site_name', 'Site Name');
$this->migrator->add('general.contact_email', 'please@change.me');
$this->migrator->add('general.sender_email', 'please@change.me');
$this->migrator->add('general.phone', '077 526 2092');
$this->migrator->add('general.legal_name', 'Your Business Name');
$this->migrator->add('general.address', 'Street Address');
$this->migrator->add('general.city', 'Harare');
$this->migrator->add('general.country', 'Zimbabwe');
$this->migrator->add('general.currency', 'USD');
$this->migrator->add('general.currency_symbol', '$');
$this->migrator->add('general.google_analytics_code', '');
$this->migrator->add('general.active', true);
}
};
Run the migration
php artisan migrate
We will need a controller for general settings
php artisan make:controller Admin\\GeneralSettingsController
We will also need a form request to update the settings so let's generate that using the command below
php artisan make:request UpdateGeneralSettingsRequest
Add the following code to it
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\UpdateGeneralSettingsRequest;
use App\Settings\GeneralSettings;
use Illuminate\Contracts\Support\Renderable;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class GeneralSettingsController extends Controller
{
/**
* Display the general settings page
*
* @param GeneralSettings $settings
* @return Renderable
*/
public function show(GeneralSettings $settings): Renderable
{
return view('admin.settings.general', [
'settings' => $settings
]);
}
/**
* Update the general settings
*
* @param UpdateGeneralSettingsRequest $request
* @param GeneralSettings $settings
* @return RedirectResponse
*/
public function update(UpdateGeneralSettingsRequest $request, GeneralSettings $settings)
{
$settings->fill($request->validated());
$settings->save();
return redirect()->route('admin.settings.general.show')
->with('success', 'You have successfully updated your settings');
}
}
Let's update the form request to validate the settings update
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Validator;
class UpdateGeneralSettingsRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'site_name' => 'required|string|max:255',
'legal_name' => 'required|string|max:255',
'contact_email' => 'required|string|email|max:255',
'sender_email' => 'required|string|email|max:255',
'phone' => 'required|numeric|min:10',
'address' => 'required|string|max:255',
'city' => 'required|string|max:255',
'country' => 'required|string|max:255',
'currency' => 'required|string|max:3',
'currency_symbol' => 'required|string|max:2',
'google_analytics_code' => 'sometimes|nullable|string',
];
}
/**
* Configure the validator instance.
*
* @param Validator $validator
* @return void
*/
public function withValidator(Validator $validator): void
{
$validator->after(function ($validator) {
$validator->setData(
collect($validator->getData())
->filter(fn ($value) => $value)
->all()
);
});
}
}
Now let's add the view
php artisan make:view admin.settings.general -e layouts.app
Then add the following blade snippet
@extends('layouts.app')
@section('title')
General Settings
@endsection
@section('content')
<section class="section">
<div class="section-header">
<div class="section-header-back">
<a href="{{ route('admin.settings.index') }}" class="btn btn-icon"><i class="fas fa-arrow-left"></i></a>
</div>
<h1>General Settings</h1>
<div class="section-header-breadcrumb">
<div class="breadcrumb-item active"><a href="#">Dashboard</a></div>
<div class="breadcrumb-item active"><a href="#">Settings</a></div>
<div class="breadcrumb-item">General Settings</div>
</div>
</div>
<div class="section-body">
<h2 class="section-title">All About General Settings</h2>
<p class="section-lead">
You can adjust all general settings here
</p>
<div id="output-status"></div>
<div class="container">
<div class="row">
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h4>Jump To</h4>
</div>
<div class="card-body">
<ul class="nav nav-pills flex-column">
<li class="nav-item"><a href="{{ route('admin.settings.general.show') }}" class="nav-link active">General</a></li>
<li class="nav-item"><a href="#" class="nav-link">Legal</a></li>
<li class="nav-item"><a href="#" class="nav-link">Social</a></li>
<li class="nav-item"><a href="#" class="nav-link">Misc</a></li>
</ul>
</div>
</div>
</div>
<div class="col-md-8">
<form id="setting-form" action="{{ route('admin.settings.general.update') }}" method="POST">
@method('PATCH')
<div class="card" id="settings-card">
<div class="card-header">
<h4>General Settings</h4>
</div>
<div class="card-body">
<p class="text-muted">General settings such as, site title, site description, address
and so on.</p>
<div class="form-group row align-items-center">
<label for="site-title" class="form-control-label col-sm-3 text-md-right">Site
Name</label>
<div class="col-sm-6 col-md-9">
<input type="text" name="site_name" class="form-control @error('site_name') is-invalid @enderror" id="site-title" value="{{ old('site_name', $settings->site_name) }}">
@error('site_name')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
</div>
<div class="form-group row align-items-center">
<label for="site-title" class="form-control-label col-sm-3 text-md-right">Contact Email</label>
<div class="col-sm-6 col-md-9">
<input type="text" name="contact_email" class="form-control @error('contact_email') is-invalid @enderror" id="site-title" value="{{ old('contact_email', $settings->contact_email) }}">
@error('contact_email')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
</div>
<div class="form-group row align-items-center">
<label for="site-title" class="form-control-label col-sm-3 text-md-right">Sender Email</label>
<div class="col-sm-6 col-md-9">
<input type="text" name="sender_email" class="form-control @error('sender_email') is-invalid @enderror" id="site-title" value="{{ old('sender_email', $settings->sender_email) }}">
@error('sender_email')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
</div>
<div class="form-group row align-items-center">
<label for="site-title" class="form-control-label col-sm-3 text-md-right">Legal Business Name</label>
<div class="col-sm-6 col-md-9">
<input type="text" name="legal_name" class="form-control @error('legal_name') is-invalid @enderror" id="site-title" value="{{ old('legal_name', $settings->legal_name) }}">
@error('legal_name')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
</div>
<div class="form-group row align-items-center">
<label for="site-title" class="form-control-label col-sm-3 text-md-right">Phone</label>
<div class="col-sm-6 col-md-9">
<input type="text" name="phone" class="form-control phone-number @error('phone') is-invalid @enderror" id="site-title" value="{{ old('phone', $settings->phone) }}">
@error('phone')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
</div>
<div class="form-group row align-items-center">
<label for="site-title" class="form-control-label col-sm-3 text-md-right">Address</label>
<div class="col-sm-6 col-md-9">
<input type="text" name="address" class="form-control @error('address') is-invalid @enderror" value="{{ old('address', $settings->address) }}">
@error('address')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
</div>
<div class="form-group row align-items-center">
<label for="site-title" class="form-control-label col-sm-3 text-md-right">Legal Business Name</label>
<div class="col-sm-6 col-md-9">
<input type="text" name="city" class="form-control @error('city') is-invalid @enderror" value="{{ old('city', $settings->city) }}">
@error('city')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
</div>
<div class="form-group row align-items-center">
<label for="site-title" class="form-control-label col-sm-3 text-md-right">Legal Business Name</label>
<div class="col-sm-6 col-md-9">
<input type="text" name="country" class="form-control @error('country') is-invalid @enderror" id="site-title" value="{{ old('country', $settings->country) }}">
@error('country')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
</div>
<div class="form-group row align-items-center">
<label for="site-title" class="form-control-label col-sm-3 text-md-right">Currency</label>
<div class="col-sm-6 col-md-9">
<input type="text" name="currency" class="form-control @error('currency') is-invalid @enderror" value="{{ old('currency', $settings->currency) }}">
@error('currency')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
</div>
<div class="form-group row align-items-center">
<label for="site-title" class="form-control-label col-sm-3 text-md-right">Currency Symbol</label>
<div class="col-sm-6 col-md-9">
<input type="text" name="currency_symbol" class="form-control @error('currency_symbol') is-invalid @enderror" id="site-title" value="{{ old('currency_symbol', $settings->currency_symbol) }}">
@error('currency_symbol')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
</div>
<div class="form-group row">
<label class="form-control-label col-sm-3 mt-3 text-md-right">Google Analytics
Code</label>
<div class="col-sm-6 col-md-9">
<textarea class="form-control codeeditor codemirror @error('google_analytics_code') is-invalid @enderror" name="google_analytics_code">{{ old('google_analytics_code', $settings?->google_analytics_code) }}</textarea>
@error('google_analytics_code')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
</div>
</div>
<div class="card-footer bg-whitesmoke text-md-right">
<button type="submit" class="btn btn-primary" id="save-btn">Save Changes</button>
<a href="#" class="btn btn-danger" type="button">Reset to default</a>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</section>
@endsection
Next let's create the routes to show and update the settings
Route::get('/settings/general', [GeneralSettingsController::class, 'show'])->name(
'settings.general.show',
);
Route::patch('/settings/general', [GeneralSettingsController::class, 'update'])->name(
'settings.general.update',
);
Finally add that link to the settings index page to allow easy navigation. Find the div containing the general settings dummy link and replace with a route to the settings show page
<div class="card-body">
<h4>General</h4>
<p>General settings such as, site title, site description, address and so on.</p>
<a href="{{ route('admin.settings.general.show') }}" class="card-cta">Change Setting <i class="fas fa-chevron-right"></i></a>
</div>
Your general settings page should like the image below
Let's move on to
Legal Settings
In every online store there's terms of service, privacy policies, refund/return policies and these will be drafted by a legal department of sorts and they can't call a developer everytime they change something in the terms.
Let's give them the ability to set this in the settings and as a bonus provide templates (copied from shopify)
Like with the general settings let's make the setting class
php artisan make:setting LegalSettings --group=general
Open the newly created setting class and add the following code
<?php
namespace App\Settings;
use Spatie\LaravelSettings\Settings;
class LegalSettings extends Settings
{
public string $refund_policy;
public string $privacy_policy;
public string $terms_of_service;
public static function group(): string
{
return 'general';
}
}
Next, let's generate the database migration for this setting
php artisan make:settings-migration CreateLegalSettings
Open the newly created migration and define the migration as follows
<?php
use Spatie\LaravelSettings\Migrations\SettingsMigration;
return new class extends SettingsMigration
{
public function up(): void
{
$this->migrator->add('legal.refund_policy', '');
$this->migrator->add('legal.privacy_policy', '');
$this->migrator->add('legal.terms_of_service', '');
}
};
Of course migrate the database
php artisan migrate
We need a controller to view and update this setting let's generate it with
php artisan make:controller Admin\\LegalSettingsController --test
Open the controller and populate it with
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\UpdateLegalSettingsRequest;
use App\Settings\LegalSettings;
use Illuminate\Contracts\Support\Renderable;
use Illuminate\Http\RedirectResponse;
class LegalSettingsController extends Controller
{
/**
* Display a listing of the resource.
*
* @param LegalSettings $settings
* @return Renderable
*/
public function index(LegalSettings $settings): Renderable
{
return view('admin.settings.legal', [
'settings' => $settings
]);
}
/**
* Update the specified resource in storage.
*
* @param UpdateLegalSettingsRequest $request
* @param LegalSettings $legalSettings
* @return RedirectResponse
*/
public function update(UpdateLegalSettingsRequest $request, LegalSettings $legalSettings): RedirectResponse
{
$legalSettings->fill($request->validated());
$legalSettings->save();
return redirect()->route('admin.settings.legal.show')
->with('success', 'You have successfully updated your settings');
}
}
This controller returns a view on the index action and updates the setting values on update but notice we don't have the UpdateLegalSettingsRequest
yet, let's create that with
php artisan make:request UpdateLegalSettingsRequest
And of course add the following code
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Validator;
class UpdateLegalSettingsRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'refund_policy' => 'nullable|string',
'privacy_policy' => 'nullable|string',
'terms_of_service' => 'nullable|string',
];
}
/**
* Configure the validator instance.
*
* @param Validator $validator
* @return void
*/
public function withValidator(Validator $validator): void
{
$validator->after(function ($validator) {
$validator->setData(
collect($validator->getData())
->filter(fn ($value) => $value)
->all()
);
});
}
}
Now let's register the routes to view and update this setting
Route::get('/settings/legal', [LegalSettingsController::class, 'index'])->name('settings.legal.show');
Route::patch('/settings/legal', [LegalSettingsController::class, 'update'])->name('settings.legal.update');
Just one thing left, let's create the view to display this setting
php artisan make:view admin.settings.legal -e layouts.app
Open the view and add the following html snippet
@extends('layouts.app')
@section('title')
Legal Settings
@endsection
@section('content')
<section class="section">
<div class="section-header">
<div class="section-header-back">
<a href="{{ route('admin.settings.index') }}" class="btn btn-icon"><i class="fas fa-arrow-left"></i></a>
</div>
<h1>Lega Settings</h1>
<div class="section-header-breadcrumb">
<div class="breadcrumb-item active"><a href="#">Dashboard</a></div>
<div class="breadcrumb-item active"><a href="#">Settings</a></div>
<div class="breadcrumb-item">General Settings</div>
</div>
</div>
<div class="section-body">
<h2 class="section-title">Legal Settings</h2>
<p class="section-lead">
You can create your own legal pages, or create them from templates and customize them. The templates are not legal advice and need to be customized for your store.
Your saved policies will be linked in the footer of your checkout. You may need to add your policies to menus in your online store
By using these templates you agree that you’ve read and agreed to the disclaimer
</p>
<div id="output-status"></div>
<div class="container">
<div class="row">
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h4>Jump To</h4>
</div>
<div class="card-body">
<ul class="nav nav-pills flex-column">
<li class="nav-item"><a href="{{ route('admin.settings.general.show') }}" class="nav-link">General</a></li>
<li class="nav-item"><a href="{{ route('admin.settings.legal.show') }}" class="nav-link active">Legal</a></li>
<li class="nav-item"><a href="#" class="nav-link">Social</a></li>
<li class="nav-item"><a href="#" class="nav-link">Misc</a></li>
</ul>
</div>
</div>
</div>
<div class="col-md-8">
<form action="{{ route('admin.settings.legal.update') }}" method="POST">
@method('PATCH')
<div class="card" id="settings-card">
<div class="card-header">
<h4>General Settings</h4>
</div>
<div class="card-body">
<p class="text-muted">Legal settings such as, terms of service and privacy policy
and so on.</p>
<div class="form-group">
<label for="site-title" class="form-control-label">Terms of Service</label>
<textarea name="terms_of_service" class="form-control @error('terms_of_service') is-invalid @enderror" {{ stimulus_controller('ckeditor') }}>{{ old('terms_of_service', $settings->terms_of_service) }}</textarea>
@error('terms_of_service')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
<div class='mt-3'>
<button type="button" class="btn btn-primary btn-sm">Create from template</button>
</div>
</div>
<div class="form-group">
<label for="site-title" class="form-control-label">Privacy Policy</label>
<textarea name="privacy_policy" class="form-control @error('privacy_policy') is-invalid @enderror" {{ stimulus_controller('ckeditor') }}>{{ old('privacy_policy', $settings->privacy_policy) }}</textarea>
@error('privacy_policy')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
<div class='mt-3'>
<button type="button" class="btn btn-primary btn-sm"> Create from template</button>
</div>
</div>
<div class="form-group">
<label for="site-title" class="form-control-label">Refund Policy</label>
<textarea name="refund_policy" class="form-control @error('refund_policy') is-invalid @enderror" id="" cols="30" rows="10" {{ stimulus_controller('ckeditor') }}>{{ old('refund_policy', $settings->refund_policy) }}</textarea>
@error('refund_policy')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
<div class='mt-3'>
<button type="button" class="btn btn-primary btn-sm">Create from template</button>
</div>
</div>
</div>
<div class="card-footer bg-whitesmoke text-md-right">
<button type="submit" class="btn btn-primary" id="save-btn">Save Changes</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</section>
@endsection
And that's it, all you need to do is add this link to the sidebars and and settings index page
I'm going to end this tutorial here, however, you should probably implement the social settings and add the ability to create legal settings from template.
For legal templates play around with turbo frames,
In the next post we will look at the customer side of the ecommerce store starting with the home page. I know sometimes I take a while to publish the next post but subscribe to the newsletter below to make sure you get an alert when I do.
Happy Codding!
Posted on August 10, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 28, 2024