• Skip to main content
  • Skip to primary sidebar
  • Skip to footer

Matt Doyle | Elated Communications

Web and WordPress Development

  • About Me
  • Blog
    • Design & Multimedia
      • Photoshop
      • Paint Shop Pro
      • Video & Audio
    • Online Marketing
      • E-Commerce
      • Social Media
    • Running a Website
      • WordPress
      • Apache
      • UNIX and Linux
      • Using FTP
    • Web Development
      • HTML
      • CSS
      • JavaScript
      • PHP
      • Perl and CGI Scripting
  • Portfolio
  • Contact Me
  • Hire Me
Home / Blog / Web Development / PHP / How to Add Article Categories to Your CMS

How to Add Article Categories to Your CMS

25 January 2012 / 68 Comments

How to Add Article Categories to Your CMS

View Demo » | Download Code

16 May 2019: This article and the code were updated for PHP7 compatibility.

Since publishing my tutorial Build a CMS in an Afternoon with PHP and MySQL, many readers have asked how to add more features to the CMS. I thought I’d answer some of these questions by writing additional tutorials that build on the original simple CMS.

In this tutorial, you’ll learn how to add article categories to the CMS. Categories give your site more flexibility: as well as listing all articles on the homepage, you can create separate section pages of your site, with each section page listing the articles belonging to a particular category.

For example, our original CMS demo lumps all types of article — news, reviews, and interviews — together on both the homepage and the archive page. By creating separate News, Reviews, and Interviews article categories in our CMS, we can then create individual archive pages for news, reviews, and interviews in our site.

You can see how this looks by clicking the View Demo link above. Notice that each article title on the homepage has a category name below it (Interviews, Reviews or News). Click a category to view its archive page, which lists all articles in that category, along with the description of the category at the top of the page.

The plan

We’re going to start with the basic CMS code from Build a CMS in an Afternoon with PHP and MySQL, and modify it to include the features needed to support categories. Here are the steps you’ll work through:

  1. Modify the database
  2. Build the Category class
  3. Modify the Article class to handle categories
  4. Modify index.php to handle category display
  5. Modify admin.php to handle listing, adding, editing, deleting and assigning categories
  6. Modify the front-end templates and stylesheet to handle category display
  7. Modify the back-end templates to handle listing, adding, editing, deleting and assigning categories

Ready to add categories to your CMS? Let’s go!

Step 1: Modify the database

Safe

The first thing we need to do is enhance the CMS’s MySQL database to support article categories. We need to create a new categories table in the database, and modify the existing articles table to include the ID of the category associated with each article.

Open up the tables.sql file from the original CMS, and make the changes highlighted in the code below:

DROP TABLE IF EXISTS categories;
CREATE TABLE categories
(
  id              smallint unsigned NOT NULL auto_increment,
  name            varchar(255) NOT NULL,                      # Name of the category
  description     text NOT NULL,                              # A short description of the category

  PRIMARY KEY     (id)
);

DROP TABLE IF EXISTS articles;
CREATE TABLE articles
(
  id              smallint unsigned NOT NULL auto_increment,
  publicationDate date NOT NULL,                              # When the article was published
  categoryId      smallint unsigned NOT NULL,                 # The article category ID
  title           varchar(255) NOT NULL,                      # Full title of the article
  summary         text NOT NULL,                              # A short summary of the article
  content         mediumtext NOT NULL,                        # The HTML content of the article

  PRIMARY KEY     (id)
);

As you can see, we’ve added a new table, categories, to store article categories. Each category has a unique ID field (id), a name to identify the category (name), and a short description of the category for displaying on the category archive page (description).

We’ve also modified the articles table to include a categoryId field, which we’ll use to associate each article with a corresponding category.

A field that links one table to another like this is known as a foreign key.

What if you already have articles in your CMS?

If you load the above tables.sql file into MySQL then it will delete any existing articles table in your cms database, and recreate the articles table from scratch. This will delete any articles already in your CMS. Not ideal!

In this situation, you want to modify the articles table while retaining the existing data in the table. Fortunately, MySQL makes this easy, thanks to its ALTER TABLE statement. In simple terms, ALTER TABLE works like this:

ALTER TABLE tableName ADD fieldName fieldDefinition AFTER existingFieldName

So if you have existing articles in your CMS, you’ll need to create your new categories table and modify your existing articles table. To do this, change your tables.sql file to the following:

DROP TABLE IF EXISTS categories;
CREATE TABLE categories
(
  id              smallint unsigned NOT NULL auto_increment,
  name            varchar(255) NOT NULL,                      # Name of the category
  description     text NOT NULL,                              # A short description of the category

  PRIMARY KEY     (id)
);

ALTER TABLE articles ADD categoryId smallint unsigned NOT NULL AFTER publicationDate;

Applying the changes

Now that you’ve edited your tables.sql file, you need to incorporate the changes into your MySQL database.

If you already have a cms database containing articles then make sure you back it up first before applying the following changes!

If you don’t have an existing cms database then you first need to create one, as described in Step 1 of the previous article:

mysql -u username -p
create database cms;
exit

Now you can load your tables.sql file into MySQL to make the changes to the database:

mysql -u username -p cms < tables.sql

Enter your password when prompted, and press Enter. MySQL reads your tables.sql file and runs the commands inside it, creating and/or modifying your tables inside your cms database.

To check that your changes have been made, first login to MySQL:

mysql -u username -p cms

Then use the SHOW TABLES and EXPLAIN commands to check your table schemas in MySQL:


mysql> show tables;
+---------------+
| Tables_in_cms |
+---------------+
| articles      |
| categories    |
+---------------+
2 rows in set (0.00 sec)

mysql> explain articles;
+-----------------+----------------------+------+-----+---------+----------------+
| Field           | Type                 | Null | Key | Default | Extra          |
+-----------------+----------------------+------+-----+---------+----------------+
| id              | smallint(5) unsigned | NO   | PRI | NULL    | auto_increment |
| publicationDate | date                 | NO   |     | NULL    |                |
| categoryId      | smallint(5) unsigned | NO   |     | NULL    |                |
| title           | varchar(255)         | NO   |     | NULL    |                |
| summary         | text                 | NO   |     | NULL    |                |
| content         | mediumtext           | NO   |     | NULL    |                |
+-----------------+----------------------+------+-----+---------+----------------+
6 rows in set (0.00 sec)

mysql> explain categories;
+-------------+----------------------+------+-----+---------+----------------+
| Field       | Type                 | Null | Key | Default | Extra          |
+-------------+----------------------+------+-----+---------+----------------+
| id          | smallint(5) unsigned | NO   | PRI | NULL    | auto_increment |
| name        | varchar(255)         | NO   |     | NULL    |                |
| description | text                 | NO   |     | NULL    |                |
+-------------+----------------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)

mysql> 

Notice the new categoryId field inside the articles table, as well as the brand new categories table.

You’ve now set up your CMS database so that it’s ready to work with categories. Time to write some code!

Step 2: Build the Category class

Cogs

Just as you previously created an Article class to store and retrieve articles in the database, you now need to create a Category class to do the same job for categories.

Within your cms folder, you’ll find your classes folder. Inside that classes folder, create a new file called Category.php, and add the following code to it:

<?php

/**
 * Class to handle article categories
 */

class Category
{
  // Properties

  /**
  * @var int The category ID from the database
  */
  public $id = null;

  /**
  * @var string Name of the category
  */
  public $name = null;

  /**
  * @var string A short description of the category
  */
  public $description = null;


  /**
  * Sets the object's properties using the values in the supplied array
  *
  * @param assoc The property values
  */

  public function __construct( $data=array() ) {
    if ( isset( $data['id'] ) ) $this->id = (int) $data['id'];
    if ( isset( $data['name'] ) ) $this->name = preg_replace ( "/[^\.\,\-\_\'\"\@\?\!\:\$ a-zA-Z0-9()]/", "", $data['name'] );
    if ( isset( $data['description'] ) ) $this->description = preg_replace ( "/[^\.\,\-\_\'\"\@\?\!\:\$ a-zA-Z0-9()]/", "", $data['description'] );
  }


  /**
  * Sets the object's properties using the edit form post values in the supplied array
  *
  * @param assoc The form post values
  */

  public function storeFormValues ( $params ) {

    // Store all the parameters
    $this->__construct( $params );
  }


  /**
  * Returns a Category object matching the given category ID
  *
  * @param int The category ID
  * @return Category|false The category object, or false if the record was not found or there was a problem
  */

  public static function getById( $id ) {
    $conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD );
    $sql = "SELECT * FROM categories WHERE id = :id";
    $st = $conn->prepare( $sql );
    $st->bindValue( ":id", $id, PDO::PARAM_INT );
    $st->execute();
    $row = $st->fetch();
    $conn = null;
    if ( $row ) return new Category( $row );
  }


  /**
  * Returns all (or a range of) Category objects in the DB
  *
  * @param int Optional The number of rows to return (default=all)
  * @return Array|false A two-element array : results => array, a list of Category objects; totalRows => Total number of categories
  */

  public static function getList( $numRows=1000000 ) {
    $conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD );
    $sql = "SELECT SQL_CALC_FOUND_ROWS * FROM categories
            ORDER BY name ASC LIMIT :numRows";

    $st = $conn->prepare( $sql );
    $st->bindValue( ":numRows", $numRows, PDO::PARAM_INT );
    $st->execute();
    $list = array();

    while ( $row = $st->fetch() ) {
      $category = new Category( $row );
      $list[] = $category;
    }

    // Now get the total number of categories that matched the criteria
    $sql = "SELECT FOUND_ROWS() AS totalRows";
    $totalRows = $conn->query( $sql )->fetch();
    $conn = null;
    return ( array ( "results" => $list, "totalRows" => $totalRows[0] ) );
  }


  /**
  * Inserts the current Category object into the database, and sets its ID property.
  */

  public function insert() {

    // Does the Category object already have an ID?
    if ( !is_null( $this->id ) ) trigger_error ( "Category::insert(): Attempt to insert a Category object that already has its ID property set (to $this->id).", E_USER_ERROR );

    // Insert the Category
    $conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD );
    $sql = "INSERT INTO categories ( name, description ) VALUES ( :name, :description )";
    $st = $conn->prepare ( $sql );
    $st->bindValue( ":name", $this->name, PDO::PARAM_STR );
    $st->bindValue( ":description", $this->description, PDO::PARAM_STR );
    $st->execute();
    $this->id = $conn->lastInsertId();
    $conn = null;
  }


  /**
  * Updates the current Category object in the database.
  */

  public function update() {

    // Does the Category object have an ID?
    if ( is_null( $this->id ) ) trigger_error ( "Category::update(): Attempt to update a Category object that does not have its ID property set.", E_USER_ERROR );
   
    // Update the Category
    $conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD );
    $sql = "UPDATE categories SET name=:name, description=:description WHERE id = :id";
    $st = $conn->prepare ( $sql );
    $st->bindValue( ":name", $this->name, PDO::PARAM_STR );
    $st->bindValue( ":description", $this->description, PDO::PARAM_STR );
    $st->bindValue( ":id", $this->id, PDO::PARAM_INT );
    $st->execute();
    $conn = null;
  }


  /**
  * Deletes the current Category object from the database.
  */

  public function delete() {

    // Does the Category object have an ID?
    if ( is_null( $this->id ) ) trigger_error ( "Category::delete(): Attempt to delete a Category object that does not have its ID property set.", E_USER_ERROR );

    // Delete the Category
    $conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD );
    $st = $conn->prepare ( "DELETE FROM categories WHERE id = :id LIMIT 1" );
    $st->bindValue( ":id", $this->id, PDO::PARAM_INT );
    $st->execute();
    $conn = null;
  }

}

