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

  1. Set up a LAMP environment on your operating system
  2. Download CodeIgniter
  3. Open the downloaded ZIP and place in your LAMP www directory
  4. Run your local webserver
  5. Go to localhost on your browser - you should see Welcome to CodeIgniter!

Database

  1. Open up a database manager application
  2. 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
  3. Create a database minimalist-blog

CodeIgniter Configuration

  1. Open where your CodeIgniter files are in file explorer

  2. 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 word index.php from your URLs
      • $config['global_xss_filtering'] = TRUE; to enable automatic XSS filtering
      • $config['csrf_protection'] = TRUE; to enable CSRF tokens
  3. 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'
  4. Have some helpful functionality autoloaded:

    • Open application/config/autoload.php
      • Set $autoload['libraries'] to array('ion_auth')
      • Set $autoload['helper'] to array('url', 'form')
  5. 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 the application and system 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

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

  1. Download Ion Auth 2
  2. Extract the files into the application folder of your CodeIgniter
  3. Open your database application and have the minimalist-blog database selected
  4. Open and run the Ion Auth SQL which is located at application/sql/ion_auth.sql
  5. 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
    • 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
    • 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

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:

  1. (Bootstrap's Grid-only Stylesheet)[https://raw.githubusercontent.com/minimalist-collection/style-guide/master/css/bootstrap-grid.css]
  2. (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 ifs and elsees - 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
  • E-mail
  • 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 your application/config
  • ZIP's libraries/Recaptcha.php should be copied to your application/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 ?> &middot; 
    </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 ?> &middot; 
            </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

Back to Reads