The New Myth family: SprintPHP Bonfire Practical CodeIgniter 3

New Myth Media Blog

Serving the New Myth Media Family.

Practical CodeIgniter 3 Released

My new book about making the most of CodeIgniter 3 is out!

This is part 2 of a small tutorial that copies along the TutsPlus Ribbit project in CodeIgniter and Bonfire. Last time we managed to get the front of the site in place and styled, all using a single home controller.

The Database

I like to start projects by getting the data layer in place, and Bonfire makes this step very easy.

We will need two additional tables for this application:

  • ribbits - contains the actual ribbits, or posts.
  • follows - a list of who follows who

To make sure the data is portable between or development setup and our production server, we'll make use of a Migration. Create a new file at application/db/migrations/001_Initial_tables.php. This will hold all of the code to setup our initial tables and modify the users table. The file should look like this:

    class Migration_Initial_tables extends Migration {

    public function up()
    {
        $sql = array();

        // Ribbits
        $sql[] = "CREATE TABLE ribbits (
                id            INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
                user_id    INT(11) UNSIGNED NOT NULL,
                ribbit        VARCHAR(140),
                created_at    DATETIME,
                PRIMARY KEY(id, user_id)
            );";

        // Follows
        $sql[] = "CREATE Table follows (
                id            INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
                user_id       INT(11) UNSIGNED NOT NULL,
                followee_id   INT(11) UNSIGNED NOT NULL,
                PRIMARY KEY(id, user_id)
            );";

        foreach ($sql as $s)
        {
            $this->db->query($s);
        }
    }

    //--------------------------------------------------------------------

    public function down()
    {
        $this->dbforge->drop_table('ribbits');
        $this->dbforge->drop_table('follows');
    }

    //--------------------------------------------------------------------

}

The migration's up() method is ran to create the database changes. In this case, we're creating the ribbits and follows tables. The down() method reverses those changes by dropping the tables.

For this tutorial, we're assuming that the app is using MySQL and so we'll stick with MySQL-specific queries. We could have just as easily used dbforge for this, like we did in the down() method.

To apply this migration we could log into the admin area and run them manually. For now, though, let's have them run automatically. Open the file application/config/application.php and scroll down to the bottom of the file. Edit the migrate.auto_app and set it to TRUE so it will check for new migrations on every page load.

    $config['migrate.auto_app']     = TRUE;

Then reload any of the pages and the migration will be ran, creating the new tables in your database.

The Models

To quickly get up to speed we'll create 2 new models that extend Bonfire's MY_Model class. This provides all of the CRUD functionality, and more, so we just need to create skeleton files and tweak a few settings.

Ribbits

First, create the new ribbit model at application/models/ribbit_model.php. We will use the bare minimum code here to keep things clear, but the Models has quite a few options you can put to use in your own applications.

    <?php if (!defined('BASEPATH')) exit('No direct script access allowed');

    class Ribbit_model extends MY_Model {

        protected $table_name   = 'ribbits';
        protected $date_format  = 'datetime';

        protected $log_user             = true;
        protected $set_created          = true;
        protected $created_on_field     = 'created_at';
        protected $created_by_field     = 'user_id';

        //--------------------------------------------------------------------
    }

This creates the Ribbit_model class that extends MY_Model to take advantage of our existing functionality. We tell the model which table to use, set our date_format to datetime to match the table structure for created_at. We also tell it which fields to automatically populate with the creation date and who created it (user_id). During any insert() calls, the model will automatically fill in the created_at date and record the current user id in the user_id field for us.

Follows

The follow_model is pretty much the same, so create a new file, follow_model.php and edit it to reflect the following:

    <?php if (!defined('BASEPATH')) exit('No direct script access allowed');

    class Follow_model extends MY_Model {

        protected $table_name   = 'follows';

        //--------------------------------------------------------------------
    }

This time, the model is much simpler, since we don't need to record any of the creation info. All we need is the table name.

The Home Page

Now that we have the data layer in place, it's time to move back to our controller layer and start hooking the pieces together.

User Registration

To kick things off, let's make sure that new users can signup. This functionality already exists in Bonfire, but doesn't match the styling and positioning that our app requires. And it probably includes too much information, to be honest, with user_meta and all, so let's replace the signup for we did last time with this new one below. This is in the application/views/home/index.php file.

    <?= Template::message(); ?>

    <img src="/assets/images/frog.jpg">
    <div class="panel right">
    <h1>New to Ribbit?</h1>
    <?= form_open(REGISTER_URL); ?>
        <input type="hidden" name="redirect_url" value="/">

        <input name="email" type="text" placeholder="Email" value="<?= set_value('email') ?>">
        <input name="username" type="text" placeholder="Username" value="<?= set_value('username') ?>">
        <input name="password" type="text" placeholder="Password">
        <input name="pass_confirm" type="password" placeholder="Password (again)">
        <input type="submit" name="register" value="Create Account">
    <?= form_close(); ?>
    </div>

While I prefer my HTML to look like HTML, I always use the form_open and form_close tags since they help provide CSRF protection in the form submission process. So modify the home controller's index method to load the form helper.

    public function index()
    {
        $this->load->helper('form');
        
        Template::render();
    }

