Building a Blog With Reactjs And Laravel

Building a Blog With Reactjs And Laravel Part 1

In this tutorial we will build a blog application using laravel framework as the backend and reactjs as the frontend.

 

 

Requirements:

  • Laravel framework (preferable 5.8)
  • Reactjs
  • React redux store
  • Token authentication

 

In this series we will build a full featured application using Laravel and Reactjs. The app we are going to implement is a small blog and we will cover creating admin panel and website using reactjs. This is a big series so be aware to follow each article carefully to get the idea. In this article we will start by preparing the project then in each article i will discuss one or more topics until we finish it.

 

What you learn from this series

  • Using Reactjs to build a big project.
  • Using React router and define routes in separate files for a big project.
  • Using Redux to manipulate the store and how to define actions and reducers.
  • Using multiple stores and combine them together using combineReducer.
  • Dealing with async requests in redux actions.
  • Accessing the store in components using props.
  • Using components inside other components and displaying modals.
  • Submitting forms and displaying validation errors.
  • Using pagination to paginate long lists.

 

For the purpose of this tutorial i will choose version 5.8 of laravel framework, so let’s begin by creating a fresh laravel project using composer:

composer create-project laravel/laravel react-laravel-blog "5.8.*" --prefer-dist

 

Add the proper database settings in .env file:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=react_laravel_blog
DB_USERNAME=<root>
DB_PASSWORD=*******

 

Next let’s create some migrations that represent those tables:

  • Categories
  • Posts
  • Comments
  • Tags

php artisan make:migration create_categories_table
php artisan make:migration create_posts_table
php artisan make:migration create_tags_table
php artisan make:migration create_comments_table
php artisan make:migration create_post_tag_table

Besides that laravel provides the users migration out of the box so we don’t need to create it. Now we will open each migration and add the needed columns to create such migration.

database/migrations/YYYY_mm_dd_XXXXX_create_categories_table

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateCategoriesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('categories', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('title');
            $table->string('slug', 355);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('categories');
    }
}

database/migrations/YYYY_mm_dd_XXXXX_create_posts_table

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreatePostsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('title');
            $table->string('slug', 355);
            $table->text('content')->nullable();
            $table->string('image')->nullable();
            $table->tinyInteger('published')->default(1)->comment('1=published 2=draft');
            $table->bigInteger('category_id')->nullable()->unsigned();
            $table->bigInteger('user_id')->nullable()->unsigned();
            $table->timestamps();

            $table->foreign('category_id')->references('id')->on('categories')->onDelete('set null');
            $table->foreign('user_id')->references('id')->on('users')->onDelete('set null');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('posts');
    }
}

database/migrations/YYYY_mm_dd_XXXXX_create_tags_table

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateTagsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('tags', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('title');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('tags');
    }
}

database/migrations/YYYY_mm_dd_XXXXX_create_post_tag_table

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreatePostTagTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('post_tag', function (Blueprint $table) {
            $table->bigInteger('post_id')->unsigned();
            $table->bigInteger('tag_id')->unsigned();
            $table->foreign('post_id')->references('id')->on('posts')
                ->onDelete('cascade');
            $table->foreign('tag_id')->references('id')->on('tags')
                ->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('post_tag');
    }
}

database/migrations/YYYY_mm_dd_XXXXX_create_comments_table

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateCommentsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('comments', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->bigInteger('user_id')->unsigned();
            $table->bigInteger('post_id')->unsigned();
            $table->mediumText('comment');
            $table->tinyInteger('approved')->default(0)->comment('0=pending 1=approved 2=disapproved');
            $table->timestamps();

            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
            $table->foreign('post_id')->references('id')->on('posts')->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('comments');
    }
}

Also we need to update the users migration, so we need to include two columns ‘is_admin‘ which indicates the admin user, and ‘api_token‘ which will be in the authentication process to store tokens when we login and register users.

database/migrations/YYYY_mm_dd_XXXXX_create_users_table

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();

            $table->tinyInteger('is_admin')->default(0);
            $table->string('api_token', 80)->unique()->nullable()->default(null);
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
}

Run

php artisan migrate

The next step is to create the models, we can do this manually or by using laravel artisan command:

php artisan make:model Category
php artisan make:model Post
php artisan make:model Tag
php artisan make:model PostTag
php artisan make:model Comment

app/Category.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Category extends Model
{
    public function posts()
    {
        return $this->hasMany(Post::class, 'category_id');
    }
}

