Chapter 11 - Laravel CRUD Operation with File Upload

Hello Artisan's, welcome to the 11th chapter of being an Artisanary. In this chapter we'll show CRUD(create-read-edit-delete) operation with file upload. So if you already complete the previous chapters/sections, then you're good to go, if not my recommendation would be please complete the previous chapters. Because we'll use the same old repository that we use in chapter 4. 

Note: Tested on Laravel 10.0

Table of Contents

  1. Install Image Intervention for Image Processing
  2. Create and Setup Repository
  3. Create and Setup Observer
  4. Create and Setup Controller
  5. Setup Model and Migration
  6. Define Routes
  7. Create and Setup View
  8. Output

Install Image Intervention for Image Processing

Image intervention is one of the most used packages for image processing. For installing image intervention fire the below command in terminal.

composer require intervention/image

And it'll enough, no need to do anything.

Create and Setup Repository

At first, we'll create a repository class called BlogRepository.php where we'll write our all database logics so that we can use the same query everywhere.

BlogRepository.php
<?php

namespace App\Repositories;

use App\Models\Blog;
use Illuminate\Support\Str;
use Intervention\Image\Facades\Image;

class BlogRepository
{
    public function all()
    {
        return Blog::latest()->paginate();
    }

    protected function createData($data)
    {
        if (array_key_exists('image', $data)) {
            $image = $data['image'];
            $image_name = Str::uuid() . '.' . $image->getClientOriginalExtension();

            $destinationPath = public_path('uploads/');

            if (!is_dir($destinationPath)) {
                mkdir($destinationPath, 0777, true);
            }
            Image::make($data['image'])->resize(50,null, function ($constraint) {
                $constraint->aspectRatio();
            })->save($destinationPath.$image_name);

            $data['image'] = 'uploads/'.$image_name;
        }

        return $data;
    }
    public function store($data)
    {
        return Blog::create($this->createData($data));
    }

    public function find($id)
    {
        return Blog::find($id);
    }

    public function update($id,$data)
    {
        $blog = $this->find($id);

        if (array_key_exists('image', $data) && $blog->image) {
            $image_path = public_path($blog->image);
            if (file_exists($image_path)) {
                unlink($image_path);
            }
        }
        return $blog->update($this->createData($data));
    }

    public function destroy($id)
    {
        return Blog::destroy($id);
    }
}

Create and Setup Observer

We'll already discuss the what is Observer and why we use observer, so I'll not repeat here.

BlogObserver.php
<?php

namespace App\Observers;

use App\Models\Blog;
use Illuminate\Support\Str;

class BlogObserver
{
    public function creating(Blog $blog)
    {
        $blog->slug = Str::slug($blog->title);
    }
    public function created(Blog $blog): void
    {
        $blog->unique_id    = 'PR-'.$blog->id;
        $blog->save();
    }

    public function updating(Blog $blog): void
    {
        $blog->slug = Str::slug($blog->title);
    }
    public function updated(Blog $blog): void
    {
        //
    }

    public function deleted(Blog $blog): void
    {
        $image_path = public_path($blog->image);
        if (file_exists($image_path)) {
            unlink($image_path);
        }
    }

    public function restored(Blog $blog): void
    {
        //
    }

    public function forceDeleted(Blog $blog): void
    {
        //
    }
}

Create and Setup Controller

Then, we'll create a controller called BlogController.php where we'll write our logic or insert the data. So, fire the below command in terminal.

php artisan make:controller BlogController -r

It'll create a file under app\Http\Controllers called BlogController.php. Now open the file and replace with below codes.

BlogController.php
<?php

namespace App\Http\Controllers;

use App\Http\Requests\BlogRequest;
use App\Repositories\BlogRepository;

class BlogController extends Controller
{
    protected $blogRepository;

    public function __construct(BlogRepository $blogRepository)
    {
        $this->blogRepository = $blogRepository;
    }

    public function index(): \Illuminate\Contracts\View\Factory|\Illuminate\Foundation\Application|\Illuminate\Contracts\View\View|\Illuminate\Contracts\Foundation\Application|\Illuminate\Http\RedirectResponse
    {
        try {
            $data = [
                'blogs' => $this->blogRepository->all()
            ];
            return view('backend.blogs.index', $data);
        } catch (\Exception $e) {
            return back()->with('error', $e->getMessage());
        }
    }