?>

This class is very similar to the original Article class, and a fair bit simpler. It contains three properties that map to the fields in the categories table — id, name and description — and a constructor method, __construct(), that creates a new Category object holding the values passed to the constructor. The class also contains a storeFormValues() method for storing the data from the submitted category edit form; methods for retrieving a single category by ID and a list of categories; and methods to insert, update, and delete the category in the database.

We need to include our new Category class file in our code files, so that the CMS code can access it. To do this, include it from within the config.php file inside your cms folder, in the same way that you included Article.php in the original CMS:

<?php
ini_set( "display_errors", true );
date_default_timezone_set( "Australia/Sydney" );  // http://www.php.net/manual/en/timezones.php
define( "DB_DSN", "mysql:host=localhost;dbname=cms" );
define( "DB_USERNAME", "username" );
define( "DB_PASSWORD", "password" );
define( "CLASS_PATH", "classes" );
define( "TEMPLATE_PATH", "templates" );
define( "HOMEPAGE_NUM_ARTICLES", 5 );
define( "ADMIN_USERNAME", "admin" );
define( "ADMIN_PASSWORD", "mypass" );
require( CLASS_PATH . "/Article.php" );
require( CLASS_PATH . "/Category.php" );

function handleException( $exception ) {
  echo "Sorry, a problem occurred. Please try later.";
  error_log( $exception->getMessage() );
}

set_exception_handler( 'handleException' );
?>

Step 3: Modify the Article class

Cogs

As well as creating the new Category class, we need to modify the existing Article class to handle categories. Here’s the updated Article.php class file. I’ve highlighted the lines of code so you can see what’s been added. Replace the code in your existing cms/classes/Article.php file with this new code:

<?php

/**
 * Class to handle articles
 */

class Article
{
  // Properties

  /**
  * @var int The article ID from the database
  */
  public $id = null;

  /**
  * @var int When the article is to be / was first published
  */
  public $publicationDate = null;

  /**
  * @var int The article category ID
  */
  public $categoryId = null;

  /**
  * @var string Full title of the article
  */
  public $title = null;

  /**
  * @var string A short summary of the article
  */
  public $summary = null;

  /**
  * @var string The HTML content of the article
  */
  public $content = null;


  /**
  * Sets the object's properties using the values in the supplied array
  *
  * @param assoc The property values
  */

  public function __construct( $data=array() ) {
    if ( isset( $data['id'] ) ) $this->id = (int) $data['id'];
    if ( isset( $data['publicationDate'] ) ) $this->publicationDate = (int) $data['publicationDate'];
    if ( isset( $data['categoryId'] ) ) $this->categoryId = (int) $data['categoryId'];
    if ( isset( $data['title'] ) ) $this->title = preg_replace ( "/[^\.\,\-\_\'\"\@\?\!\:\$ a-zA-Z0-9()]/", "", $data['title'] );
    if ( isset( $data['summary'] ) ) $this->summary = preg_replace ( "/[^\.\,\-\_\'\"\@\?\!\:\$ a-zA-Z0-9()]/", "", $data['summary'] );
    if ( isset( $data['content'] ) ) $this->content = $data['content'];
  }


  /**
  * Sets the object's properties using the edit form post values in the supplied array
  *
  * @param assoc The form post values
  */

  public function storeFormValues ( $params ) {

    // Store all the parameters
    $this->__construct( $params );

    // Parse and store the publication date
    if ( isset($params['publicationDate']) ) {
      $publicationDate = explode ( '-', $params['publicationDate'] );

      if ( count($publicationDate) == 3 ) {
        list ( $y, $m, $d ) = $publicationDate;
        $this->publicationDate = mktime ( 0, 0, 0, $m, $d, $y );
      }
    }
  }


  /**
  * Returns an Article object matching the given article ID
  *
  * @param int The article ID
  * @return Article|false The article object, or false if the record was not found or there was a problem
  */

  public static function getById( $id ) {
    $conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD );
    $sql = "SELECT *, UNIX_TIMESTAMP(publicationDate) AS publicationDate FROM articles WHERE id = :id";
    $st = $conn->prepare( $sql );
    $st->bindValue( ":id", $id, PDO::PARAM_INT );
    $st->execute();
    $row = $st->fetch();
    $conn = null;
    if ( $row ) return new Article( $row );
  }


  /**
  * Returns all (or a range of) Article objects in the DB
  *
  * @param int Optional The number of rows to return (default=all)
  * @param int Optional Return just articles in the category with this ID
  * @return Array|false A two-element array : results => array, a list of Article objects; totalRows => Total number of articles
  */

  public static function getList( $numRows=1000000, $categoryId=null ) {
    $conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD );
    $categoryClause = $categoryId ? "WHERE categoryId = :categoryId" : "";
    $sql = "SELECT SQL_CALC_FOUND_ROWS *, UNIX_TIMESTAMP(publicationDate) AS publicationDate
            FROM articles $categoryClause
            ORDER BY publicationDate DESC LIMIT :numRows";

    $st = $conn->prepare( $sql );
    $st->bindValue( ":numRows", $numRows, PDO::PARAM_INT );
    if ( $categoryId ) $st->bindValue( ":categoryId", $categoryId, PDO::PARAM_INT );
    $st->execute();
    $list = array();

    while ( $row = $st->fetch() ) {
      $article = new Article( $row );
      $list[] = $article;
    }

    // Now get the total number of articles that matched the criteria
    $sql = "SELECT FOUND_ROWS() AS totalRows";
    $totalRows = $conn->query( $sql )->fetch();
    $conn = null;
    return ( array ( "results" => $list, "totalRows" => $totalRows[0] ) );
  }


  /**
  * Inserts the current Article object into the database, and sets its ID property.
  */

  public function insert() {

    // Does the Article object already have an ID?
    if ( !is_null( $this->id ) ) trigger_error ( "Article::insert(): Attempt to insert an Article object that already has its ID property set (to $this->id).", E_USER_ERROR );

    // Insert the Article
    $conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD );
    $sql = "INSERT INTO articles ( publicationDate, categoryId, title, summary, content ) VALUES ( FROM_UNIXTIME(:publicationDate), :categoryId, :title, :summary, :content )";
    $st = $conn->prepare ( $sql );
    $st->bindValue( ":publicationDate", $this->publicationDate, PDO::PARAM_INT );
    $st->bindValue( ":categoryId", $this->categoryId, PDO::PARAM_INT );
    $st->bindValue( ":title", $this->title, PDO::PARAM_STR );
    $st->bindValue( ":summary", $this->summary, PDO::PARAM_STR );
    $st->bindValue( ":content", $this->content, PDO::PARAM_STR );
    $st->execute();
    $this->id = $conn->lastInsertId();
    $conn = null;
  }


  /**
  * Updates the current Article object in the database.
  */

  public function update() {

    // Does the Article object have an ID?
    if ( is_null( $this->id ) ) trigger_error ( "Article::update(): Attempt to update an Article object that does not have its ID property set.", E_USER_ERROR );
   
    // Update the Article
    $conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD );
    $sql = "UPDATE articles SET publicationDate=FROM_UNIXTIME(:publicationDate), categoryId=:categoryId, title=:title, summary=:summary, content=:content WHERE id = :id";
    $st = $conn->prepare ( $sql );
    $st->bindValue( ":publicationDate", $this->publicationDate, PDO::PARAM_INT );
    $st->bindValue( ":categoryId", $this->categoryId, PDO::PARAM_INT );
    $st->bindValue( ":title", $this->title, PDO::PARAM_STR );
    $st->bindValue( ":summary", $this->summary, PDO::PARAM_STR );
    $st->bindValue( ":content", $this->content, PDO::PARAM_STR );
    $st->bindValue( ":id", $this->id, PDO::PARAM_INT );
    $st->execute();
    $conn = null;
  }


  /**
  * Deletes the current Article object from the database.
  */

  public function delete() {

    // Does the Article object have an ID?
    if ( is_null( $this->id ) ) trigger_error ( "Article::delete(): Attempt to delete an Article object that does not have its ID property set.", E_USER_ERROR );

    // Delete the Article
    $conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD );
    $st = $conn->prepare ( "DELETE FROM articles WHERE id = :id LIMIT 1" );
    $st->bindValue( ":id", $this->id, PDO::PARAM_INT );
    $st->execute();
    $conn = null;
  }

}

?>

Let’s take a look at the additions we’ve made to the Article class:

  • A new categoryId property
    In order to associate an article with a category, we add an integer categoryId property to store the ID of the article’s category. We also modify the constructor method, __construct(), to store the new categoryId property in newly-created Article objects.
  • The modified getList() method
    Our original getList() method retrieves all articles in the database (optionally limited to a maximum number of records). Since we want our CMS to be able to display a list of articles in a particular category, we modify getList() to accept an optional $categoryId argument. If present, only articles in that category are returned.  

    If $categoryId is supplied, we create a $categoryClause string containing a WHERE clause to retrieve just articles with a categoryId field that matches the supplied categoryId value. We then modify the SQL SELECT statement to include this $categoryClause variable. We also add a new call to $st->bindValue() to bind the supplied $categoryId value to the SQL statement before executing it.

  • Modified insert() and update() methods
    Finally, we modify the Article class’s insert() and update() methods to accommodate the new categoryId field. We modify the SQL INSERT and UPDATE statements in each method, and also add new calls to bindValue() to pass the object’s $categoryId property to the SQL statements.

Step 4: Modify the front-end index.php script

Welcome

The next step is to make some additions to index.php — the script that displays the front-end pages of the site — so that it can handle the display of categories. Here’s the modified index.php file with the changes highlighted — replace the old index.php file in your cms folder with this new code:

<?php

require( "config.php" );
$action = isset( $_GET['action'] ) ? $_GET['action'] : "";

switch ( $action ) {
  case 'archive':
    archive();
    break;
  case 'viewArticle':
    viewArticle();
    break;
  default:
    homepage();
}

function archive() {
  $results = array();
  $categoryId = ( isset( $_GET['categoryId'] ) && $_GET['categoryId'] ) ? (int)$_GET['categoryId'] : null;
  $results['category'] = Category::getById( $categoryId );
  $data = Article::getList( 100000, $results['category'] ? $results['category']->id : null );
  $results['articles'] = $data['results'];
  $results['totalRows'] = $data['totalRows'];
  $data = Category::getList();
  $results['categories'] = array();
  foreach ( $data['results'] as $category ) $results['categories'][$category->id] = $category;
  $results['pageHeading'] = $results['category'] ?  $results['category']->name : "Article Archive";
  $results['pageTitle'] = $results['pageHeading'] . " | Widget News";
  require( TEMPLATE_PATH . "/archive.php" );
}

function viewArticle() {
  if ( !isset($_GET["articleId"]) || !$_GET["articleId"] ) {
    homepage();
    return;
  }

  $results = array();
  $results['article'] = Article::getById( (int)$_GET["articleId"] );
  $results['category'] = Category::getById( $results['article']->categoryId );
  $results['pageTitle'] = $results['article']->title . " | Widget News";
  require( TEMPLATE_PATH . "/viewArticle.php" );
}

