In this guide, you'll learn how to edit and delete post data in Laravel, before proceeding you should read the previous guides as this guide would be the continuation, here are the previous guides:
- Laravel - Basic Routing & Controllers
- Laravel Blade Templating Engine
- Laravel - Model and Database Migrations
Here is how our singular page looks like so far:
Edit Data
We would add an edit function, so, our users can edit posts easily.
Add the following into your singular view:
.....
<p>{!! $post->post_content !!}</p>
<div class="text-center pt-20">
<a href="/posts/{{$post->post_id}}/edit" class="btn btn-rounded btn-primary">Edit: {{$post->post_title}}</a>
</div>
.....
and that should give us:
This: /posts/{{$post->post_id}}/edit
finds the id of the post, for example, if the id is 14 you'll get example.com/posts/14/edit and once the user clicks the edit button, it would route it to the edit method in the PostsController.
Goto PostsController, and we add the following under the edit method:
public function edit($id) {
$post = Post::find($id); // Find the id of the post passed into the $id param
// Load the edit view
return view('posts.edit')->with('post', $post);
}
This is self explanatory, we find the id of the post passed into the $id param, we then load the edit view. Create edit.blade.php in your post folder view, here:
Inside the edit we add the following:
@extends('layouts.app')
@section('content')
<h3 class="text-center">Edit Post</h3>
{!! Form::open(['action' => ['App\Http\Controllers\PostsController@update', $post->post_id], 'method' => 'POST']) !!}
<!-- Text Title Section -->
<div class="form-group w-xl-half mw-full m-auto">
{{ Form::label('title', ' ', ['class' => '']) }}
{{-- Form Input--}}
{{ Form::text('title', $post->post_title, [
'class' => 'form-control form-control-lg h-50',
'id' => 'post-title',
'placeholder' => 'Enter Title Here',
'required' => 'required']) }}
</div>
<!-- Text Area Section -->
<div class="form-group w-xl-half mw-full m-auto">
{{ Form::label('body', ' ', ['class' => '']) }}
{{-- Body Input--}}
{{ Form::textarea('body', $post->post_content, [
'class' => 'form-control form-control-lg h-50',
'id' => 'body-area',
'placeholder' => 'You can Start Writing...']) }}
<div class="text-center pt-20">
{{Form::hidden('_method', 'PUT')}}
{{ Form::submit('Publish', ['class' =>'btn btn-primary', 'type' => 'submit']) }}
</div>
</div>
{!! Form::close() !!}
@endsection
This is pretty similar to the create view we created in the last tutorial, there are a couple of changes I made though:
First of, the form action is wrapped in an array, the first one contains the method we would be calling when the user submit the action, and the second contains the post we are submitting the action against. In our case, all we need is the post_id.
{!! Form::open(['action' => ['App\Http\Controllers\PostsController@update', $post->post_id], 'method' => 'POST']) !!}
You might wonder why we have a POST method when we should in fact have a PUT method since we are updating, here is the thing, HTML forms only support POST and GET, so, PUT and DELETE methods will be spoofed by automatically adding a _method hidden field to your form. I added that here:
<div class="text-center pt-20"> {{Form::hidden('_method', 'PUT')}} {{ Form::submit('Publish', ['class' =>'btn btn-primary', 'type' => 'submit']) }} </div>
If you recall in the last tutorial, I said you can add a default value to a form input, since we are editing, we should be able to retrieve the post by calling it using Eloquent in the default parameter, so, I added the post_content to the body default value and post_title to the title default value, and that should show the following when you click on Edit post in the singular page view:
If you hit the Publish button it won't update the post as we still need to define the update method in the PostsController, so, add the following:
public function update(Request $request, $id) {
// Validation
$this->validate($request, [
'title' => 'required',
'body' => 'required'
]);
// Edit Post
$post = Post::find($id); // find the id of the post passed when user hit the publish button
$post->post_title = $request->input('title'); // This would get the content submitted to the title input
$post->post_content = $request->input('body'); // This would get the content submitted to the body input
/* --------------------------------------------
* Programmatically get an excerpt of 5 words
* --------------------------------------------
* explode breaks the original string into an array of words, I am breaking the content submited to the body into an array
* this: $request->input('body')), I then use array_splice to get a certain range, I am using an offset of 0
* meaning, it should start from the very first word, to the 5th word.
*
* Lastly, implode combines the ranges back together into single strings, which is then stored into the post_excerpt in our sa_posts table.
*/
$post->post_excerpt = implode(' ', array_slice(explode(' ', $request->input('body')), 0, 5));
$post->save(); // Save all the above query into the table
/*
* If the $post->save() above is successful, we redirect the user to the /posts url address with a sucess message.
*/
return redirect('/posts')->with('success', 'Post Updated');
//
}
This is very similar to the store method except that I changed this:
$post = new Post; // Create new post
To
$post = Post::find($id); // find the id of the post passed when user hit the publish button
and I changed the success message to "Post Updated" that's all.
Delete Data
Now, let's implement a delete function.
You would have thought the delete logic would be similar to the edit logic we just did, but it's a bit different though, and the Laravel dev made it this way to give some sort of protection when deleting.
So, instead of adding a link for delete as we did for the edit, we use a form instead, I'll put it underneath the edit link:
<div class="text-center pt-lg-20">
<a href="/posts/{{$post->post_id}}/edit" class="btn btn-rounded btn-primary pull-left">Edit: {{$post->post_title}}</a>
{{-- Action contains the delete method plus the post id of what you are deleting--}}
{!!Form::open(['action' => ['App\Http\Controllers\PostsController@destroy', $post->post_id], 'method' => 'POST', 'class' => 'pull-right'])!!}
{{-- Hidden method to spoof the DELETE method--}}
{{Form::hidden('_method', 'DELETE')}}
{{-- Delete method with class--}}
{{Form::submit('Delete', ['class' => 'btn-rounded btn btn-danger'])}}
{!!Form::close()!!}
</div>
The action contains the delete method plus the post id of what you are deleting, and since HTML forms only support POST and GET, we spoof the DELETE method by adding a _method hidden field to the form, we then added the delete method.
Once the user clicks on the delete method it would route it to the destroy method in the PostsController, which would look like so:
public function destroy($id) {
// Delete Post
$post = Post::find($id); // find the post id that was passed when the user clicked the delete button
$post->delete();
return redirect('/posts')->with('success', 'Post Deleted');
}
and that is it.
Here is how the page looks now:
If you click the delete button, the post would be deleted and you'll get:
In the next section, we would build user authentication...
User Authentication
This is an extract from the laravel doc that explains how authentication works:
First, consider how authentication works. When using a web browser, a user will provide their username and password via a login form. If these credentials are correct, the application will store information about the authenticated user in the user's session.
A cookie issued to the browser contains the session ID so that subsequent requests to the application can associate the user with the correct session. After the session cookie is received, the application will retrieve the session data based on the session ID, note that the authentication information has been stored in the session, and will consider the user as "authenticated".
Laravel includes built-in authentication and session services which are typically accessed via the Auth and Session facades. These features provide cookie based authentication for requests that are initiated from web browsers.
They provide methods that allow you to verify a user's credentials and authenticate the user. In addition, these services will automatically store the proper data in the user's session and issue the proper session cookie. A discussion of how to use these services is contained within this documentation.
By default, Laravel create a user table for us as part of the migration process, we already discussed that in our last guide, and if you recall the migration files are under database/migrations. You can customize it as you see fit, and run the migration command to update the tables, but I'll just leave it as is.
In my mariadb client, here is how the users' table looks like:
MariaDB [simpleapp]> DESCRIBE sa_users;
+-------------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------------+------------------+------+-----+---------+----------------+
| user_id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| user_name | varchar(255) | NO | | NULL | |
| email | varchar(255) | NO | UNI | NULL | |
| email_verified_at | timestamp | YES | | NULL | |
| user_password | varchar(255) | NO | | NULL | |
| remember_token | varchar(100) | YES | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+-------------------+------------------+------+-----+---------+----------------+
8 rows in set (0.03 sec)
I made some edit in the last guide, I changed id to user_is, name to user_name, and password to user_password to keep the structure consistent with the rest of the tables I might be creating.
We can enable authentication that would take care of the user login/registration, sessions, and others.
Before running the authentication command, copy the code in your app.blade.php that is located in your layout folder in a temporary place as using the artisan auth command would replace it, and we don't want to lose the code since we would be editing for our authentication.
First, install this:
composer require laravel/ui
Laravel UI Composer package is a new first-party package that extracts the UI portion of a Laravel project ( frontend scaffolding typically provided with previous releases of Laravel ) into a separate laravel/ui package. The separate package enables the Laravel team to update, develop, and version UI scaffolding package separately from the primary framework and the main Laravel codebase.
Now, enable it by running the following command:
php artisan ui:auth
If you get this:
The [layouts/app.blade.php] view already exists. Do you want to replace it? (yes/no) [no]:
>
Make sure you have copied the app.blade.php into a temp place, and then enter yes.
You should then get: Authentication scaffolding generated successfully.
First things first, I'll change the name in the RegisterController.php to work with our table structure:
Change this:
protected function create(array $data) {
return User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
}
to:
protected function create(array $data) {
return User::create([
'user_name' => $data['name'],
'email' => $data['email'],
'user_password' => Hash::make($data['password']),
]);
}
I am changing this because I changed the user table structure in the last section, and we need to let Laravel knows about that in the RegisterController for it to work properly. If you didn't change your user table structure, you don't have to do the above changes.
Also, I'll change the fields in the app/Models/User.php like so:
<?php
namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
use HasFactory, Notifiable;
protected $primaryKey = 'user_id'; // Primary Key
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'user_name',
'email',
'user_password',
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'user_password',
'remember_token',
];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
/**
* Get the password for the user
* This also overrides the regular field "password"
*
* @return string
*/
public function getAuthPassword() {
return $this->user_password;
}
}
I changed the field to our own table structure.
If you look in the app.blade.php you'll see it has created a new template for you with a couple of logic to handle logged in user or logged out user. I copied a copy of stuff out of it, and then added it to the naviagetion.blade.php like so:
<nav class="navbar">
<!-- Navbar content (with toggle sidebar button) -->
<div class="navbar-content">
<button class="btn btn-action" type="button">
<i class="fa fa-bars" aria-hidden="true"></i>
<span class="sr-only">Toggle sidebar</span> <!-- sr-only = show only on screen readers -->
</button>
</div>
<!-- Navbar brand -->
<a href="/" class="navbar-brand">
<img src="https://cdn.devsrealm.com/serve_file_path_987654321/d5c3f610914fc1ab071adb48ee747519b07e0f40395f1ab2872b9b4b7048e436?render" alt="SimpelApp">
</a>
<!-- Navbar nav -->
<ul class="navbar-nav d-none d-md-flex"> <!-- d-none = display: none, d-md-flex = display: flex on medium screens and up (width > 768px) -->
<li class="nav-item active">
<a href="/dashboard" class="nav-link font-size-16">Dashboard</a>
</li>
<li class="nav-item">
<a href="/posts" class="nav-link font-size-16">Blog</a>
</li>
<li class="nav-item">
<a href="/about" class="nav-link font-size-16">About</a>
</li>
</ul>
<!-- Right Side Of Navbar -->
<ul class="navbar-nav ml-auto">
<!-- Authentication Links -->
@guest
@if (Route::has('login'))
<li class="navbar-content">
<a class="nav-link btn btn-primary" href="{{ route('login') }}">{{ __('Login') }}</a>
</li>
@endif
@if (Route::has('register'))
<!-- Navbar content with sign up button -->
<li class="navbar-content">
<a class="nav-link btn btn-primary" href="{{ route('register') }}">{{ __('Sign Up') }}</a>
</li>
@endif
@else
<div class="dropdown">
<button class="btn" data-toggle="dropdown" type="button" id="dropdown-toggle-btn-1" aria-haspopup="true" aria-expanded="false">
{{ Auth::user()->user_name }} <i class="fa fa-angle-down ml-5" aria-hidden="true"></i> <!-- ml-5 = margin-left: 0.5rem (5px) -->
</button>
<div class="dropdown-menu" aria-labelledby="dropdown-toggle-btn-1">
<a class="dropdown-item" href="{{ route('logout') }}"
onclick="event.preventDefault();
document.getElementById('logout-form').submit();">
{{ __('Logout') }}
</a>
<div class="dropdown-divider"></div>
<div class="dropdown-content">
<form id="logout-form" action="{{ route('logout') }}" method="POST" class="d-none">
@csrf
</form>
</div>
</div>
</div>
@endguest
</ul>
</nav>
I won't explain everything as most are just HTML junks, but here are the relevant part:
@guest
// The user is not authenticated...
@else // if a user is not a guess, that is logged in user
//
@endguest
@guest directives may be used to quickly determine if the current user is authenticated or is a guest, and if the user is a guest, we run all our login and register logic within the guest directives block.
@guest // If a user is guest
@if (Route::has('login'))
//
@endif
@if (Route::has('register'))
//
@endif
else // if a user is not a guest, that is logged in user
//
@endguest
If a route exists for login and register, we display the login and register form, and else we discard the login and register form. In our case, the route has already been automatically created for us when we issued the auth command. So, the @else means if the user is not a @guest, it then displays a dropdown item that shows the logged in user_name with a logout button.
@else
<div class="dropdown">
<button class="btn" data-toggle="dropdown" type="button" id="dropdown-toggle-btn-1" aria-haspopup="true" aria-expanded="false">
{{ Auth::user()->user_name }} <i class="fa fa-angle-down ml-5" aria-hidden="true"></i> <!-- ml-5 = margin-left: 0.5rem (5px) -->
</button>
<div class="dropdown-menu" aria-labelledby="dropdown-toggle-btn-1">
<a class="dropdown-item" href="{{ route('logout') }}"
onclick="event.preventDefault();
document.getElementById('logout-form').submit();">
{{ __('Logout') }}
</a>
<div class="dropdown-divider"></div>
<div class="dropdown-content">
<form id="logout-form" action="{{ route('logout') }}" method="POST" class="d-none">
@csrf
</form>
</div>
</div>
</div>
@endguest
So, here is how the login and register button looks:
So, when you click on either of them it would take it to the appropriate form for your user to either login or Signup. You can customize the Login and Signup view by going to the view/auth folder:
login.blade.php is for the login view, and register.blade.php is for the register or signup view.
Once you signup, you'll be automatically logged in, and redirected to the /home by default:
I don't know about you, but /home doesn't sound classic for a user dashboard, we would change the home to dashboard. Here are where you can change it:
- Goto app/Http/Controllers and rename HomeController to DashboardController
- Open the renamed DashboardController.php and rename the class from HomeController to DashboardController
- Goto routes/web.php change:
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home');
ToRoute::get('/dashboard', [App\Http\Controllers\DashboardController::class, 'index']);
- Then go into the view folder, change home.blade.php to dashboard.blade.php
- Lastly, go into app/providers/RouteServiceProder.php and change:
public const HOME = '/home';
Topublic const HOME = '/dashboard';
and that's all
Now, you can customize the dashboard to your liking.
For example, in the dashboard.blade.php view, I changed:
{{ __('You are logged in!') }}
to:
{{ __('You are logged in!') }}, {{ Auth::user()->user_name }}
This shows the login string plus the user authenticated, it would give you something like so:
Cool.
Now, let's remove the Edit and Delete button for user that aren't logged in. To do that, just wrap the edit and delete button code with @auth...@endauth, something like:
@auth
<div class="text-center pt-lg-20">
<a href="/posts/{{$post->post_id}}/edit" class="btn btn-rounded btn-primary pull-left">Edit: {{$post->post_title}}</a>
{{-- Action contains the delete method plus the post id of what you are deleting--}}
{!!Form::open(['action' => ['App\Http\Controllers\PostsController@destroy', $post->post_id], 'method' => 'POST', 'class' => 'pull-right'])!!}
{{-- Hidden method to spoof the DELETE method--}}
{{Form::hidden('_method', 'DELETE')}}
{{-- Delete method with class--}}
{{Form::submit('Delete', ['class' => 'btn-rounded btn btn-danger'])}}
{!!Form::close()!!}
</div>
@endauth
Model Relationship
Now, we can start customizing our user's dashboard by displaying options like Add new post, edit, etc. But before we do that, we should create another migration file that adds a column name user_id to the post table, this way, we can map out users that have a specific post.
Run the following command:
php artisan make:migration add_user_id_to_posts_table --table=posts
You should get an output like so:
Created Migration: 2020_12_01_075333_add_user_id_to_posts_table
Open up the file, and add the following in the up function:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddUserIdToPostsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('posts', function (Blueprint $table) {
$table->integer('user_id');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('posts', function (Blueprint $table) {
$table->dropColumn('user_id');
});
}
}
Save, and run php artisan migrate
You should get:
$ php artisan migrate Migrating: 2020_12_01_075333_add_user_id_to_posts_table Migrated: 2020_12_01_075333_add_user_id_to_posts_table (263.88ms) $
Now if we describe the structure you'll see that we have in fact added the user_id column:
MariaDB [simpleapp]> DESCRIBE sa_posts;
+------------------+------------------+------+-----+-------------------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------------+------------------+------+-----+-------------------+----------------+
| post_id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| post_title | varchar(255) | NO | | NULL | |
| post_excerpt | mediumtext | NO | | NULL | |
| post_author | varchar(255) | NO | | The_Devsrealm_Guy | |
| post_author_link | varchar(255) | YES | | NULL | |
| post_image_url | varchar(255) | YES | | NULL | |
| post_content | longtext | NO | | NULL | |
| post_status | tinyint(4) | NO | | 0 | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
| user_id | int(11) | NO | | NULL | |
+------------------+------------------+------+-----+-------------------+----------------+
If you want to roll that back, just do
php artisan migrate:rollback --path=/database/migrations/2020_12_01_075333_add_user_id_to_posts_table.php
This would rollback the last added column.
The followings are the users I have created:
MariaDB [simpleapp]> SELECT * FROM sa_users \G
*************************** 1. row ***************************
user_id: 1
user_name: james
email: james@me.com
email_verified_at: NULL
user_password: $2y$10$VUFwWlGbEY/ryr.QoX1UuOYKxfa3t85VPushE6S/94z.L3MeNXTSy
remember_token: NULL
created_at: 2020-11-30 13:52:43
updated_at: 2020-11-30 13:52:43
*************************** 2. row ***************************
user_id: 2
user_name: pascal
email: pascal@devsrealm.com
email_verified_at: NULL
user_password: $2y$10$xyxtavDv.SOqQtm4BMvDC.NbG3uyRCDdT07Dc2j6GGwqn3qkchutu
remember_token: NULL
created_at: 2020-12-01 05:54:06
updated_at: 2020-12-01 05:54:06
*************************** 3. row ***************************
user_id: 3
user_name: the_devsrealm_guy
email: devsrealmer@gmail.com
email_verified_at: NULL
user_password: $2y$10$V9DPyCUGKhFMTv.lalNLoO/v0MiUDMLAbUOiftKn5fZqiwHaoN9PW
remember_token: NULL
created_at: 2020-12-01 07:06:02
updated_at: 2020-12-01 07:06:02
Just for example, let's map the user_id of 3 to the first 8 posts, I can do that easily from my mariadb client like so:
UPDATE sa_posts SET user_id = 3
WHERE post_id IN(1,2,3,4,5,6,7,8);
Now, if we check the data in the sa_posts field, you'll see that the first 8 post has been given the user_id 3:
MariaDB [simpleapp]> SELECT * FROM sa_posts LIMIT 10 \G
*************************** 1. row ***************************
post_id: 1
post_title: Post One
post_excerpt: This is an excerpt
post_author: The_Devsrealm_Guy
post_author_link: NULL
post_image_url: NULL
post_content: This is my first blog post, I hope you enjoy reading it ;)
post_status: 0
created_at: 2020-11-28 01:47:22
updated_at: 2020-11-28 01:47:22
user_id: 3
*************************** 2. row ***************************
post_id: 2
post_title: Post Two
post_excerpt: This is an excerpt
post_author: The_Devsrealm_Guy
post_author_link: NULL
post_image_url: NULL
post_content: This is my second blog post, I hope you enjoy reading it, thanks
post_status: 0
created_at: 2020-11-28 01:59:15
updated_at: 2020-11-28 01:59:15
user_id: 3
*************************** 3. row ***************************
post_id: 3
post_title: Post Three
post_excerpt: This is an excerpt
post_author: The_Devsrealm_Guy
post_author_link: NULL
post_image_url: NULL
post_content: This is my third blog post, I hope you enjoy reading it, thanks
post_status: 0
created_at: 2020-11-28 01:59:51
updated_at: 2020-11-28 01:59:51
user_id: 3
*************************** 4. row ***************************
post_id: 4
post_title: Post Four
post_excerpt: This is an excerpt
post_author: The_Devsrealm_Guy
post_author_link: NULL
post_image_url: NULL
post_content: This is my fourth blog post, I hope you enjoy reading it, thanks
post_status: 0
created_at: 2020-11-29 11:31:25
updated_at: 2020-11-29 11:32:56
user_id: 3
*************************** 5. row ***************************
post_id: 5
post_title: Post Five
post_excerpt: This is an excerpt
post_author: The_Devsrealm_Guy
post_author_link: NULL
post_image_url: NULL
post_content: This is my fifth blog post, I hope you enjoy reading it, thanks
post_status: 0
created_at: 2020-11-29 11:33:18
updated_at: 2020-11-29 11:33:18
user_id: 3
*************************** 6. row ***************************
post_id: 6
post_title: Post Six
post_excerpt: This is an excerpt
post_author: The_Devsrealm_Guy
post_author_link: NULL
post_image_url: NULL
post_content: This is my sixth blog post, I hope you enjoy reading it, thanks
post_status: 0
created_at: 2020-11-29 11:34:07
updated_at: 2020-11-29 11:34:07
user_id: 3
*************************** 7. row ***************************
post_id: 7
post_title: Post Seven
post_excerpt: This is an excerpt
post_author: The_Devsrealm_Guy
post_author_link: NULL
post_image_url: NULL
post_content: This is my seventh blog post, I hope you enjoy reading it, thanks
post_status: 0
created_at: 2020-11-29 11:34:43
updated_at: 2020-11-29 11:34:43
user_id: 3
*************************** 8. row ***************************
post_id: 8
post_title: Post Eight
post_excerpt: This is an excerpt
post_author: The_Devsrealm_Guy
post_author_link: NULL
post_image_url: NULL
post_content: This is my Eight blog post, I hope you enjoy reading it, thanks
post_status: 0
created_at: 2020-11-29 11:35:40
updated_at: 2020-11-29 11:35:40
user_id: 3
*************************** 9. row ***************************
post_id: 9
post_title: Post Nine
post_excerpt: This is an excerpt
post_author: The_Devsrealm_Guy
post_author_link: NULL
post_image_url: NULL
post_content: This is my Ninth blog post, I hope you enjoy reading it, thanks
post_status: 0
created_at: 2020-11-29 11:36:19
updated_at: 2020-11-29 11:36:19
user_id: 0
*************************** 10. row ***************************
post_id: 10
post_title: Post Ten
post_excerpt: This is an excerpt
post_author: The_Devsrealm_Guy
post_author_link: NULL
post_image_url: NULL
post_content: Post 10, haha, my blog is growing superbly
post_status: 0
created_at: 2020-11-29 11:36:57
updated_at: 2020-11-29 11:36:57
user_id: 0
10 rows in set (0.00 sec)
MariaDB [simpleapp]>
Now, in our PostsController in the store method, I'll add the following:
$post->user_id = auth()->user()->user_id;
This adds the current user_id whenever a new post is created. The auth()->user()->user_id is used to access the current user and we put it in the user_id of the sa_posts table, here: $post->user_id
In full, here is the store method:
public function store(Request $request) {
// Validation
$this->validate($request, [
'title' => 'required',
'body' => 'required'
]);
// Create New Post
$post = new Post;
$post->post_title = $request->input('title'); // This would get the content submitted to the title input
$post->post_content = $request->input('body'); // This would get the content submitted to the body input
/* --------------------------------------------
* Programmatically get an excerpt of 5 words
* --------------------------------------------
* explode breaks the original string into an array of words, I am breaking the content submited to the body into an array
* this: $request->input('body')), I then use array_splice to get a certain range, I am using an offset of 0
* meaning, it should start from the very first word, to the 5th word.
*
* Lastly, implode combines the ranges back together into single strings, which is then stored into the post_excerpt in our sa_posts table.
*/
$post->post_excerpt = implode(' ', array_slice(explode(' ', $request->input('body')), 0, 5));
/*
* Add the current user_id whenever a new post is created
*
* The auth()->user()->user_id is used to access the current user and we put it in the user_id of the sa_posts table
*/
$post->user_id = auth()->user()->user_id;
$post->save(); // Save all the above query into the table
/*
* If the $post->save() above is successful, we redirect the user to the /posts url address with a sucess message.
*/
return redirect('/posts')->with('success', 'Post Successfully Created');
}
Now, I'll update the dropdown item to contain a create new post plus a dashboard link or just replace the @else block with this:
<div class="dropdown">
<button class="btn" data-toggle="dropdown" type="button" id="dropdown-toggle-btn-1" aria-haspopup="true" aria-expanded="false">
{{ Auth::user()->user_name }} <i class="fa fa-angle-down ml-5" aria-hidden="true"></i> <!-- ml-5 = margin-left: 0.5rem (5px) -->
</button>
<div class="dropdown-menu" aria-labelledby="dropdown-toggle-btn-1">
<a class="dropdown-item" href="/dashboard"> {{ __('Dashboard') }} </a>
<a class="dropdown-item" href="/posts/create"> {{ __('Add Post') }} </a>
<div class="dropdown-divider"></div>
<div class="dropdown-content">
<a class="dropdown-item" href="{{ route('logout') }}"
onclick="event.preventDefault();
document.getElementById('logout-form').submit();">
{{ __('Logout') }}
</a>
<form id="logout-form" action="{{ route('logout') }}" method="POST" class="d-none">
@csrf
</form>
</div>
</div>
</div>
@endguest
Cool.
Now, let's create a relationship between the posts and the users, for example, we only display the post of the user that owns that range of post, and not display the post if the current user doesn't own it.
Before creating the relationship, let's try to understand the relationship available to us when using Eloquent:
- One To One: In a one-to-one relationship, one record in a certain table is associated with one and only one record in another table, it is as simple as that. E.g a user has only one user-id, that is, an ID is assigned to only one person. Likewise, a user can only have one phone number.
- One To Many: In a one-to-many relationship, one record in a table can be associated with one or more records in another table. This is what we are going for, a user can have many posts. Likewise, a single user can have many sales order
- Many To Many: In a many-to-many relationship, multiple records in a table are associated with multiple records in another table. For example, Two tables can contain users_table and music_table, a user can own or listen to multiple songs, and a song can be listened to by multiple users. The concept is known as many to many relationships. This might be trickier to work with it, but it's not that difficult.
So, you've seen the relationships we can work with, and fortunately, Eloquent can handle most of this right off the bat, here is a link to learn more about it: Eloquent Relationship
We are going for One to Many relationships.
A one-to-many relationship is used to define relationships where a single model owns any amount of other models. For example, a user may have one or more posts. To implement a one-to-many relationship, we can define it by placing a function in our User Eloquent model(app/Models/User.php).
User can have many post so let’s define it like so:
/**
* Get the Users Post
*/
public function posts() {
return $this->hasMany('App\Models\Post', 'user_id');
}
I added the user_id to the hasMany second parameter as I want it to use it as the key, what I am saying, is the user_id field is the related field that would be used to map the relationship.
Now, let’s define a relationship on the Post model that will let us access the Post that owns the blog posts. We can define the inverse of a hasMany relationship using the belongsTo method like so:
public function user() {
return $this->belongto('App\Models\User');
}
Usage:
Once the relationship has been defined, we can access the collection of posts by accessing the posts property. Remember, since Eloquent provides "dynamic properties", we can access relationship methods as if they were defined as properties on the model:
Goto the DashboardController, and import the User model: use App\Models\User;
In the index method of the DashboardController, I'll use the following:
public function index() {
$user_id = auth()->user()->user_id; // find the user_id
$user = User::find($user_id); // Use the user_id it finds above to collect the users posts
return view('dashboard')->with('posts', $user->posts); // pass the posts it finds as an object to the dashboard view
}
and in the dashboard view, I'll structure it like so:
{{-- Display List of Blog Posts With Edit and Delete Button --}}
@if(count($posts) > 0) {{-- If user have post, show 'em --}}
<div class="container-fluid">
<div class="row justify-content-lg-start">
<div class="col-12 col-sm-5 col-md-4 col-lg-3">
<h4>{{ __('Posts') }}</h4>
</div>
</div>
@foreach($posts as $post)
<!-- First row -->
<div class="row justify-content-lg-start">
<div class="col-12 col-sm-5 col-md-4 col-lg-3">
{{$post->post_title}}
</div>
<div class="col-6 col-sm-5 col-md-4 col-lg-3 m-5">
<a href="/posts/{{$post->post_id}}/edit" class="btn btn-primary">Edit</a>
</div>
<div class="col-6 col-sm-5 col-md-4 col-lg-3 m-5">
{{-- Action contains the delete method plus the post id of what you are deleting--}}
{!!Form::open(['action' => ['App\Http\Controllers\PostsController@destroy', $post->post_id], 'method' => 'POST', 'class' => 'pull-right'])!!}
{{-- Hidden method to spoof the DELETE method--}}
{{Form::hidden('_method', 'DELETE')}}
{{-- Delete method with class--}}
{{Form::submit('Delete', ['class' => 'btn btn-danger'])}}
{!!Form::close()!!}
</div>
</div>
@endforeach
@else
<p>No Post</p>
@endif
</div> {{-- Close Container-fluid --}}
This is how it looks:
If user doesn't have any post, they would get no post, something like so:
Access Control
We still have a couple of issues:
Block Guest From Accessing Create Post Page: We have no access control for creating a post, a random user can goto /post/create, and they would be able to create a post with no issue whatsoever.
You can fix that by adding the following to the PostsController:
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth');
}
The constructor can be used to prepare the necessary property when a class is instantiated, you can also use it to run a certain code when a class is instantiated, in our case, we are firing up the middleware method. In this case, we are using middleware('auth') to verify if the user is authenticated. If the user is not authenticated, the middleware will redirect the user to the login screen. However, if the user is authenticated, the middleware will allow the request to proceed further into the application.
This way, an unauthorized user doesn't have access to anything in PostsController, which is a problem in disguise, we want to relax the access a bit, e.g non-logged in user should still be able to view our blog posts, and also view an individual page, we can do this by adding an exception to the middleware method like so:
$this->middleware('auth', ['except' => ['index', 'show']]);
This simply means, block any authorized access except the index method, and the show method, it is as simple as that.
Block User From Editing or Deleting Post That Don't Own: This is a big security risk, a user can edit posts they do not own, even if they didn't create the posts themselves. We need to do two things, the first one is to hide the edit and delete button in the singular view if the user doesn't own the post, we also need to block access when the user tries using the edit URL of the post.
For the first one, you can use: @if(Auth::user()->id == $post->user_id)
It should only show the buttons if the user owns the post, here is the code:
@auth
@if(Auth::user()->id == $post->user_id)
<div class="text-center pt-lg-20">
<a href="/posts/{{$post->post_id}}/edit" class="btn btn-rounded btn-primary pull-left">Edit: {{$post->post_title}}</a>
{{-- Action contains the delete method plus the post id of what you are deleting--}}
{!!Form::open(['action' => ['App\Http\Controllers\PostsController@destroy', $post->post_id], 'method' => 'POST', 'class' => 'pull-right'])!!}
{{-- Hidden method to spoof the DELETE method--}}
{{Form::hidden('_method', 'DELETE')}}
{{-- Delete method with class--}}
{{Form::submit('Delete', ['class' => 'btn-rounded btn btn-danger'])}}
{!!Form::close()!!}
</div>
@endif
@endauth
For the second one, goto the PostsController and add the following to the edit method:
if(auth()->user()->user_id !== $post->user_id){
return redirect('/posts')->with('error', 'Unauthorized Access');
}
We use auth()->user()->user_id to access the current user's id. We then use that user_id to check if it has a relationship to the user_id in the posts table.
For example, if the user_id is 3, and the user_id or the user_id that owns the post is 4 in the post table then that means the user doesn't own the post, I mean the user we checked here: auth()->user()->user_id, If that is so we return a danger alert.
Here is the complete code for the edit method:
public function edit($id) {
$post = Post::find($id); // Find the id of the post passed into the $id param
if(auth()->user()->user_id !== $post->user_id){
return redirect('/posts')->with('error', 'Unauthorized Access');
}
// Load the edit view
return view('posts.edit')->with('post', $post);
}
Here is an example:
Do the same thing for the delete method:
public function destroy($id) {
// Delete Post
$post = Post::find($id); // find the post id that was passed when the user clicked the delete button
if(auth()->user()->user_id !== $post->user_id){
return redirect('/posts')->with('error', 'Unauthorized Access');
}
$post->delete();
return redirect('/dashboard')->with('success', 'Post Deleted');
}