How to Add Article Categories to Your CMS
Learn how to modify a PHP and MySQL content management system to include support for article categories. Full CMS code download included.
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 button 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:
- Modify the database
- Build the
Categoryclass - Modify the
Articleclass to handle categories - Modify
index.phpto handle category display - Modify
admin.phpto handle listing, adding, editing, deleting and assigning categories - Modify the front-end templates and stylesheet to handle category display
- 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

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

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)
* @param string Optional column by which to order the categories (default="name ASC")
* @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, $order="name ASC" ) {
$conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD );
$sql = "SELECT SQL_CALC_FOUND_ROWS * FROM categories
ORDER BY " . mysql_escape_string($order) . " 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

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
* @param string Optional column by which to order the articles (default="publicationDate DESC")
* @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, $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
ORDER BY " . mysql_escape_string($order) . " 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
categoryIdproperty
In order to associate an article with a category, we add an integercategoryIdproperty to store the ID of the article's category. We also modify the constructor method,__construct(), to store the newcategoryIdproperty in newly-createdArticleobjects. - The modified
getList()method
Our originalgetList()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 modifygetList()to accept an optional$categoryIdargument. If present, only articles in that category are returned.
If$categoryIdis supplied, we create a$categoryClausestring containing aWHEREclause to retrieve just articles with acategoryIdfield that matches the suppliedcategoryIdvalue. We then modify the SQLSELECTstatement to include this$categoryClausevariable. We also add a new call to$st->bindValue()to bind the supplied$categoryIdvalue to the SQL statement before executing it. - Modified
insert()andupdate()methods
Finally, we modify theArticleclass'sinsert()andupdate()methods to accommodate the newcategoryIdfield. We modify the SQLINSERTandUPDATEstatements in each method, and also add new calls tobindValue()to pass the object's$categoryIdproperty to the SQL statements.
Step 4: Modify the front-end index.php script

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 originalarchive()function simply displayed a list of all articles in the CMS. Here, we've adapted thearchive()function to accept an optionalcategoryIdquery string parameter. IfcategoryIdis supplied, the function retrieves the correspondingCategoryobject by callingCategory::getById()and, if retrieved successfully, it passes the category ID to theArticle::getList()function to retreive just the articles in the supplied cateogory.
We've also added code to thearchive()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 acategoryIdwas 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 toviewArticle(): We retrieve theCategoryobject associated with the article by callingCategory::getById(), passing in the article's$categoryIdproperty. We store the resultingCategoryobject in$results['category']. We'll use this object to display the name of the article's category in theviewArticle.phptemplate.homepage()
Finally, we add three lines to thehomepage()function. Like the corresponding code added toarchive(), this additional code callsCategory::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

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
switchblock
We need to add some new functionality toadmin.phpto handle listing, creating, editing and deleting categories. To this end, we addlistCategories,newCategory,editCategoryanddeleteCategorycaseblocks to theswitchblock 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()andlistArticles()
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. ForlistArticles(), we also key the categories by category ID, to make it easy for thelistArticles.phptemplate 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 thelistArticles()function. It pulls all the categories from the database by callingCategory::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 thelistCategories.phptemplate file to display the categories list page.newCategory()
This lets the administrator add a new category to the database, much asnewArticle()adds a new article. If the user has submitted the category edit form then the function creates a newCategoryobject, populates it with the form data, callsinsert()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 emptyCategoryobject to use for the form, stores it in$results['category'], and includes theeditCategory.phptemplate 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 aseditArticle(). 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 theupdate()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 thecategoryIdquery string parameter, stores it in$results['category'], and includes theeditCategory.phptemplate to display the populated edit form.deleteCategory()
This is the last new function we've added toadmin.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 thecategoryIdquery 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

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&articleId=<?php echo $article->id?>"><?php echo htmlspecialchars( $article->title )?></a>
<?php if ( $article->categoryId ) { ?>
<span class="category">in <a href=".?action=archive&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&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&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&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

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&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&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&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&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!

Congratulations! You've now enhanced your CMS to support article categories. Now you're ready to try it out. Follow these steps:
- 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. - 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. - 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. - 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. - Modified the MySQL database to add a
categoriestable, as well as acategoryIdfield in thearticlestable. - Created a new
CategoryPHP class to store and retrieve categories. - Modified the
Articleclass to support thecategoryIdfield, as well as retrieve a list of articles in a given category. - Altered the
index.phpscript so that it can display category archives, as well as display category names on the homepage, archive, and View Article pages. - Modified the
admin.phpscript 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.
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:
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
Follow Elated
Related articles
Responses to this article
20 most recent responses (oldest first):
Example- i need to give 4 feb 2012 to display 3 feb 2012...
http://www.elated.com/forums/topic/5114/
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
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 [b]WHERE (id > 2)[/b]
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?
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
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
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/
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!
<form enctype="multipart/form-data" ...>
Ian
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]
Post a response
Want to add a comment, or ask a question about this article? Post a response.
To post responses you need to be a member. Not a member yet? Signing up is free, easy and only takes a minute. Sign up now.