function homepage() {
  $results = array();
  $data = Article::getList( HOMEPAGE_NUM_ARTICLES );
  $results['articles'] = $data['results'];
  $results['totalRows'] = $data['totalRows'];
  $data = Category::getList();
  $results['categories'] = array();
  foreach ( $data['results'] as $category ) $results['categories'][$category->id] = $category; 
  $results['pageTitle'] = "Widget News";
  require( TEMPLATE_PATH . "/homepage.php" );
}

?>

Let’s take a look at each of the changed functions in index.php:

  • archive()
    The original archive() function simply displayed a list of all articles in the CMS. Here, we’ve adapted the archive() function to accept an optional categoryId query string parameter. If categoryId is supplied, the function retrieves the corresponding Category object by calling Category::getById() and, if retrieved successfully, it passes the category ID to the Article::getList() function to retreive just the articles in the supplied cateogory.  

    We’ve also added code to the archive() function to retreive all the categories in the database and store them in $results['categories'], keyed by category ID. Our archive page template, archive.php, will use this array to display the name of the category that each article is in.

    Finally, we create a $results['pageHeading'] variable containing either the category name (if a categoryId was supplied), or the text “Article Archive”. We’ll display this value in the heading within the archive page. We also create a $results['pageTitle'] variable to use in the page’s <title> element. This is simply the page heading with the site name, “Widget News”, tacked onto the end.

  • viewArticle()
    We’ve made just one addition to viewArticle(): We retrieve the Category object associated with the article by calling Category::getById(), passing in the article’s $categoryId property. We store the resulting Category object in $results['category']. We’ll use this object to display the name of the article’s category in the viewArticle.php template.
  • homepage()
    Finally, we add three lines to the homepage() function. Like the corresponding code added to archive(), this additional code calls Category::getList() to retrieve all the categories in the CMS, then stores the categories in $results['categories'], keyed by category ID, so that we can display the name of each article’s category on the site homepage.

Step 5: Modify the back-end admin.php script

Lock

The admin.php file contains all the admin functions for the CMS. We need to make some changes and additions to this file to handle article categories.

Here’s the new admin.php file with the changes highlighted. Replace the old admin.php file in your cms folder with this new code:

<?php

require( "config.php" );
session_start();
$action = isset( $_GET['action'] ) ? $_GET['action'] : "";
$username = isset( $_SESSION['username'] ) ? $_SESSION['username'] : "";

if ( $action != "login" && $action != "logout" && !$username ) {
  login();
  exit;
}

switch ( $action ) {
  case 'login':
    login();
    break;
  case 'logout':
    logout();
    break;
  case 'newArticle':
    newArticle();
    break;
  case 'editArticle':
    editArticle();
    break;
  case 'deleteArticle':
    deleteArticle();
    break;
  case 'listCategories':
    listCategories();
    break;
  case 'newCategory':
    newCategory();
    break;
  case 'editCategory':
    editCategory();
    break;
  case 'deleteCategory':
    deleteCategory();
    break;
  default:
    listArticles();
}


function login() {

  $results = array();
  $results['pageTitle'] = "Admin Login | Widget News";

  if ( isset( $_POST['login'] ) ) {

    // User has posted the login form: attempt to log the user in

    if ( $_POST['username'] == ADMIN_USERNAME && $_POST['password'] == ADMIN_PASSWORD ) {

      // Login successful: Create a session and redirect to the admin homepage
      $_SESSION['username'] = ADMIN_USERNAME;
      header( "Location: admin.php" );

    } else {

      // Login failed: display an error message to the user
      $results['errorMessage'] = "Incorrect username or password. Please try again.";
      require( TEMPLATE_PATH . "/admin/loginForm.php" );
    }

  } else {

    // User has not posted the login form yet: display the form
    require( TEMPLATE_PATH . "/admin/loginForm.php" );
  }

}


function logout() {
  unset( $_SESSION['username'] );
  header( "Location: admin.php" );
}


function newArticle() {

  $results = array();
  $results['pageTitle'] = "New Article";
  $results['formAction'] = "newArticle";

  if ( isset( $_POST['saveChanges'] ) ) {

    // User has posted the article edit form: save the new article
    $article = new Article;
    $article->storeFormValues( $_POST );
    $article->insert();
    header( "Location: admin.php?status=changesSaved" );

  } elseif ( isset( $_POST['cancel'] ) ) {

    // User has cancelled their edits: return to the article list
    header( "Location: admin.php" );
  } else {

    // User has not posted the article edit form yet: display the form
    $results['article'] = new Article;
    $data = Category::getList();
    $results['categories'] = $data['results'];
    require( TEMPLATE_PATH . "/admin/editArticle.php" );
  }

}


function editArticle() {

  $results = array();
  $results['pageTitle'] = "Edit Article";
  $results['formAction'] = "editArticle";

  if ( isset( $_POST['saveChanges'] ) ) {

    // User has posted the article edit form: save the article changes

    if ( !$article = Article::getById( (int)$_POST['articleId'] ) ) {
      header( "Location: admin.php?error=articleNotFound" );
      return;
    }

    $article->storeFormValues( $_POST );
    $article->update();
    header( "Location: admin.php?status=changesSaved" );

  } elseif ( isset( $_POST['cancel'] ) ) {

    // User has cancelled their edits: return to the article list
    header( "Location: admin.php" );
  } else {

    // User has not posted the article edit form yet: display the form
    $results['article'] = Article::getById( (int)$_GET['articleId'] );
    $data = Category::getList();
    $results['categories'] = $data['results'];
    require( TEMPLATE_PATH . "/admin/editArticle.php" );
  }

}


function deleteArticle() {

  if ( !$article = Article::getById( (int)$_GET['articleId'] ) ) {
    header( "Location: admin.php?error=articleNotFound" );
    return;
  }

  $article->delete();
  header( "Location: admin.php?status=articleDeleted" );
}


function listArticles() {
  $results = array();
  $data = Article::getList();
  $results['articles'] = $data['results'];
  $results['totalRows'] = $data['totalRows'];
  $data = Category::getList();
  $results['categories'] = array();
  foreach ( $data['results'] as $category ) $results['categories'][$category->id] = $category;
  $results['pageTitle'] = "All Articles";

  if ( isset( $_GET['error'] ) ) {
    if ( $_GET['error'] == "articleNotFound" ) $results['errorMessage'] = "Error: Article not found.";
  }

  if ( isset( $_GET['status'] ) ) {
    if ( $_GET['status'] == "changesSaved" ) $results['statusMessage'] = "Your changes have been saved.";
    if ( $_GET['status'] == "articleDeleted" ) $results['statusMessage'] = "Article deleted.";
  }

  require( TEMPLATE_PATH . "/admin/listArticles.php" );
}


function listCategories() {
  $results = array();
  $data = Category::getList();
  $results['categories'] = $data['results'];
  $results['totalRows'] = $data['totalRows'];
  $results['pageTitle'] = "Article Categories";

  if ( isset( $_GET['error'] ) ) {
    if ( $_GET['error'] == "categoryNotFound" ) $results['errorMessage'] = "Error: Category not found.";
    if ( $_GET['error'] == "categoryContainsArticles" ) $results['errorMessage'] = "Error: Category contains articles. Delete the articles, or assign them to another category, before deleting this category.";
  }

  if ( isset( $_GET['status'] ) ) {
    if ( $_GET['status'] == "changesSaved" ) $results['statusMessage'] = "Your changes have been saved.";
    if ( $_GET['status'] == "categoryDeleted" ) $results['statusMessage'] = "Category deleted.";
  }

  require( TEMPLATE_PATH . "/admin/listCategories.php" );
}


function newCategory() {

  $results = array();
  $results['pageTitle'] = "New Article Category";
  $results['formAction'] = "newCategory";

  if ( isset( $_POST['saveChanges'] ) ) {

    // User has posted the category edit form: save the new category
    $category = new Category;
    $category->storeFormValues( $_POST );
    $category->insert();
    header( "Location: admin.php?action=listCategories&status=changesSaved" );

  } elseif ( isset( $_POST['cancel'] ) ) {

    // User has cancelled their edits: return to the category list
    header( "Location: admin.php?action=listCategories" );
  } else {

    // User has not posted the category edit form yet: display the form
    $results['category'] = new Category;
    require( TEMPLATE_PATH . "/admin/editCategory.php" );
  }

}


function editCategory() {

  $results = array();
  $results['pageTitle'] = "Edit Article Category";
  $results['formAction'] = "editCategory";

  if ( isset( $_POST['saveChanges'] ) ) {

    // User has posted the category edit form: save the category changes

    if ( !$category = Category::getById( (int)$_POST['categoryId'] ) ) {
      header( "Location: admin.php?action=listCategories&error=categoryNotFound" );
      return;
    }

    $category->storeFormValues( $_POST );
    $category->update();
    header( "Location: admin.php?action=listCategories&status=changesSaved" );

  } elseif ( isset( $_POST['cancel'] ) ) {

    // User has cancelled their edits: return to the category list
    header( "Location: admin.php?action=listCategories" );
  } else {

    // User has not posted the category edit form yet: display the form
    $results['category'] = Category::getById( (int)$_GET['categoryId'] );
    require( TEMPLATE_PATH . "/admin/editCategory.php" );
  }

}


function deleteCategory() {

  if ( !$category = Category::getById( (int)$_GET['categoryId'] ) ) {
    header( "Location: admin.php?action=listCategories&error=categoryNotFound" );
    return;
  }

  $articles = Article::getList( 1000000, $category->id );

  if ( $articles['totalRows'] > 0 ) {
    header( "Location: admin.php?action=listCategories&error=categoryContainsArticles" );
    return;
  }

  $category->delete();
  header( "Location: admin.php?action=listCategories&status=categoryDeleted" );
}

?>

Let’s look at each of the changes to admin.php in turn:

  • Additions to the switch block
    We need to add some new functionality to admin.php to handle listing, creating, editing and deleting categories. To this end, we add listCategories, newCategory, editCategory and deleteCategory case blocks to the switch block at the top of the file. These blocks call various functions to handle categories. We’ll look at these new functions in a moment.
  • Changes to newArticle(), editArticle() and listArticles()
    We make a small addition to each of these functions in order to retrieve the list of all categories in the database, for use in the Edit Article form and List Articles page. We store the retrieved categories in the $results['categories'] variable. For listArticles(), we also key the categories by category ID, to make it easy for the listArticles.php template to access categories by ID.
  • listCategories()
    This new function displays a list of all categories to the administrator. It works in much the same way as the listArticles() function. It pulls all the categories from the database by calling Category::getList(), then stores them in the $results['categories'] array. It also records the total number of categories in the $results['totalRows'] variable, and stores the page title in $results['pageTitle']. It then checks for various error or status codes passed in the query string, and sets the value of $results['errorMessage'] or $results['statusMessage'] accordingly. Finally, it includes the listCategories.php template file to display the categories list page.
  • newCategory()
    This lets the administrator add a new category to the database, much as newArticle() adds a new article. If the user has submitted the category edit form then the function creates a new Category object, populates it with the form data, calls insert() to insert the category into the database, and redirects to the categories list page, displaying a “changes saved” message. If the user clicked the form’s Cancel button to cancel their edits then the function simply redirects to the category list. If the user hasn’t yet submitted the form then the function creates a new empty Category object to use for the form, stores it in $results['category'], and includes the editCategory.php template to display the category edit form.
  • editCategory()
    This function edits an existing category in the database, allowing the user to change the category’s name and/or description. It follows the same pattern as editArticle(). If the edit form has been submitted, it loads the category from the database, stores the new form values in the Category object, and updates the category in the database by calling the update() method. If the user cancelled their edits then the function redirects to the category list. If the user hasn’t yet posted the form then the function loads the category specified by the categoryId query string parameter, stores it in $results['category'], and includes the editCategory.php template to display the populated edit form.
  • deleteCategory()
    This is the last new function we’ve added to admin.php, and it lets the administrator delete a category from the database. It’s called when the user clicks the Delete This Category link on the Edit Category page. First it retrieves the category specified by the categoryId query string parameter (displaying an error if the category wasn’t found). Then it checks to see if there are any articles in this category; if there are then it displays an error message and exits. If there aren’t any articles in the category then the function deletes the category and redirects to the category list, displaying a “category deleted” message.