    public function create(): \Illuminate\Contracts\View\View|\Illuminate\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\Foundation\Application
    {
        return view('backend.blogs.form');
    }
    public function store(BlogRequest $request)
    {
        try {
            $this->blogRepository->store($request->all());
            return redirect()->route('blogs.index')->with('success', 'Blog created successfully');
        } catch (\Exception $e) {
            return back()->withInput()->with('error', $e->getMessage());
        }
    }
    public function edit(string $id): \Illuminate\Contracts\View\Factory|\Illuminate\Foundation\Application|\Illuminate\Contracts\View\View|\Illuminate\Contracts\Foundation\Application|\Illuminate\Http\RedirectResponse
    {
        try {
            $data = [
                'edit' => $this->blogRepository->find($id)
            ];
            return view('backend.blogs.form', $data);
        } catch (\Exception $e) {
            return back()->with('error', $e->getMessage());
        }
    }
    public function update(BlogRequest $request, $id): \Illuminate\Http\RedirectResponse
    {
        try {
            $this->blogRepository->update($id, $request->all());
            return redirect()->route('blogs.index')->with('success', 'Blog updated successfully');
        } catch (\Exception $e) {
            return back()->withInput()->with('error', $e->getMessage());
        }
    }

    public function destroy(string $id): \Illuminate\Http\RedirectResponse
    {
        try {
            $this->blogRepository->destroy($id);
            return redirect()->route('blogs.index')->with('success', 'Blog deleted successfully');
        } catch (\Exception $e) {
            return back()->with('error', $e->getMessage());
        }
    }
}

Setup Model and Migration

Now we'll setup our model and migration file. And we also discuss the each attribute which we going to use there in the previous lecture. So I don't want to repeat it here.

Blog.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Blog extends Model
{
    use HasFactory;

    protected $fillable = [
        'title',
        'slug',
        'unique_id',
        'description',
        'image',
        'user_id',
    ];

    public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function scopeActive($query)
    {
        return $query->where('status', 1);
    }
}
2023_04_20_060332_create_blogs_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('blogs', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained('users');
            $table->string('title');
            $table->string('slug');
            $table->string('unique_id')->nullable();
            $table->text('description');
            $table->string('image');
            $table->boolean('status')->default(1);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('blogs');
    }
};

Define Routes

Put these route in web.php.

Route::resource('blogs', BlogController::class)->except(['show']);

The whole file will look like below

web.php
<?php

use App\Http\Controllers\BlogController;
use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
});

