How to Create a Blog with Laravel 5+ Part 5 (CRUD Post & Page) Part 1

Advertisement

The moment we’ve been waiting for, the ability to Create, Read, Update and Delete (CRUD) records directly from the database, in the previous part, Part 4 (Authentication) it focused on adding site authentication and making our site secure so it makes more sense that the only user with such privileges are able to implement CRUD, it is also included in our Database Schema Design in Part 3 (Database Schema Design) to include post author ID for each post.

Let’s get started

The first thing we need to do is update our Blog header section to include Menu items for Posts and Pages that link to a form where we can manage content.

_header.blade.php


<!doctype html>
<html lang="{{ config('app.locale') }}">
    <head>
        ...
        <!--nothing changed here-->
        ...
    </head>
    <body>
        
        <div class="blog-masthead">
            <div class="container">
                <nav class="blog-nav">
                    <ul class="nav navbar-nav">
                        <li><a class="blog-nav-item {{ Request::is('/') ? 'active' : '' }}" href="/">Home</a></li>
                        <li><a class="blog-nav-item {{ Request::is('show-case') ? 'active' : '' }}" href="/show-case">Show Case</a></li>
                        <li><a class="blog-nav-item {{ Request::is('services') ? 'active' : '' }}" href="/services">Services</a></li>
                        <li><a class="blog-nav-item {{ Request::is('contact') ? 'active' : '' }}" href="/contact">Contact</a></li>
                    </ul>

                    <!-- Right Side Of Navbar -->
                    <ul class="nav navbar-nav navbar-right">
                        <!-- Authentication Links -->
                        @if (Auth::guest())
                            <li><a class="blog-nav-item {{ Request::is('login') ? 'active' : '' }}" href="{{ route('login') }}">Login</a></li>
                            <li><a class="blog-nav-item {{ Request::is('register') ? 'active' : '' }}" href="{{ route('register') }}">Register</a></li>
                        @else
                            <li class="dropdown">
                                <a href="#" class="blog-nav-item dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
                                    {{ Auth::user()->name }} <span class="caret"></span>
                                </a>

                                <ul class="dropdown-menu" role="menu">
                                    <li><a href="#">All Posts</a></li>
                                    <li><a href="#">Add New</a></li>
                                    <li><a href="#">Categories</a></li>
                                    
                                    <li><hr/></li>

                                    <li><a href="#">All Pages</a></li>
                                    <li><a href="#">Add New</a></li>
                                    
                                    <li><hr/></li>
                                    
                                    <li>
                                        <a class="blog-nav-item" href="{{ route('logout') }}" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">Logout</a>
                                        
                                        <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
                                            {{ csrf_field() }}
                                        </form>
                                    </li>
                                </ul>
                            </li>
                        @endif
                    </ul>
                </nav>
            </div>
        </div>

Now that our Menu items are there and ready it’s time to create forms and blade pages, but before that, it would be ideal to create Post and Page Controller first so we’re a step-by-step process and can save us a lot of time.

Instead of creating Controller and then add methods manually, Laravel does offer Resource Controllers, what it does is it assigns the typical “CRUD” routes to a controller with a single line of code, Woah, handy.

Run the command below to start adding Controllers.

Post Controller


php artisan make:controller PostsController --resource

Page Controller

Before running the command below make sure to backup your current PagesController.php, you can remove it right after so it won’t cause any error when running the command.


php artisan make:controller PagesController --resource

To view your newly added Controllers navigate to this directory app/Http/Controllers.

Great we’re doing good, but at this stage, when you click any of your Blog pages you’ll surely get 404 Error that is because we replace the previous PagesController with a fresh new one, so to add a temporary fix, go and grab your code from the previous file and put it back just right after the new one.

Here’s how it looks on my end.


<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class PagesController extends Controller
{

    /**
    * Front page
    */
    public function getIndex() {
        return view('pages.index');
    }

    /**
    * Show case page
    */
    public function getShowCase() {
        return view('pages.showcase');
    }

    /**
    * Services page
    */
    public function getServices() {
        return view('pages.services');
    }

    /**
    * Contact page
    */
    public function getContact() {
        return view('pages.contact');
    }
    
    // ... new code goes here index(), create() and so on.

}

