Build CRUD App Laravel 12: Complete Step-by-Step Guide
This comprehensive guide will walk you through building a complete CRUD (Create, Read, Update, Delete) application using Laravel 12, the latest version of the popular PHP framework. Whether you’re a beginner looking to learn Laravel or an experienced developer wanting to brush up on your skills, this tutorial will provide you with a clear and concise roadmap. We’ll cover everything from setting up your development environment to implementing robust CRUD operations with proper validation and error handling.
Table of Contents
- Introduction to CRUD Applications and Laravel 12
- Prerequisites
- Setting up Your Development Environment
- Creating the Model and Migration
- Building the Controller
- Generating the Controller
- Implementing the Index Method (Read)
- Implementing the Create Method (Display Create Form)
- Implementing the Store Method (Create)
- Implementing the Show Method (Read Single Record)
- Implementing the Edit Method (Display Edit Form)
- Implementing the Update Method (Update)
- Implementing the Destroy Method (Delete)
- Creating the Views
- Defining the Routes
- Implementing Validation
- Error Handling and Exception Handling
- Implementing Authentication (Optional)
- Implementing Authorization (Optional)
- Testing Your CRUD Application
- Conclusion
Introduction to CRUD Applications and Laravel 12
CRUD stands for Create, Read, Update, and Delete. These are the four basic operations that can be performed on data in a database. A CRUD application provides an interface for users to perform these operations, typically through a web application.
Laravel 12, the latest iteration of the framework, provides a robust and elegant environment for building CRUD applications. Its features include:
- Eloquent ORM: Simplifies database interactions.
- Blade Templating Engine: Provides a clean and expressive syntax for creating views.
- Artisan Console: Offers powerful commands for scaffolding and managing your application.
- Routing System: Enables you to define routes and map them to controller actions.
- Validation: Provides a simple and convenient way to validate user input.
Prerequisites
Before you begin, ensure you have the following installed:
- PHP 8.1 or higher: Laravel 12 requires PHP 8.1 or greater.
- Composer: A dependency manager for PHP.
- Database Server: MySQL, PostgreSQL, SQLite, or SQL Server.
- Web Server: Apache or Nginx.
Setting up Your Development Environment
Install Composer
If you don’t have Composer installed, download and install it from the official Composer website.
Install Laravel 12
Open your terminal and navigate to the directory where you want to create your project. Then, use the Composer command to install a new Laravel 12 project:
composer create-project laravel/laravel crud-app
Replace `crud-app` with your desired project name. This command will download Laravel 12 and all its dependencies.
Database Setup
Create a new database for your application. You can use a tool like phpMyAdmin, Dbeaver, or the command line to create the database.
Next, configure your database connection in the `.env` file located in the root of your project. Update the following variables with your database credentials:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=your_database_name
DB_USERNAME=your_database_username
DB_PASSWORD=your_database_password
Replace `your_database_name`, `your_database_username`, and `your_database_password` with your actual database credentials.
Creating the Model and Migration
Let’s create a model and migration for our data. For this example, we’ll create a simple `Product` model.
Generating the Model
Use the Artisan command to generate a new model:
php artisan make:model Product
This command will create a `Product.php` file in the `app/Models` directory.
Creating the Migration
We’ll also create a migration to define the database table structure. You can create a migration along with the model using the `-m` flag:
php artisan make:model Product -m
This command will create a migration file in the `database/migrations` directory. The filename will be similar to `YYYY_MM_DD_HHMMSS_create_products_table.php`.
Defining the Migration Schema
Open the migration file and define the schema for the `products` table. For example, let’s add fields for `name`, `description`, and `price`:
<?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('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('description')->nullable();
$table->decimal('price', 8, 2);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('products');
}
};
This code defines a `products` table with the following columns:
- `id`: The primary key (auto-incrementing integer).
- `name`: A string for the product name.
- `description`: A text field for the product description (nullable).
- `price`: A decimal field for the product price.
- `timestamps`: Adds `created_at` and `updated_at` columns.
Running the Migration
Run the migration to create the table in your database:
php artisan migrate
This command will execute all pending migrations, including the one you just created.
Building the Controller
Now, let’s create a controller to handle the CRUD operations for our `Product` model.
Generating the Controller
Use the Artisan command to generate a new controller:
php artisan make:controller ProductController --resource
The `–resource` flag tells Artisan to generate a resource controller with methods for handling all CRUD operations. This will create `ProductController.php` in the `app/Http/Controllers` directory.
Implementing the Index Method (Read)
The `index` method is responsible for displaying a list of all products. Open `ProductController.php` and add the following code to the `index` method:
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use Illuminate\Http\Request;
class ProductController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
$products = Product::all();
return view('products.index', compact('products'));
}
// ... other methods ...
}
This code retrieves all products from the database using the `Product::all()` method and passes them to the `products.index` view.
Implementing the Create Method (Display Create Form)
The `create` method is responsible for displaying the form for creating a new product. Add the following code to the `create` method:
/**
* Show the form for creating a new resource.
*/
public function create()
{
return view('products.create');
}
This code simply returns the `products.create` view.
Implementing the Store Method (Create)
The `store` method is responsible for creating a new product in the database. Add the following code to the `store` method:
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'description' => 'nullable|string',
'price' => 'required|numeric|min:0',
]);
Product::create($request->all());
return redirect()->route('products.index')
->with('success', 'Product created successfully.');
}
This code first validates the request data using the `validate` method. Then, it creates a new product using the `Product::create()` method and redirects the user to the `products.index` route with a success message.
Implementing the Show Method (Read Single Record)
The `show` method is responsible for displaying a single product. Add the following code to the `show` method:
/**
* Display the specified resource.
*/
public function show(Product $product)
{
return view('products.show', compact('product'));
}
This code retrieves the product with the given ID using route model binding and passes it to the `products.show` view.
Implementing the Edit Method (Display Edit Form)
The `edit` method is responsible for displaying the form for editing an existing product. Add the following code to the `edit` method:
/**
* Show the form for editing the specified resource.
*/
public function edit(Product $product)
{
return view('products.edit', compact('product'));
}
This code retrieves the product with the given ID using route model binding and passes it to the `products.edit` view.
Implementing the Update Method (Update)
The `update` method is responsible for updating an existing product in the database. Add the following code to the `update` method:
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Product $product)
{
$request->validate([
'name' => 'required|string|max:255',
'description' => 'nullable|string',
'price' => 'required|numeric|min:0',
]);
$product->update($request->all());
return redirect()->route('products.index')
->with('success', 'Product updated successfully.');
}
This code first validates the request data using the `validate` method. Then, it updates the product using the `update` method and redirects the user to the `products.index` route with a success message.
Implementing the Destroy Method (Delete)
The `destroy` method is responsible for deleting a product from the database. Add the following code to the `destroy` method:
/**
* Remove the specified resource from storage.
*/
public function destroy(Product $product)
{
$product->delete();
return redirect()->route('products.index')
->with('success', 'Product deleted successfully.');
}
This code deletes the product using the `delete` method and redirects the user to the `products.index` route with a success message.
Creating the Views
Now, let’s create the views for displaying the data and forms. Create a `products` directory inside the `resources/views` directory.
Index View (Listing Records)
Create a file named `index.blade.php` inside the `resources/views/products` directory and add the following code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Products</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="container">
<h1>Products</h1>
<a href="{{ route('products.create') }}" class="btn btn-primary mb-3">Create New Product</a>
@if (session('success'))
<div class="alert alert-success">
{{ session('success') }}
</div>
@endif
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Price</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach ($products as $product)
<tr>
<td>{{ $product->id }}</td>
<td>{{ $product->name }}</td>
<td>{{ $product->price }}</td>
<td>
<a href="{{ route('products.show', $product->id) }}" class="btn btn-sm btn-info">View</a>
<a href="{{ route('products.edit', $product->id) }}" class="btn btn-sm btn-primary">Edit</a>
<form action="{{ route('products.destroy', $product->id) }}" method="POST" style="display: inline;">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure?')">Delete</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</body>
</html>
This view displays a table of all products with links to view, edit, and delete each product. It also displays a success message if one is present in the session. The code uses Bootstrap for basic styling. Make sure to include Bootstrap CSS in your project. You can use a CDN like in the example, or install Bootstrap using npm or yarn.
Create View (Create Form)
Create a file named `create.blade.php` inside the `resources/views/products` directory and add the following code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Create Product</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="container">
<h1>Create Product</h1>
<form action="{{ route('products.store') }}" method="POST">
@csrf
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name" value="{{ old('name') }}" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description" rows="3">{{ old('description') }}</textarea>
</div>
<div class="mb-3">
<label for="price" class="form-label">Price</label>
<input type="number" step="0.01" class="form-control" id="price" name="price" value="{{ old('price') }}" required>
</div>
<button type="submit" class="btn btn-primary">Create</button>
<a href="{{ route('products.index') }}" class="btn btn-secondary">Cancel</a>
</form>
@if ($errors->any())
<div class="alert alert-danger mt-3">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
</body>
</html>
This view displays a form for creating a new product. It includes fields for `name`, `description`, and `price`. It also displays validation errors if any are present. The `old()` helper function is used to repopulate the form fields with the previously entered values if validation fails. This provides a better user experience. Again, Bootstrap CSS is used for styling.
Show View (Display Single Record)
Create a file named `show.blade.php` inside the `resources/views/products` directory and add the following code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Product Details</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="container">
<h1>Product Details</h1>
<div class="card">
<div class="card-body">
<h5 class="card-title">{{ $product->name }}</h5>
<p class="card-text">{{ $product->description }}</p>
<p class="card-text"><strong>Price:</strong> {{ $product->price }}</p>
<a href="{{ route('products.index') }}" class="btn btn-primary">Back to Products</a>
</div>
</div>
</body>
</html>
This view displays the details of a single product. It shows the product’s name, description, and price. It also includes a link back to the product listing page. The layout utilizes a Bootstrap card for visual structure.
Edit View (Edit Form)
Create a file named `edit.blade.php` inside the `resources/views/products` directory and add the following code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Edit Product</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="container">
<h1>Edit Product</h1>
<form action="{{ route('products.update', $product->id) }}" method="POST">
@csrf
@method('PUT')
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name" value="{{ old('name', $product->name) }}" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description" rows="3">{{ old('description', $product->description) }}</textarea>
</div>
<div class="mb-3">
<label for="price" class="form-label">Price</label>
<input type="number" step="0.01" class="form-control" id="price" name="price" value="{{ old('price', $product->price) }}" required>
</div>
<button type="submit" class="btn btn-primary">Update</button>
<a href="{{ route('products.index') }}" class="btn btn-secondary">Cancel</a>
</form>
@if ($errors->any())
<div class="alert alert-danger mt-3">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
</body>
</html>
This view displays a form for editing an existing product. It includes fields for `name`, `description`, and `price`, pre-populated with the current product data. It also displays validation errors if any are present. The `@method(‘PUT’)` directive is used to spoof a PUT request, as HTML forms only support GET and POST. The `old()` helper is used to prioritize displaying previously entered (and potentially valid) values over the database values in case of validation failure.
Defining the Routes
Now, let’s define the routes for our CRUD operations.
Using Resource Routes
Laravel provides a convenient way to define routes for resource controllers using the `resource` method in the `routes/web.php` file. Add the following line to the file:
Route::resource('products', ProductController::class);
This single line of code defines all the necessary routes for the CRUD operations on the `products` resource. It automatically maps the following routes:
- `GET /products`: `ProductController@index` (Display a list of all products)
- `GET /products/create`: `ProductController@create` (Display the form for creating a new product)
- `POST /products`: `ProductController@store` (Create a new product)
- `GET /products/{product}`: `ProductController@show` (Display a single product)
- `GET /products/{product}/edit`: `ProductController@edit` (Display the form for editing an existing product)
- `PUT/PATCH /products/{product}`: `ProductController@update` (Update an existing product)
- `DELETE /products/{product}`: `ProductController@destroy` (Delete a product)
Defining Routes Manually
Alternatively, you can define the routes manually if you need more control over the route names or middleware. For example:
Route::get('/products', [ProductController::class, 'index'])->name('products.index');
Route::get('/products/create', [ProductController::class, 'create'])->name('products.create');
Route::post('/products', [ProductController::class, 'store'])->name('products.store');
Route::get('/products/{product}', [ProductController::class, 'show'])->name('products.show');
Route::get('/products/{product}/edit', [ProductController::class, 'edit'])->name('products.edit');
Route::put('/products/{product}', [ProductController::class, 'update'])->name('products.update');
Route::delete('/products/{product}', [ProductController::class, 'destroy'])->name('products.destroy');
This code defines each route individually and assigns a name to each route using the `name()` method. This allows you to easily generate URLs to these routes using the `route()` helper function in your views and controllers.
Implementing Validation
Validating user input is crucial for ensuring data integrity and preventing security vulnerabilities.
Defining Validation Rules
We’ve already seen how to use the `validate()` method in the controller to define validation rules. The `validate()` method takes an array of rules as its argument. Here are some common validation rules:
- `required`: The field is required.
- `string`: The field must be a string.
- `numeric`: The field must be a number.
- `integer`: The field must be an integer.
- `email`: The field must be a valid email address.
- `min:value`: The field must have a minimum value.
- `max:value`: The field must have a maximum value.
- `unique:table,column`: The field must be unique in the specified table and column.
Using Form Requests
For more complex validation scenarios, you can use Form Requests. Form Requests are classes that encapsulate the validation logic for a specific request.
To create a Form Request, use the Artisan command:
php artisan make:request StoreProductRequest
This command will create a `StoreProductRequest.php` file in the `app/Http/Requests` directory. Open the file and define the authorization and rules methods:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreProductRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true; // Or implement your authorization logic
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
*/
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'description' => 'nullable|string',
'price' => 'required|numeric|min:0',
];
}
}
The `authorize()` method determines if the user is authorized to make the request. By default, it returns `true`. You can implement your own authorization logic here.
The `rules()` method returns an array of validation rules. This is the same array you would pass to the `validate()` method in the controller.
To use the Form Request in your controller, type-hint it in the `store` and `update` methods:
public