Route::get('/dashboard', function () {
    return view('dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');

Route::middleware('auth')->group(function () {
    Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
    Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
    Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');

    Route::resource('blogs', BlogController::class)->except(['show']);
});

require __DIR__.'/auth.php';

Create and Setup View

So at first we'll create a blade file where we put our flash messages which we discuss in our previous chapter. And later we'll just @include() in every page.

alert.blade.php
@if(session()->has('success'))
<div class="alert alert-success  alert-dismissible fade show" role="alert">
    <strong>Success!</strong> {{ session()->get('success') }}
    <button type="button" class="close" data-dismiss="alert" aria-label="Close">
        <span aria-hidden="true">&times;</span>
    </button>
</div>
@endif
@if(session()->has('error'))
<div class="alert alert-danger  alert-dismissible fade show" role="alert">
    <strong>Error!</strong> {{ session()->get('error') }}
    <button type="button" class="close" data-dismiss="alert" aria-label="Close">
        <span aria-hidden="true">&times;</span>
    </button>
</div>
@endif
@if(session()->has('warning'))
<div class="alert alert-success  alert-dismissible fade show" role="alert">
    <strong>Warning!</strong> {{ session()->get('warning') }}
    <button type="button" class="close" data-dismiss="alert" aria-label="Close">
        <span aria-hidden="true">&times;</span>
    </button>
</div>
@endif
@if(session()->has('info'))
<div class="alert alert-success  alert-dismissible fade show" role="alert">
    <strong>Info!</strong> {{ session()->get('info') }}
    <button type="button" class="close" data-dismiss="alert" aria-label="Close">
        <span aria-hidden="true">&times;</span>
    </button>
</div>
@endif

Now we'll setup our form. We'll use this form in both for creating and updating the blog.

form.blade.php
@extends('backend.layouts.master')
@section('title', isset($edit) ?__('Edit Blog') : __('Create Blog'))
@section('content')
    <div class="content-header">
        <div class="container-fluid">
            <div class="row mb-2">
                <div class="col-sm-6">
                    <h1 class="m-0">Blog</h1>
                </div><!-- /.col -->
                <div class="col-sm-6">
                    <ol class="breadcrumb float-sm-right">
                        <li class="breadcrumb-item"><a href="{{ route('dashboard') }}">Dashboard</a></li>
                        <li class="breadcrumb-item active">{{ isset($edit) ?__('Edit Blog') : __('Create Blog') }}</li>
                    </ol>
                </div><!-- /.col -->
            </div><!-- /.row -->
        </div><!-- /.container-fluid -->
    </div>
    <!-- /.content-header -->
    <!-- Main content -->
    <section class="content">
        <div class="container-fluid">
            <div class="row">
                <div class="col-md-12">
                    @include('layouts.alert')
                    <div class="card">
                        <div class="card-header">
                            <h3 class="card-title">{{ isset($edit) ?__('Edit Blog') : __('Create Blog') }}</h3>
                        </div>
                        @php
                            $route = isset($edit) ? route('blogs.update',$edit->id) : route('blogs.store');
                        @endphp
                        <!-- /.card-header -->
                        <form action="{{ $route }}" method="post" enctype="multipart/form-data">@csrf
                            @isset($edit)
                                @method('PUT')
                            @endisset
                            <input type="hidden" name="user_id" value="{{ isset($edit) ? $edit->user_id : auth()->id() }}">
                            <div class="card-body">
                                <div class="row">
                                    <div class="col-lg-12">
                                        <div class="form-group">
                                            <label for="title">{{ __('Title') }}</label>
                                            <input name="title" id="title" type="text"
                                                   class="form-control" placeholder="{{ __('Enter Title') }}" value="{{ old('title',@$edit->title) }}">
                                            <span class="text-danger">{{ $errors->first('title') }}</span>
                                        </div>
                                    </div>
                                    <div class="col-lg-6">
                                        <div class="form-group">
                                            <label for="image">{{ __('Image') }}</label>
                                            <input name="image" id="image" type="file"
                                                   class="form-control">
                                            <span class="text-danger">{{ $errors->first('image') }}</span>
                                        </div>
                                    </div>
                                    <div class="col-lg-6">
                                        <div class="form-group">
                                            <label for="status">{{ __('Status') }}</label>
                                            <select name="status" id="status"
                                                    class="form-control text-capitalize">
                                                <option value="1" {{ old('title',@$edit->title) == 1 ? 'selected' : '' }}>{{ __('Active') }}</option>
                                                <option value="0" {{ old('title',@$edit->title) == 0 ? 'selected' : '' }}>{{ __('Inactive') }}</option>
                                            </select>
                                            <span class="text-danger error">{{ $errors->first('status') }}</span>
                                        </div>
                                    </div>
                                    <div class="col-lg-12">
                                        <div class="form-group">
                                            <label for="summernote">{{ __('Description') }}</label>
                                            <textarea id="summernote" name="description" class="summernote">{{ old('description',@$edit->description) }}</textarea>
                                            <span class="text-danger error">{{ $errors->first('description') }}</span>
                                        </div>
                                    </div>
                                </div>

                            </div>
                            <div class="card-footer text-right">
                                <button type="submit" class="btn btn-primary">{{ __('Save')}}</button>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </section>
@endsection
@push('js')
    <script>
        $('#summernote').summernote({
            placeholder: 'Enter Description',
            tabsize: 2,
            height: 250
        });
    </script>
@endpush

And here we'll show our blog list

index.blade.php
@extends('backend.layouts.master')
@section('title',__('Blog List'))
@section('content')
    <div class="content-header">
        <div class="container-fluid">
            <div class="row mb-2">
                <div class="col-sm-6">
                    <h1 class="m-0">Blogs</h1>
                </div><!-- /.col -->
                <div class="col-sm-6">
                    <ol class="breadcrumb float-sm-right">
                        <li class="breadcrumb-item"><a href="{{ route('dashboard') }}">{{ __('Dashboard') }}</a></li>
                        <li class="breadcrumb-item active">Blog List</li>
                    </ol>
                </div><!-- /.col -->
            </div><!-- /.row -->
        </div><!-- /.container-fluid -->
    </div>
    <!-- /.content-header -->

    <!-- Main content -->
    <section class="content">
        <div class="container-fluid">
            <div class="row">
                <div class="col-md-12">
                    @include('layouts.alert')
                    <div class="card">
                        <div class="card-header">
                            <h3 class="card-title">{{__('Blogs')}}</h3>
                        </div>
                        <!-- /.card-header -->
                        <div class="card-body p-0">
                            <table class="table table-sm">
                                <thead>
                                <tr>
                                    <th style="width: 10px">#</th>
                                    <th>{{__('Image')}}</th>
                                    <th>{{__('Title')}}</th>
                                    <th>{{__('Status')}}</th>
                                    <th style="width: 40px">{{ __('Action')}}</th>
                                </tr>
                                </thead>
                                <tbody>
                                @forelse($blogs as $key=> $blog)
                                    <tr>
                                        <td>{{ $blogs->firstItem() + $key }}</td>
                                        <td>{{ $blog->title }}</td>
                                        <td><img src="{{ asset($blog->image) }}" alt="{{ $blog->title }}"></td>
                                        <td><span @class([
                                            'badge',
                                            'badge-success' => $blog->status == 1,
                                            'badge-danger' => $blog->status == 0,
                                        ])>{{ $blog->status == 1 ? 'Active' : 'Inactive' }}</span></td>
                                        <td>
                                            <a href="{{ route('blogs.edit', $blog->id) }}" class="btn view btn-block btn-primary btn-xs">{{ __('Edit') }}</a>
                                            <form action="{{ route('blogs.destroy', $blog->id) }}" method="post">
                                                @csrf
                                                @method('DELETE')
                                                <button type="submit" class="btn btn-block btn-danger btn-xs">
                                                    {{__('Delete')}}
                                                </button>
                                            </form>
                                        </td>
                                    </tr>
                                    @empty
                                    <tr>
                                        <td colspan="5" class="text-center">
                                            {{ __('No Data Found') }}
                                        </td>
                                    </tr>
                                @endforelse
                                </tbody>
                            </table>
                        </div>
                        <!-- /.card-body -->
                        <!-- /.card-body -->
                        <div class="card-footer clearfix">
                            {{ $blogs->links() }}
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </section>
@endsection

And now we'll update sidebar

sidebar.blade.php
<aside class="main-sidebar sidebar-dark-primary elevation-4">
    <!-- Brand Logo -->
    <a href="index3.html" class="brand-link">
        <img src="{{ asset('assets/backend/img/AdminLTELogo.png') }}" alt="AdminLTE Logo" class="brand-image img-circle elevation-3" style="opacity: .8">
        <span class="brand-text font-weight-light">AdminLTE 3</span>
    </a>

    <!-- Sidebar -->
    <div class="sidebar">
        <!-- Sidebar user panel (optional) -->
        <div class="user-panel mt-3 pb-3 mb-3 d-flex">
            <div class="image">
                <img src="{{ asset('assets/backend/img/user2-160x160.jpg')}}" class="img-circle elevation-2" alt="User Image">
            </div>
            <div class="info">
                <a href="#" class="d-block">Alexander Pierce</a>
            </div>
        </div>

        <!-- SidebarSearch Form -->
        <div class="form-inline">
            <div class="input-group" data-widget="sidebar-search">
                <input class="form-control form-control-sidebar" type="search" placeholder="Search" aria-label="Search">
                <div class="input-group-append">
                    <button class="btn btn-sidebar">
                        <i class="fas fa-search fa-fw"></i>
                    </button>
                </div>
            </div>
        </div>

        <!-- Sidebar Menu -->
        <nav class="mt-2">
            <ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
                <li class="nav-item">
                    <a href="{{ route('dashboard') }}" class="nav-link">
                        <i class="nav-icon fas fa-tachometer-alt"></i>
                        <p>
                            Dashboard
                        </p>
                    </a>
                </li>
                <li class="nav-item {{ request()->is('blogs*')  ? 'menu-open' : '' }}">
                    <a href="#" class="nav-link {{ request()->is('blogs*')  ? 'active' : '' }}">
                        <i class="nav-icon fas fa-blog"></i>
                        <p>
                            Blog
                            <i class="right fas fa-angle-left"></i>
                        </p>
                    </a>
                    <ul class="nav nav-treeview">
                        <li class="nav-item">
                            <a href="{{ route('blogs.index') }}" class="nav-link {{ request()->is('blogs') || request()->is('blogs/*')  ? 'active' : '' }}">
                                <i class="fa fa-list nav-icon"></i>
                                <p>All Blogs</p>
                            </a>
                        </li>
                        <li class="nav-item">
                            <a href="{{ route('blogs.create') }}" class="nav-link {{ request()->is('blogs/create')  ? 'active' : '' }}">
                                <i class="fas fa-blog nav-icon"></i>
                                <p>Create Blogs</p>
                            </a>
                        </li>
                    </ul>
                </li>
            </ul>
        </nav>
        <!-- /.sidebar-menu -->
    </div>
    <!-- /.sidebar -->
</aside>

Output

And finally, we're ready with our setup. It's time to check our output. Now go to http://artisanary.test/blogs, If everything goes well (hope so) we can see the below output.

 

So, it's time to say goodbye for today. We saw the CRUD operation with file upload. And yes keep up to date with the Github repository. That's it for today. See you in my next chapter. Happy coding ๐Ÿ™‚.