app/Comment.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    protected $appends = ['date_formatted'];

    public function post()
    {
        return $this->belongsTo(Post::class, 'post_id');
    }

    public function user()
    {
        return $this->belongsTo(User::class, 'user_id');
    }

    public function getDateFormattedAttribute()
    {
        return \Carbon\Carbon::parse($this->created_at)->format('Y/m/d h:i a');
    }
}

app/Post.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{

    protected $appends = ["image_url", "date_formatted", "excerpt"];


    /**
    * return the image url to be displayed on react templates
    */
    public function getImageUrlAttribute()
    {
        return $this->image!=""?url("uploads/" . $this->image):"";
    }

    public function category()
    {
        return $this->belongsTo(Category::class, 'category_id');
    }

    public function user()
    {
        return $this->belongsTo(User::class, 'user_id');
    }

    public function comments()
    {
        return $this->hasMany(Comment::class, 'post_id')->with('user', 'post');
    }


    /**
    * approved comments to be displayed on react website
    */
    public function approvedComments()
    {
        return $this->hasMany(Comment::class, 'post_id')->with('user', 'post')->where('approved', 1);
    }

    public function tags()
    {
        return $this->belongsToMany(Tag::class, 'post_tag', 'post_id', 'tag_id');
    }

    public function getDateFormattedAttribute()
    {
        return \Carbon\Carbon::parse($this->created_at)->format('F d, Y');
    }

    public function getExcerptAttribute()
    {
        return substr(strip_tags($this->content), 0, 100);
    }
}

app/Tag.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Tag extends Model
{
    public function posts()
    {
        return $this->belongsToMany(Post::class, 'post_tag', 'post_id', 'tag_id');
    }
}

app/User.php

<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password', 'is_admin'
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token'
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];


    public function posts()
    {
        return $this->hasMany(Post::class, 'user_id');
    }

    public function comments()
    {
        return $this->hasMany(Comment::class, 'user_id');
    }
}

In the above code i have added the required relations in each model that we will need when we retrieve or insert. At first i have setup a relation between categories and posts. The category has many posts and the post belongs to one category represented as:

public function posts()
    {
        return $this->hasMany(Post::class, 'category_id');
    }

public function category()
    {
        return $this->belongsTo(Category::class, 'category_id');
    }

Next i have added a relation between post and comment, the comment belongs to one post and the post has many comments. Also the post belongs to one user and the user has many posts.

For the tags the relation is many to many so the post belongs to many tags and tag belongs to many posts so i added two relations in both models which is the belongsToMany() method like this in Post model:

public function tags()
    {
        return $this->belongsToMany(Tag::class, 'post_tag', 'tag_id', 'post_id');
    }

Users Table Seeder

Let’s create a seeder for the users table. We will insert the admin user for our app.

php artisan make:seeder UserTableSeeder

database/seeds/UserTableSeeder.php

<?php

use Illuminate\Database\Seeder;

class UserTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $user = \App\User::create([
            'name' => 'admin',
            'email' => 'admin@email.com',
            'password' => bcrypt('admin'),
            'is_admin' => 1
        ]);
    }
}

Update database/seeds/DatabaseSeeder.php to include the seeder

<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
         $this->call(UserTableSeeder::class);
    }
}

In terminal run

php artisan db:seed

Now after we created the database and models let’s move on to create the controllers.

 

Handling Authentication

The first step we want to handle is the authentication process as all other system operations based on this process for example you can’t add or delete posts unless you login.

Open app/Exceptions/Handler.php and update it as follows:

<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;

class Handler extends ExceptionHandler
{
    /**
     * A list of the exception types that are not reported.
     *
     * @var array
     */
    protected $dontReport = [
        //
    ];

    /**
     * A list of the inputs that are never flashed for validation exceptions.
     *
     * @var array
     */
    protected $dontFlash = [
        'password',
        'password_confirmation',
    ];

    /**
     * Report or log an exception.
     *
     * @param  \Exception  $exception
     * @return void
     */
    public function report(Exception $exception)
    {
        parent::report($exception);
    }

    /**
     * Render an exception into an HTTP response.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Exception  $exception
     * @return \Illuminate\Http\Response
     */
    public function render($request, Exception $exception)
    {
        return parent::render($request, $exception);
    }

    protected function unauthenticated($request, AuthenticationException $exception)
    {
        if ($request->expectsJson()) {
            return response()->json(['state' => 0, 'message' => 'Unauthenticated.'], 401);
        }

        return redirect()->guest(route('auth.login'));
    }
}