Now that Controllers have been added successfully, next is we need to update our routes to read the new resource we’ve just created.

Our routes will look like this now.


<?php

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', 'PagesController@getIndex')->name('home');
Route::get('show-case', 'PagesController@getShowCase');
Route::get('services', 'PagesController@getServices');
Route::get('contact', 'PagesController@getContact');

Route::resource('pages', 'PagesController');
Route::resource('posts', 'PostsController');

Auth::routes();

Route List

Artisan command to display list of ruotes, this is gonna be useful soon.


php artisan route:list

views/posts

So here I’ve created a new directory for posts and lists of blade files, I’ve added a comment for each file and it’s self-explanatory.


resources
...
- posts
- - create.blade.php	// This file is used to create a new post item
- - edit.blade.php		// This file is used to edit selected post item
- - index.blade.php		// This file is used to display lists of added posts
- - show.blade.php		// This file is used to display newly added post (optional)
...

PostsController.php

Complete PostsController code, I’ve added comment so I wouldn’t go through each line of code later 😀


<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\Http\Controllers\Controller;
use App\Http\Requests;
use App\Post;
use Session;

class PostsController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        // Fetch data in pagination so only 10 posts per page
        // To get all data you may use get() method
        $posts = Post::where('post_type', 'post')->paginate( 10 );

        return view('posts.index', ['posts' => $posts]);
    }

    /**
     * Show the form for creating a new resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function create()
    {
        // Directly display `posts.create` view blade file
        return view('posts.create');
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        // Validate and filter user inputted data
        // Refer to `References` for more info
        $this->validate($request, [
            'post_title'        => 'required',
            'post_slug'         => 'required|alpha_dash|max:200|unique:posts,post_slug',
            'post_content'      => 'required',
        ]);

        // Create a new Post Model initialization
        // There are many ways to coke an Egg and same in storing data to database in Laravel
        // You might use or prefer this one https://laravel.com/docs/5.4/queries#inserts
        // I just love using Eloquent
        $post = new Post;

        $post->author_ID        = $request->author_ID;
        $post->post_type        = $request->post_type;
        $post->post_title       = $request->post_title;
        $post->post_slug        = $request->post_slug;
        $post->post_content     = $request->post_content;
        $post->category_ID      = $request->category_ID;

        $post->save();
        
        // Store data for only a single request and destory
        Session::flash( 'sucess', 'Post published.' );

        // Redirect to `posts.show` route
        // Use route:list to view the `Action` or where this routes going to
        return redirect()->route('posts.show', $post->id);
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function show($id)
    {
        $post = Post::findOrFail( $id );

        return view('posts.show', [ 'post' => $post ]);
    }

    /**
     * Show the form for editing the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function edit($id)
    {
        $post = Post::findOrFail( $id );

        return view('posts.edit', [ 'post' => $post ]);
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, $id)
    {
        $this->validate($request, [
            'post_title'        => 'required',
            'post_slug'         => 'required|alpha_dash|max:200|unique:posts,post_slug,'.$id,
            'post_content'      => 'required',
        ]);

        // You might prefer to use this one instead https://laravel.com/docs/5.4/queries#updates
        // You choose
        $post = Post::findOrFail($id);

        $post->author_ID        = $request->input('author_ID');
        $post->post_type        = $request->input('post_type');
        $post->post_title       = $request->input('post_title');
        $post->post_slug        = $request->input('post_slug');
        $post->post_content     = $request->input('post_content');
        $post->category_ID      = $request->input('category_ID');

        $post->save();

        Session::flash('success', 'Post updated.');

        return redirect()->route('posts.edit', $id);
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function destroy($id)
    {
        // Retrieve records and throw an exception if a model is not found
        $post = Post::findOrFail( $id );

        // https://laravel.com/docs/5.4/queries#deletes
        $post->delete();
        
        Session::flash('success', 'Post deleted.');
        
        return redirect()->route('posts.index');
    }
}

Now that we already have PostsController added, it’s time to work on our Views.

Advertisement

_header.blade.php


<!doctype html>
<html lang="{{ config('app.locale') }}">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        
        <!-- CSRF Token -->
        <meta name="csrf-token" content="{{ csrf_token() }}">
        
        <title>{{ config('app.name') }} @yield('title')</title>
        
        <!-- Bootstrap core CSS -->
        <link rel="stylesheet" href="{{ asset('css/app.css') }}" />
        
        <!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
        <link rel="stylesheet" href="{{ asset('css/ie10-viewport-bug-workaround.css') }}" />
        
        <!-- Custom styles for this template -->
        <link rel="stylesheet" href="{{ asset('css/blog.css') }}" />
        
        @yield('stylesheet')
    </head>
    <body>
        
        <div class="blog-masthead">
            <div class="container">
                <nav class="blog-nav">
                    <ul class="nav navbar-nav">
                        <li><a class="blog-nav-item {{ Request::is('/') ? 'active' : '' }}" href="/">Home</a></li>
                        <li><a class="blog-nav-item {{ Request::is('show-case') ? 'active' : '' }}" href="/show-case">Show Case</a></li>
                        <li><a class="blog-nav-item {{ Request::is('services') ? 'active' : '' }}" href="/services">Services</a></li>
                        <li><a class="blog-nav-item {{ Request::is('contact') ? 'active' : '' }}" href="/contact">Contact</a></li>
                    </ul>

                    <!-- Right Side Of Navbar -->
                    <ul class="nav navbar-nav navbar-right">
                        <!-- Authentication Links -->
                        @if (Auth::guest())
                            <li><a class="blog-nav-item {{ Request::is('login') ? 'active' : '' }}" href="{{ route('login') }}">Login</a></li>
                            <li><a class="blog-nav-item {{ Request::is('register') ? 'active' : '' }}" href="{{ route('register') }}">Register</a></li>
                        
                        @else

                            <li class="dropdown">
                                <a href="#" class="blog-nav-item dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
                                    {{ Auth::user()->name }} <span class="caret"></span>
                                </a>

                                <ul class="dropdown-menu" role="menu">
                                    <li><a href="{{ route('posts.index') }}">All Posts</a></li>
                                    <li><a href="{{ route('posts.create') }}">Add New</a></li>
                                    <li><a href="{{ route('categories.index') }}">Categories</a></li>
                                    
                                    <li><hr/></li>
                                    
                                    <li><a href="{{ route('pages.index') }}">All Pages</a></li>
                                    <li><a href="{{ route('pages.create') }}">Add New</a></li>
                
                                    <li><hr/></li>
                                    
                                    <li>
                                        <a class="blog-nav-item" href="{{ route('logout') }}" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">Logout</a>
                                        
                                        <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
                                            {{ csrf_field() }}
                                        </form>
                                    </li>
                                </ul>
                            </li>
                        @endif
                    </ul>
                </nav>
            </div>
        </div>

        {{--
            Check if there is a success Session key
            If So display the Session key value
            
            More to this later
        --}}
        <div class="container">
            <div class="row">
                <div class="col-md-12">

                    @if( Session::has('success') )
                        <div class="mt-5 alert alert-success" role="alert">
                            {{ Session::get('success') }}
                        </div>
                    @endif

                </div>
            </div>
        </div>

index.blade.php


@extends('main')

@section('title', '| Posts')

@section('content')

	<div class="container">
		
		{{-- Check if current user is logged-in or a guest --}}
		@if (Auth::guest())
			
			<p class="mt-5">Cheatn?, please <a href="/login/">login</a> to continue.</p>
			
		@else
			
			<div class="blog-header">
		        <h1 class="blog-title">Posts <a class="btn btn-sm btn-primary" href="{{ route('posts.create') }}">Add New</a></h1>
		    </div>
			
			<div class="row">
				<div class="col-md-12">
					
					<table class="table">
						<tr>
							<th>Title</th>
							<th>Author</th>
							<th>Content</th>
							<th>Category</th>
							<th>Date</th>
							<th>&nbsp;</th>
						</tr>
						<tr>
							{{-- Blade if and else --}}
							@if( $posts->count() )
								{{-- Blade foreach --}}
								@foreach( $posts as $post )
									<tr>
										<td>
											<strong>
												<a href="{{ route('posts.edit', $post->id) }}">
													{{ $post->post_title }}
												</a>
											</strong>
										</td>
										<td>{{ $post->author_ID }}</td>
										<td>
											@if ( strlen( $post->post_content ) > 60 )
												{{ substr( $post->post_content, 0, 60 ) }} ...
											@else
												{{ $post->post_content }}
											@endif
										</td>
										<td>
											<a href="#">
												{{ $post->category_ID }}
											</a>
										</td>
										<td>Published {{ date( 'j/m/Y', strtotime( $post->created_at ) ) }}</td>
										<td>
                                            <form class="d-inline" action="{{ route('posts.destroy', $post->id) }}" method="POST">
                                                {{ csrf_field() }}
                                                {{ method_field('DELETE') }}

                                                <input type="submit" value="Delete" class="btn btn-sm btn-danger" />
                                            </form>

                                            <a class="btn btn-sm btn-info" href="{{ route('posts.edit', $post->id) }}">Edit</a>
                                        </td>
									</tr>
								@endforeach
							@else
                                
                                <tr>
                                    <td colspan="5">No post has been added yet!</td>
                                </tr>

                            @endif
						</tr>
					</table>

					{{ $posts->links() }}

				</div>
			</div>

		@endif

	</div>
	
@endsection

create.blade.php


@extends('main')

@section('title', '| Add New Post')

@section('content')

	<div class="container">
		
		{{-- Check if current user is logged-in or a guest --}}
		@if (Auth::guest())
			
			<p class="mt-5">Please <a href="/login/">login</a> to add a new post.</p>
			
		@else

			<div class="blog-header">
		        <h1 class="blog-title">Add New Post</h1>
		    </div>

			<div class="row">
				<div class="push-md-2 col-md-8">
					
					<form action="{{ route('posts.store') }}" method="POST">
						{{ csrf_field() }}
						<input type="hidden" name="author_ID" value="{{ Auth::id() }}" />
						<input type="hidden" name="post_type" value="post" />
						
						<div class="form-group{{ $errors->has('post_title') ? ' has-error' : '' }}">
							<label for="post_title">Title</label> <br/>
							<input type="text" name="post_title" id="post_title" value="{{ old('post_title') }}" />
							
							@if ($errors->has('post_title'))
	                            <span class="help-block">
	                                <strong>{{ $errors->first('post_title') }}</strong>
	                            </span>
	                        @endif
						</div>

						<div class="form-group{{ $errors->has('post_slug') ? ' has-error' : '' }}">
							<label for="post_slug">Slug</label> <br/>
							<input type="text" name="post_slug" id="post_slug" value="{{ old('post_slug') }}" />

							@if ($errors->has('post_slug'))
	                            <span class="help-block">
	                                <strong>{{ $errors->first('post_slug') }}</strong>
	                            </span>
	                        @endif
						</div>
						
						<div class="form-group{{ $errors->has('post_content') ? ' has-error' : '' }}">
							<label for="post_content">Content</label> <br/>
							<textarea name="post_content" id="post_content" cols="80" rows="6">{{ old('post_content') }}</textarea>

							@if ($errors->has('post_content'))
	                            <span class="help-block">
	                                <strong>{{ $errors->first('post_content') }}</strong>
	                            </span>
	                        @endif
						</div>
						
						<div class="form-group">
							<label for="title">Category</label> <br/>
							<select name="category_ID" id="category_ID">
								<option value="1">1</option>
								<option value="2">2</option>
							</select>
						</div>

						<div class="form-group">
							<input type="submit" class="btn btn-primary" value="Publish" />
							<a class="btn btn-primary" href="{{ route('posts.index') }}">Cancel</a>
						</div>
					</form>

				</div>
			</div>

		@endif

	</div>
	
@endsection

edit.blade.php


@extends('main')

@section('title', '| Edit Post')

@section('content')
	
	<div class="container">
		
		{{-- Check if current user is logged-in or a guest --}}
		@if (Auth::guest())
			
			<p class="mt-5">Cheatn?, please <a href="/login/">login</a> to continue.</p>
			
		@else
			<div class="blog-header">
		        <h1 class="blog-title">Edit Post <a class="btn btn-sm btn-primary" href="{{ route('posts.show', $post->id) }}">Preview Changes</a></h1>
		    </div>

			<div class="row">
				<div class="push-md-2 col-md-8">
					
					{{--
						Check route:list for `posts.update` for more info
						URL is posts/{post}, `{post}` meaning that we have to supply ID
					--}}
					<form action="{{ route('posts.update', $post->id) }}" method="POST">
						{{ csrf_field() }}
						
						{{--
							HTML forms do not support PUT, PATCH or DELETE actions.
							So, when defining PUT, PATCH or  DELETE routes that are called from an HTML form,
							you will need to add a hidden _method field to the form.
						--}}
						{{ method_field('PUT') }}
						
						<input type="hidden" name="author_ID" value="{{ Auth::id() }}" />
						<input type="hidden" name="post_type" value="post" />
						
						<div class="form-group{{ $errors->has('post_title') ? ' has-error' : '' }}">
							<label for="post_title">Title</label> <br/>
							<input type="text" name="post_title" id="post_title" value="{{ $post->post_title }}" />
							
							@if ($errors->has('post_title'))
	                            <span class="help-block">
	                                <strong>{{ $errors->first('post_title') }}</strong>
	                            </span>
	                        @endif
						</div>

						<div class="form-group{{ $errors->has('post_slug') ? ' has-error' : '' }}">
							<label for="post_slug">Slug</label> <br/>
							<input type="text" name="post_slug" id="post_slug" value="{{ $post->post_slug }}" />

							@if ($errors->has('post_slug'))
	                            <span class="help-block">
	                                <strong>{{ $errors->first('post_slug') }}</strong>
	                            </span>
	                        @endif
						</div>
						
						<div class="form-group{{ $errors->has('post_content') ? ' has-error' : '' }}">
							<label for="post_content">Content</label> <br/>
							<textarea name="post_content" id="post_content" cols="80" rows="6">{{ $post->post_content }}</textarea>
							
							@if ($errors->has('post_content'))
	                            <span class="help-block">
	                                <strong>{{ $errors->first('post_content') }}</strong>
	                            </span>
	                        @endif
						</div>
						
						<div class="form-group">
							<label for="title">Category</label> <br/>
							<select name="category_ID" id="category_ID">
								<option value="1" {{ 1 == $post->category_ID ? 'selected="selected"' : '' }}>1</option>
								<option value="2" {{ 2 == $post->category_ID ? 'selected="selected"' : '' }}>2</option>
							</select>
						</div>

						<div class="form-group">
							<input type="submit" class="btn btn-primary" value="Update" />
							<a class="btn btn-primary" href="{{ route('posts.index') }}">Cancel</a>
						</div>
					</form> 

				</div>
			</div>

		@endif

	</div>
	
@endsection

show.blade.php


@extends('main')

@section('title', '| Post')

@section('content')
		
	<div class="container">
		
		{{-- Check if current user is logged-in or a guest --}}
		@if (Auth::guest())
			
			<p class="mt-5">Cheatn?, please <a href="/login/">login</a> to continue.</p>
			
		@else

			<div class="blog-header">
		        <h1 class="blog-title">{{ $post->post_title }}</h1>
		        <p>{{ $post->category_ID }} / {{ date('M j, Y', strtotime( $post->created_at )) }} <a href="{{ route('posts.edit', $post->id) }}">{Edit}</a></p>
		    </div>
			
			<div class="row">
				<div class="col-sm-12 blog-main">

					<div class="blog-content">
						{{-- Inserts HTML line breaks before all newlines in a string --}}
						{!! nl2br( $post->post_content ) !!}
					</div>

				</div>
			</div>
			
		@endif

	</div>
	
@endsection

{{ vs {!!

By default, Blade {{ }} statements are automatically sent through PHP’s htmlentities function to prevent XSS attacks. If you do not want your data to be escaped, you may use the following {!! !!}

Wow what a long article, I think let’s break it into two pieces :-), see ya in Part 2, which focuses on Pages Controllers and Views.

That’s it happy coding ^_^

References and Credits

Database: Query Builder

Form Method Spoofing

CSRF Protection

Available Validation Rules

Flash Data

Advertisement