Laravel 10 CRUD
Laravel 10 CRUD
Fajarwz
Laravel 10 CRUD and Image Upload Tutorial with Laravel Breeze and Repo Example
Updated Mar 17, 2024
Authentication
CRUD
Laravel
Laravel Breeze
PHP
Table of Contents
I included the link to the example repository at the conclusion of this tutorial. And if you are looking for the Laravel 11 version, you can read it here.
Laravel is a popular open-source PHP framework that makes it easy to build web applications. It provides a powerful set of tools and features for web developers, such as routing, middleware, database integration, and templating.
Laravel 10 is the latest version of Laravel, released in 2023. It comes with several new features and improvements, such as support for PHP 8.1, enhanced security features, and performance optimizations.
In this tutorial, we will create a simple CRUD (Create, Read, Update, Delete) application using Laravel, one of the most popular PHP frameworks. The CRUD application will allow us to manage a list of blog posts, including creating new posts, reading existing posts, updating posts,
and deleting posts. In addition to that, we will also learn how to upload and manage images for the blog posts.
By the end of this tutorial, you will have a good understanding of how to build a basic CRUD application in Laravel and how to work with images in the application. Let’s get started!
This command will create a new Laravel 10 project in a directory named blog-laravel10-crud-image.
After downloading Laravel Breeze via Composer, we can run the breeze:install Artisan command to install Laravel Breeze in our Laravel 10 application:
php artisan breeze:install
A prompt will appear, asking which stack we want to install. Choose “blade”. Another prompt may appear asking if we want to enable dark mode support. For now, we won’t use it, so answer “no”. Finally, a prompt may appear asking if we want to use the Pest testing framework.
Currently, we won’t use it, so answer “no”.
This command will create a new migration file in the database/migrations directory.
Open the migration file and add the following code to create the posts table:
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->text('featured_image');
$table->timestamps();
});
}
{
Schema::dropIfExists('posts');
}
This code will create a posts table with an id, title, content, featured_image, and timestamps columns.
This command will run all the outstanding migrations in the database/migrations directory.
Open the routes/web.php file and update it with the following code:
<?php
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\PostController;
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');
// Add the following route to the existing routes because we want the posts route accessible to authenticated users only.
// We'll use a resource route because it contains all the exact routes we need for a typical CRUD application.
Route::resource('posts', PostController::class);
});
require __DIR__.'/auth.php';
This adds the posts resource route to the existing routes and applies the auth middleware to restrict access to authenticated users only. The PostController is set up as a resource controller to handle all the typical CRUD operations.
Read also:
Easy Laravel Localization Tutorial With Blog Use Case and Repo Example
Laravel Export/Import: Step-by-Step Guide with Repo Example
This command will create a new PostController with all the resource methods in the app/Http/Controllers directory.
Open the PostController and add the following code to the class:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
// Use the Post Model
use App\Models\Post;
// We will use Form Request to validate incoming requests from our store and update method
use App\Http\Requests\Post\StoreRequest;
use App\Http\Requests\Post\UpdateRequest;
/**
* Show the form for creating a new resource.
*/
public function create(): Response
{
return response()->view('posts.form');
}
/**
* Store a newly created resource in storage.
*/
public function store(StoreRequest $request): RedirectResponse
{
$validated = $request->validated();
if ($request->hasFile('featured_image')) {
// put image in the public storage
$filePath = Storage::disk('public')->put('images/posts/featured-images', request()->file('featured_image'));
$validated['featured_image'] = $filePath;
}
if($create) {
// add flash for the success notification
session()->flash('notif.success', 'Post created successfully!');
return redirect()->route('posts.index');
}
return abort(500);
}
/**
* Display the specified resource.
*/
public function show(string $id): Response
{
return response()->view('posts.show', [
'post' => Post::findOrFail($id),
]);
}
/**
* Show the form for editing the specified resource.
*/
public function edit(string $id): Response
{
return response()->view('posts.form', [
'post' => Post::findOrFail($id),
]);
}
/**
* Update the specified resource in storage.
*/
public function update(UpdateRequest $request, string $id): RedirectResponse
{
$post = Post::findOrFail($id);
$validated = $request->validated();
if ($request->hasFile('featured_image')) {
// delete image
Storage::disk('public')->delete($post->featured_image);
$update = $post->update($validated);
if($update) {
session()->flash('notif.success', 'Post updated successfully!');
return redirect()->route('posts.index');
}
return abort(500);
}
/**
* Remove the specified resource from storage.
*/
public function destroy(string $id): RedirectResponse
{
$post = Post::findOrFail($id);
Storage::disk('public')->delete($post->featured_image);
$delete = $post->delete($id);
if($delete) {
session()->flash('notif.success', 'Post deleted successfully!');
return redirect()->route('posts.index');
}
return abort(500);
}
}
The code for this class is similar to the code for StoreRequest except that the featured_image rule is set to nullable:
public function authorize(): bool
{
// dont' forget to set this as true
return true;
}
This command generates a new model file named Post.php within the app/Models directory.
Since we will be writing additional styles to our app, we need to start the Tailwind CLI build process while we code our views. To do this, run the following command:
npx tailwindcss -i ./resources/css/app.css -o ./resources/css/main.css --watch
This command will generate a new file at resources/css/main.css. Now, we need to tell Vite that we will be using main.css instead of app.css.
We will reuse resources/views/layouts/app.blade.php layout so update the Vite code in the head section to the following:
{{-- some other code --}}
@vite(['resources/css/main.css', 'resources/js/app.js'])
</head>
@if (Session::has('notif.success'))
<div class="bg-blue-300 mt-2 p-4">
<span class="text-white">{{ Session::get('notif.success') }}</span>
</div>
@endif
</div>
</header>
@endif
Read also:
<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">
<table class="border-collapse table-auto w-full text-sm">
<thead>
<tr>
<th class="border-b font-medium p-4 pl-8 pt-0 pb-3 text-slate-400 text-left">Title</th>
<th class="border-b font-medium p-4 pl-8 pt-0 pb-3 text-slate-400 text-left">Created At</th>
<th class="border-b font-medium p-4 pl-8 pt-0 pb-3 text-slate-400 text-left">Updated At</th>
<th class="border-b font-medium p-4 pl-8 pt-0 pb-3 text-slate-400 text-left">Action</th>
</tr>
</thead>
<tbody class="bg-white">
{{-- populate our post data --}}
@foreach ($posts as $post)
<tr>
<td class="border-b border-slate-100 dark:border-slate-700 p-4 pl-8 text-slate-500 dark:text-slate-400">{{ $post->title }}</td>
<td class="border-b border-slate-100 dark:border-slate-700 p-4 pl-8 text-slate-500 dark:text-slate-400">{{ $post->created_at }}</td>
<td class="border-b border-slate-100 dark:border-slate-700 p-4 pl-8 text-slate-500 dark:text-slate-400">{{ $post->updated_at }}</td>
<td class="border-b border-slate-100 dark:border-slate-700 p-4 pl-8 text-slate-500 dark:text-slate-400">
<a href="{{ route('posts.show', $post->id) }}" class="border border-blue-500 hover:bg-blue-500 hover:text-white px-4 py-2 rounded-md">SHOW</a>
<a href="{{ route('posts.edit', $post->id) }}" class="border border-yellow-500 hover:bg-yellow-500 hover:text-white px-4 py-2 rounded-md">EDIT</a>
{{-- add delete button using form tag --}}
<form method="post" action="{{ route('posts.destroy', $post->id) }}" class="inline">
@csrf
@method('delete')
<button class="border border-red-500 hover:bg-red-500 hover:text-white px-4 py-2 rounded-md">DELETE</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
</x-app-layout>
<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">
<div class="mb-6">
<h2 class="text-lg font-medium text-gray-900">
{{ 'Title' }}
</h2>
<div class="mb-6">
<h2 class="text-lg font-medium text-gray-900">
{{ 'Updated At' }}
</h2>
<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">
{{-- don't forget to add multipart/form-data so we can accept file in our form --}}
<form method="post" action="{{ isset($post) ? route('posts.update', $post->id) : route('posts.store') }}" class="mt-6 space-y-6" enctype="multipart/form-data">class="mt-6 space-y-6">
@csrf
{{-- add @method('put') for edit mode --}}
@isset($post)
@method('put')
@endisset
<div>
<x-input-label for="title" value="Title" />
<x-text-input id="title" name="title" type="text" class="mt-1 block w-full" :value="$post->title ?? old('title')" required autofocus />
<x-input-error class="mt-2" :messages="$errors->get('title')" />
</div>
<div>
<x-input-label for="content" value="Content" />
{{-- use textarea-input component that we will create after this --}}
<x-textarea-input id="content" name="content" class="mt-1 block w-full" required autofocus>{{ $post->content ?? old('content') }}</x-textarea-input>
<x-input-error class="mt-2" :messages="$errors->get('content')" />
</div>
<div>
<x-input-label for="featured_image" value="Featured Image" />
<label class="block mt-2">
<span class="sr-only">Choose image</span>
<input type="file" id="featured_image" name="featured_image" class="block w-full text-sm text-slate-500
file:mr-4 file:py-2 file:px-4
file:rounded-full file:border-0
file:text-sm file:font-semibold
file:bg-violet-50 file:text-violet-700
hover:file:bg-violet-100
"/>
</label>
<div class="shrink-0 my-2">
<img id="featured_image_preview" class="h-64 w-128 object-cover rounded-md" src="{{ isset($post) ? Storage::url($post->featured_image) : '' }}" alt="Featured image preview" />
</div>
<x-input-error class="mt-2" :messages="$errors->get('featured_image')" />
</div>
<textarea {{ $disabled ? 'disabled' : '' }} {!! $attributes->merge(['class' => 'border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm']) !!}>{{$slot}}</textarea>
<div class="ml-1">
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</div>
</button>
</x-slot>
<x-slot name="content">
<x-dropdown-link :href="route('profile.edit')">
{{ __('Profile') }}
</x-dropdown-link>
<x-dropdown-link :href="route('logout')"
onclick="event.preventDefault();
this.closest('form').submit();">
{{ __('Log Out') }}
</x-dropdown-link>
</form>
</x-slot>
</x-dropdown>
</div>
<x-responsive-nav-link :href="route('logout')"
onclick="event.preventDefault();
this.closest('form').submit();">
{{ __('Log Out') }}
</x-responsive-nav-link>
</form>
</div>
</div>
</div>
</nav>
Here we add our Posts link to the desktop and mobile version navigation.
Once you have set up your frontend dependencies, you can run your local development server by running the following command:
php artisan serve
Read also:
Welcome Page
You will see the following Laravel 10 Welcome Page. You will also see Log in and Register link at the navbar because we already installed Laravel Breeze.
Dashboard Page
Here is how the Dashboard page looks like. You should also see an additional link at the navbar named Posts:
Here is how the post list page in /posts should look like. It should have a table with Show, Edit, and Delete buttons in each record:
show-post.png
Here is how the Create Post page should look like with image preview. This is also similar to the Edit Post page:
create-post.png
Notification Display
create-update-notif.png
Conclusion
In this tutorial, we have learned how to create a simple CRUD application using Laravel 8 with Laravel Breeze for authentication. We have covered the basic concepts of Laravel, such as routing, controllers, views, and models, as well as using components from Laravel Breeze to handle
user authentication.
We also learned how to use Tailwind CSS and Vite to style our application and optimize asset bundling. Additionally, we covered how to handle flash notifications and file uploading in our application.
Overall, this tutorial provides a solid foundation for building CRUD applications using Laravel, and with the knowledge gained, you can easily extend this application to meet your specific requirements.
I'm a full-stack web developer who loves to share my software engineering journey and build software solutions to help businesses succeed.
Email me
Ads
https://fajarwz.com/blog/laravel-10-crud-and-ima
Link copied
Support
Trakteer
Subscribe
Sign up for my email newsletter and never miss a beat in the world of web development. Stay up-to-date on the latest trends, techniques, and tools. Don't miss out on valuable insights. Subscribe now!
Fajarwz's photo
Laravel 10 CRUD and Image Upload Tutorial with Laravel Breeze and... file:///D:/NOGAE%20FORMATION/Laravel/Laravel%2010%20CR...
Recent Posts
Laravel 11: CRUD and File Upload Tutorial With Laravel Breeze
Email Authentication and Verification in Next.js 14 With Next Auth and Prisma
All Tags
API
Authentication
Automated Testing
Back-end
Brevo
Browser Testing
Bug Hunt
Clean Code
Coding Best Practice
Communication
CRUD
CSR
CSS
CSS Transitions
Database
Documentation
Eloquent ORM
Export-Import
Front-end
Go
HTML
Hugo
ISR
JavaScript
JWT
Laravel
Laravel Breeze
Laravel Dusk
Laravel Scout
Localization
Lunr
Mail
Meilisearch
Next.js
Nginx
Nodemailer
Notification System
OAuth
OpenAPI Swagger
Pagination
PHP
PostgreSQL
Prisma
Productivity
Query Scope
React
react-paginate
Remote Work
Rest API
Search
Server Actions
Slack
SMS
Software Development
Soketi
SSG
SSR
Static Site
TDD
Twilio
TypeScript
Web Development
Web Server
WebSockets
Zod