April 2019
CRUD operations are the basis for many websites and something which Laravel excels in.
The blog I'm creating for this example isn't anything complex, but it will go through many important aspects and features of Laravel.
If you want to code along with me, you will need to have a local PHP server running. You can follow my article here to get started with Homestead and create a fresh project.
Otherwise, you can run the finished code and see how it all works by following the instructions here:
The first step is to make a Model for the posts.
We will also need a database Migration for the model to interact with, as well as a Controller to handle requests for the model.
Luckily, Laravel provides an excellent Command Line Interface called Artisan which allows us to generate all this (and much more), rather than doing everything manually.
To create these, SSH into the Virtual Machine and change directory to the code folder (if using Homestead):
vagrant ssh
cd code
We are now ready to generate what is needed.
The 'make:model' command will make the model, however adding '-mcr' will also create a Migration and a Controller.
The 'r' indicates that we want to create a Resource Controller. This removes alot of work when making a CRUD system by assigning all the desired routes automatically.
php artisan make:model Post -mcr
If it all went well you will see this:
These new files are found at:
There is a small bit of configuration required in the Model and Migration files, but the majority of the time will be spent in the Controller. Generating these files has actually accomplished a lot in the background as you will see further in the article.
rm database/migrations/2014* resources/views/welcome.blade.php
Before going any further, we should remove a couple of files which won't be used in this project and will just clutter it up, specifically relating to user registration.
Remove the two default User Migrations provided by Laravel (they have a timestamp of 2014, so the below will remove them both). Also remove the default 'Welcome' view for the homepage:
There is also a User API route which isn't needed. Open the file at '/routes/api.php' and remove the route (underneath the comments). This will prevent the route showing when we are listing all the routes.
To view all registered routes, run this:
php artisan route:list
As you can see, we only have one route defined ('/'), for navigating to the homepage/root. Let's change that.
We need to assign routes which match up to the relevant methods on the Controller, which will then allow handling of requests.
Laravel makes this super easy. As we have previously indicated that our Post Model is a Resource, we can define all the routes we need with one line in '/routes/web.php'.
While we are here, lets change the default ('/') view to refer to the Post Controller, as the homepage will be listing the latest posts.
Route::get('/', 'PostController@index');
Route::resource('posts', 'PostController');
Change the contents of the file to:
List the routes again, and you will see that we now have all the routes needed for a CRUD system! 🎉
Take a look at the Post Controller (/app/Http/Controllers/PostController.php), and you will see that all the routes match up with the methods Laravel has already generated. This has cut out a considerable amount of work.
Next, we will move onto the database and define which fields we need for a blog post.
Open up the Posts Migration file at '/database/migrations'.
When working with databases, I find it useful to first write down all columns that will be needed and think about any special conditions. For a post, we will have:
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('title', 100)->unique();
$table->string('content', 2000);
$table->string('category', 30);
$table->string('slug', 200)->unique();
$table->timestamps();
});
}
Now that the fields are mapped out, add them to the 'up' method, which will be used to add the fields listed to the post database table:
All available column types can be found here.
Now, run the migrations to call the 'up' method and insert the new columns to the database:
php artisan migrate
If you connect to the database you will see the new 'posts' table with the columns we specified, ready to accept data:
That's the initial setup done. Now it's time to move on to creating the forms and handling the requests.
When working with Blade, it's important to use a 'layout' file. The contents of this file will carry across all views, thereby eliminating repeated code.
Create the layout file at /resources/views/layout.blade.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>@yield('title') - Laravel Blog</title>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css"
/>
</head>
<body>
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a href="{{ route('posts.index') }}" class="navbar-item">
<img src="{{ asset('img/logo.svg') }}" width="112" height="28" />
</a>
<a
role="button"
class="navbar-burger"
aria-label="menu"
aria-expanded="false"
data-target="navMenu"
>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navMenu" class="navbar-menu">
<div class="navbar-start">
<a href="{{ route('posts.index') }}" class="navbar-item">
All Posts
</a>
</div>
<div class="navbar-end">
<div class="navbar-item">
<div class="buttons">
<a href="{{ route('posts.create') }}" class="button is-info">
<strong>New Post</strong>
</a>
</div>
</div>
</div>
</div>
</nav>
<section class="section">
<div class="container">
<div class="columns is-centered">
<div class="column is-8">
@if (session('notification'))
<div class="notification is-primary">
{{ session('notification') }}
</div>
@endif @yield('content')
</div>
</div>
</div>
</section>
<footer class="footer">
<div class="content has-text-centered">
<p>
<strong>Laravel Blog</strong> |
<a href="https://stevencotterill.com">Steven Cotterill</a>
</p>
</div>
</footer>
<script src="{{ asset('js/nav.js') }}"></script>
</body>
</html>
As usual, I'll be using Bulma for quick styling; I won't be focusing much on design here as it doesn't matter for this project.
The contents of the layout above is basic HTML, with a few Blade directives thrown in, such as 'route()' to link to the routes previously defined, meaning that if the URLs ever change the links will continue to work. There is also a section for a notification, which will be handled later. Yielding 'content' will allow the insertion of content from other views into the layout.
One quick thing; Bulma doesn't come with any JavaScript. To get the mobile navigation to work, create a file at:
/public/js/nav.js
Add the following, and don't worry about looking through this code as it isn't in the scope of this article:
document.addEventListener('DOMContentLoaded', () => {
const $navbarBurgers = Array.prototype.slice.call(
document.querySelectorAll('.navbar-burger'),
0
)
if ($navbarBurgers.length > 0) {
$navbarBurgers.forEach(el => {
el.addEventListener('click', () => {
const target = el.dataset.target
const $target = document.getElementById(target)
el.classList.toggle('is-active')
$target.classList.toggle('is-active')
})
})
}
})
Time to move onto the good stuff! Before posts can be displayed we will need to create some, so this will be the first step.
Open up the PostsController and make this change to the 'create' method:
public function create()
{
// Show create post form
return view('posts.create');
}
The means that when a user navigates to the route we have previously defined at /posts/create (remember, php artisan route:list will show all defined routes), the Controller will return a create view located in the /resources/views/posts folder. Create this folder and file:
/resources/views/posts/create.blade.php
Add the following, and I'll explain some bits after:
@section('title', 'New Post')
@extends('layout')
@section('content')
<h1 class="title">Create a new post</h1>
<form method="post" action="{{ route('posts.store') }}">
@csrf
@include('partials.errors')
<div class="field">
<label class="label">Title</label>
<div class="control">
<input type="text" name="title" value="{{ old('title') }}" class="input" placeholder="Title" minlength="5" maxlength="100" required />
</div>
</div>
<div class="field">
<label class="label">Content</label>
<div class="control">
<textarea name="content" class="textarea" placeholder="Content" minlength="5" maxlength="2000" required rows="10">{{ old('content') }}</textarea>
</div>
</div>
<div class="field">
<label class="label">Category</label>
<div class="control">
<div class="select">
<select name="category" required>
<option value="" disabled selected>Select category</option>
<option value="html" {{ old('category') === 'html' ? 'selected' : null }}>HTML</option>
<option value="css" {{ old('category') === 'css' ? 'selected' : null }}>CSS</option>
<option value="javascript" {{ old('category') === 'javascript' ? 'selected' : null }}>JavaScript</option>
<option value="php" {{ old('category') === 'php' ? 'selected' : null }}>PHP</option>
</select>
</div>
</div>
</div>
<div class="field">
<div class="control">
<button type="submit" class="button is-link is-outlined">Publish</button>
</div>
</div>
</form>
@endsection
The above creates a HTML form with inputs for Title, Content and Category.
A few notes on the blade directives used:
Now is a good time to create the error partial. This is a reusable file which will be used for all forms. Create a 'partials' folder and add the errors file:
/resources/views/partials/errors.blade.php
Add the following, which is checking if there are errors and displaying them if so:
@if ($errors->any())
<div class="notification is-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
Refresh the page at /posts/create and the form will be shown:
If the form is submitted at this point nothing will be shown as we haven't defined what should happen.
Look at the 'action' at the start of the form. A 'post' request is being sent on submission to 'posts.store'. This relates to the 'store' method in the Post Controller.
I've added comments here to explain each part, but you can see how little code Laravel needs to deal with requests:
public function store(Request $request)
{
// Validate posted form data
$validated = $request->validate([
'title' => 'required|string|unique:posts|min:5|max:100',
'content' => 'required|string|min:5|max:2000',
'category' => 'required|string|max:30'
]);
// Create slug from title
$validated['slug'] = Str::slug($validated['title'], '-');
// Create and save post with validated data
$post = Post::create($validated);
// Redirect the user to the created post with a success notification
return redirect(route('posts.show', [$post->slug]))->with('notification', 'Post created!');
}
At the top of the Controller where the 'use' statements are, add this one so we can use a Laravel Helper to create slugs:
use Illuminate\Support\Str;
This is all looking great so far!
I'm using Mass-assignment here to save the entire request in one go. To enable this, the 'fillable' fields need to be set in the Model file:
/app/Post.php
class Post extends Model
{
// Set mass-assignable fields
protected $fillable = ['title', 'content', 'category', 'slug'];
I also want to link to posts using a unique 'slug' rather than the default ID. Add this as well and we are done with this file:
/**
* Get the route key for the model.
*
* @return string
*/
public function getRouteKeyName()
{
return 'slug';
}
}
The create form is now ready to use!
When the form is submitted, the validation rules set are checked against the data received. If it fails, the relevant errors will be shown and the user's input will be retained in the form fields. On success, a new post will be added to the database.
You probably noticed that when the post was created the user is redirected to a blank page at '/posts/slug', which is the correct URL. There is no view defined for viewing the individual post, which brings us to the next part of the CRUD system.
The 'Read' part is pretty easy as all it involves is displaying the content.
After form submission, the user is redirected to the 'posts.show' route, which relates to the 'show' Controller method. Change the method to this in the Post Controller:
public function show(Post $post)
{
// Pass current post to view
return view('posts.show', compact('post'));
}
This view now needs to be created at:
/resources/views/posts/show.blade.php
Add the following to it:
@section('title', $post->title)
@extends('layout')
@section('content')
@include('partials.summary')
@endsection
I've included a partial here named 'summary', as the display of the individual post is going to be reused on the homepage.
Create the partial at:
/resources/views/partials/summary.blade.php
And add the code to display the Post details:
<div class="content">
<a href="{{ route('posts.show', [$post->slug]) }}">
<h1 class="title">{{ $post->title }}</h1>
</a>
<p><b>Posted:</b> {{ $post->created_at->diffForHumans() }}</p>
<p><b>Category:</b> {{ $post->category }}</p>
<p>{!! nl2br(e($post->content)) !!}</p>
<form method="post" action="{{ route('posts.destroy', [$post->slug]) }}">
@csrf @method('delete')
<div class="field is-grouped">
<div class="control">
<a
href="{{ route('posts.edit', [$post->slug])}}"
class="button is-info is-outlined"
>
Edit
</a>
</div>
<div class="control">
<button type="submit" class="button is-danger is-outlined">
Delete
</button>
</div>
</div>
</form>
</div>
A few notes about some Blade directives and other bits I haven't covered yet:
Give the post a refresh and we can now see the post details!
Everything looks as it should here. The date formatting is working, everything is being displayed, and the notification we added earlier in the layout file is working.
Now that the display of a single post is working it's time to show a list of the most recent posts. This relates to the 'index' method of the Post Controller, so let's update it:
public function index()
{
// Get all Posts, ordered by the newest first
$posts = Post::latest()->get();
// Pass Post Collection to view
return view('posts.index', compact('posts'));
}
Then create the relevant view:
/resources/views/posts/index.blade.php
Add the following code, which loops through each post passed to the view and displays a post summary:
@section('title', 'Home')
@extends('layout')
@section('content')
@foreach ($posts as $post)
@include('partials.summary')
@endforeach
@endsection
The next bit of functionality to add to the blog is to allow editing/updating of posts. Thankfully, this is very similar to the process for creating a post, so there isn't much to explain here. Change the 'edit' method in the Post Controller, which is responsible for providing the form view:
public function edit(Post $post)
{
return view('posts.edit', compact('post'));
}
Create the new view at:
/resources/views/posts/edit.blade.php
This form is very similar to the 'create' form, except it has a hidden field to indicate that this is sending a 'patch' request to update the Post:
@section('title', 'Edit Post')
@section('action', route('posts.create'))
@extends('layout')
@section('content')
<h1 class="title">Edit: {{ $post->title }}</h1>
<form method="post" action="{{ route('posts.update', [$post->slug]) }}">
@csrf
@method('patch')
@include('partials.errors')
<div class="field">
<label class="label">Title</label>
<div class="control">
<input type="text" name="title" value="{{ $post->title }}" class="input" placeholder="Title" minlength="5" maxlength="100" required />
</div>
</div>
<div class="field">
<label class="label">Content</label>
<div class="control">
<textarea name="content" class="textarea" placeholder="Content" minlength="5" maxlength="2000" required rows="10">{{ $post->content }}</textarea>
</div>
</div>
<div class="field">
<label class="label">Category</label>
<div class="control">
<div class="select">
<select name="category" required>
<option value="" disabled selected>Select category</option>
<option value="html" {{ $post->category === 'html' ? 'selected' : null }}>HTML</option>
<option value="css" {{ $post->category === 'css' ? 'selected' : null }}>CSS</option>
<option value="javascript" {{ $post->category === 'javascript' ? 'selected' : null }}>JavaScript</option>
<option value="php" {{ $post->category === 'php' ? 'selected' : null }}>PHP</option>
</select>
</div>
</div>
</div>
<div class="field">
<div class="control">
<button type="submit" class="button is-link is-outlined">Update</button>
</div>
</div>
</form>
@endsection
Now to handle the request for when the update is made via the form. This is handled by the 'update' method in the Post Controller, which should be the below:
public function update(Request $request, Post $post)
{
// Validate posted form data
$validated = $request->validate([
'title' => 'required|string|unique:posts|min:5|max:100',
'content' => 'required|string|min:5|max:2000',
'category' => 'required|string|max:30'
]);
// Create slug from title
$validated['slug'] = Str::slug($validated['title'], '-');
// Update Post with validated data
$post->update($validated);
// Redirect the user to the created post woth an updated notification
return redirect(route('posts.index', [$post->slug]))->with('notification', 'Post updated!');
}
Click the 'edit' button on one of the posts, change the content and update it and the changes will be reflected after you have been redirected.
And now onto the final piece of the puzzle, and one of the easiest! You should see by now how quick it is to work with these requests when the initial setup has been handled.
Update the 'destroy' method of the Post Controller to this:
public function destroy(Post $post)
{
// Delete the specified Post
$post->delete();
// Redirect user with a deleted notification
return redirect(route('posts.index'))->with('notification', '"' . $post->title . '" deleted!');
}
Click the 'delete' button on any of the posts and it will be removed from the blog, with a notification being shown to confirm this:
So there you have it; a basic CRUD blog using Laravel and Blade. Hopefully this will help clear up some things or make you want to get started working with Laravel.