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:
- Modify the database
- Build the
Category
class - Modify the
Article
class to handle categories - Modify
index.php
to handle category display - Modify
admin.php
to 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.
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 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) * @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

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 integercategoryId
property to store the ID of the article’s category. We also modify the constructor method,__construct()
, to store the newcategoryId
property in newly-createdArticle
objects. - 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$categoryId
argument. If present, only articles in that category are returned.If
$categoryId
is supplied, we create a$categoryClause
string containing aWHERE
clause to retrieve just articles with acategoryId
field that matches the suppliedcategoryId
value. We then modify the SQLSELECT
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()
andupdate()
methods
Finally, we modify theArticle
class’sinsert()
andupdate()
methods to accommodate the newcategoryId
field. We modify the SQLINSERT
andUPDATE
statements in each method, and also add new calls tobindValue()
to pass the object’s$categoryId
property 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 optionalcategoryId
query string parameter. IfcategoryId
is supplied, the function retrieves the correspondingCategory
object 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 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 acategoryId
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 toviewArticle()
: We retrieve theCategory
object associated with the article by callingCategory::getById()
, passing in the article’s$categoryId
property. We store the resultingCategory
object in$results['category']
. We’ll use this object to display the name of the article’s category in theviewArticle.php
template.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
switch
block
We need to add some new functionality toadmin.php
to handle listing, creating, editing and deleting categories. To this end, we addlistCategories
,newCategory
,editCategory
anddeleteCategory
case
blocks to theswitch
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()
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.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 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.php
template 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 newCategory
object, 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 emptyCategory
object to use for the form, stores it in$results['category']
, and includes theeditCategory.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 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 thecategoryId
query string parameter, stores it in$results['category']
, and includes theeditCategory.php
template 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 thecategoryId
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

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.
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.
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 acategoryId
field in thearticles
table. - Created a new
Category
PHP class to store and retrieve categories. - Modified the
Article
class to support thecategoryId
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!
thanks again, matt. you was done another great tutorial. 🙂
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.
@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.
@chotikarn @Selfcoded @mubuuba Thanks for the kind words 🙂
Cheers,
Matt
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
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
@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.
@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
@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.
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]
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…
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.
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?
@hocfictum
i’m facing same problem like you.
maybe our database does not support foreign key or transaction.
@hocfictum: Oops, that’s a bug in the code.
It should be:
I’ll fix up the tutorial!
Thanks
Matt
@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.
@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:
Then modify login() in admin.php to allow either user to login:
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.
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]
@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
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.
@csturner: Thanks for the suggestions 🙂
Also, how do you add comments for each article?
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…
Also, when i edit article, some wierd \\\\ start appearing…Please help…
@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/
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.
@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
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:
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? 🙁
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
@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:
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
@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
@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/
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!
@hch: This is how you do it:
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
@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.
can you explain with xampp?please…
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?
@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().
@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:
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]
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
I’m looking for this for my simple project. It’s so helpful, how to add some tags on article.
thanks in advance
“tags” meaning what exactly?
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!
http://dev.mysql.com/doc/refman/5.0/en/getting-unique-id.html
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.
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.
“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)
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.
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
Found a solution:
<?php echo htmlspecialchars( $results[‘categories’][$article->categoryId]->name )?>
displays the name of the category
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]
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.
[Edited by jatin333 on 05-May-16 05:16]
Thanks it is solved Now every thing works fine
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…
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
“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:
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
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.
“If i want to retrive/display data from specific category,”
Specify the category ID you want to display in the URL parameters
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 🙂
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!??
You need to get PHP to display the error message so you can debug it.
Thankyou Matt, I got that figured out in the end… Thankyou.
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
seems to have cut out most of my comment….. :/
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:
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
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.
You need to get PHP to display the error message so you can debug it.
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