Step 6: Modify the front-end templates and stylesheet

Category Archive screenshot

Now that we’ve added category support to the database and main PHP code, we need to modify the templates to handle categories. First of all, let’s alter the front-end templates so that they can display category names, as well as show category archive pages.

1. homepage.php

We’ll make a small change to the homepage.php template — which displays the site home page — so that each article’s category is displayed below its title. Here’s the modified file with changes highlighted — replace your old cms/templates/homepage.php file with this code:

<?php include "templates/include/header.php" ?>

      <ul id="headlines">

<?php foreach ( $results['articles'] as $article ) { ?>

        <li>
          <h2>
            <span class="pubDate"><?php echo date('j F', $article->publicationDate)?></span><a href=".?action=viewArticle&amp;articleId=<?php echo $article->id?>"><?php echo htmlspecialchars( $article->title )?></a>
            <?php if ( $article->categoryId ) { ?>
            <span class="category">in <a href=".?action=archive&amp;categoryId=<?php echo $article->categoryId?>"><?php echo htmlspecialchars( $results['categories'][$article->categoryId]->name )?></a></span>
            <?php } ?>
          </h2>
          <p class="summary"><?php echo htmlspecialchars( $article->summary )?></p>
        </li>

<?php } ?>

      </ul>

      <p><a href="./?action=archive">Article Archive</a></p>

<?php include "templates/include/footer.php" ?>

As you can see, the added code checks to see if the article is in a category by looking at its $categoryId property. If it is, the code inserts a <span> element into the page, containing the category name. It does this by looking up the correct Category object in the supplied $results['categories'] array based on the category’s ID, then displaying the category’s name property.

The code also wraps the category name in a link that points to the category’s archive page, so that the visitor can easily view other articles in the same category.

2. archive.php

The archive.php template displays the article archive. In the original CMS, the archive simply listed all articles. However, in this version of the CMS, the archive() function in index.php can also list just the articles in a given category. Therefore we need to make a couple of changes to the template to accommodate this.

Here’s the modified template. Replace your old cms/templates/archive.php file with the following code:

<?php include "templates/include/header.php" ?>

      <h1><?php echo htmlspecialchars( $results['pageHeading'] ) ?></h1>
<?php if ( $results['category'] ) { ?>
      <h3 class="categoryDescription"><?php echo htmlspecialchars( $results['category']->description ) ?></h3>
<?php } ?>

      <ul id="headlines" class="archive">

<?php foreach ( $results['articles'] as $article ) { ?>

        <li>
          <h2>
            <span class="pubDate"><?php echo date('j F Y', $article->publicationDate)?></span><a href=".?action=viewArticle&amp;articleId=<?php echo $article->id?>"><?php echo htmlspecialchars( $article->title )?></a>
<?php if ( !$results['category'] && $article->categoryId ) { ?>
            <span class="category">in <a href=".?action=archive&amp;categoryId=<?php echo $article->categoryId?>"><?php echo htmlspecialchars( $results['categories'][$article->categoryId]->name ) ?></a></span>
<?php } ?>            
          </h2>
          <p class="summary"><?php echo htmlspecialchars( $article->summary )?></p>
        </li>

<?php } ?>

      </ul>

      <p><?php echo $results['totalRows']?> article<?php echo ( $results['totalRows'] != 1 ) ? 's' : '' ?> in total.</p>

      <p><a href="./">Return to Homepage</a></p>

<?php include "templates/include/footer.php" ?>

To start with, we’ve modified the <h1> heading so that it displays the value of $results['pageHeading'], rather than just “Article Archive”. This is because our new archive() function dynamically creates the page heading, depending on the category being viewed.

Next, we’ve added some code that checks whether we’re displaying an archive for a particular category — as opposed to an archive of all articles — by looking for a $results['category'] variable. If the variable is found then we’re displaying a category archive, so the code displays a heading containing the category’s description.

Further down the template, within the loop that displays each article, we’ve made one last addition. This new code is triggered if we’re displaying an archive of all articles — rather than a category archive — and if the current article has a category. In this scenario, the code adds a <span> containing the category name, linked to the category archive so the visitor can explore more articles in the category.

3. viewArticle.php

viewArticle.php is the template that displays a single article page. Here’s the changed file — replace your old cms/templates/viewArticle.php file with this code:

<?php include "templates/include/header.php" ?>

      <h1 style="width: 75%;"><?php echo htmlspecialchars( $results['article']->title )?></h1>
      <div style="width: 75%; font-style: italic;"><?php echo htmlspecialchars( $results['article']->summary )?></div>
      <div style="width: 75%;"><?php echo $results['article']->content?></div>
      <p class="pubDate">Published on <?php echo date('j F Y', $results['article']->publicationDate)?>
<?php if ( $results['category'] ) { ?>
        in <a href="./?action=archive&amp;categoryId=<?php echo $results['category']->id?>"><?php echo htmlspecialchars( $results['category']->name ) ?></a>
<?php } ?>
      </p>

      <p><a href="./">Return to Homepage</a></p>

<?php include "templates/include/footer.php" ?>

As you can see, we’ve just made one small addition to this file that displays the article’s category at the bottom of the article. The category name is linked to the corresponding category archive page.

4. The stylesheet

We need to make a few small tweaks to the stylesheet file, style.css, in order to style the category names and descriptions in the homepage and archive page.

Here’s the new style.css file with the changes highlighted. Save it over your old style.css file in your cms folder:

/* Style the body and outer container */

body {
  margin: 0;
  color: #333;
  background-color: #00a0b0;
  font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
  line-height: 1.5em;
}

#container {
  width: 960px;
  background: #fff;
  margin: 20px auto;
  padding: 20px;
  -moz-border-radius: 5px;
  -webkit-border-radius: 5px;
  border-radius: 5px;
}


/* The logo and footer */

#logo {
  display: block;
  width: 300px;
  padding: 0 660px 20px 0;
  border: none;
  border-bottom: 1px solid #00a0b0;
  margin-bottom: 40px;
}

#footer {
  border-top: 1px solid #00a0b0;
  margin-top: 40px;
  padding: 20px 0 0 0;
  font-size: .8em;
}


/* Headings */

h1 {
  color: #eb6841;
  margin-bottom: 30px;
  line-height: 1.2em;
}

h2, h2 a {
  color: #edc951;
}

h2 a {
  text-decoration: none;
}

h3.categoryDescription {
  margin-top: -20px;
  margin-bottom: 40px;
}


/* Article headlines */

#headlines {
  list-style: none;
  padding-left: 0;
  width: 75%;
}

#headlines li {
  margin-bottom: 2em;
  clear: both;
}

.pubDate {
  font-size: .8em;
  color: #eb6841;
  text-transform: uppercase;
}

#headlines .pubDate {
  display: block;
  width: 100px;
  padding-top: 4px;
  float: left;
  font-size: .5em;
  vertical-align: middle;
}

#headlines.archive .pubDate {
  width: 130px;
}

.summary {
  padding-left: 100px;
}

#headlines.archive .summary {
  padding-left: 130px;
}

.category {
  font-style: italic;
  font-weight: normal;
  font-size: 60%;
  color: #999;
  display: block;
  line-height: 2em;
}

.category a {
  color: #999;
  text-decoration: underline;
}


/* "You are logged in..." header on admin pages */

#adminHeader {
  width: 940px;
  padding: 0 10px;
  border-bottom: 1px solid #00a0b0;
  margin: -30px 0 40px 0;
  font-size: 0.8em;
}


/* Style the form with a coloured background, along with curved corners and a drop shadow */

form {
  margin: 20px auto;
  padding: 40px 20px;
  overflow: auto;
  background: #fff4cf;
  border: 1px solid #666;
  -moz-border-radius: 5px;
  -webkit-border-radius: 5px;  
  border-radius: 5px;
  -moz-box-shadow: 0 0 .5em rgba(0, 0, 0, .8);
  -webkit-box-shadow: 0 0 .5em rgba(0, 0, 0, .8);
  box-shadow: 0 0 .5em rgba(0, 0, 0, .8);
}


/* Give form elements consistent margin, padding and line height */

form ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

form ul li {
  margin: .9em 0 0 0;
  padding: 0;
}

form * {
  line-height: 1em;
}


/* The field labels */

label {
  display: block;
  float: left;
  clear: left;
  text-align: right;
  width: 15%;
  padding: .4em 0 0 0;
  margin: .15em .5em 0 0;
}


/* The fields */

input, select, textarea {
  display: block;
  margin: 0;
  padding: .4em;
  width: 80%;
}

input, textarea, .date {
  border: 2px solid #666;
  -moz-border-radius: 5px;
  -webkit-border-radius: 5px;    
  border-radius: 5px;
  background: #fff;
}

input {
  font-size: .9em;
}

select {
  padding: 0;
  margin-bottom: 2.5em;
  position: relative;
  top: .7em;
}

textarea {
  font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
  font-size: .9em;
  height: 5em;
  line-height: 1.5em;
}

textarea#content {
  font-family: "Courier New", courier, fixed;
}
  

/* Place a border around focused fields */

form *:focus {
  border: 2px solid #7c412b;
  outline: none;
}


/* Display correctly filled-in fields with a green background */

input:valid, textarea:valid {
  background: #efe;
}


/* Submit buttons */

.buttons {
  text-align: center;
  margin: 40px 0 0 0;
}

input[type="submit"] {
  display: inline;
  margin: 0 20px;
  width: 12em;
  padding: 10px;
  border: 2px solid #7c412b;
  -moz-border-radius: 5px;
  -webkit-border-radius: 5px;  
  border-radius: 5px;
  -moz-box-shadow: 0 0 .5em rgba(0, 0, 0, .8);
  -webkit-box-shadow: 0 0 .5em rgba(0, 0, 0, .8);
  box-shadow: 0 0 .5em rgba(0, 0, 0, .8);
  color: #fff;
  background: #ef7d50;
  font-weight: bold;
  -webkit-appearance: none;
}

input[type="submit"]:hover, input[type="submit"]:active {
  cursor: pointer;
  background: #fff;
  color: #ef7d50;
}

input[type="submit"]:active {
  background: #eee;
  -moz-box-shadow: 0 0 .5em rgba(0, 0, 0, .8) inset;
  -webkit-box-shadow: 0 0 .5em rgba(0, 0, 0, .8) inset;
  box-shadow: 0 0 .5em rgba(0, 0, 0, .8) inset;
}


/* Tables */

table {
  width: 100%;
  border-collapse: collapse;
}

tr, th, td {
  padding: 10px;
  margin: 0;
  text-align: left;
}

table, th {
  border: 1px solid #00a0b0;
}

th {
  border-left: none;
  border-right: none;
  background: #ef7d50;
  color: #fff;
  cursor: default;
}

tr:nth-child(odd) {
  background: #fff4cf;
}

tr:nth-child(even) {
  background: #fff;
}

tr:hover {
  background: #ddd;
  cursor: pointer;
}


/* Status and error boxes */

.statusMessage, .errorMessage {
  font-size: .8em;
  padding: .5em;
  margin: 2em 0;
  -moz-border-radius: 5px;
  -webkit-border-radius: 5px;
  border-radius: 5px; 
  -moz-box-shadow: 0 0 .5em rgba(0, 0, 0, .8);
  -webkit-box-shadow: 0 0 .5em rgba(0, 0, 0, .8);
  -box-shadow: 0 0 .5em rgba(0, 0, 0, .8);
}

.statusMessage {
  background-color: #2b2;
  border: 1px solid #080;
  color: #fff;
}

.errorMessage {
  background-color: #f22;
  border: 1px solid #800;
  color: #fff;
}

These tweaks include adding an h3.categoryDescription selector to style the category description at the top of the archive page; tweaking the #headlines styles to accommodate category names; and adding a .category class for displaying the category names in the homepage and archive page.

Step 7: Modify the back-end templates

Categories List screenshot

The last step is to tweak the back-end admin templates. We need to add a couple of templates to handle listing, adding, editing and deleting article categories. We also need to alter the Articles List page to display article categories; modify the article edit form so that the administrator can assign a category to an article; and tweak the admin page header to include an Edit Categories menu option.

Add listCategories.php

First, we’ll create listCategories.php, the template to display the list of categories in the database. This is very similar to listArticles.php, the article list template.

Here’s the code — save it as listCategories.php inside your cms/templates/admin folder:

<?php include "templates/include/header.php" ?>
<?php include "templates/admin/include/header.php" ?>

      <h1>Article Categories</h1>

<?php if ( isset( $results['errorMessage'] ) ) { ?>
        <div class="errorMessage"><?php echo $results['errorMessage'] ?></div>
<?php } ?>


<?php if ( isset( $results['statusMessage'] ) ) { ?>
        <div class="statusMessage"><?php echo $results['statusMessage'] ?></div>
<?php } ?>

      <table>
        <tr>
          <th>Category</th>
        </tr>

<?php foreach ( $results['categories'] as $category ) { ?>

        <tr onclick="location='admin.php?action=editCategory&amp;categoryId=<?php echo $category->id?>'">
          <td>
            <?php echo $category->name?>
          </td>
        </tr>

<?php } ?>

      </table>

      <p><?php echo $results['totalRows']?> categor<?php echo ( $results['totalRows'] != 1 ) ? 'ies' : 'y' ?> in total.</p>

      <p><a href="admin.php?action=newCategory">Add a New Category</a></p>

<?php include "templates/include/footer.php" ?>

This template is pretty straightforward. It includes the standard page header and admin header files, displays an “Article Categories” header along with any error or status message, then displays a table of categories in the database. It loops through each category in the $results['categories'] array, outputting a table row containing the category name. The table row is linked to admin.php?action=editCategory, passing in the category ID, so the administrator can edit the category by clicking the table row.

Finally, the template displays the total number of categories in the database, adds a link to let the administrator add a new category, and includes the page footer template.

Add editCategory.php

editCategory.php displays the category edit form, allowing the administrator to add a new category or edit an existing category. It follows the same basic pattern as editArticle.php, although it’s a bit simpler.

Save the following code as editCategory.php inside your cms/templates/admin folder:

<?php include "templates/include/header.php" ?>
<?php include "templates/admin/include/header.php" ?>

      <h1><?php echo $results['pageTitle']?></h1>

      <form action="admin.php?action=<?php echo $results['formAction']?>" method="post">
        <input type="hidden" name="categoryId" value="<?php echo $results['category']->id ?>"/>

<?php if ( isset( $results['errorMessage'] ) ) { ?>
        <div class="errorMessage"><?php echo $results['errorMessage'] ?></div>
<?php } ?>

        <ul>

          <li>
            <label for="name">Category Name</label>
            <input type="text" name="name" id="name" placeholder="Name of the category" required autofocus maxlength="255" value="<?php echo htmlspecialchars( $results['category']->name )?>" />
          </li>

          <li>
            <label for="description">Description</label>
            <textarea name="description" id="description" placeholder="Brief description of the category" required maxlength="1000" style="height: 5em;"><?php echo htmlspecialchars( $results['category']->description )?></textarea>
          </li>

        </ul>

        <div class="buttons">
          <input type="submit" name="saveChanges" value="Save Changes" />
          <input type="submit" formnovalidate name="cancel" value="Cancel" />
        </div>

      </form>

<?php if ( $results['category']->id ) { ?>
      <p><a href="admin.php?action=deleteCategory&amp;categoryId=<?php echo $results['category']->id ?>" onclick="return confirm('Delete This Category?')">Delete This Category</a></p>
<?php } ?>

<?php include "templates/include/footer.php" ?>

The template includes the usual header files, then displays the contents of $results['pageTitle'], which will either be “New Article Category” or “Edit Article Category”. It then creates a form that submits to admin.php, passing the value of $results['formAction'] ("newCategory" or "editCategory") in the action parameter.

The form itself includes a hidden field, categoryId, to track the ID of the currently-edited article (if any); any error message that needs to be displayed; fields for the category name and description; and Save Changes and Cancel buttons.

Finally, the template includes a link to delete the currently-edited category, as well as the page footer template.

Tweak listArticles.php

We need to make some small changes to the Articles List template to accommodate categories. Here’s the new template with changes highlighted — save this code over your old cms/templates/admin/listArticles.php file:

<?php include "templates/include/header.php" ?>
<?php include "templates/admin/include/header.php" ?>

      <h1>All Articles</h1>

<?php if ( isset( $results['errorMessage'] ) ) { ?>
        <div class="errorMessage"><?php echo $results['errorMessage'] ?></div>
<?php } ?>


<?php if ( isset( $results['statusMessage'] ) ) { ?>
        <div class="statusMessage"><?php echo $results['statusMessage'] ?></div>
<?php } ?>

      <table>
        <tr>
          <th>Publication Date</th>
          <th>Article</th>
          <th>Category</th>
        </tr>

<?php foreach ( $results['articles'] as $article ) { ?>

        <tr onclick="location='admin.php?action=editArticle&amp;articleId=<?php echo $article->id?>'">
          <td><?php echo date('j M Y', $article->publicationDate)?></td>
          <td>
            <?php echo $article->title?>
          </td>
          <td>
            <?php echo $results['categories'][$article->categoryId]->name?>
          </td>
        </tr>

<?php } ?>

      </table>

      <p><?php echo $results['totalRows']?> article<?php echo ( $results['totalRows'] != 1 ) ? 's' : '' ?> in total.</p>

      <p><a href="admin.php?action=newArticle">Add a New Article</a></p>

<?php include "templates/include/footer.php" ?>

As you can see, we’ve added a new Category column to the articles list. Within the loop to display the article rows, the code looks up the article’s associated Category object by ID in the $results['categories'] array, and outputs the category’s name.

In this template — as well as in editArticle.php — we’ve replaced the old hard-coded adminHeader div with a templates/admin/include/header.php include at the top of the template. This makes it easier for us to tweak the admin header, which we’ll do in a moment.

Tweak editArticle.php

We also need to modify editArticle.php — the article edit form — to allow the administrator to assign a category to an article.

Here’s the new template with changes highlighted. Replace your existing cms/templates/admin/editArticle.php file with this one:

<?php include "templates/include/header.php" ?>
<?php include "templates/admin/include/header.php" ?>

      <h1><?php echo $results['pageTitle']?></h1>

      <form action="admin.php?action=<?php echo $results['formAction']?>" method="post">
        <input type="hidden" name="articleId" value="<?php echo $results['article']->id ?>"/>

<?php if ( isset( $results['errorMessage'] ) ) { ?>
        <div class="errorMessage"><?php echo $results['errorMessage'] ?></div>
<?php } ?>

        <ul>

          <li>
            <label for="title">Article Title</label>
            <input type="text" name="title" id="title" placeholder="Name of the article" required autofocus maxlength="255" value="<?php echo htmlspecialchars( $results['article']->title )?>" />
          </li>

          <li>
            <label for="summary">Article Summary</label>
            <textarea name="summary" id="summary" placeholder="Brief description of the article" required maxlength="1000" style="height: 5em;"><?php echo htmlspecialchars( $results['article']->summary )?></textarea>
          </li>

          <li>
            <label for="content">Article Content</label>
            <textarea name="content" id="content" placeholder="The HTML content of the article" required maxlength="100000" style="height: 30em;"><?php echo htmlspecialchars( $results['article']->content )?></textarea>
          </li>

          <li>
            <label for="categoryId">Article Category</label>
            <select name="categoryId">
              <option value="0"<?php echo !$results['article']->categoryId ? " selected" : ""?>>(none)</option>
            <?php foreach ( $results['categories'] as $category ) { ?>
              <option value="<?php echo $category->id?>"<?php echo ( $category->id == $results['article']->categoryId ) ? " selected" : ""?>><?php echo htmlspecialchars( $category->name )?></option>
            <?php } ?>
            </select>
          </li>

          <li>
            <label for="publicationDate">Publication Date</label>
            <input type="date" name="publicationDate" id="publicationDate" placeholder="YYYY-MM-DD" required maxlength="10" value="<?php echo $results['article']->publicationDate ? date( "Y-m-d", $results['article']->publicationDate ) : "" ?>" />
          </li>


        </ul>

        <div class="buttons">
          <input type="submit" name="saveChanges" value="Save Changes" />
          <input type="submit" formnovalidate name="cancel" value="Cancel" />
        </div>

      </form>

<?php if ( $results['article']->id ) { ?>
      <p><a href="admin.php?action=deleteArticle&amp;articleId=<?php echo $results['article']->id ?>" onclick="return confirm('Delete This Article?')">Delete This Article</a></p>
<?php } ?>

<?php include "templates/include/footer.php" ?>

Here we’ve added an Article Category field to the form. This is a select menu containing all the categories in the database, pulled from the $results['categories'] array. Each option element stores the category’s ID in its value attribute, and displays the category’s name. If editing an existing article, the code also checks if the current option’s category ID matches the article’s current category ID; if it does then it adds the selected attribute to pre-select the option.

The select element also includes a “(none)” option at the top of the list, with a value of zero. This allows the administrator to create an article that isn’t associated with any category.

Tweak the admin header

The last thing we need to change is the header displayed in the CMS admin pages. Currently this includes a small menu with Edit Articles and Log Out links. We need to add an Edit Categories link to the menu so that the administrator can view, add, edit and delete categories.

In the existing CMS, this header is hard-coded into each of the admin templates, listArticles.php and editArticle.php. Rather than having to add the new option to each template, we’ll move the header markup to a separate header.php file. We then only have to make the change — and any future header changes — in one place.

We’ve already changed the listArticles.php and editArticle.php templates to include this header file in the steps above, and our new listCategories.php and editCategories.php templates already include the header file too. So all we need to do is create the header file.

So create an include folder inside your cms/templates/admin folder, and save the following code as header.php inside this new include folder:

      <div id="adminHeader">
        <h2>Widget News Admin</h2>
        <p>You are logged in as <b><?php echo htmlspecialchars( $_SESSION['username']) ?></b>. <a href="admin.php?action=listArticles">Edit Articles</a> <a href="admin.php?action=listCategories">Edit Categories</a> <a href="admin.php?action=logout"?>Log Out</a></p>
      </div>

As you can see, this is the markup that was previously hard-coded in the listArticles.php and editArticle.php templates, with a new “Edit Categories” option that links to admin.php?action=listCategories.

Try it out!

Edit Article screenshot

Congratulations! You’ve now enhanced your CMS to support article categories. Now you’re ready to try it out. Follow these steps:

  1. Log in
    Open your browser and visit the base URL of your CMS — for example, http://localhost/cms/. Click the Site Admin link in the footer, and log in.
  2. Create categories
    Click the Edit Categories link in the menu at the top of the page to view the list of categories, which will be empty to start with. Click Add a New Category at the bottom of the page to add new categories. You can also edit existing categories by clicking them in the list.
  3. Assign articles to categories
    Now click Edit Articles at the top of the page to add and edit your articles. When you create a new article, or edit an existing article, you can put the article into one of your categories using the Article Category menu in the form.
  4. View the results
    Click the Widget News logo at the top of the page to view the site front-end. Notice the category name that appears below each article’s headline. You can click the category name to browse all articles in that category. Try clicking an article headline to view the article — notice that the category name appears at the end of the article. Again, you can click the category name to browse that category’s archive page.

You can also try out the demo on our server too! (Bear in mind that the demo is read-only, so you can’t save changes to articles or categories.)

Summary

In this tutorial, you’ve taken the original content management system from my first tutorial and extended it to support article categories. You’ve:

  • Modified the MySQL database to add a categories table, as well as a categoryId field in the articles table.
  • Created a new Category PHP class to store and retrieve categories.
  • Modified the Article class to support the categoryId field, as well as retrieve a list of articles in a given category.
  • Altered the index.php script so that it can display category archives, as well as display category names on the homepage, archive, and View Article pages.
  • Modified the admin.php script to allow the administrator to list, add, edit and delete categories, as well as assign categories to articles.
  • Modified the front-end templates and stylesheet to display article categories on the homepage, archive pages and article pages.
  • Added back-end templates to handle category listing and editing, and tweaked the article list template and edit form so that the administrator can put articles into categories. You also tweaked the admin header to include an “Edit Categories” option.

Now that your CMS has been enhanced with categories, you can use it to create more varied websites that have several different sections of content. Enjoy!

What other features would you like to add to this CMS? A visitor comments system? Pagination on the homepage and article pages? Pagination within articles themselves? Post your feature request in the comments below, and maybe I’ll write a tutorial on it next time! -Matt

Filed Under: PHP Tagged With: blog, categories, cms, mysql, php, posts, Tutorial, web development

Reader Interactions