To use Bonfire's existing code, we send users to the users module upon form submittal. Unfortunately, that will take us back to users/register upon failure, so we will have to make some small modifications to the Users module. For real projects, you should copy the entire users module and paste it into application/modules before making changes. For this example, though, we'll simply modify the existing code.

Open up bonfire/modules/users/controllers/users.php. Scroll down to the register method, and let's get started.

The first thing we want to do is to make sure that we can tell it where we want to redirect to. We're including this in a hidden $_POST var, so add the following line just after the opening braces:

    $redirect_url = $this->input->post('redirect_url') ? $this->input->post('redirect_url') : REGISTER_URL;

Scroll down to around lines 583 and 588, and modify the redirect value to use our new value.

    Template::redirect($redirect_url);
    Template::set_message(lang('us_registration_fail'), 'error');
    redirect($redirect_url);

We also need to catch when validation fails, so add the following code after the closing brace around line 591:

    if ($this->input->post('redirect_url'))
    {
    Template::set_message( validation_errors() , 'error');
    redirect($redirect_url);
    }

This sets an error message for us and redirects us back to the home page, since we can't display validation errors like we normally would.

Now, validation will always fail since this method requires a few fields that we don't require. So, to fix things up, remove the required validation rules from language, timezone, and display_name on lines 460-462 since we're not collecting those. We'll leave the lines there, though, in case we change our minds in the future. Also, comment out lines 485-487 where we're composing the $data array to send to the user model.

If we try to submit an empty form, we see one more error we didn't expect: 'The Country field is required'. That is due to a required flag on a user_meta value. Let's remedy that. Open up application/config/user_meta.php and removed the required flag from the 'country' array's rules.

Flash Messages

When there are validation errors, this will show a small 'flash message' using the Template library's message features. We do need to style the message, though, since it's built to use Bootstrap for it's CSS.

Open up application/config/application.php config file again. This time, scroll down to line 175, where it's setting up the template to use for our flash messages. Let's simplify things a bit here.

    $config['template.message_template'] =<<<EOD
     <div class="alert {type}">
        {message}
    </div>
    EOD;

Then, edit our theme's CSS file to make these styles a bit more pleasing. Edit public/themes/ribbit/css/style.less and add the following code at the bottom:

.alert {
    padding: 0.5em;
    border: 1px solid #ccc;
    background: #efefef;
    margin-bottom: 1em;
    line-height: 1.5;
}
.alert.error {
    border-color: #ebccd1;
    background-color: #f2dede;
    p {
        color: #b94a48;
        margin: 0;
    }
}
.alert.success {
    border-color: #d6e9c6;
    background-color: #dff0d8;
    p {
        color: #468847;
        margin: 0;
    }
}

Ah. Much better.

Now, we can register a new user and we still have all of the validation types that Bonfire offers at our disposal (though we might have to style some more pages to get the full complement of functionality, like password resets, etc.)

User Login

Now that users can sign up, it's time to allow them to log in and start using the site.

First, we need to start by modifying the simple form in the page's header. Open up public/themes/ribbit/index.php and replace the existing form with the following code:

    <?php if ($this->auth->is_logged_in()) : ?>
         <a href="<?= site_url('logout') ?>">Logout</a>
    <?php else: ?>

      <?= form_open(LOGIN_URL, 'class="login"') ?>
           <input type="hidden" name="redirect_url" value="/">

           <input type="email" name="login" placeholder="Email">
           <input type="password" name="password" placeholder="Password">
           <input type="submit" name="log-me-in" value="Login">
      <?= form_close(); ?>
    <?php endif; ?>

If the user is not logged in, then a login form is shown. If they are, then a simple Sign Out link is shown. This requires that the Auth library be loaded, which it's not currently. So let's add a constructor to our home controller and load it there. We'll also move our form helper loading to here, since all pages will need it for the login menu.

    public function __construct()
    {
    parent::__construct();

    $this->load->library('users/auth');
    $this->load->helper('form');
    }

We need to adjust the CSS a little bit to allow for a submit button. Edit style.less again, adding the following couple of lines:

    form.login input[type="email"],
    form.login input[type="password"] {
    width: 190px;
    }

All on one line again. Whew.

Now that things are looking nicer, we need to make a quick tweak to our users controller again, just to allow us to redirect back to the home page on errors. Add the following quick redirect check at line 108, just after the closing brace for the if statement:

    if ($this->input->post('redirect_url'))
    {
    redirect($this->input->post('redirect_url'));
    }

Once a user is logged in, we don't want them to see the front page again, but the login script is going to send them back there anyway. So, let's modify the home controller's index page to check if the user is logged in. If they are, it should redirect them to the 'buddies' page.

    public function index()
    {
    if ($this->auth->is_logged_in())
    {
        redirect('buddies');
    }

    Template::render();
    }

Until Next Time

There we have it. We've gotten our data layer setup and ready to go, and integrated user registration, login and logout into Bonfire. And it's been pretty painless so far.

Next time, we'll wrap up and flesh out our buddies, ribbits and profiles pages.