Here we overrided the unauthenticated() method which called when user tries to access a protected resource, in this case i check if the request is a json request then it returns a json response with ‘unauthenticated’ message otherwise it redirect to the login page.

 

Open app/Http/Middleware/Authenticate.php and update it as follows

<?php

namespace App\Http\Middleware;

use Illuminate\Auth\Middleware\Authenticate as Middleware;

class Authenticate extends Middleware
{
    /**
     * Get the path the user should be redirected to when they are not authenticated.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return string
     */
    protected function redirectTo($request)
    {
        if ($request->expectsJson()) {
             return response()->json(['state' => 0, 'message' => 'Unauthenticated'], 401);
        } else {
             return route('login');
        }
    }
}

 

Login and register controllers

We need to update both the login and register controllers so that they can work with api requests.

Open app/Http/Controllers/Auth/LoginController.php

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;

class LoginController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Login Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles authenticating users for the application and
    | redirecting them to your home screen. The controller uses a trait
    | to conveniently provide its functionality to your applications.
    |
    */

    use AuthenticatesUsers;

    /**
     * Where to redirect users after login.
     *
     * @var string
     */
    protected $redirectTo = '/home';

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest')->except('logout', 'checkAuth');
    }

    public function login(Request $request)
    {
        $this->validateLogin($request);

        if ($this->attemptLogin($request)) {
            $user = $this->guard()->user();


            $api_token = Str::random(60);

            $user->api_token = $api_token;

            $user->save();

            return response()->json([
                'user' => $user->toArray(),
            ]);
        }

        return $this->sendFailedLoginResponse($request);
    }

    public function logout(Request $request)
    {
        $user = Auth::guard('api')->user();

        if ($user) {
            $user->api_token = null;
            $user->save();

            return response()->json(['data' => 'User logged out.'], 200);
        }

        return response()->json(['state' => 0, 'message' => 'Unauthenticated'], 401);
    }

    public function checkAuth(Request $request)
    {
        $user = Auth::guard('api')->user();

        if ($user && $user->is_admin) {
            return response()->json(['state' => 1], 200);
        }

        return response()->json(['state' => 0], 401);
    }
}

In the above code i have override the logout and login methods. The login method store the api token after successful login and return this token in the response. The logout method clears the api token.

app/Http/Controllers/Auth/RegisterController.php

<?php

namespace App\Http\Controllers\Auth;

use App\User;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Support\Str;

class RegisterController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Register Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles the registration of new users as well as their
    | validation and creation. By default this controller uses a trait to
    | provide this functionality without requiring any additional code.
    |
    */

    use RegistersUsers;

    /**
     * Where to redirect users after registration.
     *
     * @var string
     */
    protected $redirectTo = '/home';

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest');
    }

    /**
     * Get a validator for an incoming registration request.
     *
     * @param  array  $data
     * @return \Illuminate\Contracts\Validation\Validator
     */
    protected function validator(array $data)
    {
        return Validator::make($data, [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'string', 'min:6'],
        ]);
    }

    /**
     * Create a new user instance after a valid registration.
     *
     * @param  array  $data
     * @return \App\User
     */
    protected function create(array $data)
    {
        return User::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => bcrypt($data['password']),
        ]);
    }

    public function register(Request $request)
    {
        // Here the request is validated. The validator method is located
        // inside the RegisterController, and makes sure the name, email
        // password and password_confirmation fields are required.
        $this->validator($request->all())->validate();

        $user = $this->create($request->all());

        // After the user is created, he's logged in.
        $this->guard()->login($user);

        // And finally this is the hook that we want. If there is no
        // registered() method or it returns null, redirect him to
        // some other URL. In our case, we just need to implement
        // that method to return the correct response.
        return $this->registered($request, $user)
            ?: redirect($this->redirectPath());
    }

    protected function registered(Request $request, $user)
    {
        $user->api_token = Str::random(60);

        $user->save();

        return response()->json(['user' => $user->toArray()], 201);
    }
}

In the register controller we save the api_token with the user and return it back in the response.

Now add the auth routes in routes/api.php

routes/api.php

Route::post('login', 'Auth\\LoginController@login')->name('login');
Route::post('register', 'Auth\\RegisterController@register')->name('register');

Route::get('logout', 'Auth\\LoginController@logout')->name('logout');

Route::get('check-auth', 'Auth\\LoginController@checkAuth')->name('logout');

To confirm that login and register routes is working, you can try them in Postman as shown in these screenshots:

postman login

postman register

postman logout

 

Continue to part2: Blog Controllers

 

0 0 vote
Article Rating
Share this: