Step 5 – Administer static pages for our multilanguage CMS – creating the pages

(created at: April 01, 2015; last update: April 03, 2015)
Let’s try and do a interface for creating and editing static pages in our multilanguage CMS. First of all what is a “static page”? A static page is a page that doesn’t change (a lot) in time, doesn’t have a publication date and is independent of any time span.

First we will create the tables. I am thinking of keeping the pages’ content in two tables. One that will keep the common data related to all translations (like the id of the page, the parent of the page, the order of importance inside the parent of page, the creation date, modified date, deleted date), and the other one will keep the translations of the pages:

CREATE TABLE `pages` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `parent_id` int(11) unsigned NOT NULL DEFAULT '0',
  `order` int(4) unsigned NOT NULL DEFAULT '0',
  `created_at` datetime DEFAULT NULL,
  `updated_at` datetime DEFAULT NULL,
  `deleted_at` datetime DEFAULT NULL,
  `created_by` int(11) NULL,
  `updated_by` int(11) NULL,
  `deleted_by` int(11) NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

CREATE TABLE `page_translations` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `page_id` int(11) NOT NULL,
  `language_slug` varchar(5) NOT NULL,
  `title` varchar(255) NOT NULL,
  `menu_title` varchar(255) NOT NULL,
  `teaser` mediumtext NOT NULL,
  `content` longtext NOT NULL,
  `page_title` varchar(100) NOT NULL,
  `page_description` varchar(170) NOT NULL,
  `page_keywords` varchar(255) NOT NULL,
  `created_at` datetime DEFAULT NULL,
  `updated_at` datetime DEFAULT NULL,
  `deleted_at` datetime DEFAULT NULL,
  `created_by` int(11) NULL,
  `updated_by` int(11) NULL,
  `deleted_by` int(11) NULL,
  PRIMARY KEY (`id`),
  KEY `page_id` (`page_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

Let’s create our Pages.php inside application/controllers/admin directory:

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

class Pages extends Admin_Controller
{

  function __construct()
  {
    parent::__construct();
    if(!$this->ion_auth->in_group('admin'))
    {
      $this->session->set_flashdata('message','You are not allowed to visit the Pages page');
      redirect('admin','refresh');
    }
    $this->load->model('page_model');
    $this->load->model('page_translation_model');
    $this->load->model('language_model');
    $this->load->library('form_validation');
    $this->load->helper('text');
  }

  public function index()
  {
    //$this->render('admin/pages/index_view');
  }
...

As you can see, we already have loaded the necessary models needed to work with pages, the form validation library and the text helper. I will tell you later why we needed it.

Now let’s do the create() method. This one will receive two parameters: the language slug and page_id. The language slug will allow us to put a hidden input element and also to have the parent pages available. If no language is set, the application will create the page in the default language. The page id, if one is set will tell our application that the form will be used only for a translation and not for creation of a new page.

Let’s see how we use the language slug and page id:

public function create($language_slug = NULL, $page_id = 0)
{
  $language_slug = (isset($language_slug) && array_key_exists($language_slug, $this->langs)) ? $language_slug : $this->current_lang;

  $this->data['content_language'] = $this->langs[$language_slug]['name'];
  $this->data['language_slug'] = $language_slug;
  $page = $this->page_model->get($page_id);
  if($page_id != 0 && $page==FALSE)
  {
    $page_id = 0;
  }
  if($this->page_translation_model->where(array('page_id'=>$page_id,'language_slug'=>$language_slug))->get())
  {
    $this->session->set_flashdata('message', 'A translation for that page already exists.');
    redirect('admin/pages', 'refresh');
  }
  $this->data['page'] = $page;
  $this->data['page_id'] = $page_id;
  $pages = $this->page_model->where('language_slug',$language_slug)->order_by('menu_title')->fields('id,menu_title')->get_all();
  $this->data['parent_pages'] = array('0'=>'No parent page');
  if(!empty($pages))
  {
    foreach($pages as $page)
    {
      $this->data['parent_pages'][$page->id] = $page->menu_title;
    }
  }
...
...

Now, before outputing the form, we need to setup the rules. Considering that we don’t want to repeat ourselves, what if we keep those rules inside the models? Let’s keep in mind that we have rules for inserting data and rules for updating data.

So, for now, let’s put these rules inside the Page_model:

<?php

defined('BASEPATH') OR exit('No direct script access allowed');

class Page_model extends MY_Model
{

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

    public $rules = array(
        'insert' => array(
            'parent_id' => array('field'=>'parent_id','label'=>'Parent ID','rules'=>'trim|is_natural|required'),
            'title' => array('field'=>'title','label'=>'Title','rules'=>'trim|required'),
            'menu_title' => array('field'=>'menu_title','label'=>'Menu title','rules'=>''),
            'slug' => array('field'=>'slug', 'label'=>'Slug', 'rules'=>'trim'),
            'order' => array('field'=>'order','label'=>'Order','rules'=>'trim|is_natural|required'),
            'teaser' => array('field'=>'teaser','label'=>'Teaser','rules'=>'trim'),
            'content' => array('field'=>'content','label'=>'Content','rules'=>'trim'),
            'page_title' => array('field'=>'page_title','label'=>'Page title','rules'=>'trim'),
            'page_description' => array('field'=>'page_description','label'=>'Page description','rules'=>'trim'),
            'page_keywords' => array('field'=>'page_keywords','label'=>'Page keywords','rules'=>'trim'),
            'page_id' => array('field'=>'page_id', 'label'=>'Page ID', 'rules'=>'trim|is_natural|required'),
            'language_slug' => array('field'=>'language_slug','label'=>'language_slug','rules'=>'trim|required')
        ),
        'update' => array()
    );
}

As you can see, this Page_model is extending a MY_Model. The MY_Model I am using is the one I’ve made and is posted on Github: https://github.com/avenirer/CodeIgniter-MY_Model. You can simply move that MY_Model.php into application/core.

Returning to our Pages controller, let’s set the rules and ask the application if the form validation ended with success. If it didn’t, we load the form view.

$rules = $this->page_model->rules;
$this->form_validation->set_rules($rules['insert']);
if($this->form_validation->run()===FALSE)
{
  $this->render('admin/pages/create_view');
}
else
{
  ...

Now, the else part. So, if all went well with our form… Oh… where is the form… Let’s create the form inside application/views/admin/pages/create_view.php

<?php defined('BASEPATH') OR exit('No direct script access allowed');?>
<div class="container" style="margin-top:60px;">
    <div class="row">
        <div class="col-lg-12">
            <h1>Add Page in <?php echo strtolower($content_language);?></h1>
            <?php echo form_open('',array('class'=>'form-horizontal'));?>
            <div class="form-group">
                <?php
                echo form_label('Parent page','parent_id');
                echo form_dropdown('parent_id',$parent_pages,set_value('parent_id',(isset($page->parent_id) ? $page->parent_id : '0')),'class="form-control"');
                ?>
            </div>
            <div class="form-group">
                <?php
                echo form_label('Title','title');
                echo form_error('title');
                echo form_input('title',set_value('title'),'class="form-control"');
                ?>
            </div>
            <div class="form-group">
                <?php
                echo form_label('Menu title','menu_title');
                echo form_error('menu_title');
                echo form_input('menu_title',set_value('menu_title'),'class="form-control"');
                ?>
            </div>
            <div class="form-group">
                <?php
                echo form_label('Slug','slug');
                echo form_error('slug');
                echo form_input('slug',set_value('slug'),'class="form-control"');
                ?>
            </div>
            <div class="form-group">
                <?php
                echo form_label('Order','order');
                echo form_error('order');
                echo form_input('order',set_value('order', (isset($page->order) ? $page->order : '0')),'class="form-control"');
                ?>
            </div>
            <div class="form-group">
                <?php
                echo form_label('Teaser','teaser');
                echo form_error('teaser');
                echo form_textarea('teaser',set_value('teaser'),'class="form-control"');
                ?>
            </div>
            <div class="form-group">
                <?php
                echo form_label('Content','content');
                echo form_error('content');
                echo form_textarea('content',set_value('content','',false),'class="form-control"');
                ?>
            </div>

            <div class="form-group">
                <?php
                echo form_label('Page title','page_title');
                echo form_error('page_title');
                echo form_input('page_title',set_value('page_title'),'class="form-control"');
                ?>
            </div>
            <div class="form-group">
                <?php
                echo form_label('Page description','page_description');
                echo form_error('page_description');
                echo form_input('page_description',set_value('page_description'),'class="form-control"');
                ?>
            </div>
            <div class="form-group">
                <?php
                echo form_label('Keywords','page_keywords');
                echo form_error('page_keywords');
                echo form_input('page_keywords',set_value('page_keywords'),'class="form-control"');
                ?>
            </div>
            <?php echo form_error('page_id');?>
            <?php echo form_hidden('page_id',set_value('page_id',$page_id));?>
            <?php echo form_error('language_slug');?>
            <?php echo form_hidden('language_slug',set_value('language_slug',$language_slug));?>
            <?php
            $submit_button = 'Add page';
            if($page_id!=0) $submit_button = 'Add translation';
            echo form_submit('submit', $submit_button, 'class="btn btn-primary btn-lg btn-block"');?>
            <?php echo anchor('/admin/pages', 'Cancel','class="btn btn-default btn-lg btn-block"');?>
            <?php echo form_close();?>
        </div>
    </div>
</div>

Now, returning to our else block that we started before creating the view… If the form was submitted and the form validation passed, we will retrieve all the data from the input:

$parent_id = $this->input->post('parent_id');
$title = $this->input->post('title');
$menu_title = (strlen($this->input->post('menu_title')) > 0) ? $this->input->post('menu_title') : $title;
$slug = (strlen($this->input->post('slug')) > 0) ? url_title($this->input->post('slug'),'-',TRUE) : url_title(convert_accented_characters($title),'-',TRUE);
$order = $this->input->post('order');
$content = $this->input->post('content');
$teaser = (strlen($this->input->post('teaser')) > 0) ? $this->input->post('teaser') : substr($content, 0, strpos($content, '<!--more-->'));
$page_title = (strlen($this->input->post('page_title')) > 0) ? $this->input->post('page_title') : $title;
$page_description = (strlen($this->input->post('page_description')) > 0) ? $this->input->post('page_description') : ellipsize($teaser, 160);
$page_keywords = $this->input->post('page_keywords');
$page_id = $this->input->post('page_id');
$language_slug = $this->input->post('language_slug');

It’s in here that I’ve used the text helper. See function ellipsize()…

As you can see, we make sure we use everything we have, even if the user is not filling all the input fields. Also, you can at any time insert a <!–more–> comment inside the content and the application will take the string until it meets that html comment and make the teaser (just like WordPress).

Now what do we do with all this data. First we make sure we have a page id. If the page id is 0, that means we need to create a new row inside the pages table.

if ($page_id == 0)
{
  $page_id = $this->page_model->insert(array('parent_id' => $parent_id, 'order' => $order));
}

After we’ve retrieved the page id, we create the translation and the slug:

$insert_data = array('page_id'=>$page_id,'title' => $title, 'menu_title' => $menu_title, 'teaser' => $teaser,'content' => $content,'page_title' => $page_title, 'page_description' => $page_description,'page_keywords' => $page_keywords,'language_slug' => $language_slug,'created_by'=>$this->user_id);

if($translation_id = $this->page_translation_model->insert($insert_data))
{
  $url = $this->_verify_slug($slug,$language_slug);
  $this->slug_model->insert(array(
    'content_type'=>'page',
    'content_id'=>$page_id,
    'translation_id'=>$translation_id,
    'language_slug'=>$language_slug,
    'url'=>$url,
    'created_by'=>$this->user_id));
}

redirect('admin/pages','refresh');

Let’s explain what we did here. First of all we filled the created_by column with a variable that doesn’t exist $this->user_id. Let’s create that variable inside our Admin_Controller, taking it from the users table:

class Admin_Controller extends MY_Controller
{

  function __construct()
  {
    parent::__construct();
    $this->load->library('ion_auth');
    $this->load->helper('url');
    if(!$this->ion_auth->logged_in())
    {
      //redirect them to the login page
      redirect('admin/user/login', 'refresh');
    }
    $current_user = $this->ion_auth->user()->row();
    // get the current user's ID
    $this->user_id = $current_user->id;
    $this->data['current_user'] = $current_user;
    $this->data['current_user_menu'] = '';
    if($this->ion_auth->in_group('admin'))
    {
      $this->data['current_user_menu'] = $this->load->view('templates/_parts/user_menu_admin_view.php', NULL, TRUE);
    }

    $this->data['page_title'] = 'CI App - Dashboard';
  }
  protected function render($the_view = NULL, $template = 'admin_master')
  {
    parent::render($the_view, $template);
  }
}

Now… you also may have noticed that we used some sort of new method named _verify_slug() which received the slug and the language slug as parameters. This private method that we will define in the current controller will verify if the slug is already defined in a “slugs” table. If it exists it will append to the slug an incremental value until it finds a slug that doesn’t exist:

private function _verify_slug($str,$language)
{
  $this->load->model('slug_model');
  if($this->slug_model->where(array('url'=>$str,'language_slug'=>$language))->get() !== FALSE)
  {
    $parts = explode('-',$str);
    if(is_numeric($parts[sizeof($parts)-1]))
    {
      $parts[sizeof($parts)-1] = $parts[sizeof($parts)-1]++;
    }
    else
    {
      $parts[] = '1';
    }
    $str = implode('-',$parts);
    $this->_verify_slug($str,$language);
  }
  return $str;
}

But what about the “slugs” table? Let’s create it:

CREATE TABLE `slugs` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`content_type` varchar(150) NOT NULL,
`content_id` int(11) NOT NULL,
`translation_id` int(11) NOT NULL,
`language_slug` varchar(5) NOT NULL,
`url` varchar(255) NOT NULL,
`redirect` int(11) NOT NULL DEFAULT '0',
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
`deleted_at` datetime DEFAULT NULL,
`created_by` int(11) NOT NULL,
`updated_by` int(11) NOT NULL,
`deleted_by` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `url` (`url`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

Once we’ve created the “slugs” table we also have to create a Slug_model. So let’s create a file named Slug_model.php inside application/models directory:

<?php

defined('BASEPATH') OR exit('No direct script access allowed');

class Slug_model extends MY_Model
{

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

And that’s it for the creation of pages.

Now let’s see the whole controller again:

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

class Pages extends Admin_Controller
{

	function __construct()
	{
		parent::__construct();
        if(!$this->ion_auth->in_group('admin'))
        {
            $this->session->set_flashdata('message','You are not allowed to visit the Pages page');
            redirect('admin','refresh');
        }
        $this->load->model('page_model');
        $this->load->model('page_translation_model');
        $this->load->model('language_model');
        $this->load->library('form_validation');
        $this->load->helper('text');
	}

	public function index()
	{
		$this->render('admin/pages/index_view');
	}

    public function create($language_slug = NULL, $page_id = 0)
    {
        $language_slug = (isset($language_slug) && array_key_exists($language_slug, $this->langs)) ? $language_slug : $this->current_lang;

        $this->data['content_language'] = $this->langs[$language_slug]['name'];
        $this->data['language_slug'] = $language_slug;
        if($page_id != 0 && $this->page_model->get($page_id)===FALSE)
        {
            $page_id = 0;
        }
        if($this->page_translation_model->where(array('page_id'=>$page_id,'language_slug'=>$language_slug))->get())
        {
            $this->session->set_flashdata('message', 'A translation for that page already exists.');
            redirect('admin/pages', 'refresh');
        }
        $this->data['page_id'] = $page_id;
        $pages = $this->page_translation_model->where('language_slug',$language_slug)->order_by('menu_title')->fields('id,menu_title')->get_all();
        $this->data['parent_pages'] = array('0'=>'No parent page');
        if(!empty($pages))
        {
            foreach($pages as $page)
            {
                $this->data['parent_pages'][$page->id] = $page->menu_title;
            }
        }

        $rules = $this->page_model->rules;
        $this->form_validation->set_rules($rules['insert']);
        if($this->form_validation->run()===FALSE)
        {
            $this->render('admin/pages/create_view');
        }
        else
        {
            $parent_id = $this->input->post('parent_id');
            $title = $this->input->post('title');
            $menu_title = (strlen($this->input->post('menu_title')) > 0) ? $this->input->post('menu_title') : $title;
            $slug = (strlen($this->input->post('slug')) > 0) ? url_title($this->input->post('slug'),'-',TRUE) : url_title(convert_accented_characters($title),'-',TRUE);
            $order = $this->input->post('order');
            $content = $this->input->post('content');
            $teaser = (strlen($this->input->post('teaser')) > 0) ? $this->input->post('teaser') : substr($content, 0, strpos($content, '<!--more-->'));
            $page_title = (strlen($this->input->post('page_title')) > 0) ? $this->input->post('page_title') : $title;
            $page_description = (strlen($this->input->post('page_description')) > 0) ? $this->input->post('page_description') : ellipsize($teaser, 160);
            $page_keywords = $this->input->post('page_keywords');
            $page_id = $this->input->post('page_id');
            $language_slug = $this->input->post('language_slug');
            if ($page_id == 0)
            {
                $page_id = $this->page_model->insert(array('parent_id' => $parent_id, 'order' => $order, 'created_by'=>$this->user_id));
            }

            $insert_data = array('page_id'=>$page_id,'title' => $title, 'menu_title' => $menu_title, 'teaser' => $teaser,'content' => $content,'page_title' => $page_title, 'page_description' => $page_description,'page_keywords' => $page_keywords,'language_slug' => $language_slug,'created_by'=>$this->user_id);

            if($translation_id = $this->page_translation_model->insert($insert_data))
            {
                $url = $this->_verify_slug($slug,$language_slug);
                $this->slug_model->insert(array(
                    'content_type'=>'page',
                    'content_id'=>$page_id,
                    'translation_id'=>$translation_id,
                    'language_slug'=>$language_slug,
                    'url'=>$url,
                    'created_by'=>$this->user_id));
                //$this->slug_model->where(array('content_type'=>'page','content_id'=>$page_id,'id !='=>$slug_id))->update(array('redirect'=>$slug_id));
            }

            redirect('admin/pages','refresh');

        }


    }
    private function _verify_slug($str,$language)
    {
        $this->load->model('slug_model');
        if($this->slug_model->where(array('url'=>$str,'language_slug'=>$language))->get() !== FALSE)
        {
            $parts = explode('-',$str);
            if(is_numeric($parts[sizeof($parts)-1]))
            {
                $parts[sizeof($parts)-1] = $parts[sizeof($parts)-1]++;
            }
            else
            {
                $parts[] = '1';
            }
            $str = implode('-',$parts);
            $this->_verify_slug($str,$language);
        }
        return $str;
    }
}

Next we will see how to create the index() method for the Pages controller.

7 comments

  1. Hi,

    here :
    “if($translation_id = $this->page_translation_model->insert($insert_data))
    {
    $url = $this->_verify_slug($slug,$language_slug);
    $this->slug_model->insert(array(

    i don’t see where did you load the slug model? , you use it but you did not loaded,
    it is not a better solution to put the 100 line $this->load->model(‘slug_model’); in the constructor?

    i did not tested yet if is working (your version) if the model is available because you call the function = $this->_verify_slug($slug,$language_slug); and only after you call the insert method of slug_model.

    1. Yes, I guess that is what I did, I’ve put it inside the constructor. It is a much better idea to load the models in the constructor, as they are usually used by more than one controller method.

  2. I’m guessing this method of holding the page hierarchy uses the Adjacency List Model. In your experience how many levels deep can your page hierarchy go using this method?

    1. Hello,

      To be honest is more like an Adjacency List Model combined with 50% of a Nested Set Model (as it doesn’t have a left and a right parameters but an order parameter). This model can go as deep as you want. Combined with the theory I presented in here (http://avenir.ro/revisiting-the-multilevel-menu-in-php-with-some-additional-code-bootstrap-framework/), the retrieval of the whole tree would only need one database query and one pass through the whole result set.

  3. Hi,
    After create_view.php everything becomes blur! you did not mention where we have to load next codes just you said we put this code this code and that code!

    Can you be more specific please?

    1. You are right. Sorry about that. After creating the view, you should return to the “else” block, and add that code I am talking about.

  4. Hey dude nice tutorial but your url github about MY_Model extend is using branch 3.0 and that branch doesn’t have count() method.. fix it and nice work by the way..

Leave a Reply

Your email address will not be published. Required fields are marked *

No spam? * Time limit is exhausted. Please reload CAPTCHA.