I don't know if you have read my guide on Creating a Tiny PHP MVC Framework From Scratch where we create a tiny MVC framework in the hope of understanding the concepts of how major frameworks implement MVC patterns. Here is a useful excerpt:
Controller: Hey, Mr. Model the user is requesting for the homepage of devsrealm.com, the user want the data of the homepage. Note here: I said data not display
Model: Sir Controller, here are the data.
Controller: Passes the data provided by the model to the view for display
View: The view then presents the information to the user, the View is the information that is being presented to a user, it would typically be a web-page
So, the model would be dealing with our database data in this guide, you might need to follow the previous guide on Laravel first as the project I am working on would carry along to this guide:
Creating a Database
Let's start with creating a database, I'll do so from my mariadb client:
CREATE DATABASE simpleapp
CHARACTER SET utf8mb4
COLLATE utf8mb4_general_ci;
You can learn more about Creating Databases and Tables in MariaDB
Creating a PostsController
That's all you have to do, we would use Laravel migration for the table, columns, and data. Don't worry, it is super simple.
First, create a PostsController using artisan:
$ php artisan make:controller PostsController
I trust you not to include the dollar sign I guess ;). If done correctly, you'll get : Controller created successfully.
As usual the PostsController would be created in App\Http\Controllers;
You'll automatically get the controller boilerplate, if somehow, you can't use the terminal, create PostsController.php in the Controllers folder as seen in the image above, and add the following code:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class PostsController extends Controller
{
//
}
It uses namespace App\Http\Controller, which brings in the Request class so we can handle the request, and lastly, we are extending the core controllers.
Creating a Post Model
Now, let's create a Post model with the migration, again using artisan, do the following:
$ php artisan make:model Post -m
Model created successfully.
Created Migration: 2020_11_27_230914_create_posts_table
$
This created two things, the first one is the Post model which is located in app/Models, the file would be titled Post.php, and contains the following:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use HasFactory;
}
The second one is the migration, and it is located in the database/migrations folder, mine is titled 2020_11_27_230914_create_posts_table.php it might differ depending on your current date, it contains the following:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePostsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('posts');
}
}
First, we have a class CreatePostsTable that extends the Migration class. Also, we have an up and down function, you might have guessed what this does. One creates posts and the other drop posts if it exists.
Migrate
When you run the migrate command (we would do so in a moment), the up function will create a posts table, with an ID column, and it is going to add two timestamp columns, one will be created_at, and the other will be updated_at. This will be automatically filled when we insert data through our application.
Before going further, we need to keep a structure in mind, you should make sure your table names reflect the goal of the project. So, here are the preferred table naming convention:
- Use a lower case table name, this would ensure uniformity on systems that are not case insensitive e.g Linux, so, again, make sure your table names are in lower cases
- Prefix your table names, e.g if we are working on a project called simpleapp, we can prefix the table name as sa_posts for posts related fields, sa_options for admin settings, sa_comments for user-related comment fields, etc
- Avoid using spaces, numbers, and crazy characters.
Before running the command, I'll add more fields to the up function:
public function up() {
Schema::create('posts', function (Blueprint $table) {
$table->increments('post_id');
$table->string('post_title');
$table->mediumText('post_excerpt');
$table->string('post_author')->default('The_Devsrealm_Guy');
$table->string('post_author_link')->nullable();
$table->string('post_image_url')->nullable();
$table->longText('post_content');
$table->tinyInteger('post_status')->default(0);
$table->timestamps();
});
}
We might not even use all of this right now, let me point out some of them. I set the id column as auto-increment (primary key), and the rest are strings with different lengths except tinyInteger which is an integer, and timestamps which is of date type.
To use a prefix for all the table, change it in config/database.php:
Chang it the prefix from
'mysql' => [
'prefix' => '',
],
To
'mysql' => [
'prefix' => 'sa_',
],
Feel free to use your own prefix.
There are still a couple of stuff to do, Laravel by default include a couple of migration files, for 2014_10_12_000000_create_users_table.php
change it from:
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('users');
}
To:
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->increments('user_id');
$table->string('user_name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('user_password');
$table->rememberToken();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('sa_users');
}
This is just to keep things organize and consistent across the tables. I think we should leave the rest of the default migration file as is.
Lasly, go to .env file, and change your database config:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=simpleapp
DB_USERNAME=DATABASE_USER
DB_PASSWORD=DATABASE_PASS
My own database name is simpleapp, which is why I added it above, change your user and pass also.
To run the migration, you do:
php artisan migrate
Fixing: Specified Key Was Too Long
If you get errors such as: SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes (SQL: alter table `sa_users` add unique `sa_users_email_unique`(`email`))
then we need to understand why we getting the error before fixing it. First thing first, Laravel uses the utf8mb4 character set by default, which includes support for storing "emojis" in the database.
A utf8mb4 is a variable-length encoder that stores characters in 1 – 4 bytes. Encode is a way of converting one thing to the other, Words and sentences in text are created from characters. Examples of characters include the English letter A, $, x, ^, etc.
The characters are stored in the computer as one or more bytes, in the case of utf8mb4, it can use up to 4 bytes to store just a character, that is the point I am trying to make. The maximum index length that an InnoDB Engine can have is 767 bytes (at least, in my current version of mariadb), the issue we have is that the value we are trying to add is over the limit.
You can fix this error by increasing the limit of the maximum index length, you can do that by changing mariadb config like so:
innodb_file_format = Barracuda
innodb_file_per_table = on
innodb_default_row_format = dynamic
innodb_large_prefix = 1
innodb_file_format_max = Barracuda
On Linux, add it in /etc/mysql/mariadb.conf.d/50-server.cnf under the [mariadb] section header.
This would change the limit to 3072 bytes.
Lastly, run the queries one at a time in your mariadb client:
SET GLOBAL innodb_file_format = Barracuda;
SET GLOBAL innodb_file_per_table = on;
SET GLOBAL innodb_default_row_format = dynamic;
SET GLOBAL innodb_large_prefix = 1;
SET GLOBAL innodb_file_format_max = Barracuda;
Drop any existing table you have before, and run the php artisan migrate command again, and you should have the following:
$ php artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_000000_create_users_table (105.41ms)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated: 2014_10_12_100000_create_password_resets_table (134.19ms)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated: 2019_08_19_000000_create_failed_jobs_table (110.54ms)
Migrating: 2020_11_27_230914_create_posts_table
Migrated: 2020_11_27_230914_create_posts_table (165.05ms)
$
I know the database stuff might be confusing a little, but feel free to ask me any question if you are stuck.
In my mariadb client, this is what I have:
MariaDB [simpleapp]> SHOW TABLES;
+---------------------+
| Tables_in_simpleapp |
+---------------------+
| sa_failed_jobs |
| sa_migrations |
| sa_password_resets |
| sa_posts |
| sa_users |
+---------------------+
The sa_posts table is the one we created, and the sa_users table is the one we edited. The sa_migrations table help keeps track of our migration, there is also a sa_password_resets and sa_failed_jobs table created by default for us.
If I describe the sa_posts table, you'll see the structures are in tact:
+------------------+------------------+------+-----+-------------------+----------------+
| 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 | |
+------------------+------------------+------+-----+-------------------+----------------+
Using Artisan Tinker
We have post_id, title, excerpt, timestamps, etc. I think we are good to go. We can now add data. There are two ways you can do this, we can either use artisan tinker or just our regular SQL way. Let's go for the tinker method as that is simpler than regular SQL.
Tinker lets you interact with the database using eloquent, which is an ORM that makes it easy to work with databases even if you have no knowledge of SQL.
Launch tinker like so:
$ php artisan tinker
Psy Shell v0.10.4 (PHP 7.4.12 — cli) by Justin Hileman
>>>
It would open a Psy shell for you to interact with. here is an example that count post in the sa_posts table:
>>> Post::count();
[!] Aliasing 'Post' to 'App\Models\Post' for this Tinker session.
=> 0
As you can see we have 0 post. Let's add some data:
>>> $post = new Post();
=> App\Models\Post {#4125}
>>> $post->post_title = 'Post One'
=> "Post One"
>>> $post->post_content = 'This is my first blog post, I hope you enjoyed reading it'
=> "This is my first blog post, I hope you enjoyed reading it"
>>> $post->post_excerpt = 'This is an excerpt'
=> "This is an excerpt"
>>> $post->save();
=> true
We started by creating a new instance of the Post model: $post = new Post();
This result: => App\Models\Post {#4125} is the id of the instance, and it is been held in memory for the current instance. I then added fields for post_title, post_content, and post_excerpt
To execute the query, we do: $post->save(); That would create the first entry in the sa_posts table:
MariaDB [simpleapp]> SELECT * FROM sa_posts \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
The created_at and updated_at get filled for us automatically, let's create 2 more post with tinker, you'll have to create a new instance for every new post you add, don't worry it would generate new id for each instances:
>>> $post = new Post();
=> App\Models\Post {#4178}
>>> $post->post_title = 'Post Two'
=> "Post Two"
>>> $post->post_content = 'This is my second blog post, I hope you enjoy reading it, thanks'
=> "This is my second blog post, I hope you enjoy reading it, thanks"
>>> $post->post_excerpt = 'This is an excerpt'
=> "This is an excerpt"
>>> $post->save();
=> true
>>> $post = new Post();
=> App\Models\Post {#3239}
>>> $post->post_title = 'Post Three'
=> "Post Three"
>>> $post->post_content = 'This is my third blog post, I hope you enjoy reading it, thanks'
=> "This is my third blog post, I hope you enjoy reading it, thanks"
>>> $post->post_excerpt = 'This is an excerpt'
=> "This is an excerpt"
>>> $post->save();
=> true
>>>
Here is my table:
MariaDB [simpleapp]> SELECT * FROM sa_posts \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
*************************** 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
*************************** 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
3 rows in set (0.01 sec)
Now, we have data to play with in our application. We need to create functions in our PostsController that would handle the data, something like showing the data, creating a new one, editing, updating, and the likes.
Instead of creating all of this manually, just delete the PostsController and create a new like so:
php artisan make:controller PostsController --resource
and that is gonna create the following for us:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class PostsController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id)
{
//
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit($id)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
//
}
}
It created the necessary functions right off the bat. The index and create function doesn't need any argument. edit, show and destroy method needs an id, at least, you need to know the id you are editing or show or destroying/deleting.
store(Request $request) we are going to submit to this from a form, so, that would take in a request object.
update(Request $request, $id) will take in requests and id because we are submitting the form to it, and we need to know which one to update.
Adding Route To The PostsController
We need to add a route to all of this, and we can do it this way:
Route::resource('posts', PostsController::class); // Create route for all the function in the controller
Make sure you do use App\Http\Controllers\PostsController; at the top of the page or else it won't work.
This will create all the routes you need to the method or functions in the PostsController. You can use php artisan route:list to check all your routes:
+--------+-----------+-------------------+---------------+------------------------------------------------+------------+
| Domain | Method | URI | Name | Action | Middleware |
+--------+-----------+-------------------+---------------+------------------------------------------------+------------+
| | GET|HEAD | / | | App\Http\Controllers\PagesController@index | web |
| | GET|HEAD | api/user | | Closure | api |
| | | | | | auth:api |
| | GET|HEAD | dashboard | | App\Http\Controllers\PagesController@dashboard | web |
| | GET|HEAD | posts | posts.index | App\Http\Controllers\PostsController@index | web |
| | POST | posts | posts.store | App\Http\Controllers\PostsController@store | web |
| | GET|HEAD | posts/create | posts.create | App\Http\Controllers\PostsController@create | web |
| | GET|HEAD | posts/{post} | posts.show | App\Http\Controllers\PostsController@show | web |
| | PUT|PATCH | posts/{post} | posts.update | App\Http\Controllers\PostsController@update | web |
| | DELETE | posts/{post} | posts.destroy | App\Http\Controllers\PostsController@destroy | web |
| | GET|HEAD | posts/{post}/edit | posts.edit | App\Http\Controllers\PostsController@edit | web |
+--------+-----------+-------------------+---------------+------------------------------------------------+------------+
When a client sends a get request to example.com/post it would load the index method in the PostsController, if it is a post request, it would load the store method in the PostsController, and so on. Laravel route::resource saves us time for having to create a route for each and every of this.
Fetching Data With Eloquent
If you create a model called Post, by default the table name is going to be posts (plural), if you create a mode called User, the table is going to be users.
Although, we changed the settings by adding a prefix which it would respect.
If you ever want to change the table name, primary key, and even if you want timestamp enabled, you can do something like so in your Post model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use HasFactory;
protected $table = 'posts'; // Table Name
protected $primaryKey = 'post_id'; // Primary Key
protected $timestamps = true; // Enabled
/**
* The number of models to return for pagination.
*
* @var int
*/
protected $perPage = 5;
}
Make sure you not prefixing the table name if you've already done that in the config/database.php file.
This would override the parent Model class, if you want more stuff included, you can find it in \vendor\laravel\framework\src\Illuminate\Database\Eloquent\Model.php, there are lots of options you can copy over to your child model.
That is that, now, let's work with the PostsController index method, that would take care of showing post whenever user visits website.com/posts.
So, far, here is how our application looks:
The post in the image above are hardcoded, and that is from the previous tutorial.
We would be fetching data from the database for this guide instead of hardcoding.
First create a folder named posts in the view folder, and create a new file named index.blade.php
In the index.blade.php, I'll have:
@extends('layouts.app')
@section('content')
<h1 class="text-center">Post Page</h1>
@endsection
and in the index method of the PostsController, I'll have:
public function index()
{
// Post Page
return view('posts.index');
}
Now, when you go to Post URL, you should have the following:
As you can see it loaded the view. The next step is to bring in the model class into the PostsController, import it using: use App\Post:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Post; // Bring in the Post Model
And now, we can use any of the model functions using Eloquent. Here is an example, if I do:
public function index() {
// Post Page
return Post::all();
return view('posts.index');
}
If I access the post page, it would return the following:
Which is all our post data in the sa_posts table. Once you use the return Post::all(), it would stop execution immediately it spits out the data.
To pass it into our view, we would put the array data into a variable, and we then pass it into our view with the ->with method like so:
...
public function index() {
// Post Page
$posts = Post::all();
return view('posts.index')->with('posts', $posts);
}
...
In the index.blade.php, we can then loop through it, but before looping through it, here is an array of how the data is structured:
object(Illuminate\Database\Eloquent\Collection)#1204 (1) {
["items":protected]=>
array(3) {
[0]=>
object(App\Models\Post)#1205 (27) {
["table":protected]=>
string(5) "posts"
["primaryKey":protected]=>
string(7) "post_id"
["timestamps"]=>
bool(true)
["perPage":protected]=>
int(5)
["connection":protected]=>
string(5) "mysql"
["keyType":protected]=>
string(3) "int"
["incrementing"]=>
bool(true)
["with":protected]=>
array(0) {
}
["withCount":protected]=>
array(0) {
}
["exists"]=>
bool(true)
["wasRecentlyCreated"]=>
bool(false)
["attributes":protected]=>
array(10) {
["post_id"]=>
int(1)
["post_title"]=>
string(8) "Post One"
["post_excerpt"]=>
string(18) "This is an excerpt"
["post_author"]=>
string(17) "The_Devsrealm_Guy"
["post_author_link"]=>
NULL
["post_image_url"]=>
NULL
["post_content"]=>
string(58) "This is my first blog post, I hope you enjoy reading it ;)"
["post_status"]=>
int(0)
["created_at"]=>
string(19) "2020-11-28 01:47:22"
["updated_at"]=>
string(19) "2020-11-28 01:47:22"
}
["original":protected]=>
array(10) {
["post_id"]=>
int(1)
["post_title"]=>
string(8) "Post One"
["post_excerpt"]=>
string(18) "This is an excerpt"
["post_author"]=>
string(17) "The_Devsrealm_Guy"
["post_author_link"]=>
NULL
["post_image_url"]=>
NULL
["post_content"]=>
string(58) "This is my first blog post, I hope you enjoy reading it ;)"
["post_status"]=>
int(0)
["created_at"]=>
string(19) "2020-11-28 01:47:22"
["updated_at"]=>
string(19) "2020-11-28 01:47:22"
}
["changes":protected]=>
array(0) {
}
["casts":protected]=>
array(0) {
}
["classCastCache":protected]=>
array(0) {
}
["dates":protected]=>
array(0) {
}
["dateFormat":protected]=>
NULL
["appends":protected]=>
array(0) {
}
["dispatchesEvents":protected]=>
array(0) {
}
["observables":protected]=>
array(0) {
}
["relations":protected]=>
array(0) {
}
["touches":protected]=>
array(0) {
}
["hidden":protected]=>
array(0) {
}
["visible":protected]=>
array(0) {
}
["fillable":protected]=>
array(0) {
}
["guarded":protected]=>
array(1) {
[0]=>
string(1) "*"
}
}
[1]=>
object(App\Models\Post)#1206 (27) {
["table":protected]=>
string(5) "posts"
["primaryKey":protected]=>
string(7) "post_id"
["timestamps"]=>
bool(true)
["perPage":protected]=>
int(5)
["connection":protected]=>
string(5) "mysql"
["keyType":protected]=>
string(3) "int"
["incrementing"]=>
bool(true)
["with":protected]=>
array(0) {
}
["withCount":protected]=>
array(0) {
}
["exists"]=>
bool(true)
["wasRecentlyCreated"]=>
bool(false)
["attributes":protected]=>
array(10) {
["post_id"]=>
int(2)
["post_title"]=>
string(8) "Post Two"
["post_excerpt"]=>
string(18) "This is an excerpt"
["post_author"]=>
string(17) "The_Devsrealm_Guy"
["post_author_link"]=>
NULL
["post_image_url"]=>
NULL
["post_content"]=>
string(64) "This is my second blog post, I hope you enjoy reading it, thanks"
["post_status"]=>
int(0)
["created_at"]=>
string(19) "2020-11-28 01:59:15"
["updated_at"]=>
string(19) "2020-11-28 01:59:15"
}
["original":protected]=>
array(10) {
["post_id"]=>
int(2)
["post_title"]=>
string(8) "Post Two"
["post_excerpt"]=>
string(18) "This is an excerpt"
["post_author"]=>
string(17) "The_Devsrealm_Guy"
["post_author_link"]=>
NULL
["post_image_url"]=>
NULL
["post_content"]=>
string(64) "This is my second blog post, I hope you enjoy reading it, thanks"
["post_status"]=>
int(0)
["created_at"]=>
string(19) "2020-11-28 01:59:15"
["updated_at"]=>
string(19) "2020-11-28 01:59:15"
}
["changes":protected]=>
array(0) {
}
["casts":protected]=>
array(0) {
}
["classCastCache":protected]=>
array(0) {
}
["dates":protected]=>
array(0) {
}
["dateFormat":protected]=>
NULL
["appends":protected]=>
array(0) {
}
["dispatchesEvents":protected]=>
array(0) {
}
["observables":protected]=>
array(0) {
}
["relations":protected]=>
array(0) {
}
["touches":protected]=>
array(0) {
}
["hidden":protected]=>
array(0) {
}
["visible":protected]=>
array(0) {
}
["fillable":protected]=>
array(0) {
}
["guarded":protected]=>
array(1) {
[0]=>
string(1) "*"
}
}
[2]=>
object(App\Models\Post)#1207 (27) {
["table":protected]=>
string(5) "posts"
["primaryKey":protected]=>
string(7) "post_id"
["timestamps"]=>
bool(true)
["perPage":protected]=>
int(5)
["connection":protected]=>
string(5) "mysql"
["keyType":protected]=>
string(3) "int"
["incrementing"]=>
bool(true)
["with":protected]=>
array(0) {
}
["withCount":protected]=>
array(0) {
}
["exists"]=>
bool(true)
["wasRecentlyCreated"]=>
bool(false)
["attributes":protected]=>
array(10) {
["post_id"]=>
int(3)
["post_title"]=>
string(10) "Post Three"
["post_excerpt"]=>
string(18) "This is an excerpt"
["post_author"]=>
string(17) "The_Devsrealm_Guy"
["post_author_link"]=>
NULL
["post_image_url"]=>
NULL
["post_content"]=>
string(63) "This is my third blog post, I hope you enjoy reading it, thanks"
["post_status"]=>
int(0)
["created_at"]=>
string(19) "2020-11-28 01:59:51"
["updated_at"]=>
string(19) "2020-11-28 01:59:51"
}
["original":protected]=>
array(10) {
["post_id"]=>
int(3)
["post_title"]=>
string(10) "Post Three"
["post_excerpt"]=>
string(18) "This is an excerpt"
["post_author"]=>
string(17) "The_Devsrealm_Guy"
["post_author_link"]=>
NULL
["post_image_url"]=>
NULL
["post_content"]=>
string(63) "This is my third blog post, I hope you enjoy reading it, thanks"
["post_status"]=>
int(0)
["created_at"]=>
string(19) "2020-11-28 01:59:51"
["updated_at"]=>
string(19) "2020-11-28 01:59:51"
}
["changes":protected]=>
array(0) {
}
["casts":protected]=>
array(0) {
}
["classCastCache":protected]=>
array(0) {
}
["dates":protected]=>
array(0) {
}
["dateFormat":protected]=>
NULL
["appends":protected]=>
array(0) {
}
["dispatchesEvents":protected]=>
array(0) {
}
["observables":protected]=>
array(0) {
}
["relations":protected]=>
array(0) {
}
["touches":protected]=>
array(0) {
}
["hidden":protected]=>
array(0) {
}
["visible":protected]=>
array(0) {
}
["fillable":protected]=>
array(0) {
}
["guarded":protected]=>
array(1) {
[0]=>
string(1) "*"
}
}
}
}
Yh, really long and clunky, but I have a general idea of how I want the data to be looped. Here is what I'll do in my view, if posts is greater that zero or there are posts, we loop through it, and display whatever is there, if otherwise, we output, no post.
@extends('layouts.app')
@section('content')
<h1 class="text-center">Post Page</h1>
<div class="d-flex">
@if(count($posts) > 0 )
@foreach($posts as $post)
<div class="w-600 mw-full d-flex align-items-center"> <!-- w-400 = width: 40rem (400px), mw-full = max-width: 100% -->
<div class="card p-0 "> <!-- p-0 = padding: 0 -->
<img src="https://www.gethalfmoon.com/static/site/img/image-1.png" class="img-fluid rounded-top" alt="..."> <!-- rounded-top = rounded corners on the top -->
<!-- Nested content container inside card -->
<div class="content">
<h2 class="content-title">
{{$post->post_title}}
</h2>
<p class="text-muted">
{{$post->post_content}}
</p>
<div class="text-right"> <!-- text-right = text-align: right -->
<a href="#" class="btn">Read more</a>
</div>
</div>
</div>
</div>
@endforeach
@else
<p>No Posts</p>
@endif
</div>
@endsection
I am using halfmoon CSS framework, so, it might look clunky to you, but here is the relevant part:
@if(count($posts) > 0 )
If $post is greater than zero, we then loop:
@foreach($posts as $post)
To access the data value of a certain field each time it loops through, I do something like this for the post_title:
{{$post->post_title}}
for the content, I do:
{{$post->post_content}}
I am still hardcoding the image link, I'll change it later on. If post is not available, we then print No Post:
@else
<p>No Posts</p>
@endif
Here is the result:
All, of our post are been displayed. Cool. Let's add more fields, like the date created. Here is the relevant section, I added it beneath the post title:
...
<div class="content">
<h2 class="content-title">
{{$post->post_title}}
</h2>
<div>
<span class="text-muted">
<i class="fa fa-clock-o mr-5" aria-hidden="true"></i>{{$post->created_at}} <!-- mr-5 = margin-right: 0.5rem (5px) -->
</span>
</div>
<p class="text-muted">
{{$post->post_content}}
</p>
<div class="text-right"> <!-- text-right = text-align: right -->
<a href="#" class="btn">Read more</a>
</div>
</div>
...
Output:
Interesting, we would implement the author name later on. For now, I'll go to my navbar, and reference the post, like so:
<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>
I added that in my navigation.blade.php, if you haven't been following from the previous guide, you might want to do so by reading the post I link to at the start of this guide.
and now we have:
Good. Now, let's make the Read more button link to each post.
We can do it like so:
<div class="text-right"> <!-- text-right = text-align: right --> <a href="/posts/{{$post->post_id}}" class="btn">Read more</a> </div>
Now, the read more button would link to each individual post, but when clicked it won't show anything, and that is what the show method in the PostsController is created for, we would implement the function in the show method
We would use the show method in the PostsController for that, here is how it looks:
public function show($id) {
// Show Individual Post
}
It has an id parameter which would be passed into it from the URL, e.g example.com/post/1. 1 would be passed in.
To fetch it from the database using Eloquent, we can do:
public function show($id) {
// Show Individual Post
return Post::find($id);
}
It is as simple as that, and when you click the post 1 read more, you'll get:
when you click post 2, you get something similar. So, let's put it into a variable, and we then load a view, so, edit the show method like so:
public function show($id) {
// Show Individual Post
$post = Post::find($id);
return view('posts.singular')->with('post', $post)
}
The singular view would hold only a post. create a new file named singular.blade.php in your view folder:
and in the view folder, I'll use the following:
Note: You don't have to loop through the $posts variable anymore, you should have access to it:
@extends('layouts.app')
@section('content')
<div class="content text-extra-letter-spacing">
<h1 class=" text-center">{{$post->post_title}}</h1>
<div>
<span class="text-muted">
<i class="fa fa-clock-o mr-5"
aria-hidden="true"></i>Published On: {{$post->created_at}} <!-- mr-5 = margin-right: 0.5rem (5px) -->
</span>
</div>
<p>{{$post->post_content}}</p>
</div>
@endsection
When I click Post 1 Read more, I get:
Cool. Let's add a breadcrumb:
Breadcrumb In Laravel
@extends('layouts.app')
@section('content')
<div class="content text-extra-letter-spacing">
<nav aria-label="Breadcrumb navigation example">
<ul class="breadcrumb text-left">
<li class="breadcrumb-item"><a href="/">Home</a></li>
<li class="breadcrumb-item"><a href="/posts">Posts</a></li>
<li class="breadcrumb-item active" aria-current="page"><a href="#">{{$post->post_title}}</a></li>
</ul>
</nav>
<h1 class=" text-center">{{$post->post_title}}</h1>
<div>
<span class="text-muted">
<i class="fa fa-clock-o mr-5"
aria-hidden="true"></i> Published On: {{$post->created_at}} <!-- mr-5 = margin-right: 0.5rem (5px) -->
</span>
</div>
<p>{{$post->post_content}}</p>
</div>
@endsection
You can use whatever CSS framework you like, the relevant part is the way I am referencing the $post items. Here is the output:
Pagination With Laravel
Let's create a pagination, you can easily do it with Eloquent this way:
public function index() {
// Post Page
$posts = Post::paginate(2);
return view('posts.index')->with('posts', $posts);
}
and in our view, I'll add the following under the @endsection:
Note: I am using $posts and not $post, since you want to handle the whole posts pagination, you should use the variable that contains the array of all the post, which in our case is $posts.
...
...
</div>
@endforeach
@else
<p>No Posts</p>
@endif
</div>
{{$posts->links()}}
@endsection
By default, the views rendered to display the pagination links are compatible with the Tailwind CSS framework. Since, I am not using Tailwind, I can customize the pagination views by exporting it to resources/views/vendor directory using the vendor:publish command:
php artisan vendor:publish --tag=laravel-pagination
This command will place the views in the resources/views/vendor/pagination directory. The tailwind.blade.php file within this directory corresponds to the default pagination view. I'll edit it to work with my framework. Before I show you the code I came up with, here is my pagination:
Here is the code:
@if ($paginator->hasPages())
<nav role="navigation" aria-label="{{ __('Pagination Navigation') }}" class="flex items-center justify-between">
<ul>
{{-- Previous Page Link --}}
@if ($paginator->onFirstPage())
<li class="page-item disabled" aria-disabled="true" aria-label="@lang('pagination.previous')">
<span class="page-link" aria-hidden="true">‹</span>
</li>
@else
<li class="page-item">
<a href="{{ $paginator->previousPageUrl() }}" class="page-link">
<i class="fa fa-angle-left" aria-hidden="true"></i>
<span class="sr-only"> {!! __('pagination.previous') !!}</span> <!-- sr-only = only for screen readers -->
</a>
</li>
@endif
{{-- Pagination Elements --}}
@foreach ($elements as $element)
{{-- "Three Dots" Separator --}}
@if (is_string($element))
<li class="page-item disabled" aria-disabled="true"><span class="page-link">{{ $element }}</span></li>
@endif
{{-- Array Of Links --}}
@if (is_array($element))
@foreach ($element as $page => $url)
@if ($page == $paginator->currentPage())
<li class="page-item active">
<a href="#" class="page-link" tabindex="-1">{{ $page }}</a>
</li>
@else
<li class="page-item"><a href="{{ $url }}" class="page-link" aria-label="{{ __('Go to page :page', ['page' => $page]) }}">{{ $page }}</a></li>
@endif
@endforeach
@endif
@endforeach
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
<li class="page-item">
<a href="{{ $paginator->nextPageUrl() }}" class="page-link" aria-label="{{ __('pagination.next') }}">
<i class="fa fa-angle-right" aria-hidden="true"></i>
<span class="sr-only">Next</span> <!-- sr-only = only for screen readers -->
</a>
</li>
@else
<li class="page-item disabled">
<a href="#" class="page-link" aria-label="{{ __('pagination.next') }}">
<i class="fa fa-angle-right" aria-hidden="true"></i>
<span class="sr-only">Next</span> <!-- sr-only = only for screen readers -->
</a>
</li>
@endif
</ul>
<!-- Example: Showing 3 to 3 of 3 results -->
<div>
<!-- Start Show of-->
<p class="text-sm text-gray-700 leading-5">
{!! __('Showing') !!}
<span class="font-medium">{{ $paginator->firstItem() }}</span>
{!! __('to') !!}
<span class="font-medium">{{ $paginator->lastItem() }}</span>
{!! __('of') !!}
<span class="font-medium">{{ $paginator->total() }}</span>
{!! __('results') !!}
</p>
</div> <!-- End Show of-->
</nav>
@endif
Let me give you a brief explanation on how it works.
I wrapped the pagination in:
@if ($paginator->hasPages())
----
----
@endif
The $paginator->haspages() method determines if there are enough items to split into multiple pages. If there are enough items to split into multiple pages, we then introduce the paginator navigation:
@if ($paginator->hasPages())
<nav role="navigation" aria-label="{{ __('Pagination Navigation') }}" class="flex items-center justify-between">
----
----
</nav>
@endif
The aria-label is an attribute designed to help assistive technology (e.g. screen readers), using this would help visually impaired users when using screen readers.
Going further, I wrapped it in UL tags and preceded it by determining if the paginator is on the first page, if true, I disable the previous page link, which makes sense since if you are on the first page you can't go to any previous link, but if otherwise, that is if you are not on the first page, I use the {{ $paginator->previousPageUrl() }} in the anchor tag to get the URL for the previous page:
@if ($paginator->hasPages())
<nav role="navigation" aria-label="{{ __('Pagination Navigation') }}" class="flex items-center justify-between">
<ul>
{{-- Previous Page Link --}}
@if ($paginator->onFirstPage())
<li class="page-item disabled" aria-disabled="true" aria-label="@lang('pagination.previous')">
<span class="page-link" aria-hidden="true">‹</span>
</li>
@else
<li class="page-item">
<a href="{{ $paginator->previousPageUrl() }}" class="page-link">
<i class="fa fa-angle-left" aria-hidden="true"></i>
<span class="sr-only"> {!! __('pagination.previous') !!}</span> <!-- sr-only = only for screen readers -->
</a>
</li>
@endif
----
----
</ul>
</nav>
@endif
The classes I am adding is from halfmoon CSS framework, you can edit it with yours if you are using a different framework or if you prefer to do things your way.
Going further, I added the pagination element:
{{-- Pagination Elements --}}
@foreach ($elements as $element)
{{-- "Three Dots" Separator --}}
@if (is_string($element))
<li class="page-item disabled" aria-disabled="true"><span class="page-link">{{ $element }}</span></li>
@endif
{{-- Array Of Links --}}
@if (is_array($element))
@foreach ($element as $page => $url)
@if ($page == $paginator->currentPage())
<li class="page-item active">
<a href="#" class="page-link" tabindex="-1">{{ $page }}</a>
</li>
@else
<li class="page-item"><a href="{{ $url }}" class="page-link" aria-label="{{ __('Go to page :page', ['page' => $page]) }}">{{ $page }}</a></li>
@endif
@endforeach
@endif
@endforeach
The pagination template loops over an $elements array that is defined in Illuminate\Pagination\LengthAwarePaginator. This uses a class named Illuminate\Pagination\UrlWindow to construct arrays of links and the three dots items as appropriate.
If you want to configure how many links to show on either side of the dots, you can do like so in your index.blade.php:
{{$posts->onEachSide(1)->links()}}
onEachSide(1) show the number of pages before and after ACTIVE page. If for example you have a pagination of 1, you'll get:
onEachSide(2) would show 2 pages before and after Active page, hope you get that now.
Here are some useful info, if paginator is on current page: @if ($page == $paginator->currentPage()), I added an active class to the li tag, here is how it would look like:
It would give it a distinct color so our users can know they are on the current page when paginating. If otherwise, we add the regular li tag coupled with the a tag. Again, the aria-label is to aid visually impaired users.
<li class="page-item"><a href="{{ $url }}" class="page-link" aria-label="{{ __('Go to page :page', ['page' => $page]) }}">{{ $page }}</a></li>
I think that explains how the pagination works.
Other useful Stuff You can do With Eloquent
Here are other stuff you can do with Eloquent:
- Retrieving A Record By Primary Key:
$post = Post::find(1);
- Delete a record:
$post = Post::delete(id);
- Order a record desc:
$post = Post::orderBy('title, 'desc')->get();
- Order a record asc:
$post = Post::orderBy('title, 'asc')->get();
- Find post by title:
$post = Post::where('title', 'Post One')->get();
- Retrieve a user by email address:
$user = User::where('email', '=', $email)->first();
Regular SQL queries in Laravel
There are cases where you might want to use the regular SQL queries. If you want to use a regular SQL, you import the DB library using use Illuminate\Support\Facades\DB;
at the top of your controller.
An example of using the regular SQL: $posts = DB::select('SELECT * FROM sa_posts LIMIT 2');
You can then loop through the posts array in your view. This would limit the post to only 2. It is equivalent of $posts = Post::all()->take(2); in Eloquent.
Here is another in the context of index method in a UsersController:
public function index() {
$users = DB::table('users')->get();
return view('user.index')->with('users', $users);
}
The get method returns an Illuminate\Support\Collection containing the results where each result is an instance of the PHP stdClass object. You may access each column's value by accessing the column as a property of the object:
foreach ($users as $user) {
echo $user->name;
}
If you need to retrieve a single row from the database table, you may use the first method. This method will return a single stdClass object:
$user = DB::table('users')->where('name', 'John')->first();
return view('user.index')->with('user', $user);
In your view, you then do:
echo $user->name;
To retrieve a single row by its id column value, use the find method:
$user = DB::table('users')->find(3);
You can read more about using SQL queries in Laravel
As you can see, there are a couple of ways you can do the same thing. If you are new to SQL, you might want to use Eloquent, if you are proficient in SQL, just use the regular queries, Laravel query builder or the regular SQL uses the PHP Data Objects (PDO), so, no need to worry about SQL injection attack, and it'a more faster if you are building a site that would be using millions of data. It's your choice really, it's up to you, for the rest of this guide and future guides I'll use Eloquent as that might be easy for my readers.
Forms and Saving Data
If you recall, we have a create function in our PostsController, and if a user sends a get request to the /posts/create page, it would fire the create function in the PostsController, here are the route if you want a refresher:
+--------+-----------+-------------------+---------------+------------------------------------------------+------------+
| Domain | Method | URI | Name | Action | Middleware |
+--------+-----------+-------------------+---------------+------------------------------------------------+------------+
| | GET|HEAD | / | | App\Http\Controllers\PagesController@index | web |
| | GET|HEAD | api/user | | Closure | api |
| | | | | | auth:api |
| | GET|HEAD | dashboard | | App\Http\Controllers\PagesController@dashboard | web |
| | GET|HEAD | posts | posts.index | App\Http\Controllers\PostsController@index | web |
| | POST | posts | posts.store | App\Http\Controllers\PostsController@store | web |
| | GET|HEAD | posts/create | posts.create | App\Http\Controllers\PostsController@create | web |
| | GET|HEAD | posts/{post} | posts.show | App\Http\Controllers\PostsController@show | web |
| | PUT|PATCH | posts/{post} | posts.update | App\Http\Controllers\PostsController@update | web |
| | DELETE | posts/{post} | posts.destroy | App\Http\Controllers\PostsController@destroy | web |
| | GET|HEAD | posts/{post}/edit | posts.edit | App\Http\Controllers\PostsController@edit | web |
+--------+-----------+-------------------+---------------+------------------------------------------------+------------+
By default, if you go to the /posts/create page in your browser, it won't return a blank page, and that is because there nothing in the create function. First create a create.blade.php in your posts view:
For now, I'll add the following to the create view:
@extends('layouts.app')
@section('content')
<h1 class="text-center">Add New Post</h1>
@endsection
In your PostsController, return the view like so:
.........
public function create()
{
// Method to add or create new post
return view('posts.create');
}
.........
So, if you go to yourwebsite.com/posts/create it would return the view:
Easy enough I guess. The next thing is building the forms for collecting the new post. We can do this ourselves but forms are more trickier to build securely, so, let's use the LaravelCollective HTML package to fasten the form development pace.
I'll show you how to do this, but here is the doc if you are interested to learn more: LaravelCollective HTML Forms
Install the package using composer like so:
composer require laravelcollective/html
Once installed, it would be automatically added to require in composer:
Next, add your new provider to the providers array of config/app.php: Collective\Html\HtmlServiceProvider::class,
Example:
Finally, add two class aliases to the aliases array of config/app.php:
'Form' => Collective\Html\FormFacade::class, 'Html' => Collective\Html\HtmlFacade::class,
For example:
Don't get things twisted, the provider arrays are all of the service provider classes that will be loaded for your application. Note that many of these are "deferred" providers, meaning they will not be loaded on every request, but only when the services they provide are actually needed.
The class aliases helps to shorten the class name when referencing it, whenever you want to reference your class anywhere in your project, you have to just call the alias and not the whole ClassName. For example, instead of calling: Collective\Html\FormFacade::class anytime we need the Form class, we can just call echo Form::radio('category_id', '1'); which is clearer, laravel would automatically linked the alias to the actual namespace, cool, let's move further.
To open up a form, we would add the following to our create view:
@extends('layouts.app')
@section('content')
<h3 class="text-center">Add New Post</h3>
{!! Form::open(['action' => 'App\Http\Controllers\PostsController@store']) !!}
{!! Form::close() !!}
@endsection
I added action, plus the method we are submitting the form input to, which is the store method in the PostsController, by default, a POST method will be assumed, which is what we need, however, you can add it just to be confident it is there like so:
{!! Form::open(['action' => 'App\Http\Controllers\PostsController@store', 'method' => 'POST']) !!}
If you want to add something like so within your form:
<div class="form-group">
<label for="title" class="required">Title</label>
<input type="text" class="form-control" id="post-title" placeholder="Enter Title Here" required="required">
</div>
This is how you would do it with the Laravel Collective HTML builder:
{!! Form::open(['action' => 'App\Http\Controllers\PostsController@store', 'method' => 'POST']) !!}
<!-- Text Title Section -->
<div class="form-group w-xl-half mw-full m-auto">
{{ Form::label('title', 'Title', ['class' => 'required']) }}
{{-- Form Input--}}
{{ Form::text('text', '', [
'class' => 'form-control form-control-lg h-50',
'id' => 'post-title',
'placeholder' => 'Enter Title Here',
'required' => 'required']) }}
</div>
{!! Form::close() !!}
In the second parameter of the Form::text you'll notice I added an empty string ' ', you can fill it if you want to set some default value, which isn't something I want. This is the output:
Before I add the text area, I want to remove the Title* so it won't be too clunky, I'll simply change the label
from
{{ Form::label('title', 'Title', ['class' => 'required']) }}
to
{{ Form::label('title', ' ', ['class' => '']) }}
and now we have:
For the text area, I'll have:
<!-- Text Area Section -->
<div class="form-group w-xl-half mw-full m-auto">
{{ Form::label('body', ' ', ['class' => '']) }}
{{-- Body Input--}}
{{ Form::textarea('title', '', [
'class' => 'form-control form-control-lg h-50',
'id' => 'body-area',
'placeholder' => 'You can Start Writing...']) }}
</div>
All together, we have:
@extends('layouts.app')
@section('content')
<h3 class="text-center">Add New Post</h3>
{!! Form::open(['action' => 'App\Http\Controllers\PostsController@store', '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', '', [
'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', '', [
'class' => 'form-control form-control-lg h-50',
'id' => 'body-area',
'placeholder' => 'You can Start Writing...']) }}
</div>
{!! Form::close() !!}
@endsection
Cool. Let's add a submit or publish button, I'll add it at the end of the body area like so:
<!-- Text Area Section -->
<div class="form-group w-xl-half mw-full m-auto">
{{ Form::label('body', ' ', ['class' => '']) }}
{{-- Body Input--}}
{{ Form::textarea('body', '', [
'class' => 'form-control form-control-lg h-50',
'id' => 'body-area',
'placeholder' => 'You can Start Writing...']) }}
<div class="text-center pt-20">
{{ Form::submit('Publish', ['class' =>'btn btn-primary', 'type' => 'submit']) }}
</div>
</div>
I have mentioned this like thousand of times, I am using the halfmoon css framework, so, the classes I have been using are from halfmoon, change the classes to your preferred framework classes. Here is the output with the button incorporated:
If you push the publish button without filling the title section, you would get Please fill out this field, and that is because I added a required option when I was building that above. That is one form of validation, we would also add a new validation on the server side level just to be safe.
If you've fill the two input, and you push the publish button it would make a POST request to the store method in the PostsController. Before we go on, let's create an helper function that would handle success notification or error, create a new folder called inc in your view folder, and add a file name notifications.blade.php:
Add the following:
{{--Notifications for multiple validation error or single validation error --}}
@if(count($errors) > 0)
@foreach($errors->all() as $error)
<div class="alert alert-danger w-500 mw-full m-auto" role="alert">
<button class="close" onclick="this.parentNode.classList.add('dispose')" type="button" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<h4 class="alert-heading">Error</h4>
{{$error}}
</div>
@endforeach
@endif
{{--If you user POST request is successful, do below --}}
@if(session('success'))
<div class="alert alert-success w-500 mw-full m-auto" role="alert">
<button class="close" onclick="this.parentNode.classList.add('dispose')" type="button" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<h4 class="alert-heading">Sucess</h4>
{{session('success')}}
</div>
@endif
{{--If you user POST request isn't successful, do below --}}
@if(session('error'))
<div class="alert alert-danger w-500 mw-full m-auto" role="alert">
<button class="close" onclick="this.parentNode.classList.add('dispose')" type="button" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<h4 class="alert-heading">Error</h4>
{{session('error')}}
</div>
@endif
In the app.blade.php, I'll include it like so:
..........
..........
<div class="page-wrapper with-navbar">
@include('layouts.navigation')
<div class="content-wrapper text-center"> {{-- Content Wrapper Starts --}}
<div class="container-fluid">
@include('inc.notifications')
@yield('content')
</div>
</div> {{-- Content Wrapper Ends --}}
</div> {{-- page-wrapper Ends --}}
..........
..........
Having done that, here is the code for the store method plus a comments block that explains how it all works:
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));
$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');
}
I am doing an extra validation on the server side level, and another useful stuff I am doing is the way I am programmatically getting the excerpt of each post saved. I default it to 5, but you can change it at will.
If I now add a new post, I would first get redirected to the /posts page coupled with a success message like so:
If you add a title with no content, you'll get:
Cool.
In my index.blade.php, I'll change the post_content to post_excerpt, so, I can see the excerpt instead of the full post.
.....
<p class="text-muted">
{{$post->post_excerpt}}
</p>
.....
and if we view, the blog page, we get:
It respects our 5 word excerpt, brilliant.
Also, feel free to put a create post link in your navbar, so, users can easily add a new post.
Integrating CKEditor 5 In Laravel 8
One more thing I'll love to do is to integrate a nice looking editor for our text area, I'll go with CKEditor 5, it is free, open-source, and easy to implement.
Include the following before the closing body tag in the app.blade.php:
<script src="https://cdn.ckeditor.com/ckeditor5/23.1.0/classic/ckeditor.js"></script>
<script>
ClassicEditor
.create( document.querySelector( '#body-area' ) )
.catch( error => {
console.error( error );
} );
</script>
The 'body-area' id is the tag we used in our form text area, CKEditor is gonna replace it with its editor, and now we have this:
If you write anything and click publish, it would store the content as HTML in your table. If you view the post, it won't parse it because in our singular.blade.php we escape the data before displaying the value, and this is done to avoid XSS attacks and all sorts of malicious scripts. To unescape it, we change:
<p>{{$post->post_content}}</p>
To:
<p>{!!$post->post_content!!}</p>
This should be used if you're sure that the value of your variable doesn't have any malicious script, however, you should be fine when using it in this context as we are only fetching HTML, and we don't want it escaped, besides CKEditor prevents any injection somewhat, although you should always run your own checks just to be sure. On a side note, if you are giving access to a trusted user or if you are only the user, you should be fine, but trust goes a long way ;) or just use this package: HTML Purifier and have a rest of mind.
One last thing, let's give our users an option to upload a featured image to a post. First, add an enctype of multipart/form-data to the form action like so:
{!! Form::open(['action' => 'App\Http\Controllers\PostsController@store', 'method' => 'POST', 'enctype' => 'multipart/form-data']) !!}
The 'enctype' => 'multipart/form-data' is used when uploading a file. Next up, I add the following to my create view:
<!-- File Image Upload -->
<div class="form-group mt-10">
<div class="custom-file">
{{ Form::file('featured_image', ['class' => '', 'id' => 'picture']) }}
<label for="picture">Choose Featured Image</label>
</div>
</div>
and this is what it displays:
You can choose an image just fine but it won't get attached to the post as we haven't defined that function yet.
First, let's define a new storage disk that laravel would store our images, go to: config/filesystem.php and add the following to your disk array:
'public_uploads' => [
'driver' => 'local',
'root' => public_path() . '/uploads',
'url' => env('APP_URL').'/public',
'visibility' => 'public',
],
If you don't know how to add it, here is how mine looks:
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app'),
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
],
// Added it below
'public_uploads' => [
'driver' => 'local',
'root' => public_path() . '/uploads',
'url' => env('APP_URL').'/public',
'visibility' => 'public',
],
...........
...........
When we were creating the structure for our post table, we added the following:
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 | |
+------------------+------------------+------+-----+-------------------+----------------+
10 rows in set (0.00 sec)
In the sa_posts table, we have a field name post_image_url, this is where we would save the image URL of whatever the user uploads. This isn't actually saving the image but a link to the image uploaded on our server, probably somewhere in your public folder, we would implement the actual upload in the PostsController.
Before adding the code, bring in the str helper library using: use Illuminate\Support\Str; // Bring in the string helper library
in your PostsController.
// Check if the featured image has been uploaded or better still, checks if the publish button has been clicked
if ($request->has('featured_image')) {
$image_name = $request->file('featured_image')->getClientOriginalName(); // Full file name e.g book.jpg
$image_extension = $request->file('featured_image')->extension(); // only extension e.g jpg
$date_format = date('Y_m_d_H_i_s'); // date format
$onlyfilename = pathinfo($image_name)['filename']; // e.g book or my book, would be my_book
/*
* The Str::slug method generates a URL friendly "slug" from the given string:
*
* $slug = Str::slug('Laravel 8 Framework', '_');
* // laravel-5-framework
*/
$cleanfilename = Str::slug($onlyfilename, '_');
// Each image will have the date of upload appended to its url to prevent user replacing the image if filename is the same
$storeimage = $cleanfilename . '_'. $date_format . '.' . $image_extension;
// Upload Image
/*
* I configured a new storage disk to store the image directly in the public/uploads folder instead of the default storage folder
*/
$image_path = $request->file('featured_image')->storeAs('/images', $storeimage, 'public_uploads');
}
Added explanation to the code above, in the Create new Post of the store method, we store the name in the post_image_url field like so:
// Create New Post
.................
.................
$post->post_image_url = $storeimage;
$post->save(); // Save all the above query into the table
To use the image in your views, e.g in your singular.blade.php, you can link to it like so:
<img style="" src="/uploads/images/{{$post->post_image_url}}">
In your edit view, you can also add the image upload functionality like so:
@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', 'enctype' => 'multipart/form-data']) !!}
<!-- 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>
<!-- File Image Upload -->
<div class="form-group mt-10">
<div class="custom-file">
{{ Form::file('featured_image', ['class' => '', 'id' => 'picture']) }}
<label for="picture">Choose Featured Image</label>
</div>
</div>
</div>
{!! Form::close() !!}
@endsection
Added it under <! -- File Image Upload -->. Also, don't forget the enctype or else you won't be able to upload the image.
You might also need to update an already added post image, for that, we can edit the update method like so:
public function update(Request $request, $id) {
// Validation
$this->validate($request, [
'title' => 'required',
'body' => 'required',
'featured_image' => 'image|mimes:jpeg,png,jpg|nullable|max:1024'
]);
// Check if the featured image has been uploaded or better still, checks if the publish button has been clicked
if ($request->has('featured_image')) {
$image_name = $request->file('featured_image')->getClientOriginalName(); // Full file name e.g book.jpg
$image_extension = $request->file('featured_image')->extension(); // only extension e.g jpg
$date_format = date('Y_m_d_H_i_s'); // date format
/*
* The Str::slug method generates a URL friendly "slug" from the given string:
*
* $slug = Str::slug('Laravel 8 Framework', '_');
* // laravel-5-framework
*/
$onlyfilename = pathinfo(Str::slug($image_name, '_'))['filename']; // e.g book or my book, would be my_book
// Each image will have the date of upload appended to its url to prevent user replacing the image if filename is the same
$storeimage = $onlyfilename . '_'. $date_format . '.' . $image_extension;
// Upload Image
$image_path = $request->file('featured_image')->storeAs('/images', $storeimage, 'public_uploads');
}
// 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
// If user truly added an image, update the tables
if ($request->has('featured_image')) {
$post->post_image_url = $storeimage;
}
$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 updated successful, we redirect the user to the /posts url address with a sucess message.
*/
return redirect('/posts')->with('success', 'Post Updated');
}
We copied the one in the store method to the update method, I also check if the user truly updated anything, if true, we then set the new filename:
if ($request->has('featured_image')) {
$post->post_image_url = $storeimage;
}
We still have a little issue, when user updates or create a new post, we should redirect it back to that post they are editing and not back to our post page, you can do it by using the following return:
return redirect("/posts/$post->post_id/edit")->with('success', 'Post Successfully Created');
We are using $post->post_id to access the $post object, remember we are still in the controller.
If you ever want to delete a featured image when a post is deleted, you can use the following in your destroy method:
Before adding the code, bring in the storage library using: use Illuminate\Support\Facades\Storage;
in your PostsController. To delete, you do:
Storage::disk('public_uploads')->delete("images/$post->post_image_url");
I am specifying the storage disk we created before, which is public_upload. Here is our the destroy method looks altogether:
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');
}
Storage::disk('public_uploads')->delete("images/$post->post_image_url");
$post->delete();
return redirect('/dashboard')->with('success', 'Post Deleted');
}
Integrating TinyMCE5 In Laravel 8
If for some reason you can't integrate with CKEDITOR 5, then TinyMCE is a good option and even more robust and better than CK. Integration is also super simple.
Goto tinymce self-hosted section: https://www.tiny.cloud/get-tiny/self-hosted/ and download the latest version.
Add it to your JS folder
Before proceeding, you can also include UNISHARP filemanager right off the bat to work with your tinymce editor:
Install using:
composer require unisharp/laravel-filemanager
Publish the package’s config and assets :
php artisan vendor:publish --tag=lfm_config
php artisan vendor:publish --tag=lfm_public
Clear cache to be sure everything is working fine:
php artisan route:clear
php artisan config:clear
In your config/filesystems.php, you can configure your tinymce disk as follows, this is the place your images and stuff are gonna be placed:
// Tinymce Disk
'public_tmce' => [
'driver' => 'local',
'root' => public_path() . '/uploads',
'url' => env('APP_URL').'/uploads',
'visibility' => 'public',
],
and in config/lfm.php, change the disk to:
'disk' => 'public_tmce',
Anything you upload would be placed under public/uploads
Now, in your app.blade.php layout, you can include tinymce like so:
<script src="{{ asset('js/tinymce/tinymce.min.js') }}"></script>
<script>
const editor_config = {
path_absolute: "/",
selector: '#body-area',
relative_urls: false,
plugins: [
"advlist autolink lists link image charmap print preview hr anchor pagebreak",
"searchreplace wordcount visualblocks visualchars code fullscreen",
"insertdatetime media nonbreaking save table directionality",
"emoticons template paste textpattern"
],
toolbar: "insertfile undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image media",
file_picker_callback: function (callback, value, meta) {
var x = window.innerWidth || document.documentElement.clientWidth || document.getElementsByTagName('body')[0].clientWidth;
var y = window.innerHeight || document.documentElement.clientHeight || document.getElementsByTagName('body')[0].clientHeight;
var cmsURL = editor_config.path_absolute + 'laravel-filemanager?editor=' + meta.fieldname;
if (meta.filetype == 'image') {
cmsURL = cmsURL + "&type=Images";
} else {
cmsURL = cmsURL + "&type=Files";
}
tinyMCE.activeEditor.windowManager.openUrl({
url: cmsURL,
title: 'Filemanager',
width: x * 0.8,
height: y * 0.8,
resizable: "yes",
close_previous: "no",
onMessage: (api, message) => {
callback(message.content);
}
});
}
};
tinymce.init(editor_config);
</script>
As you can see, the selector is #body-aread, so, make sure you have that selector wherever you are using it, e.g in post/create.blade.php, it looks like so:
<!-- Text Area Section -->
<div class="form-group w-xl-half mw-full m-auto">
{{ Form::label('body', ' ', ['class' => '']) }}
{{-- Body Input--}}
{{ Form::textarea('body', '', [
'class' => 'form-control form-control-lg h-50',
'id' => 'body-area',
'placeholder' => 'You can Start Writing...']) }}
<div class="text-center pt-20">
{{ Form::submit('Publish', ['class' =>'btn btn-primary', 'type' => 'submit']) }}
</div>
</div>
Here is an image: