PHP Blog Walkthrough with CodeIgniter
The following is a walkthrough on how to build a blog in CodeIgniter 3.1
It will have the following features:
- Posts - Create, modify, delete
- Tags - Posts can have tags, view posts by tags
- Comments - Readers can leave comments
- Authentication - Blog owner has a user account
- Draft - Post can be in draft status or published status
Table of Contents
Installation
Files
- Set up a LAMP environment on your operating system
- Download CodeIgniter
- Open the downloaded ZIP and place in your LAMP
www
directory - Run your local webserver
- Go to localhost on your browser - you should see
Welcome to CodeIgniter!
Database
- Open up a database manager application
- Create a new session Log in with the MySQL credentials you set up with your LAMP installation If you did not set one up, try: Hostname:
localhost
User:root
No Password - Create a database
minimalist-blog
CodeIgniter Configuration
-
Open where your CodeIgniter files are in file explorer
-
Basic configuration:
- Open
application/config/config.php
- Look for the following existing options and replace the values:
$config['base_url'] = 'http://localhost/'
to set the base URL$config['index_page'] = '';
to remove the wordindex.php
from your URLs$config['global_xss_filtering'] = TRUE;
to enable automatic XSS filtering$config['csrf_protection'] = TRUE;
to enable CSRF tokens
- Open
-
Tell CodeIgniter about the database:
- Open
application/config/database.php
- Set
'hostname'
,'username'
and'password'
to match your database session - Set
'database'
to'minimalist-blog'
- Set
- Open
-
Have some helpful functionality autoloaded:
- Open
application/config/autoload.php
- Set
$autoload['libraries']
toarray('ion_auth')
- Set
$autoload['helper']
toarray('url', 'form')
- Set
- Open
-
Make the URLs look prettier by removing the
index.php
:- Create a file called
.htaccess
in the root of your CodeIgniter folder (so that it is next to theapplication
andsystem
folders - Open it using a text editor and insert and save:
<IfModule mod_rewrite.c> RewriteEngine on RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php/$1 [L] </IfModule>
- Turn on Apache's
mod_rewrite
module (method will depend on operating system) - Going to
http://localhost/welcome
on your browser should show the welcome page
- Create a file called
Part 1: Authentication System
We want to be able to be a user on this blog and have the ability to log in.
We will be using Ion Auth 2
- Download Ion Auth 2
- Extract the files into the
application
folder of your CodeIgniter - Open your database application and have the
minimalist-blog
database selected - Open and run the Ion Auth SQL which is located at
application/sql/ion_auth.sql
- Ion Auth has a default admin user - we should deactivate this user and create our own
- Go to
http://localhost/auth/login
on your browser - Log in with Ion Auth's default admin user:
- Username:
admin@admin.com
- Password:
password
- Username:
- Create a user:
- Go to
http://localhost/minimalist-blog/auth
- Click
Create New User
- Fill in the fields and submit the form
- Back on the user list page, click
Edit
for the user you just created - Select the
admin
group and save
- Go to
- Log out by going to
http://localhost/minimalist-blog/auth/logout
- Log in as the new user at
http://localhost/minimalist-blog/auth/login
- Go to
http://localhost/minimalist-blog/auth
- Click
Active
on the default admin user to deactivate the user
- Go to
We now have our own user account, and the default admin account is disabled!
Part 2: Creating Posts
Creating the table
We will first need to create a posts
table in our database.
To start with, a blog post will have the following:
- Post ID
- Title
- Author
- Content
- Publish Date
Create this table by running the table SQL in your database manager:
CREATE TABLE `posts` (
`post_id` INT NOT NULL AUTO_INCREMENT,
`title` VARCHAR(50) NOT NULL,
`author` INT(11) UNSIGNED NOT NULL,
`content` TEXT NOT NULL,
`publish_date` DATETIME NULL DEFAULT NULL,
PRIMARY KEY (`id`),
INDEX `author` (`author`),
CONSTRAINT `FK__users` FOREIGN KEY (`author`) REFERENCES `users` (`id`)
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB;
Creating the form
We will create form to create a new post with.
Create a folder called posts
inside application/views
.
Inside the posts
folder, create a new file called create.php
.
Save the following HTML form into create.php
:
<?php echo form_open() ?>
<label>Post Title</label><br>
<input type="text" class="form-control" name="title"><br>
<label>Post Content</label><br>
<textarea name="content"></textarea>
<input type="submit" class="btn btn-primary" value="Post">
<?php echo form_close() ?>
To be able to view this form, we will need to set up a controller function for it.
Creating the controller
We will display the form on a page at the URL http://localhost/posts/create
.
To make this URL happen, we will make a new controller called Posts
and a function called create
inside that controller.
Create a file called Posts.php
(not posts.php
) inside application/controllers
with the following base controller code:
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class Post extends CI_Controller {
}
Now we create a function called create
which will display our form that we created.
Put the following inside the Post controller class:
...
public function create()
{
$this->load->view('posts/create');
}
...
Now we can go to http://localhost/posts/create
to see the form.
The form current does not do anything though - we want the view's form data to be passed into the controller.
Passing the Form Data
First we will edit the form view to pass the data to the correct place.
In application/views/posts/create.php
, replace the first line,
<?php echo form_open() ?>
with:
<?php echo form_open('posts/create') ?>
This will tell the form to submit its data to our controller function.
Now in our controller function, we'll print out the data that we get to make sure the data is being received.
In our Posts
controller, edit the create
function:
public function create() {
if($this->input->method() == 'post')
{
var_dump($this->input->post());
}
$this->load->view('posts/create');
}
PHP's var_dump
function will print out the contents of the posted form, only if the form has been posted.
Now if you go to http://localhost/posts/create
and post the form, you may see something like this printed at the top of the page:
array (size=2)
'title' => string 'My First Post' (length=13)
'content' => string 'Some content!' (length=13)
The form data has successfully been passed from the view to the controller!
Preparing the data
We have a working form which takes in the post title and post content, but to make a valid post we are still missing some information:
- Author - the user who submitted the post
- Publish Date - the date the post was created
We will define the two, and finally put all the information we have together inside one array in the create
function:
...
public function create() {
if($this->input->method() == 'post')
{
// Get user ID of current user
$author = $this->ion_auth->user()->row();
$author = $author->id;
// Get current date time
$publish_date = date("Y-m-d H:i:s");
// Put it all together
$data = array(
'title' => $this->input->post('title'),
'content' => $this->input->post('content'),
'author' => $author,
'publish_date' =>$publish_date
);
// Print it out
var_dump($data);
}
$this->load->view('posts/create');
}
...
We are printing out the final $data
array after form submission, so go ahead and submit the form again (make sure you are still logged in).
You should see something like this at the top of the page after you submit the form:
array (size=4)
'title' => string 'My First Post' (length=13)
'content' => string '123' (length=3)
'author' => string '3' (length=1)
'publish_date' => string '2017-10-22 21:54:43' (length=19)
We now have all the required information to make a blog post, and are ready to insert it into the database!
Creating the model
Now we need CodeIgniter to insert a row in the posts
table.
Create a new file called Posts_model.php
(not posts_mode.php
) in applications/model
.
The base model should look like this:
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class Posts_model extends CI_Model {
public function __construct()
{
$this->load->database();
}
}
Now create a function that inserts the row - it should take in an array of post data.
Insert the function under the __construct()
function inside the model
public function create($data)
{
$this->db->insert('posts', $data);
}
We are now ready for the controller to call this function.
Putting it together
We now need the controller to pass the form data to the model function.
First, we need to load the model into the controller.
In the Posts
controller, above the create
function create a __construct
function which loads the model:
public function __construct()
{
parent::__construct();
$this->load->model('posts_model');
}
Now in the create
function, replace the print lines -
// Print it out
var_dump($data);
with this to send it to the model:
// Create the post
$this->posts_model->create($data);
Go to the form page and submit a post (make sure you are logged in). After submission, check the posts
table with your database manager - you should see your new post!
Validation
There are a few conditions in which we don't want to create a post:
- When the title is empty
- When the content is empty
- When the user is not logged in
We will use CodeIgniter's form_validation
library to help us out in putting validation in our Posts
controller.
First add a line into the __construct
function to load the helper:
...
$this->load->library('form_validation');
...
Next, add some validation rules to the create
function inside where we've confirmed it's a post request:
...
public function create()
{
if($this->input->method() == 'post')
{
$this->form_validation->set_rules('title', 'Title', 'required');
$this->form_validation->set_rules('content', 'Content', 'required');
...
Now we will surround the call to the model function with a check to make sure there are no validation errors:
...
if($this->form_validation->run() !== FALSE)
{
// Create the post
$this->posts_model->create($data);
}
...
This way, we are only passing information to the model if we're sure the title and contents have something in it.
If there are any validation errors, we want to display them on the form.
We will change the form view to show errors using form_error
for each field. We'll load up any submitted data using set_value
- this way, if the user wrote a long post but forgot the title, they don't have to rewrite the post again!
<?php echo form_open() ?>
<label>Post Title</label><br>
<input type="text" class="form-control" name="title" value="<?php echo set_value('title') ?>">
<?php echo form_error('title'); ?>
<label>Post Content</label><br>
<textarea name="content"><?php echo set_value('content') ?></textarea>
<?php echo form_error('content'); ?>
<input type="submit" class="btn btn-primary" value="Post">
<?php echo form_close() ?>
Try it out - If you type nothing in the title but type some content, after submission you will get an error telling you that the title is required, while still keeping the content that you typed.
We also want to make sure the user is logged in - for this, we will make it so that users who are not logged in can't see the form at all.
At the start of the function we'll check if they are logged in and redirect them to the login page if they are not:
...
public function create()
{
if (!$this->ion_auth->logged_in())
{
redirect('auth/login');
}
...
Now if you log out (http://localhost/auth/logout
) and visit the form, you will be taken to the login page.
You now have a working post creation form!
Part 3: Viewing a Post
Creating the view function
We want to be able to view the post that we just created.
We'll set this up at the URL http://localhost/posts/view/{post_id}
.
To start, let's create that controller function.
Create a new function view
in our Posts
controller - it should take in a $post_id
:
...
public function view($post_id)
{
echo $post_id;
}
...
We're also echoing out the $post_id
- let's go to http://localhost/posts/view/1
to see that 1
be printed on the page.
Getting the post from the table
Now that we have the $post_id
that we want to load, we need to give it to the model, which will then ask the database.
In our Posts_model
function, create a new function get_post
- it should take in a $post_id
.
...
public function get_post($post_id)
{
}
...
Inside this function we'll ask the database to get the row where the $post_id
is matching:
...
$result = $this->db->get_where('posts', array('post_id' => $post_id));
return $result->first_row();
...
The model function is now ready for the controller to use.
Back in our Posts
controller, replace the echo
line with:
...
$post = $this->posts_model->get_post($post_id);
var_dump($post);
...
The var_dump
will print out the $post
that we receive from the model.
Now if you go to http://localhost/posts/view/1
again, you should see your post:
object(stdClass)[27]
public 'post_id' => string '1' (length=1)
public 'title' => string 'My First Post' (length=13)
public 'author' => string '3' (length=1)
public 'content' => string '123' (length=3)
public 'publish_date' => string '2017-10-22 22:19:02' (length=19)
Creating the view
We will now create the HTML page to display the post data in a prettier way.
Inside the folder application/views/posts
, create a new file called view.php
.
We'll do a quick draft of the HTML to start with, using dummy text:
<h1>Fake Post Title</h1>
<div class="post-info">
<span class="author">Posted by Fake Author</span>
<span class="date">on January 1 1999 12:00PM</span>
</div>
<div class="post-content">
Here's where the post content is supposed to go!
</div>
Now replace the var_dump
line in our view
function in the Posts
controller:
...
$this->load->view('posts/view');
...
Now if you revisit the URL on your browser, you should see the view with the placeholder text.
To load the post data, we'll need to first pass it to the view - edit that same line:
...
$this->load->view('posts/view', array('post' => $post));
...
Next we can go to the view and replace our placeholder text with the $post
properties:
<h1><?php echo $post->title ?></h1>
<div class="post-info">
<span class="author">Posted by <?php echo $post->author ?></span>
<span class="date">on <?php echo date('j F Y \a\t H:ia', strtotime($post->publish_date)) ?></span>
</div>
<div class="post-content">
<?php echo $post->content ?>
</div>
Refreshing the page on the browser, we now see the real post data!
But there's one problem - the author of the post is disaplyed as a number (the user ID), instead of their name.
We'll need to ask the model to also get the user information when fetching post data.
Since the author
property is the user ID, we can ask the ion_auth
library to get the user data with it.
Edit the get_post
function in the Posts_model
to replace the post's author
property with the user data:
...
public function get_post($post_id)
{
$result = $this->db->get_where('posts', array('post_id' => $post_id))->first_row();
if($result)
{
$author = $this->ion_auth->user($result->author)->row();
$result->author = $author;
}
return $result;
}
...
Now back at our view, the $post
's author
property holds all the user data so we can ask for the name:
...
<span class="author">Posted by <?php echo $post->author->first_name . ' ' . $post->author->last_name ?></span>
...
Finally, we'll wrap this whole view with a check to catch any invalid posts:
<?php if($post): ?>
<h1><?php echo $post->title ?></h1>
<div class="post-info">
<span class="author">Posted by <?php echo $post->author->first_name . ' ' . $post->author->last_name ?></span>
<span class="date">on <?php echo date('j F Y \a\t H:ia', strtotime($post->publish_date)) ?></span>
</div>
<div class="post-content">
<?php echo $post->content ?>
</div>
<?php else: ?>
<h1>Post not found</h1>
<p>The post you were looking for could not be found.</p>
<?php endif ?>
If we go to http://localhost/posts/view/1
, we'll see the full post with the author name. If we try to go to a post that doesn't exist, like http://localhost/posts/view/123
, the page will tell you that it doesn't exist.
The view page is complete!
Part 4: List of Posts
We have a page to create posts, and we have a page to view a single post.
In this part, we'll be making a page that shows a list of posts.
Creating the controller function
We'll have the list at the URL http://localhost/posts/all
.
For that, we will need to create a new function all
in our Posts
controller:
...
public function all()
{
$posts = array();
var_dump($posts);
}
...
At the moment, we have a placeholder array where the posts should go, and a var_dump
to print out this posts list (which is empty!)
We can confirm this by going to http://localhost/posts/all
- you should see an empty array printed out.
We need $posts
to hold an array of all our blog posts - we'll have to ask the model for that.
Creating the model function
Our model function will ask the database for all the published blog posts.
Create a new function get_posts
inside Posts_model
:
...
public function get_posts()
{
$this->db->order_by('publish_date', 'DESC');
$result = $this->db->get('posts')->results();
return $result;
}
...
This will return an array of all our posts - but before we go back to our controller to call this function, remember when we had to load the author for the post in the post view because $post->author
was just showing the user ID? We'll make that same change here for each of the posts. Our get_posts
would then look like this:
...
public function get_posts()
{
$results = $this->db->get('posts')->results();
foreach ($results as $key => $result) {
$author = $this->ion_auth->user($result->author)->row();
$results[$key]->author = $author;
}
return $results;
}
...
Now we will go back to our controller and edit the all
function to call get_posts
:
...
$posts = $this->posts_model->get_posts();
var_dump($posts);
...
Let's create a second post - head over to http://localhost/posts/create
and create a blog post.
Once you've done that, go to http://localhost/posts/all
- you should see two blog posts.
Creating the view
With the data prepared, we are ready to create the view.
We'll have it be a simple HTML list.
In the folder application/views/posts
, create a new file called all.php
and type in a placeholder list:
<ul class="posts-list">
<li>
<div class="post-title">Post Title 1</div>
<div class="post-info">Posted 1 Jan 1999 at 12:00AM by Mystery Author</div>
</li>
<li>
<div class="post-title">Post Title 2</div>
<div class="post-info">Posted 1 Jan 1999 at 12:00AM by Mystery Author</div>
</li>
<li>
<div class="post-title">Post Title 3</div>
<div class="post-info">Posted 1 Jan 1999 at 12:00AM by Mystery Author</div>
</li>
</ul>
Back in our all
controller function, remove the var_dump
line and load in the new view, remembering to pass the $posts
variable:
...
$this->load->view('posts/all', array('posts' => $posts);
...
Refreshing the page on the browser, we see a static list.
Change the view list to use the $posts
array - we'll also make the titles link to the single view pages:
<ul class="posts-list">
<?php foreach($posts as $post): ?>
<li>
<div class="post-title"><?php echo anchor( base_url("posts/view/{$post->post_id}"), $post->title) ?></div>
<div class="post-info">Posted <?php echo date('j F Y \a\t H:ia', strtotime($post->publish_date)) ?> by <?php echo $post->author->first_name . ' ' . $post->author->last_name ?></div>
</li>
<?php endforeach ?>
</ul>
Let's also add in a button that links you to the post creation form, if you're logged in:
<?php $this->load->view('header.php') ?>
<h1>Posts</h1>
<?php if($this->ion_auth->logged_in()): ?>
<a href="<?php echo base_url("posts/create") ?>">
<button type="button">New Post</button>
</a>
<?php endif ?>
<ul class="posts-list">
<?php foreach($posts as $post): ?>
<li>
<div class="post-title"><?php echo anchor( base_url("posts/view/{$post->post_id}"), $post->title) ?></div>
<div class="post-info">Posted <?php echo date('j F Y \a\t H:ia', strtotime($post->publish_date)) ?> by <?php echo $post->author->first_name . ' ' . $post->author->last_name ?></div>
</li>
<?php endforeach ?>
</ul>
<?php $this->load->view('footer.php') ?>
We now have a page that lists all the posts!
Part 5: Template
The site now has basic funcitonality but is missing any form of styling.
In this part we will give each page a shared header and a footer, and add styling.
Basic HTML tags
Open application/views/posts/all.php
. This was our list of all posts.
Currently, it's missing a lot of HTML tags, and has no header of footer.
Let's start with giving it tags to make it a valid HTML document:
<!DOCTYPE html>
<html>
<head>
<title>minimalist-blog</title>
</head>
<body>
<ul class="posts-list">
<?php foreach($posts as $post): ?>
<li>
<div class="post-title"><?php echo anchor( base_url("posts/view/{$post->post_id}"), $post->title) ?></div>
<div class="post-info">Posted <?php echo date('j F Y \a\t H:ia', strtotime($post->publish_date)) ?> by <?php echo $post->author->first_name . ' ' . $post->author->last_name ?></div>
</li>
<?php endforeach ?>
</ul>
</body>
</html>
Stylesheets
Now inside the body tag, we'll wrap our content with some container tags.
...
<div class="container">
<div class="row">
<div class="col-md-12">
<ul class="posts-list">
<?php foreach($posts as $post): ?>
<li>
<div class="post-title"><?php echo anchor( base_url("posts/view/{$post->post_id}"), $post->title) ?></div>
<div class="post-info">Posted <?php echo date('j F Y \a\t H:ia', strtotime($post->publish_date)) ?> by <?php echo $post->author->first_name . ' ' . $post->author->last_name ?></div>
</li>
<?php endforeach ?>
</ul>
</div>
</div>
</div>
...
Refreshing the http://localhost/posts/all
page, you won't see a difference in appearance other than a new page title.
We need to link stylesheets to this document.
In your CodeIgniter root, create a folder called css
to put our CSS sheets in - this should be next to your application
and system
folders.
The stylesheets I will use will be:
- (Bootstrap's Grid-only Stylesheet)[https://raw.githubusercontent.com/minimalist-collection/style-guide/master/css/bootstrap-grid.css]
- (minimalist-collection's Stylesheet)[https://raw.githubusercontent.com/minimalist-collection/style-guide/master/css/style.css]
Save the above as bootstrap-grid.css
and style.css
inside the css
folder you just created.
In the <head>
tags of the all.php
view file, link the two sheets:
...
<head>
<title>minimalist-blog</title>
<link rel="stylesheet" type="text/css" href="<?php echo base_url('css/bootstrap-grid.css') ?>">
<link rel="stylesheet" type="text/css" href="<?php echo base_url('css/style.css') ?>">
</head>
...
Refreshing the page, we'll see those div
tags we wrapped our content around makes the content be more in the center now.
Standard page elements
Next up, inside this main container we'll give the page a header with a site title, page title and a footer.
...
<div class="container">
<div class="header row">
<div class="col-md-12">
<div class="site-title">minimalist-blog</div>
</div>
</div>
<div class="main row">
<div class="col-md-12">
<h1>Posts</h1>
<ul class="posts-list">
<?php foreach($posts as $post): ?>
<li>
<div class="post-title"><?php echo anchor( base_url("posts/view/{$post->post_id}"), $post->title) ?></div>
<div class="post-info">Posted <?php echo date('j F Y \a\t H:ia', strtotime($post->publish_date)) ?> by <?php echo $post->author->first_name . ' ' . $post->author->last_name ?></div>
</li>
<?php endforeach ?>
</ul>
</div>
</div>
<div class="footer row">
<div class="col-md-12">
Copyright © <?php echo date("Y") ?> minimalist-blog
</div>
</div>
</div>
...
The page looks much more like a web page now - now we need to make the same change to all other views.
Loading headers and footers as views
To prevent having to copy and paste all these tags on all the views, we'll put one copy of the top and bottom tags in two files - header.php
and footer.php
.
In a new file application/views/header.php
, enter the tags above the content:
...
<!DOCTYPE html>
<html>
<head>
<title>minimalist-blog</title>
<link rel="stylesheet" type="text/css" href="<?php echo base_url('css/bootstrap-grid.css') ?>">
<link rel="stylesheet" type="text/css" href="<?php echo base_url('css/style.css') ?>">
<link rel="stylesheet" type="text/css" href="<?php echo base_url('css/blog.css') ?>">
</head>
<body>
<div class="container">
<div class="header row">
<div class="col-md-12">
<div class="site-title">minimalist-blog</div>
</div>
</div>
<div class="main row">
<div class="col-md-12">
...
In another new files application/view/footer.php
, enter the tags below the content:
...
</div>
</div>
<div class="footer row">
<div class="col-md-12">
Copyright © <?php echo date("Y") ?> minimalist-blog
</div>
</div>
</div>
</body>
</html>
...
We will now call these two views from the all.php
file:
<?php $this->load->view('header.php') ?>
<ul class="posts-list">
<?php foreach($posts as $post): ?>
<li>
<div class="post-title"><?php echo anchor( base_url("posts/view/{$post->post_id}"), $post->title) ?></div>
<div class="post-info">Posted <?php echo date('j F Y \a\t H:ia', strtotime($post->publish_date)) ?> by <?php echo $post->author->first_name . ' ' . $post->author->last_name ?></div>
</li>
<?php endforeach ?>
</ul>
<?php $this->load->view('footer.php') ?>
Refresh the page on your browser to see that the styling is still there.
Apply the same change with placing the header and footer views on the first and last lines of the other views:
application/views/posts/create.php
application/views/posts/create.php
We can also apply similar changes to Ion Auth's login form (and others) too.
Here is what application/views/auth/login.php
could be changed to:
<?php $this->load->view('header.php') ?>
<h1>Login</h1>
<div id="infoMessage"><?php echo $message ?></div>
<?php echo form_open("auth/login") ?>
<?php echo lang('login_identity_label', 'identity') ?>
<?php echo form_input($identity) ?>
<?php echo lang('login_password_label', 'password') ?>
<?php echo form_input($password) ?>
<?php echo lang('login_remember_label', 'remember') ?>
<?php echo form_checkbox('remember', '1', FALSE, 'id="remember"') ?>
<br>
<?php echo form_submit('submit', lang('login_submit_btn')) ?>
<?php echo form_close() ?>
<a href="forgot_password"><?php echo lang('login_forgot_password') ?></a>
<?php $this->load->view('footer.php') ?>
Our pages are now valid HTML documents with easy headers and footers!
Part 6: Edit Posts
Nice job for making it this far!
In this part we'll make a page that will allow us to edit existing blog posts.
Creating the controller function
We'll make the edit page be on the URL http://localhost/posts/edit/1
.
Create a function edit
in the Posts.php
controller, and have it take in the post ID. Also make it so that only logged-in people can access it:
...
public function edit($post_id)
{
if (!$this->ion_auth->logged_in())
{
redirect('auth/login');
}
}
...
To edit a post, we'll need to fetch it first. The view
function from the same controller does just that - so let's copy that over:
...
public function edit($post_id)
{
if (!$this->ion_auth->logged_in())
{
redirect('auth/login');
}
$post = $this->posts_model->get_post($post_id);
$this->load->view('posts/edit', array('post' => $post));
}
...
For the page, we don't want to display posts/view
- we want to use a new view that we'll make next called edit
.
Edit the line that loads the view so that it uses a view posts/edit
instead:
...
$this->load->view('posts/edit', array('post' => $post));
...
Creating the view
The form is going to be very similar to our form create page, so we'll start by copying that view.
Create a new file edit.php
inside application/views/posts
, and copy the content over from create.php
then edit the page title:
<?php $this->load->view('header.php') ?>
<h1>Edit Post</h1>
<?php echo form_open() ?>
<label>Post Title</label><br>
<input type="text" class="form-control" name="title" value="<?php echo set_value('title') ?>">
<?php echo form_error('title'); ?>
<label>Post Content</label><br>
<textarea name="content"><?php echo set_value('content') ?></textarea>
<?php echo form_error('content'); ?>
<input type="submit" class="btn btn-primary" value="Post">
<a href="<?php echo base_url("posts/view/{$post->post_id}") ?>">
<button type="button" class="btn btn-default">Cancel</button>
</a>
<?php echo form_close() ?>
<?php $this->load->view('footer.php') ?>
Now if we go to http://localhost/posts/edit/1
, we'll see an empty posting form.
The controller supposedly has sent us the post data, so we'll double check that by doing a var_dump
above the page title in the view:
...
<?php var_dump($post) ?>
...
Refreshing the page, you should see your post dumped out.
Currently our form is blank because the value is determined by set_value
- which gets the suubmitted form data. It's blank because we haven't submitted the form yet.
When the form hasn't been submitted yet, we want to load up the existing post's data.
We'll do that with a ternary statement which will look like this:
echo set_value('title') ? set_value('title') : $post->title
This means "If set_value
has something, then echo set_value
's data, otherwise load the data from the $post
".
Go ahead and make those changes to the form view:
<?php $this->load->view('header.php') ?>
<h1>Edit Post</h1>
<?php echo form_open() ?>
<label>Post Title</label><br>
<input type="text" class="form-control" name="title" value="<?php echo set_value('title') ? set_value('title') : $post->title ?>">
<?php echo form_error('title'); ?>
<label>Post Content</label><br>
<textarea name="content"><?php echo set_value('content') ? set_value('content') : $post->content ?></textarea>
<?php echo form_error('content'); ?>
<input type="submit" class="btn btn-primary" value="Post">
<a href="<?php echo base_url("posts/view/{$post->post_id}") ?>">
<button type="button" class="btn btn-default">Cancel</button>
</a>
<?php echo form_close() ?>
<?php $this->load->view('footer.php') ?>
Now if you refresh the page, you'll see the post data prepopulated. Neat.
Processing the edit form data
Now that we have the form ready, we'll need to modify our edit
function in the controller to take in the form and update the post.
It's going to look very similar to the create
function code. Put in the form processing just above where the view is loaded:
...
if($this->input->method() == 'post')
{
$this->form_validation->set_rules('title', 'Title', 'required');
$this->form_validation->set_rules('content', 'Content', 'required');
if($this->form_validation->run() !== FALSE)
{
$data = array(
'title' => $this->input->post('title'),
'content' => $this->input->post('content')
);
// Update the post
$this->posts_model->update($post_id, $data);
redirect("posts/view/$post_id");
}
}
...
It calls $this->posts_model->update($post_id, $data)
, but that function doesn't exist yet. We'll create that now.
Updating the database
The model would now have to update the existing post in the database.
Create a new function update
in our Posts_model.php
. It should take in the $post_id
and form $data
:
...
public function update($post_id, $data)
{
}
...
Now build a query which updates the row that matches the $post_id
:
...
public function update($post_id, $data)
{
$this->db->where('post_id', $post_id);
$this->db->update('posts', $data);
}
...
Now if you go to http://localhost/posts/edit/1
, make a change you will see the updated post.
The edit function is complete!
WSYWIG Editor
When creating and editing our posts, we want to be able to do text formatting (bold, lists ...) without having to type in all the HTML code.
That's what WSYWIG editors are for - We'll use a Javascript one called TinyMCE.
Download TinyMCE by clicking the 'Download' button under 'Download TinyMCE Community' (here)[https://www.tinymce.com/download/].
Open up the ZIP, go into the tinymce
folder and extract the js
folder in there to your CodeIgniter root.
We're set to load the JS - go the footer.php
view and link the JS file just before the closing </body
tag:
...
<script src="<?php echo base_url('js/tinymce/tinymce.min.js') ?>"></script>
...
Before we start up TinyMCE, we need to indicate on our forms somehow which fields we want to have WSYWIG active.
We'll use a class name - go into create.php
and edit the textarea
tag to give it a class:
...
<textarea name="content" class="tinymce"><?php echo set_value('content') ?></textarea>
...
Now we can call TinyMCE and tell it to make all elements with the class tinymce
a WSYWIG editor.
Below where we loaded TinyMCE's scrpit in footer.php
, put in the JS code to start it up:
...
<script>
tinymce.init({
selector: '.tinymce'
});
</script>
...
Refresh the post creation form and you should see the WSYWIG editor.
By default there's a lot of features enabled - we'll configure it a bit so it looks simpler.
Edit that same script again:
...
<script>
tinymce.init({
selector: '.tinymce',
height : '450',
plugins : 'link image lists',
toolbar: 'undo redo | styleselect | bold italic underline strikethrough | link image | bullist numlist',
menubar: false,
statusbar: false,
style_formats: [
{title: 'Header', format: 'h2'},
{title: 'Subheading', format: 'h3'},
{title: 'Minor Heading', format: 'h4'}
],
content_css : "<?php echo base_url('css/style.css') ?>"
});
</script>
...
Refresh the page to see the toolbar changes, and if you're happy with it, add the tinymce
class to edit.php
's form too.
Our WSYWIG editor is complete!
Error Styling
At the moment, if you go to the post creation form and submit an empty post, it will give you some errors - in a very basic appearance.
In this section we will make the error messages red, with an additional notification that appears at the top to make clear any failures.
To make the error messages red we'll need to wrap all the errors with a certain class. There is a configuration for this in CodeIgniter.
Create a new file form_validation.php
inside application/config
. Place this in the file and save:
...
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
$config['error_prefix'] = '<div class="form-error">';
$config['error_suffix'] = '</div>';
...
Now if you try to submit an empty form, the error messages will be in red.
We'll want the borders of the text fields to be in red too - again, we will wrap the inputs in a class.
Edit create.php
so that each input is inside a div that will have the class has-error
if there is an error for that field:
<?php $this->load->view('header.php') ?>
<h1>Create Post</h1>
<?php echo form_open() ?>
<div class="<?php if(form_error('title')) echo 'has-error' ?>">
<label>Post Title</label><br>
<input type="text" class="form-control" name="title" value="<?php echo set_value('title') ?>">
<?php echo form_error('title'); ?>
</div>
<div class="<?php if(form_error('content')) echo 'has-error' ?>">
<label>Post Content</label><br>
<textarea name="content" class="tinymce"><?php echo set_value('content') ?></textarea>
<?php echo form_error('content'); ?>
</div>
<input type="submit" class="btn btn-primary" value="Post">
<a href="<?php echo base_url("posts/all") ?>">
<button type="button" class="btn btn-default">Cancel</button>
</a>
<?php echo form_close() ?>
<?php $this->load->view('footer.php') ?>
We also snuck in a cancel button for the form which links back to the posts listing page.
Try creating an empty post again, you will see that the text borders are now also red. Great!
Next is the notification. We will do this through CodeIgniter's flashdata, which allows you to send temporary messages to the next page.
Edit the create
function in Posts.php
to set the flashdata if the form validation fails:
...
if($this->form_validation->run() !== FALSE)
{
// Create the post
$this->posts_model->create($data);
}
else
{
$this->session->set_flashdata('error', 'There are errors in the post.');
}
...
Now go to the view create.php
and echo out the flash data below the page heading:
...
<?php if($this->session->flashdata('error') && $this->input->method() == 'post'): ?>
<div class="alert alert-error" role="alert">
<?php echo $this->session->flashdata('error') ?>
</div>
<?php endif ?>
...
Now if you create an empty post you will see a nice red rectangle at the top with the general error.
We'll add in a link on the post view so we can get to editing posts easily.
Place this above where the footer is loaded in view.php
...
<?php if($this->ion_auth->logged_in()): ?>
<a href="<?php echo base_url("posts/edit/{$post->post_id}") ?>">
<button type="button" class="btn btn-default">Edit</button>
</a>
<?php endif ?>
...
After doing the same modifications to the edit post function, the error styling is complete!
Part 7: Pagination
Currently, our post listing page lists all the posts - which means when the blog has 100 posts, that page is going to be very long!
We'll need to set a limit on how many posts are visible per page, and put pagination links on the bottom.
CodeIgniter does have a (pagination function)[https://www.codeigniter.com/userguide3/libraries/pagination.html], but it is a little clunky and not suitable for situations where there are many pages.
We'll be using our own.
Dummy Data
To see if our pagination link is working, let's insert 100 posts into our posts
table in the database.
Copy the SQL from (here)[] and run it using your database manager software.
It should have inserted 100 records into the posts
table.
Confirm it did by going to http://localhost/posts/all
- you should see all the posts.
Settings
First we will set the limit of number of posts per page.
Open application/config/constants.php
. These are constant variables that can be accessed from any of our models, controllers and views. Place a new constant PER_PAGE
to the bottom of the file and save:
...
define('PER_PAGE', 10);
Editing our controllers
We will need a function to process the results so that it only returns the PER_PAGE
quantity of posts.
Because we might want to access this pagination function outside of the posts pages, we'll put it somewhere more accessible - as a helper function.
To create this new helper, make a new file pagination_helper.php
inside application/helpers
, and put the functon in there:
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
if ( ! function_exists('paginate'))
{
function paginate(&$results)
{
$CI =& get_instance();
$info = array('total' => count($results), 'limit' => PER_PAGE);
$page = $CI->input->get('page');
$offset = ($page - 1) * PER_PAGE;
$length = PER_PAGE;
$pages = ceil(count($results) / $length);
if($page)
{
$results = array_slice($results, $offset, $length);
}
$results = array_slice($results, 0, $length);
return $info;
}
}
This takes in a set of results, and slices it up to the appropriate quantity. It also returns information like how many pages the posts page should have.
Now we need to apply this to the all
function:
...
public function all()
{
$this->load->helper('pagination');
$posts = $this->posts_model->get_posts();
$pagination = paginate($posts);
$this->load->view('posts/all', array('posts' => $posts, 'pagination' => $pagination));
}
...
We pass through the $posts
for the function to slice, and store the pagination information in $pagination
. Remember that helper functions must also be loaded in first.
We also pass $pagination
into the view, since they'll need that information to print the page numbers.
Editing the view
Now the pagination printing can have a lot of tedious logic behind it. Consider these:
If we have a small number of pages, we want to display ... < 1 2 3 >
If we have a lot of pages, we want to display ... < 1 2 3 ... 10 >
But if we're in the middle of those pages, we want to display ... < 1 .. 4 5 6 ... 10 >
You can imagine that's a lot of if
s and else
es - so we won't go through that here, instead we'll conveniently use an existing one.
Create a new view pagination.php
inside application/views
, and paste the view code (here)[] into it.
Now we will edit this pagination view into our post listing.
Edit all.php
inside application/views/posts
and load the pagination view above the footer:
...
<?php $this->load->view('pagination') ?>
...
Now if you go to http://localhost/posts/all
, the page won't be as long and there will be pagination links at the bottom!
Part 8: Home page
We have all the posts pages, but what about our home page, http://localhost/
? It's still showing the CodeIgniter welcome message!
In this part we will create a home page that shows the recent posts, shows the previews of each post, and add a side bar to our site.
Creating the view
We said we wanted the home page to show a list of our posts - all.php
currently does that, so we'll start by duplicating that view.
Create a new file home.php
inside application/views
, and copy in the contents of application/views/posts/all.php
.
Remove the page title and new post button, add in a line for content and a line for linking to the posting:
<?php $this->load->view('header.php') ?>
<ul class="posts-list-home">
<?php foreach($posts as $post): ?>
<li>
<div class="post-title"><?php echo anchor( base_url("posts/view/{$post->post_id}"), $post->title) ?></div>
<div class="post-info">Posted <?php echo date('j F Y \a\t H:ia', strtotime($post->publish_date)) ?> by <?php echo $post->author->first_name . ' ' . $post->author->last_name ?></div>
<div class="post-content"><?php echo $post->content ?></div>
<?php echo anchor( base_url("posts/view/{$post->post_id}"), 'Read More') ?>
</li>
<?php endforeach ?>
</ul>
<?php $this->load->view('pagination') ?>
<?php $this->load->view('footer.php') ?>
Editing the controller
The home controller is over at Welcome.php
inside application/controllers
.
Edit the controller so that it loads in the posts_model
and passes a list of posts and pagination information to our new home.php
view:
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class Welcome extends CI_Controller {
public function __construct()
{
parent::__construct();
$this->load->model('posts_model');
}
public function index()
{
$this->load->helper('pagination');
$posts = $this->posts_model->get_posts();
$pagination = paginate($posts);
$this->load->view('home', array('posts' => $posts, 'pagination' => $pagination));
}
}
Now when we go to http://localhost/
, we'll see a list of posts.
Blog Title
In the home page, we want the blog name to be the main header. We'll do a check for the page location and make the blog name h1
if it's the home page.
Edit header.php
in application/views
:
<!DOCTYPE html>
<html>
<head>
<title>minimalist-blog</title>
<link rel="stylesheet" type="text/css" href="<?php echo base_url('css/bootstrap-grid.css') ?>">
<link rel="stylesheet" type="text/css" href="<?php echo base_url('css/style.css') ?>">
<link rel="stylesheet" type="text/css" href="<?php echo base_url('css/blog.css') ?>">
</head>
<body>
<div class="container">
<div class="header row">
<div class="col-md-12">
<?php if(!$this->uri->segment(1)): ?>
<h1 class="site-title"><a href="<?php echo base_url() ?>">minimalist-blog</a></h1>
<?php else: ?>
<div class="site-title"><a href="<?php echo base_url() ?>">minimalist-blog</a></div>
<?php endif ?>
</div>
</div>
<div class="main row">
<div class="col-md-12">
The blog titles are now also links to the home page.
Content Preview
Currently, we're showing the entire post for each post in the home page, which isn't ideal since it can make the page very long.
The listing should be changed so that it only shows the first 500 characters of the post.
There are two parts to this:
- Truncating the post content, and
- Making sure the truncated post content is valid HTML
If we don't do the second point, it's possible the string will truncate as ... <a href="google.co
- which may cause the browser to try to wrap all the rest of the page inside this broken link tag.
For the first point, we will make another helper.
Create a file post_helper
inside application/helpers
, and put the content preview function in there:
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
if ( ! function_exists('preview'))
{
function preview($content, $limit)
{
$CI =& get_instance();
if(strlen($content) < $limit)
{
return $content;
}
$last_word_pos = strrpos(substr($content, 0, $limit), ' ');
$truncated_content = substr($content, 0, $last_word_pos);
$preview = close_tags($truncated_content);
return $preview . ' ...';
}
}
This function will cut the content close to 500 characters, and pass it to close_tags
for it to validate the HTML.
close_tags
will be in our second helper, html_helper
- but because this helper already exists within CodeIgniter, we will be extending it.
To extend to an existing CodeIgniter helper, simply make the file the same way but with MY_
prepended to the file name.
Create a file MY_html_helper.php
inside application/helpers
, and place the close_tags
function:
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
if ( ! function_exists('close_tags'))
{
function close_tags($html)
{
preg_match_all('#<(img|br|hr|input)*[^>]*$#iU', $html, $result);
if(!empty($result[1]))
{
$html .= "\">";
}
preg_match_all('#<(?!meta|img|br|hr|input\b)\b([a-z]+)(?: .*)?(?<![/|/ ])>#iU', $html, $result);
$openedtags = $result[1];
preg_match_all('#</([a-z]+)>#iU', $html, $result);
$closedtags = $result[1];
$len_opened = count($openedtags);
if (count($closedtags) == $len_opened) {
return $html;
}
$openedtags = array_reverse($openedtags);
for ($i=0; $i < $len_opened; $i++) {
if (!in_array($openedtags[$i], $closedtags)) {
$html .= '</'.$openedtags[$i].'>';
} else {
unset($closedtags[array_search($openedtags[$i], $closedtags)]);
}
}
return $html;
}
}
Don't worry too much about what this function does, it uses a lot of regex to try and find open tags and attempts to close them.
Now we must call this from home.php
. Before we can do that though, we need to load the helpers from the controller.
Edit Welcome.php
's index
function and load the two helpers at the top of the function:
...
$this->load->helper('html');
$this->load->helper('post');
...
Now inside he home.php
view, replace where we echo out the $post->content
with a call to the preview
function:
...
<div class="post-content"><?php echo preview($post->content, 500) ?></div>
...
Refresh the home page to see the post previews!
Part 8: Comments
In this part we'll be adding a new feature, comments - anonymous readers will be able to leave their name, email and comment for each post of the blog.
Creating the table
Each comment will have these properties:
- A comment ID
- The post that it belongs to
- Name
- Comment content
- Date
This translates to this MySQL table structure - run it on your database manager:
CREATE TABLE `comments` (
`comment_id` int(11) NOT NULL AUTO_INCREMENT,
`post_id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
`email` varchar(255) DEFAULT NULL,
`content` text NOT NULL,
`date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`comment_id`),
KEY `post_id` (`post_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Creating the model
Create a new file Comments_model.php
in applications/models
.
It will need a create
function and a get_comments
function, which we are alreadt familiar with with the posts model:
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class Comments_model extends CI_Model {
public function __construct()
{
$this->load->database();
}
public function create($data)
{
$this->db->insert('comments', $data);
}
public function get_comments($post_id)
{
$this->db->order_by('date', 'DESC');
$this->db->where('post_id', $post_id);
return $this->db->get('comments')->result();
}
}
Creating the controller
The controller will take in comment form submissions.
Create a file Comments.php
under application/controllers
.
It will need a function add
to add comments submitted through a post page:
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class Comments extends CI_Controller {
public function __construct()
{
parent::__construct();
$this->load->model('comments_model');
$this->load->library('form_validation');
}
public function add($post_id)
{
if($this->input->method() == 'post')
{
// Validation here ...
if($this->form_validation->run() !== FALSE)
{
$data = array();
$this->comments_model->create($data);
redirect("posts/view/$post_id#comments");
}
}
$this->load->view('comments/error', array('post_id' => $post_id));
}
}
The add
function is not done yet, but consider the two outcomes when someone tries to add a comment:
- It succeeds form validation, creates a comment, and redirects them to the post page, or
- It fails form validation, and the user is taken to a page where they are shown the errors
Keeping those in mind, we will move onto the views.
Creating the view
We'll create a separate view for the comments form, then load that view into the post view.
Create a new view comments.php
in applications/views/comments
- you'll also need to create the new folder comments
.
In this view, we will display both the comment form, and the full comments least below it.
First the form:
<?php echo form_open('comments/add') ?>
<label>Name</label><br>
<input type="text" class="form-control" name="name" value="<?php echo set_value('name') ?>">
<?php echo form_error('name'); ?>
<label>E-mail (optional)</label><br>
<input type="email" class="form-control" name="email" value="<?php echo set_value('email') ?>">
<label>Comment</label><br>
<textarea name="content"><?php echo set_value('content') ?></textarea>
<input type="submit" class="btn btn-primary" value="Comment">
<?php echo form_close() ?>
Then the validation:
<?php if($this->input->method() == 'post' && $this->session->flashdata('error')): ?>
<div class="alert alert-danger alert-dismissable">
<button type="button" class="close fa-times fa"></button>
<?php echo $this->session->flashdata('error') ?>
</div>
<?php endif ?>
<?php echo form_open('comments/add') ?>
<div class="<?php if(form_error('name')) echo 'has-error' ?>">
<label>Name</label><br>
<input type="text" class="form-control" name="name" value="<?php echo set_value('name') ?>">
<?php echo form_error('name'); ?>
</div>
<div class="<?php if(form_error('email')) echo 'has-error' ?>">
<label>E-mail (optional)</label><br>
<input type="email" class="form-control" name="email" value="<?php echo set_value('email') ?>">
<?php echo form_error('email'); ?>
</div>
<div class="<?php if(form_error('content')) echo 'has-error' ?>">
<label>Comment</label><br>
<textarea name="content"><?php echo set_value('content') ?></textarea>
<?php echo form_error('content'); ?>
</div>
<input type="submit" class="btn btn-primary" value="Comment">
<?php echo form_close() ?>
Then the list of comments at the bottom:
...
<?php if(!empty($comments)): ?>
<h2>Comments<span class="text-muted"> (<?php echo count($comments) ?>)</h2>
<?php foreach($comments as $comment): ?>
<div class="comment">
<div class="name"><?php echo $comment->name ?></div>
<div class="content"><?php echo $comment->content ?></div>
<div class="date"><?php echo date('d M Y H:ia', strtotime($comment->date)) ?></div>
</div>
<?php endforeach ?>
<?php endif ?>
Now we need to show this in the posts view.
In Posts.php
, load the comments_model
in the __construct
function:
...
$this->load->model('comments_model');
...
In the view
function, get the comments and pass them on to the view:
...
public function view($post_id)
{
$post = $this->posts_model->get_post($post_id);
$comments = $this->comments_model->get_comments($post_id);
$this->load->view('posts/view', array('post' => $post, 'comments' => $comments));
}
...
Now in the post view - application/views/posts/view.php
- load the comment.php
view above the footer:
...
<a name="comments"></a>
<h2>Add Comment</h2>
<?php $this->load->view('comments/comments', array('post_id' => $post->post_id)) ?>
...
Now if you look at a posting you should see a comments form at the bottom.
Won't be able to post comments yet though - we'll need to go back to the controller to process the submission.
Form validation
Back at the add
function in the Comments.php
controller, we'll need to make a few changes.
Adding in validation and setting the $data
variable to pass to the model, we should end up with something like:
...
public function add($post_id)
{
if($this->input->method() == 'post')
{
$this->form_validation->set_rules('name', 'Name', 'required|max_length[50]');
$this->form_validation->set_rules('email', 'Email', 'required|valid_email');
$this->form_validation->set_rules('content', 'Message', 'required|max_length[1000]');
if($this->form_validation->run() !== FALSE)
{
$data = array(
'post_id' => $post_id,
'name' => $this->input->post('name'),
'email' => $this->input->post('email'),
'content' => $this->input->post('content'),
);
$this->comments_model->create($data);
redirect("posts/view/$post_id#comments");
}
else
{
$this->session->set_flashdata('error', 'There are errors in the comment.');
}
}
$this->load->view('comments/error', array('post_id' => $post_id));
}
...
Now if you go to a post, fill in the name, email and comment and submit the form, you will see a new comment pop up!
All that's left to do is to set up the error view:
Create a view error.php
inside application/views/comments
:
...
<?php $this->load->view('header') ?>
<h1>Add a Comment</h1>
<?php $this->load->view('comments/comments', array('post_id' => $post_id)) ?>
<?php $this->load->view('footer') ?>
...
Now if you try to leave some fields blank, you will get an error - if you correct the errors, then the comment will be posted!
reCAPTCHA
You always want to have some kind of bot detection with forms online - we will be using Google's reCAPTCHA.
First, get your keys from their (website)[https://www.google.com/recaptcha/admin]. This will work even if you are only using localhost
.
Select "reCAPTCHA V2" and Register - you can put in localhost
in the domains list. You will then get two keys, a site key and a secret key. We'll need those soon.
Instead of writing a reCAPTCHA validator from scratch, we will be using someone else's. Download the reCAPTCHA library for CodeIgniter (here)[https://github.com/appleboy/CodeIgniter-reCAPTCHA].
We only need two files from this ZIP:
- ZIP's
config/recaptcha.php
should be copied to yourapplication/config
- ZIP's
libraries/Recaptcha.php
should be copied to yourapplication/libraries
Edit your application/config/recaptcha.php
so that the site key and secret key matches the keys you just received from Google.
Load the recaptcha
library in the __construct
function of both your Posts.php
and Comments.php
controllers:
...
$this->load->library('recaptcha');
...
Put the reCAPTCHA HTML element in the comments.php
form by calling $this->recaptcha->getWidget()
, above the submit button:
...
<?php echo $this->recaptcha->getWidget() ?>
...
Now if you view the comments form on your browser, you should see a reCAPTCHA widget (it should not have any errors on it).
To validate the reCAPTCHA, edit the add
function of Comments.php
to call $this->recaptcha->verifyResponse($recaptcha)
:
...
public function add($post_id)
{
if($this->input->method() == 'post')
{
$recaptcha = $this->input->post('g-recaptcha-response');
$response = $this->recaptcha->verifyResponse($recaptcha);
if(isset($response['success']) && $response['success'] !== TRUE)
{
$this->session->set_flashdata('error', 'Please complete the ReCAPTCHA.');
$this->load->view('comments/error', array('post_id' => $post_id));
return;
}
...
Now if you submit the comment form without the reCAPTCHA, it should give you an error.
If you submit the comments form with all the fields and the reCAPTCHA completed, it should post a comment.
If you get PHP errors upon trying to submit a comment with the reCAPTCHA, look for information on turning on php_openssl, which is the PHP OpenSSL extention.
With the reCAPTCHA done, that completes the comment functionality!
Part 9: Tags
In this part we will create a tagging functioanlity, which will allow the user to categorize their posts.
Creating the table
Each tag will have an ID, a post ID that it's associated to, and the tag label.
Create the table by running the code below in your database manager:
CREATE TABLE `tags` (
`tag_id` INT(11) NOT NULL AUTO_INCREMENT,
`post_id` INT(11) NULL DEFAULT NULL,
`label` VARCHAR(50) NULL DEFAULT NULL,
PRIMARY KEY (`tag_id`),
INDEX `pid` (`post_id`)
)
ENGINE=MyISAM;
Editing the forms
We will need to take in an extra field in our post forms.
In create.php
, add the new field below the post content field:
...
<div class="<?php if(form_error('tags')) echo 'has-error' ?>">
<label>Post Tags</label><br>
<input type="text" class="form-control" name="tags" value="<?php echo set_value('tags') ?>">
<?php echo form_error('tags'); ?>
</div>
...
And in edit.php
:
...
<div class="<?php if(form_error('tags')) echo 'has-error' ?>">
<label>Post Tags</label><br>
<input type="text" class="form-control" name="tags" value="<?php echo set_value('tags') ? set_value('tags') : $post->tags ?>">
<?php echo form_error('tags'); ?>
</div>
...
Fetching the tags
Our model now needs to return the tags associated with each fetched post - remember that one post may have many tags.
We'll need to come up with the right SQL query to get us that data.
In your tags
table, create two tags for your posts by picking out a valid post_id
(I used 10
) in your blog and giving them two tags. And example of what the table may look like after you do this:
-------------------------
tag_id | post_id | label
-------------------------
1 | 10 | test
2 | 10 | apple
-------------------------
We will now try to run a query that will fetch us all of the post information, as well as the post's tags. Run this query and match the post_id
with the post that you made the tags for:
SELECT posts.`*`, GROUP_CONCAT(tags.label) AS 'tags'
FROM posts
LEFT JOIN tags ON posts.post_id = tags.post_id
WHERE posts.post_id = 10
After running this test SQL you should see a row with all the post information, and a column at the end called tags
with the value test, apple
.
Now that we know what query to run, we'll translate those into CodeIgniter's query builder and modify the get_post
function of the Post_model
to use the new query:
...
public function get_post($post_id)
{
$this->db->select('posts.*, GROUP_CONCAT(tags.label) AS tags');
$this->db->join('tags', 'posts.post_id = tags.post_id', 'left');
$this->db->where('posts.post_id', $post_id);
$result = $this->db->get('posts')->first_row();
if($result)
{
$author = $this->ion_auth->user($result->author)->row();
$result->author = $author;
}
return $result;
}
...
Now if we go to edit that post we gave the two tags to at http://localhost/posts/edit/10
, you will see an extra field with the two tags.
Assigning the tags
We want to be able to put new tags (or take away tags) using this field on the form.
The field is comma separated, so we would want to split the tags by the commas and create a new row in tags
for each tag.
In the edit
function Posts.php
controller, edit the $data
variable to also store the tags as an array:
...
$data = array(
'title' => $this->input->post('title'),
'content' => $this->input->post('content'),
'tags' => explode( ',', $this->input->post('tags'))
);
...
Now in our update
function in Posts_model.php
, we will extract the tags array and pass it on to another model function to process:
...
public function update($post_id, $data)
{
$tags = $data['tags'];
unset($data['tags']);
$this->db->where('post_id', $post_id);
$this->db->update('posts', $data);
$this->set_tags($post_id, $tags);
}
...
The new function in the model, set_tags
, will first delete all the tags of the post, and then insert the new set of tags. The function will do some cleaning of the tag, to make sure it will be URL-friendly.
public function set_tags($post_id, $tags)
{
$this->db->delete('tags', array('post_id' => $post_id));
foreach ($tags as $tag) {
$tag = trim($tag);
$tag = str_replace(' ', '-', $tag);
$tag = preg_replace('/[^A-Za-z0-9\-]/', '', $tag);
$data = array(
'post_id' => $post_id,
'label' => $tag
);
$query = $this->db->get_where('tags', $data);
if(!$query->num_rows())
{
$this->db->insert('tags', $data);
}
}
}
Now if you edit the post to add or remove tags, the changes will be applied.
We need to make similar changes to the post creation feature.
In the controller, edit the create
function so that the data passes the tags:
...
$data = array(
'title' => $this->input->post('title'),
'content' => $this->input->post('content'),
'author' => $author,
'publish_date' => $publish_date,
'tags' => explode( ',', $this->input->post('tags'))
);
...
We also need to call set_tags
from the create
function in Posts_model.php
:
...
public function create($data)
{
$tags = $data['tags'];
unset($data['tags']);
$this->db->insert('posts', $data);
$post_id = $this->db->insert_id();
$this->set_tags($post_id, $tags);
}
...
Now if you create a post with tags, the tags will be applied.
You will notice that if you have spaces in your tags, they will show as hypens in the text input. There are also spaces missing after the commas.
We will fix this by editing the $post->tags
property in the edit
function in the controller. Make the modification towards the end of the function, before loading the view:
...
$post->tags = str_replace('-', ' ', $post->tags);
$post->tags = str_replace(',', ', ', $post->tags);
$this->load->view('posts/edit', array('post' => $post));
}
...
The tags input box will now show the spaces and have better space formatting.
We will now show the tags on the posts page and home page.
On the posts view view.php
, print out the tags just before the edit button:
...
<?php if($tags = array_filter(explode(',', $post->tags))): ?>
<div class="post-tags">
Tagged
<?php foreach($tags as $tag): ?>
<?php echo anchor( base_url("posts/tagged/$tag"), str_replace('-', ' ', $tag)) ?><?php if(end($tags) !== $tag) echo ', ' ?>
<?php endforeach ?>
</div>
<?php endif ?>
...
For the home page, edit the Posts_model
to fetch the tags with the posts over at the get_posts
function:
...
public function get_posts()
{
$this->db->select('posts.*, GROUP_CONCAT(tags.label) AS tags');
$this->db->order_by('publish_date', 'DESC');
$this->db->order_by('post_id', 'DESC');
$this->db->group_by('post_id');
$this->db->join('tags', 'posts.post_id = tags.post_id', 'left');
$results = $this->db->get('posts')->result();
foreach ($results as $key => $result) {
$author = $this->ion_auth->user($result->author)->row();
$results[$key]->author = $author;
}
return $results;
}
...
Now print out each of their tags below the content in the view home.php
, just above the "Read More" link:
...
<?php if($tags = array_filter(explode(',', $post->tags))): ?>
<div class="post-tags">
Tagged
<?php foreach($tags as $tag): ?>
<?php echo anchor( base_url("posts/tagged/$tag"), str_replace('-', ' ', $tag)) ?><?php if(end($tags) !== $tag) echo ', ' ?>
<?php endforeach ?> ·
</div>
<?php endif ?>
<?php echo anchor( base_url("posts/view/{$post->post_id}"), 'Read More') ?>
...
Go to the home page and you will see each of the tags are now listed - and link to http://localhost/posts/tagged/{tag_name}
, which is a controller function that doesn't exist yet.
We'll make that function now - it will show a list of posts under that tag.
First, create a new controller function tagged
in Posts.php
which will pass the $tag
to the model and the view:
...
public function tagged($tag)
{
$this->load->helper('pagination');
$this->load->helper('html');
$this->load->helper('post');
$posts = $this->posts_model->get_by_tag($tag);
$pagination = paginate($posts);
$this->load->view('posts/tagged', array('posts' => $posts, 'pagination' => $pagination, 'tag' => $tag));
}
...
The function calls get_by_tag
, which is a function we will create in our Posts_model.php
.
It will get a list of posts with the passed in $tag
:
...
public function get_by_tag($tag)
{
$this->db->select('posts.*, GROUP_CONCAT(tags.label) AS tags');
$this->db->order_by('publish_date', 'DESC');
$this->db->order_by('post_id', 'DESC');
$this->db->group_by('post_id');
$this->db->where('tags.label', $tag);
$this->db->join('tags', 'posts.post_id = tags.post_id', 'left');
$results = $this->db->get('posts')->result();
foreach ($results as $key => $result) {
$author = $this->ion_auth->user($result->author)->row();
$results[$key]->author = $author;
}
return $results;
}
...
Now we may create a view tagged.php
in application/views/posts
that displays the list of posts:
...
<?php $this->load->view('header.php') ?>
<h1><?php echo ucfirst($tag) ?></h1>
<div class="sub">Posts tagged with <?php echo ucfirst($tag) ?></div>
<ul class="posts-list">
<?php foreach($posts as $post): ?>
<li>
<div class="post-title"><?php echo anchor( base_url("posts/view/{$post->post_id}"), $post->title) ?></div>
<div class="post-info">Posted <?php echo date('j F Y \a\t H:ia', strtotime($post->publish_date)) ?> by <?php echo $post->author->first_name . ' ' . $post->author->last_name ?></div>
<div class="post-content"><?php echo preview($post->content, 500) ?></div>
<?php if($tags = array_filter(explode(',', $post->tags))): ?>
<div class="post-tags">
Tagged
<?php foreach($tags as $tag): ?>
<?php echo anchor( base_url("posts/tagged/$tag"), str_replace('-', ' ', $tag)) ?><?php if(end($tags) !== $tag) echo ', ' ?>
<?php endforeach ?> ·
</div>
<?php endif ?>
<?php echo anchor( base_url("posts/view/{$post->post_id}"), 'Read More') ?>
</li>
<?php endforeach ?>
</ul>
<?php $this->load->view('pagination') ?>
<?php $this->load->view('footer.php') ?>
...
Now if you go to the home page and click on a tag that a post has, it will take you to a page with all the posts with that tag!
Part 10: Deleting Posts
This short part will allow us to delete posts from the edit forms.
Editing the form
In edit.php
, add a function for the delete action next to the post button:
...
<input type="submit" class="btn btn-danger" name="delete" onclick="return confirm('Are you sure you want to delete this post?');" value="Delete">
...
Editing the controller
Inside the edit
function in the Posts.php
controller, check if the delete button was pressed and make a call to the model for deletion. Place it above the form validation:
...
if( $this->input->post('delete') )
{
$this->posts_model->delete($post_id);
redirect('/');
}
...
Editing the model
Add a new function delete
to Posts_model.php
:
...
public function delete($post_id)
{
$this->db->where('post_id', $post_id);
$this->db->delete('posts');
}
...
Now if you edit a post and delete it, the post will be removed!
Part 11: Draft Posts
For the final part we will be allowing users to save their posts as "Draft" - Posts set to draft will not be visible to the public.
In our tables, we'll interpret a post with NULL
publish_date
to be a draft post.
Editing the forms
In our post form, we want a second button that says "Save as Draft" next to our "Post" button.
Add the button into create.php
and edit.php
in application/views/posts
:
...
<input type="submit" class="btn btn-primary" name="draft" value="Save as Draft">
...
Editing the controller
Now in our controller all posts saved with the draft button should not have a publish date.
Edit the create
function in Posts.php
, where the $publish_date
is set:
...
if( $this->input->post('draft') )
{
$publish_date = null;
}
else
{
$publish_date = date("Y-m-d H:i:s");
}
...
Modify the edit
function just above where it call's the movel'd update
function to pass in a null
publish_date
if the user selects to save as draft:
...
if( $this->input->post('draft') )
{
$data['publish_date'] = null;
}
else
{
$data['publish_date'] = $post->publish_date ? $post->publish_date : date("Y-m-d H:i:s");
}
...
We must also change the view
function so that it does not show the post if it is in draft - we will make it so that we can see it if we are logged in:
...
public function view($post_id)
{
$post = $this->posts_model->get_post($post_id);
if(!$post->publish_date && !$this->ion_auth->logged_in())
{
$post = null;
$comments = null;
}
else
{
$comments = $this->comments_model->get_comments($post_id);
}
$this->load->view('posts/view', array('post' => $post, 'comments' => $comments));
}
...
Now if you create or edit a post and save it as a draft, the publish_date
will be NULL
, and will appear as the last posts on public lists.
Editing the model
Instead of appearing last on the post listings, we want draft posting to not appear at all.
Edit the get_posts
function to add a where clause, excluding posts with a publish_date
of NULL
:
...
$this->db->where('posts.publish_date IS NOT NULL');
...
While we're here, we'll create a new function called get_drafts
which will get a list of draft posts:
...
public function get_drafts()
{
$this->db->order_by('publish_date', 'DESC');
$this->db->order_by('post_id', 'DESC');
$this->db->where('posts.publish_date IS NULL');
$results = $this->db->get('posts')->result();
foreach ($results as $key => $result) {
$author = $this->ion_auth->user($result->author)->row();
$results[$key]->author = $author;
}
return $results;
}
...
Now the public post listings will not show any draft posts.
Draft posts listing
We will need some way to view the draft postings, so we will create a page for http://localhost/posts/drafts
.
The page will show a list of all the draft posts with an option to edit them.
In the Posts.php
controller, create a function drafts
which will show the draft posts to logged in users:
public function drafts()
{
if (!$this->ion_auth->logged_in())
{
redirect('auth/login');
}
$this->load->helper('pagination');
$posts = $this->posts_model->get_drafts();
$pagination = paginate($posts);
$this->load->view('posts/drafts', array('posts' => $posts, 'pagination' => $pagination));
}
Create a view drafts.php
in application/views/posts
to display this list:
<?php $this->load->view('header.php') ?>
<h1>Draft Posts</h1>
<a href="<?php echo base_url("posts/create") ?>">
<button type="button">New Post</button>
</a>
<ul class="posts-list">
<?php foreach($posts as $post): ?>
<li>
<div class="post-title"><?php echo anchor( base_url("posts/view/{$post->post_id}"), $post->title) ?></div>
<div class="post-content"><?php echo preview($post->content, 200) ?></div>
<?php echo anchor( base_url("posts/edit/{$post->post_id}"), 'Edit') ?>
</li>
<?php endforeach ?>
</ul>
<?php $this->load->view('pagination') ?>
<?php $this->load->view('footer.php') ?>
Now go to http://localhost/posts/drafts
, where you will see a list of draft posts!
That's it!
That concludes the walkthrough of writing a blog on CodeIgniter.
Congratulations :D