Building a Blog in Laravel – Part3 – Create a post

Now to get on to building functionality. The first item on the list is to create a post. For this to work we will need to be able to mock new users and the actual post data. The first test we will write is to check that the site presents the form for creating a new post.

Note: If this is the first article in the series please start at the beginning.

Tests for new post form

<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\User;
use App\Posts;
use Illuminate\Foundation\Testing\DatabaseMigrations;


class PostTestWithoutMiddleware extends TestCase
{
    use DatabaseMigrations;

    /**
     * Check user can login and see new-post form
     */
    public function testNewPostGet()
    {
        $user = $this->authenticateUser();

        $this->actingAs($user)
            ->withSession(['foo' => 'bar'])
            ->get('new-post')
            ->assertSuccessful();
    }

    /**
     * Check invalid login/nologin cannot open /new-post
     */
    public function testCreateNewPostInvalidUser()
    {
        $user = factory(User::class)->create();

        $this->actingAs($user)
            ->withSession(['users' => 'fred bloggs'])
            ->get('/new-post')
            ->assertStatus(302);
    }

    /**
     * Check valid user can open /new-post
     */
    public function testNewPostCreation()
    {
        $this->withoutMiddleware();

        $user = $this->authenticateUser();

        $case = factory(Posts::class)->raw(
            [
                'title' => 'test',
                'body' => '123',
                'slug' => 'adkf'
            ]);

        $response = $this->actingAs($user)
            ->post('/new-post', $case);

        $response->assertStatus(302);
        $response->assertSee('edit/test');
    }

    /**
     * Create mock user ensuring role is set to author
     *
     * @return User
     */
    public function authenticateUser(): User
    {
        return factory(User::class)->create([
            'id' => 1,
            'name' => 'fred',
            'role' => 'author']);
    }
}

These will of course fail. Now we need to write the code to get it passing. This will involve creating the Routes, Models & Views.

Create the route in routes/web.php

Route::get('new-post', 'PostController@create');

Create the Post controller

php artisan make:controller PostController

This will create the file app/Http/Controllers/PostController.php. In this add the following code:

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class PostController extends Controller
{
    public function create(Request $request)
    {
        if($request->user()->can_post())
        {
            return view('posts.create');
        }
        else
        {
            return redirect('/')->withErrors('You have not sufficient permissions for writing post');
        }
    }
}

Now the test should pass. Unfortunately it didn’t. After quite a bit of research I found that I had two issues here. Firstly the test wouldn’t pass without the view working. So this had to be resolved. Secondly the documentation regarding testing authentication doesn’t seem to work once I got the view working. I managed to combine several different ways of doing this until I got a working test. This is as follows:

public function testNewPostGet()
{
    $user = factory(User::class)->create([
        'role' => 'author'
    ]);

    $this->actingAs($user)
        ->withSession(['foo' => 'bar'])
        ->get('/new-post')
        ->assertSuccessful();

}

You can see in the above cost that we actually create a user. The important part is to assign the role to one that has permission to create a post. Author will suffice in this instance.

App Template

Now to create the views. Under resources/views create app.blade.php with the following content:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Blog Demo | Find All Together</title>
    <link href="{{ asset('/css/app.css') }}" rel="stylesheet">
    <!-- Fonts -->
    <link href='//fonts.googleapis.com/css?family=Roboto:400,300' rel='stylesheet' type='text/css'>
    <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
    <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
    <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
    <![endif]-->
  </head>
  <body>
    <nav class="navbar navbar-default">
      <div class="container-fluid">
        <div class="navbar-header">
          <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
            <span class="sr-only">Toggle Navigation</span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </button>
          <a class="navbar-brand" href="http://www.findalltogether.com">Find All Together</a>
        </div>
        <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
          <ul class="nav navbar-nav">
            <li>
              <a href="{{ url('/') }}">Home</a>
            </li>
          </ul>
          <ul class="nav navbar-nav navbar-right">
            @if (Auth::guest())
            <li>
              <a href="{{ url('/login') }}">Login</a>
            </li>
            <li>
              <a href="{{ url('/register') }}">Register</a>
            </li>
            @else
            <li class="dropdown">
              <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">{{ Auth::user()->name }} <span class="caret"></span></a>
              <ul class="dropdown-menu" role="menu">
                @if (Auth::user()->can_post())
                <li>
                  <a href="{{ url('/new-post') }}">Add new post</a>
                </li>
                <li>
                  <a href="{{ url('/user/'.Auth::id().'/posts') }}">My Posts</a>
                </li>
                @endif
                <li>
                  <a href="{{ url('/user/'.Auth::id()) }}">My Profile</a>
                </li>
                <li>
                  <a href="{{ url('/auth/logout') }}">Logout</a>
                </li>
              </ul>
            </li>
            @endif
          </ul>
        </div>
      </div>
    </nav>
    <div class="container">
      @if (Session::has('message'))
      <div class="flash alert-info">
        <p class="panel-body">
          {{ Session::get('message') }}
        </p>
      </div>
      @endif
      @if ($errors->any())
      <div class='flash alert-danger'>
        <ul class="panel-body">
          @foreach ( $errors->all() as $error )
          <li>
            {{ $error }}
          </li>
          @endforeach
        </ul>
      </div>
      @endif
      <div class="row">
        <div class="col-md-10 col-md-offset-1">
          <div class="panel panel-default">
            <div class="panel-heading">
              <h2>@yield('title')</h2>
              @yield('title-meta')
            </div>
            <div class="panel-body">
              @yield('content')
            </div>
          </div>
        </div>
      </div>
      <div class="row">
        <div class="col-md-10 col-md-offset-1">
          <p>Copyright © 2015 | <a href="http://www.findalltogether.com">Find All Together</a></p>
        </div>
      </div>
    </div>
    <!-- Scripts -->
    <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.1/js/bootstrap.min.js"></script>
  </body>
</html>

Create Template

Next under resources/views/posts create create.blade.php with the following content:

@extends('app')
@section('title')
    Add New Post
@endsection
@section('content')
    <form action="/new-post" method="post">
        <input type="hidden" name="_token" value="{{ csrf_token() }}">
        <div class="form-group">
            <input required="required" value="{{ old('title') }}" placeholder="Enter title here" type="text" name = "title"class="form-control" />
        </div>
        <div class="form-group">
            <textarea name='body'class="form-control">{{ old('body') }}</textarea>
        </div>
        <input type="submit" name='publish' class="btn btn-success" value = "Publish"/>
        <input type="submit" name='save' class="btn btn-default" value = "Save Draft" />
    </form>
@endsection

Tests will still fail, so onto the models. You will need the create/edit three files in the app/ folders. These are as follows:

User Model

Create or Edit app/User.php

<?php namespace App;
use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
class User extends Model implements AuthenticatableContract, CanResetPasswordContract {
    use Authenticatable, CanResetPassword;
    /**
     * The database table used by the model.
     *
     * @var string
     */
    protected $table = 'users';
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['name', 'email', 'password'];
    /**
     * The attributes excluded from the model's JSON form.
     *
     * @var array
     */
    protected $hidden = ['password', 'remember_token'];
    // user has many posts
    public function posts()
    {
        return $this->hasMany('App\Posts','author_id');
    }
    // user has many comments
    public function comments()
    {
        return $this->hasMany('App\Comments','from_user');
    }
    public function can_post()
    {
        $role = $this->role;
        if($role == 'author' || $role == 'admin')
        {
            return true;
        }
        return false;
    }
    public function is_admin()
    {
        $role = $this->role;
        if($role == 'admin')
        {
            return true;
        }
        return false;
    }
}

Posts Model

Create or Edit app/Posts.php

<?php namespace App;
use Illuminate\Database\Eloquent\Model;
// instance of Posts class will refer to posts table in database
class Posts extends Model {
    //restricts columns from modifying
    protected $guarded = [];
    // posts has many comments
    // returns all comments on that post
    public function comments()
    {
        return $this->hasMany('App\Comments','on_post');
    }
    // returns the instance of the user who is author of that post
    public function author()
    {
        return $this->belongsTo('App\User','author_id');
    }
}

Comments Model

Create or Edit app/Comments.php

<?php namespace App;
use Illuminate\Database\Eloquent\Model;
class Comments extends Model {
    //comments table in database
    protected $guarded = [];
    // user who has commented
    public function author()
    {
        return $this->belongsTo('App\User','from_user');
    }
    // returns post of any comment
    public function post()
    {
        return $this->belongsTo('App\Posts','on_post');
    }
}

Mock Factory

One challenge during this was how to mock the data for the post itself. As you can see in the test I have used the same method as for authentication. Little did I know this worked due to a factory class using Faker/Generator. Once I worked that out I created a new factory under database/factories. This is as follows:

<?php

use Faker\Generator as Faker;

/*
|--------------------------------------------------------------------------
| Model Factories
|--------------------------------------------------------------------------
|
| This directory should contain each of the model factory definitions for
| your application. Factories provide a convenient way to generate new
| model instances for testing / seeding your application's database.
|
*/

$factory->define(App\Posts::class, function (Faker $faker) {
    $id = 1;

    return [
        'title' => $faker->unique()->title,
        'body' => $faker->text,
        'author_id' => $id,
        'slug' => $faker->text,
        'active' => $faker->boolean,
        'id' => $id
    ];
});

Note: When using the faker plugin you need to be aware of the formatter directives. These are specified in vendor/fzaninotto/faker/src/Faker/Generator.php

Now the tests should pass.

Copyright © 2020 | Ben Hutton