Comments

  1. chotikarn says

    25 January 2012 at 7:44 pm

    thanks again, matt. you was done another great tutorial. 🙂

    Reply
  2. Selfcoded says

    26 January 2012 at 12:09 am

    I already have code for an article that uses multiple categories so I wasn’t expecting much but then I saw the Category and Article classes and now I’ll be looking to add that type of code for mine.

    Reply
  3. mubuuba says

    26 January 2012 at 5:19 pm

    @Matt you are the man, another wonderful neatly coded class. It is really nice.I hope others will love it too, cuz it is just easy and beautiful.

    Reply
  4. matt says

    27 January 2012 at 12:29 am

    @chotikarn @Selfcoded @mubuuba Thanks for the kind words 🙂

    Cheers,
    Matt

    Reply
  5. mubuuba says

    28 January 2012 at 2:25 pm

    Hi Matt, Hope you doing good.
    I have tried to add image to the cms in the front page , meaning that a small picture could be added to the news headlines. For example,
    [Here Matt’s picture] Matt’s Explanation of creating this CMS.
    you see here picture with news on the right side, if pic is available can be added the news or interview on the front-page.

    I have created this class with three properties

    class Image
    {
    	public $id = null;
    	public $path = null;
    	public $article_Id = null;
    	
    	/*
    	 * Constructor
    	 * @params $id primary key in database to be incremented.
    	 * @params $path path of the upload image
    	 * @params $article_id, foriegn key to be gotten from the article
    	 */
    	public function __construct($pic = array())
    	{
    		if (isset($pic['id']))
    			$this->id = (int)$pic['id'];
    			
    		if(isset($pic['img']))
    			$this->path = $pic['path'];
    			
    		if(isset($pic[article_Id]))
    			$this->article_Id = (int)$pic['article_Id'];
    			
    			
    	}
    

    i have created the corrosponding database to this class.
    I am finding it little bit unnecessary to program too much as i have tried, you may know easier way. Please tell me what i need to do to add images to articles when they are available

    Is it a good to do it like that or could there be an easier way to do it.
    I just want that i could add picture to the interviews and news when theres pic for it and in viewArtical page, there can be more than one picture added to the page, but the front page only one picture.

    I know you already did a good job. I would like that you explain me the easiest way or the best way to do it.

    Matt thanks a lot.
    Mubo

    Reply
  6. chotikarn says

    28 January 2012 at 8:53 pm

    @mubuuba

    i’m looking for something like that too. i’m googling for days but there is no one suit to this CMS.
    so, your code is a good starting.

    hope matt have idea for this image upload for each articles.

    Reply
  7. matt says

    30 January 2012 at 12:57 am

    @mubuuba @chotikarn: The most straightforward way would be to create an imageFilename text field in your articles table (and Article class and article edit form). Upload the image to an uploads folder on your server (via FTP) then enter the image’s file name in the article edit form. When displaying the article, generate an <img> element in the article markup. Append the value of imageFilename to the URL of the uploads folder to get the src attribute for the <img> element (eg “http://example.com/uploads/myimage.jpg”).

    However, if you want the administrator to be able to upload an image via the CMS when they add or edit an article then things get more complicated. See here for a bit more info:

    http://www.elated.com/forums/topic/5114/#post20643

    I might write a tutorial on this if there’s enough demand 🙂

    Matt

    Reply
  8. mubuuba says

    31 January 2012 at 9:31 am

    @Matt.
    Hi to Matt and all Elated Community.
    would you be expanding your cms, like adding commenting class, so that visitors can comment on Articles, or image uploading by cms administrators?

    Thanks a lot.
    Cheers.

    Reply
  9. hocfictum says

    1 February 2012 at 3:37 am

    Hi a real noob in php here:

    For the Article Category when I’m adding/editing an article I get two notices saying:
    Notice: Undefined variable: category in ..xampphtdocscmstemplatesadmineditArticle.php on line 33

    Notice: Trying to get property of non-object in ..xampphtdocscmstemplatesadmineditArticle.php on line 33
    selected>(none)

    What’s it saying? I mean when I choose a category then there’s no problem so is it just telling me to that I really really have to select a category? or is there something wrong?

    Another thing is that when I publish any article the publication date shows : “1 Jan 1970” what am I doing wrong :/

    [Edited by hocfictum on 01-Feb-12 03:37]

    Reply
  10. iJelle says

    1 February 2012 at 1:39 pm

    Well, i’am trying to create a image upload within the script for days (still not succeed), so a tutorial would really help me!

    It would be nice to attach a file to the Article anyway…

    Reply
  11. mooho058 says

    1 February 2012 at 5:34 pm

    Hi
    I tried to add Cyrillic articles and categories but it’s not working.
    It’s not inserting Cyrillic data into my database.
    How to solve this? please hurry.
    Sorry for my bad english.

    Reply
  12. hocfictum says

    2 February 2012 at 1:38 am

    Hullo, I fixed the second issue (date) but the notices are still bugging me –
    Can you also point me in the right direction if I wanted to create a user(limited) who isn’t allowed to add categories?

    Reply
  13. chotikarn says

    2 February 2012 at 2:14 pm

    @hocfictum

    i’m facing same problem like you.

    maybe our database does not support foreign key or transaction.

    Reply
  14. matt says

    2 February 2012 at 10:05 pm

    @hocfictum: Oops, that’s a bug in the code.

                  <option value="0"<?php echo !$category->id ? " selected" : ""?>>(none)</option>
    

    It should be:

                  <option value="0"<?php echo !$results['article']->categoryId ? " selected" : ""?>>(none)</option>
    

    I’ll fix up the tutorial!

    Thanks
    Matt

    Reply
  15. matt says

    2 February 2012 at 10:08 pm

    @mooho058: See:

    http://www.elated.com/forums/topic/5114/#post21052
    http://www.elated.com/forums/topic/5114/#post21056

    You need to adjust your regular expression(s) to include Cyrillic characters.

    Reply
  16. matt says

    2 February 2012 at 10:35 pm

    @hocfictum: “Can you also point me in the right direction if I wanted to create a user(limited) who isn’t allowed to add categories?”

    If you’re sure you’re only going to want 2 admin users then you could create a new “user” by hacking config.php:

    define( "SUBADMIN_USERNAME", "subadmin" );
    define( "SUBADMIN_PASSWORD", "anotherpass" );
    

    Then modify login() in admin.php to allow either user to login:

        if ( $_POST['username'] == ADMIN_USERNAME && $_POST['password'] == ADMIN_PASSWORD || $_POST['username'] == SUBADMIN_USERNAME && $_POST['password'] == SUBADMIN_PASSWORD ) {
    
    ...etc...
    

    Then hack the newCategory()/editCategory()/deleteCategory() functions to check that $_SESSION[‘username’] == ADMIN_USERNAME. If it doesn’t, display an error message and exit.

    If you think that you might want more admins later, with more fine-grained control, then the “proper” way to do it is to create a new admins table in your database, and store the admin usernames/passwords in there. You’d probably want to create an Admin class too, to handle storing/retrieving administrators, as well as “meta-admin” functions that let you add/edit/delete admins. But that’s a fair bit more work.

    Reply
  17. metalsniper63 says

    23 February 2012 at 5:45 pm

    Hello i just want to know how can i create a categories menu???
    Thanks in advance
    Also i would like to add pagination to this, could you point me in the right direction???

    [Edited by metalsniper63 on 23-Feb-12 17:48]

    Reply
  18. matt says

    27 February 2012 at 10:13 pm

    @metalsniper63: Not sure what you mean by a “categories menu”. If you mean navigation links to the different category archive pages then I’d just hard-code them in an unordered list in templates/include/header.php, and style the list appropriately.

    I may write a tutorial on pagination at some point. In the meantime, see here:

    http://www.elated.com/forums/topic/5114/#post22300

    Reply
  19. csturner says

    29 February 2012 at 12:00 am

    This is going great. Features I hope you will talk about in the future are :
    Session timeout, log out after a certain amount of time
    Improvements to the editor
    Clean Urls

    Of course, I am trying to work some of these out on my own, but I am sure you could do this things better.

    Love the tutorials, please keep them coming.

    Reply
  20. matt says

    1 March 2012 at 5:11 am

    @csturner: Thanks for the suggestions 🙂

    Reply
  21. adityasaky says

    3 March 2012 at 9:38 am

    Also, how do you add comments for each article?

    Reply
  22. adityasaky says

    3 March 2012 at 10:25 am

    When I edit an article from my dashboard, in publication date, i’m having to give a date ahead.

    Example- i need to give 4 feb 2012 to display 3 feb 2012…

    Reply
  23. adityasaky says

    3 March 2012 at 10:27 am

    Also, when i edit article, some wierd \\\\ start appearing…Please help…

    Reply
  24. matt says

    5 March 2012 at 12:29 am

    @adityasaky: You’ll find answers to all of your questions by searching through the topic for the original tutorial:

    http://www.elated.com/forums/topic/5114/

    Reply
  25. csturner says

    5 March 2012 at 1:26 pm

    Was just thinking, another useful feature would be allowing customers to post to testimonials. ie, Have a special post page which allows posting under a specific categoryId, with complete sanitation of the input. This no doubt, becomes very dangerous.

    Reply
  26. mubuuba says

    26 March 2012 at 8:47 pm

    @Matt
    hi matt, hope u doing good.
    i went through the forum , to check wheather my question was answered before.

    Our CMS get the news articles first. order by date DESC.
    but what about if publish 10 articles a day and i want that the news article displayed at the top of the page by time. For example i published 10 articles today, date is same. But different hours and i want to see the news articles by time (hour) displayed at the top.

    As we have already got date field in our database, i wonder wheather i could modify or need another additional field which holds my timestamp.
    headlines.
    >Microsoft anounces IE 9 date published and time.
    >Microsoft anounces IE 10 date published and time.
    >Microsoft anounces IE 11 date published and time.
    Thanks

    Reply
  27. jj says

    4 April 2012 at 12:49 pm

    Hello there,
    I need someones brilliant knowledge and mind to help me out!

    The thing is, I want to display all articles and categories as it is in this tutorial, but i want to exclude the articles and categories with the ID 1 and ID 2 (both modified to the same ID in DB).

    Beginner as I am, i tried this in Article.php:

       public static function getList( $numRows=1000000, $categoryId=null, $order="publicationDate DESC" ) {
        $conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD );
        $categoryClause = $categoryId ? "WHERE categoryId = :categoryId" : "";
        $sql = "SELECT SQL_CALC_FOUND_ROWS *, UNIX_TIMESTAMP(publicationDate) AS publicationDate
                FROM articles $categoryClause WHERE (id > 2)
                ORDER BY " . mysql_escape_string($order) . " LIMIT :numRows";
    

    It works in the homepage.php and Article.php but not when i’m accessing a category, like ‘images’.

    What did I do wrong this time? 🙁

    Reply
  28. metalsniper63 says

    4 April 2012 at 12:51 pm

    From the top of my head your code seems fine. but remember that categories are handled by categories.php in classes, you should put that code in there

    Reply
  29. jj says

    4 April 2012 at 7:23 pm

    @metalsniper63

    Hmm.. Yes sir, you are totally correct with the ‘categories.php’-thingy.

    But,
    Instead of manipulating the DB in the wrong way, and have a inflexible website with a “not so good idea after all”, I decided to just create a new function like Categories, but call it Info. And it works 😀

    The only thing now is that I can not get data to display without the foreach{}.

    I’ve got the code:

    function viewInfo() {
      $results = array();
      $data = Info::getList();
      $results['staticinfo'] = $data['results'];
      $results['pageTitle'] = "Info | Kanske";
      require( TEMPLATE_PATH . "/viewInfo.php" );}
    

    in index.php.

    Its working, but as i’m a real beginner (this is my absolute first PHP project), I have a hard time gettin’ my head around it, hehe.

    Well, if a smart person with allot of knowledge sees this, give me a hint or something, caus I’m stuck xD

    Best regards,
    JJ

    Reply
  30. matt says

    5 April 2012 at 5:38 am

    @mubuuba: Modify your publicationDate database column to be a datetime type instead of date:

    http://dev.mysql.com/doc/refman/5.5/en/datetime.html

    Then modify the calls to the PHP date() function inside the templates (homepage.php etc) to also display the time in the format you want, as well as the date:

    http://php.net/manual/en/function.date.php

    Reply
  31. matt says

    5 April 2012 at 6:21 am

    @jj: I don’t totally understand what you want to do, but your code looks like it will run OK. 🙂 Maybe you could post the problem along with your complete code in a new topic, and we can take a closer look:

    http://www.elated.com/forums/authoring-and-programming/topic/new/

    Reply
  32. hch says

    16 April 2012 at 8:05 pm

    Hi Matt,
    Thank you very much for the tutorials.

    I have several days trying to upload images from editArticle but I could not. It seems that I can not change the enctype to multipart / form-data.

    I hope you can help me with this.
    Greetings and Thank You!

    Reply
  33. matt says

    4 May 2012 at 5:05 am

    @hch: This is how you do it:

    <form enctype="multipart/form-data" ...>
    
    Reply
  34. snookian says

    12 June 2012 at 11:22 am

    Hi, how can you make it so that each article can have multiple categories EG sports, News. and Computing,news. So that both articles would appear in news but only then appear in the sub category they belong, I need something that can have up to 5 categories. Any help appreciated.

    Ian

    Reply
  35. matt says

    15 June 2012 at 3:51 am

    @snookian: Many ways to do that. If you’re happy with a maximum of 5 cats per article then I’d probably just add 5 fields to your articles table (category1Id, category2Id, category3Id, category4Id, category5Id). Otherwise you’ll need an “articleCategories” link table to link each article with its categories in the categories table, which becomes a bit more complex.

    Reply
  36. dick_soad says

    27 June 2012 at 2:16 am

    can you explain with xampp?please…

    Reply
  37. lyall says

    4 December 2012 at 9:11 am

    Hi, i’ve been implementing this tutorial into mysite and all the php is working fine, however I have two versions of my site one for mobile one for desktop so if a mobile browser is detected it will go to the mobile verison of my site now ive modified all the template files to suite my mobile design problem now is the desktop version is using the same template files im new to templating with php how would I go about sorting this out?

    Reply
  38. matt says

    18 December 2012 at 6:50 pm

    @lyall: I’d probably edit config.php and replace the TEMPLATE_PATH constant with TEMPLATE_PATH_DESKTOP and TEMPLATE_PATH_MOBILE. Then add a simple getTemplatePath() utility function to config.php that detects the browser type and returns the appropriate template path. Then replace all instances of TEMPLATE_PATH in the code with getTemplatePath().

    Reply
  39. snookian says

    30 April 2013 at 8:15 am

    @matt, Please help! Im super stumped. I want the description for each category to appear once you have selected the category.
    So i want a input box under each category drop down list like so:

              <li>
                <label for="categoryId">Article Category</label>
                <select name="categoryId">
                  <option value="0"<?php echo !$results['article']->categoryId ? " selected" : ""?>>(none)</option>
                <?php foreach ( $results['categories'] as $category ) { ?>
                  <option value="<?php echo $category->id?>"<?php echo ( $category->id == $results['article']->categoryId ) ? " selected" : ""?>><?php echo htmlspecialchars( $category->name )?></option>
                <?php } ?>
                </select>
              </li>
    
              <li>
                <label for="description">Description</label>
                <input type="text" name="description" id="description" value="<?php echo $results['category']->description ?>" />
              </li>
    

    And then when you select reviews for example the description i put for reviews will appear in the description field. Any help getting me towards my goal will be great.

    (I want/need this in the new article page/form)

    Ian

    [Edited by snookian on 30-Apr-13 09:49]

    Reply
  40. kelechi says

    13 June 2013 at 8:06 am

    Hello Sir I tried combining the tutorial for image upload and category upload but is not working. Having debugged it, I discovered that the object has not extension property. Please tell me what to do

    Reply
  41. afahrurroji says

    30 June 2014 at 11:11 pm

    I’m looking for this for my simple project. It’s so helpful, how to add some tags on article.
    thanks in advance

    Reply
  42. chrishirst says

    1 July 2014 at 3:50 am

    “tags” meaning what exactly?

    Reply
  43. Catie86 says

    2 July 2014 at 10:12 am

    This is a fantastic tutorial! I was completely stuck before I found this.

    Does anyone know how I can store the last inserted article Id into another table? As in what this tutorial does with the category Id, but in reverse?

    I’ve tried running a transaction in the insert function of the Article class but this causes an error with the image uploader from one of the other tutorials.

    Any help would be appreciated. Thank you!

    Reply
  44. chrishirst says

    2 July 2014 at 4:51 pm

    http://dev.mysql.com/doc/refman/5.0/en/getting-unique-id.html

    Reply
  45. Catie86 says

    2 July 2014 at 9:42 pm

    Thank you chrishirst, I figured it out.

    Can someone please tell my how to insert a time field (time only) into the database? I have been beating my head against the wall.

    Reply
  46. afahrurroji says

    3 July 2014 at 2:59 am

    Hi chris,

    Thanks for question. Like categories but it’s different. Tags are similar to categories, but they are generally used to describe your post in more detail. You can see in WordPress that usually uses tags in post.

    Reply
  47. chrishirst says

    3 July 2014 at 4:29 pm

    “Can someone please tell my how to insert a time field (time only) into the database”?

    Use a timestamp field type and just insert the time parts, bearing in mind that when you read it back, the date will be 01/01/1970 (the UNIX Epoch)

    Reply
  48. chrishirst says

    3 July 2014 at 4:35 pm

    OK, so those kind of ‘tags’

    So basically it’s EXACTLY the same as “categories” it’s only the way you display them that differs.

    Reply
  49. Jam says

    13 April 2015 at 7:43 am

    Good day! How can I add in the admin, category names in the file listArticles.php.
    Example: article name, category name.
    $article->categoryId displays only the category id

    Reply
  50. Jam says

    13 April 2015 at 8:49 am

    Found a solution:
    <?php echo htmlspecialchars( $results[‘categories’][$article->categoryId]->name )?>
    displays the name of the category

    Reply
  51. Daniel Keith says

    7 May 2015 at 2:50 am

    Hi all,
    It’s always good to have a professional developer around to support and make your work easy.
    If you want help regarding your website design and functionality in any regard, contact us.
    Spam REMOVED

    [Edited by chrishirst on 07-May-15 04:14]

    Reply
  52. jatin333 says

    4 May 2016 at 10:44 am

    When i open edit article. I am not getting categories name in Article Category , only (none) shown, So when ever i edit any article it’s category changes to no category after saving the article.

    More over second Question is when i click any category name under title of any article it shows all articles of all categories not of the particular category.

    <?php include "templates/include/header.php" ?>
    <?php include "templates/admin/include/header.php" ?>
    
        
    
          <div id="adminHeader">
            <h2>Bedi News Admin</h2>
            <p>You are logged in as <b><?php echo htmlspecialchars( $_SESSION['username']) ?></b>. <a href="admin.php?action=logout"?>Log out</a></p>
          </div>
    
          <h1><?php echo $results['pageTitle']?></h1>
    
          <form action="admin.php?action=<?php echo $results['formAction']?>" method="post" enctype="multipart/form-data" onsubmit="closeKeepAlive()">
            <input type="hidden" name="articleId" value="<?php echo $results['article']->id ?>"/>
    
    <?php if ( isset( $results['errorMessage'] ) ) { ?>
            <div class="errorMessage"><?php echo $results['errorMessage'] ?></div>
    <?php } ?>
    
            <ul>
    
              <li>
                <label for="title">Article Title</label>
                <input type="text" name="title" id="title" placeholder="Name of the article" required autofocus maxlength="255" value="<?php echo  $results['article']->title ?>" />
              </li>
    
              <li>
                <label for="summary">Article Summary</label>
                <textarea name="summary" id="summary" placeholder="Brief description of the article" required maxlength="1000" style="height: 5em;"><?php echo  $results['article']->summary ?></textarea>
              </li>
    
              <li>
                <label for="content">Article Content</label>
                <textarea name="content" id="content" placeholder="The HTML content of the article" required maxlength="100000" style="height: 30em;"><?php echo  $results['article']->content ?></textarea>
              </li>
    		  
    		  <li>
                <label for="categoryId">Article Category</label>
                <select name="categoryId">
                  <option value="0"<?php echo !$results['article']->categoryId ? " selected" : ""?>>(none)</option>
                <?php foreach ( $results['categories'] as $category ) { ?>
                  <option value="<?php echo $category->id?>"<?php echo ( $category->id == $results['article']->categoryId ) ? " selected" : ""?>><?php echo htmlspecialchars( $category->name )?></option>
                <?php } ?>
                </select>
              </li>
    		  
    		  
    
              <li>
                <label for="publicationDate">Publication Date</label>
                <input type="date" name="publicationDate" id="publicationDate" placeholder="YYYY-MM-DD" required maxlength="10" value="<?php echo $results['article']->publicationDate ? date( "Y-m-d", $results['article']->publicationDate ) : "" ?>" />
              </li>
    
              <?php if ( $results['article'] && $imagePath = $results['article']->getImagePath() ) { ?>
              <li>
                <label>Current Image</label>
                <img id="articleImage" src="<?php echo $imagePath ?>" alt="Article Image" />
              </li>
    
              <li>
                <label></label>
                <input type="checkbox" name="deleteImage" id="deleteImage" value="yes"/ > <label for="deleteImage">Delete</label>
              </li>
              <?php } ?>
    
              <li>
                <label for="image">New Image</label>
                <input type="file" name="image" id="image" placeholder="Choose an image to upload" maxlength="255" />
              </li>
    
            </ul>
    
            <div class="buttons">
              <input type="submit" name="saveChanges" value="Save Changes" />
              <input type="submit" formnovalidate name="cancel" value="Cancel" />
            </div>
    
          </form>
    
    <?php if ( $results['article']->id ) { ?>
          <p><a href="admin.php?action=deleteArticle&amp;articleId=<?php echo $results['article']->id ?>" onclick="return confirm('Delete This Article?')">Delete This Article</a></p>
    <?php } ?>
    
    <?php include "templates/include/footer.php" ?>
    

    [Edited by jatin333 on 05-May-16 05:16]

    Reply
  53. jatin333 says

    5 May 2016 at 10:16 am

    Thanks it is solved Now every thing works fine

    Reply
  54. BusyWitch says

    20 October 2016 at 7:00 pm

    Hi… followed the tutorials (love them, by the way) and I am now trying to figure out how to do a list of the article categories for a side menu, similiar to the list of categories in the admin area… i’ve got the menu showing the category names with a clickable url to that archive/category, however, it is throwing a php notice (PHP Notice: Undefined property: Category::$categoryId) and I can’t seem to figure out why.

    The following code is what i have added to the templates/homepage.php file to create the list of categories…

    <h2>Article Categories</h2>
    <ul>
    <?php foreach ( $results['categories'] as $category ) { ?>
    <li><a href=".?action=article_category&amp;categoryId=<?php echo $category->categoryId?>"><?php echo $category->name?></a></li>
    <?php } ?>
    </ul>
    

    Is there perhaps something that i need to add to the index.php file or some silly little something that i’m missing.

    Thanks for any and all help with this… <3

    Reply
  55. chrishirst says

    21 October 2016 at 6:28 am

    “Is there perhaps something that i need to add to the index.php file or some silly little something that i’m missing.”

    Yes there are, and the required changes are detailed in the original article;

    http://www.elated.com/articles/add-article-categories-to-your-cms/

    Scroll down to step 4:

    Reply
  56. BusyWitch says

    5 November 2016 at 5:01 pm

    Thank you so very much, Chris. I managed to find time to spend on this project and went back over step 4 of original article, as you suggested, and figured it out.

    Thanks again for the awesome tutorials and for pointing me in the right direction… <3

    Reply
  57. assistpen says

    16 April 2017 at 12:31 pm

    Hi,

    Its great tutorials, but i have one query, the query below :-

    If i want to retrive/display data from specific category, then what i have to do or what i have to do changes in code please help.

    Please revert on my mail ASAP.

    Reply
  58. chrishirst says

    18 April 2017 at 7:05 am

    “If i want to retrive/display data from specific category,”

    Specify the category ID you want to display in the URL parameters

    Reply
  59. KB says

    11 September 2019 at 5:26 pm

    This is really great, easy to follow and informative!
    The only “problem” I seem to have and cannot figure out how to change if that if no category are selected it posts “Notice: Undefined offset: 0 in /cms/templates/admin/listArticles.php on line 30
    Notice: Trying to get property ‘name’ of non-object in /cms/templates/admin/listArticles.php on line 30” on the category column in ListArticles.
    When I edit and select a category all is good, but if I don’t it sends the notice and I would like to just make it say “none”, “null” or something of the like.
    If I figure it out before someone answers I’ll try and post what I did 🙂

    Reply
  60. Mike Lowe says

    21 February 2020 at 1:52 pm

    Hi There…. I’ve just updated the script for the CATEGORIES, but when I “Add New Category” II just get a blank screen?

    I have added all extra code (although if you’ve already added the IMAGE code, it’s pretty difficult to unpick)

    Could you please help me out?
    I’m pretty sure I’ve added ALL code!??

    Reply
    • Matt Doyle says

      21 February 2020 at 10:13 pm

      You need to get PHP to display the error message so you can debug it.

      Reply
      • Mike Lowe says

        22 February 2020 at 12:16 pm

        Thankyou Matt, I got that figured out in the end… Thankyou.

        Reply
  61. Mike Lowe says

    22 February 2020 at 12:23 pm

    OK, so I got it ALL working…. Now I would like to call only Category=1 into my homepage slider, (as a FEATURED set only….)

    This is my code:

    *** CALL categoryId=1 to display HERE???? ***

    <a href=".?action=archive&categoryId=categoryId?>”>
    <div class="slide" data-thumb="getImagePath() ) { ?>”>
    <img src="” alt=”Article Thumbnail” />
    <img src="getImagePath() ) { ?>”>

    title )?>
    categoryId ) { ?>
    in <a href=".?action=archive&categoryId=categoryId?>”>categoryId]->name )?>

    Hope that makes sense????
    My URL: http://www.thedesign4mula.co.uk/v1/
    GREAT code & thouroughly easy to use & way better than sifting through 1000s of lines of pointless code like some other CMS’s

    ANY help much appreciated!

    Thanks again

    Reply
    • Mike Lowe says

      22 February 2020 at 12:24 pm

      seems to have cut out most of my comment….. :/

      Reply
  62. Mike Lowe says

    22 February 2020 at 12:28 pm

    OK, so I got it ALL working…. Now I would like to call only Category=1 into my homepage slider, (as a FEATURED set only….)

    This is my code:

    <!-- FEATURED SLIDER -->
    <section id="slider" class="slider-element boxed-slider">
    	
    <div class="container clearfix">
    
    		<div class="fslider" data-easing="easeInQuad">
    			<div class="flexslider">
    				<div class="slider-wrap">
    					*** CALL categoryId=1 to display HERE???? ***  <?php foreach ( $results['articles'] as $article ) { ?> ***
    					
    						<a href=".?action=archive&amp;categoryId=<?php echo $article->categoryId?>">
    							<div class="slide" data-thumb="<?php if ( $imagePath = $article->getImagePath() ) { ?>">
    								
    							<img src="<?php echo $imagePath?>" alt="Article Thumbnail" /><?php } ?>
    							<img src="<?php if ( $imagePath = $article->getImagePath() ) { ?><?php } ?>">
    						</a>
    							<p class="flex-caption slider-caption-bg"><?php echo htmlspecialchars( $article->title )?></p>
    							
    							<?php if ( $article->categoryId ) { ?>
    								<span class="category">in <a href=".?action=archive&amp;categoryId=<?php echo $article->categoryId?>"><?php echo htmlspecialchars( $results['categories'][$article->categoryId]->name )?></a></span>
    							<?php } ?>
    							</div>
    					<?php } ?>
    				</div>
    			</div>
    		</div>
    	</div>
    </section>
    <!-- FEATURED SLIDER END -->
    

    Hope that makes sense????
    My URL: http://www.thedesign4mula.co.uk/v1/
    GREAT code & thouroughly easy to use & way better than sifting through 1000s of lines of pointless code like some other CMS’s

    ANY help much appreciated!

    Thanks again

    Reply
  63. Devashish says

    22 March 2021 at 2:26 pm

    Could you please tell me, Which type of changes have you done for making application compatible for php7. because I have downloaded your code but it not working on php7.2. getting error like : “Sorry, a problem occurred. Please try later.”.
    and I have seen you have used “mysql_escape_string” instead of “mysqli_real_escape_string”. but in php7 all suggested me you need to use “mysqli_real_escape_string” instead of “mysql_escape_string”. These type of issues i am getting, that`s why application is not working.
    Please help me.

    Reply
    • Matt Doyle says

      23 March 2021 at 1:41 am

      You need to get PHP to display the error message so you can debug it.

      Reply
  64. Jatin333 says

    4 June 2021 at 8:55 am

    I have mixed the codes of uploading categories and upload images, cms is working fine, image is uploading to folder, but there is nothing updated in imageExtension in database, It is not showing any error. If any combined code is available (uploading categories and upload images) please send on my mail. Or give me the idea, which part i should check, for this problem. both codes are working well separately. thanks. Further i have seen that if category table removed, then it will save image extension.
    Please help

    Reply

Leave a Reply Cancel reply

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

To include a block of code in your comment, surround it with <pre> ... </pre> tags. You can include smaller code snippets inside some normal text by surrounding them with <code> ... </code> tags.

Allowed tags in comments: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre> .

Primary Sidebar

Hire Matt!

Matt Doyle headshot

Need a little help with your website? I have over 20 years of web development experience under my belt. Let’s chat!

Matt Doyle - Codeable Expert Certificate

Hire Me Today

Call Me: +61 2 8006 0622

Stay in Touch!

Subscribe to get a quick email whenever I add new articles, free goodies, or special offers. I won’t spam you.

Subscribe

Recent Posts

  • Make a Rotatable 3D Product Boxshot with Three.js
  • Speed Up Your WordPress Website: 11 Simple Steps to a Faster Site
  • Reboot!
  • Wordfence Tutorial: How to Keep Your WordPress Site Safe from Hackers
  • How to Make Awesome-Looking Images for Your Website

Footer

Contact Matt

  • Email Me
  • Call Me: +61 2 8006 0622

Follow Matt

  • E-mail
  • Facebook
  • GitHub
  • LinkedIn
  • Twitter

Copyright © 1996-2023 Elated Communications. All rights reserved.
Affiliate Disclaimer | Privacy Policy | Terms of Use | Service T&C | Credits