Creating a Laravel CRUD blog

How to build a simple Laravel CRUD (Create, Read, Update, Delete) blog using the built-in Blade template system

April 2019

CRUD operations are the basis for many websites and something which Laravel excels in.

In this series, I will be going through how to create a simple blog using Laravel. Here is the outline:

  • Part 1: Create the blog using Laravel and Blade templates
  • Part 2: Update the blog to provide a REST API

The blog itself 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:

Getting started

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:

Generating the Post Model, Migration and Resource Controller

These new files are found at:

  • Model: /app/Post.php
  • Migration: /database/migrations/timecreateposts_table.php
  • Controller: /app/Http/Controllers/PostController.php

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

Cleaning up

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

A list of all routes

As you can see, we only have one route defined ('/'), for navigating to the homepage/root. Let's change that.

Mapping CRUD/Resource Routes

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! 🎉

A list of all routes after defining the resource routes

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.

Database Migrations

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();
    });
}
  • Title: A string, with a max-length of 100 characters. This will need to be unique (each title can only be used once)
  • Content: A string, with a max-length of 2000 characters
  • Category: A string, with a max-length of 30 characters
  • Slug: A string, with a max-length of 200 characters. This also needs to be unique as it will be used in the URL to navigate to posts

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

Running the Database Migration

If you connect to the database you will see the new 'posts' table with the columns we specified, ready to accept data:

Empty database with the new columns shown

That's the initial setup done. Now it's time to move on to creating the forms and handling the requests.

Layout file

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 is basic HTML, with a few Blade directives thrown in. These are added via '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.

Add this to the layout file:

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')
      })
    })
  }
})

CRUD - Create

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:

  • @section('title', 'New Post'): Defines the page title, which is yielded in the layout
  • @extends('layout'): Extend/use the layout
  • @section('content') ... @endsection: Everything in here goes into the 'content' section of the layout
  • @csrf: This creates a hidden field with a unique token for each session, providing security against Cross-site request forgery
  • @include('partials.errors'): Includes an errors partial, which will be created next. This will show any errors returned if the form submission wasn't successful
  • old('field_name'): If the form submission wasn't successful this will add the data previously entered to the form. This means that the user won't have to re-enter everything each time they submit

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:

Create new post form

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.

CRUD - Read

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:

  • $post->field_name: This allows access to the the current Post data which was passed through from the 'show' method in the Post Controller previously
  • {{ $post->created_at->diffForHumans() }}: Laravel comes with Carbon built in to handle dates. 'diffForHumans' is a method which is being used to format the 'created_at' into a readable string
  • {!! nl2br(e($post->content)) !!}: The default syntax in Blade to access data ({{ field }}) will strip whitespace, which will need to be kept when working with a textarea. This workaround still applies validation but runs the output through nl2br to insert line-breaks
  • @method('delete'): The 'delete' action isn't supported by forms. This creates a hidden field for Laravel, which tells it that the form is actually submitting a 'delete' request rather than 'post'

Give the post a refresh and we can now see the post details!

View post

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

CRUD - Update

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.

Editing a post

CRUD - Delete

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:

Deleting a post

Finishing up

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.


Want to hire me for a project, or to fix something on your website? Get in touch 😀