Vous êtes sur la page 1sur 45

Build a CMS in an Afternoon with PHP and MySQL

Building a content management system can seem like a daunting task to the novice PHP
developer. However, it needn't be that difficult. In this tutorial I'll show you how to build a
basic, but fully functional, CMS from scratch in just a few hours. Yes, it can be done!

Along the way, you'll learn how to create MySQL databases and tables; how to work with
PHP objects, constants, includes, sessions, and other features; how to separate business
logic from presentation; how to make your PHP code more secure, and much more!

Before you begin, check out the finished product by clicking the View Demo button above.
(For security reasons this demo is read-only, so you can't add, change or delete articles.)
You can also click the Download Code button above to download the complete PHP code
for the CMS, so you can run it on your own server.

For this tutorial, you'll need to have the Apache web server with PHP installed, as well as
the MySQL database server running on your computer. Setting all this up is beyond the
scope of the tutorial, but a really easy way to do it is simply to installXAMPP on your
computer.

The feature list


Our first job is to work out exactly what we want our CMS to do. The CMS will have the
following features:

Front end:

o The homepage, listing the 5 most recent articles

o The article listing page, listing all articles

o The "view article" page, letting visitors see a single article

Back end:

o Admin login/logout

o List all articles

o Add a new article

o Edit an existing article

o Delete an existing article

Each article will have an associated headline, summary, and publication date.
Planning it out
Here are the steps we'll need to follow to create our CMS:

1. Create the database

2. Create the articles database table


3. Make a configuration file

4. Build the Article class


5. Write the front-end index.php script
6. Write the back-end admin.php script
7. Create the front-end templates

8. Create the back-end templates

9. Create the stylesheet and logo image

This page contains all the code for the CMS, ready for you to copy and paste into your own
files. If you don't want to create the files yourself, simply download thefinished zip file,
which contains all the code files and folders.

Ready? Grab a cup of tea, and let's get coding!

Step 1: Create the database

The first thing we need to do is create a MySQL database to store our content. You can do
this as follows:

1. Run the mysql client program


Open a terminal window and enter the following:

mysql -u username -p

Then enter your MySQL password when prompted.


username should be a user that has permission to create databases. If you're working
on a development server, such as your own computer, then you can use the root user
for this, to save having to create a new user.

2. Create the database


At the mysql> prompt, type:

create database cms;

Then press Enter.

3. Quit the mysql client program


At the mysql> prompt, type:

exit

Then press Enter.

That's it! You've now created a new, empty database, into which you can put your database
tables and content.

Some web server setups let you create databases via a web-based tool such as cPanel or
Plesk (in fact sometimes this is the only way to create MySQL databases). If you're not sure
what to do on your server, ask your tech support team for help.

Step 2: Create the articles database table


Our simple CMS has just one database table: articles. This, as you'd imagine, holds all of
the articles in the system.

Let's create the schema for the table. A table's schema describes the types of data that the
table can hold, as well as other information about the table.

Create a text file called tables.sql somewhere on your hard drive. Add the following
code to the file:

DROP TABLE IF EXISTS articles;


CREATE TABLE articles
(
id smallint unsigned NOT NULL auto_increment,
publicationDate date NOT NULL, # When
the article was published
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)


);

The above code defines the schema for the articles table. It's written in SQL, the
language used to create and manipulate databases in MySQL (and most other database
systems).

Let's break the above code down a little:

1. Create the articles table


DROP TABLE IF EXISTS articles removes any existing articles table (and data
be careful!) if it already exists. We do this because we can't define a table with the same
name as an existing table.

CREATE TABLE articles ( ) creates the new articles table. The stuff inside the
parentheses defines the structure of the data within the table, explained below...
2. Give each article a unique ID
We're now ready to define our table structure. A table consists of a number
of fields(also called columns). Each field holds a specific type of information about each
article.

First, we create an id field. This has a smallint unsigned (unsigned small integer)
data type, which means it can hold whole numbers from 0 to 65,535. This lets our CMS
hold up to 65,535 articles. We also specify the NOT NULL attribute, which means the
field can't be empty (null) this makes life easier for us. We also add
theauto_increment attribute, which tells MySQL to assign a new, unique value to an
article's id field when the article record is created. So the first article will have an idof
1, the second will have an id of 2, and so on. We'll use this unique value as a handle to
refer to the article that we want to display or edit in the CMS.
3. Add the publicationDate field
The next line creates the publicationDate field, which stores the date that each
article was published. This field has a data type of date, which means it can store date
values.
4. Add the title field
Next we create the title field to hold each article's title. It has a data type
ofvarchar(255), which means it can store a string of up to 255 characters.
5. Add the summary and content fields
The last 2 fields, summary and content, hold a short summary of the article and the
article's HTML content respectively. summary has a text data type (which can hold up
to 65,535 characters) and content has a mediumtext data type (which can hold up to
16,777,215 characters).
6. Add the primary key
The last line inside the CREATE TABLE statement defines a key for the table. A key is
also called an index, and in simple terms it makes it quicker to find data in the table, at
the expense of some extra storage space.

We make the id field a PRIMARY KEY. Each table can only have a single PRIMARY KEY;
this is the key that uniquely identifies each record in the table. In addition, by adding
this key, MySQL can retrieve an article based on its ID very quickly.

Now that we've created our table schema, we need to load it into MySQL to create the table
itself. The easiest way to do this is to open up a terminal window and change to the folder
containing your tables.sql file, then run this command:

mysql -u username -p cms < tables.sql

...where username is your MySQL username. cms is the name of the database that you
created in Step 1.

Enter your password when prompted. MySQL then loads and runs the code in
yourtables.sql file, creating the articles table inside the cms database.

You can also use a web-based admin tool such as phpMyAdmin to run
yourtables.sql code and create the table. phpMyAdmin comes pre-installed with most
web hosting accounts.

Step 3: Make a configuration file

Now that you've created your database, you're ready to start writing your PHP code. Let's
start by creating a configuration file to store various useful settings for our CMS. This file
will be used by all the script files in our CMS.

First, create a cms folder somewhere in the local website on your computer, to hold all the
files relating to the CMS. If you're running XAMPP then the local website will be in
an htdocs folder inside your XAMPP folder. Or, if you prefer, you can create a brand new
website just for your CMS, and put all the files in that new website's document root folder.

Inside the cms folder, create a file called config.php with the following code:

1
2 <?php
3 ini_set( "display_errors", true );
4 date_default_timezone_set( "Australia/Sydney" ); //
http://www.php.net/manual/en/timezones.php
5 define( "DB_DSN", "mysql:host=localhost;dbname=cms" );
6 define( "DB_USERNAME", "username" );
7 define( "DB_PASSWORD", "password" );
8 define( "CLASS_PATH", "classes" );
9 define( "TEMPLATE_PATH", "templates" );
define( "HOMEPAGE_NUM_ARTICLES", 5 );
10define( "ADMIN_USERNAME", "admin" );
11define( "ADMIN_PASSWORD", "mypass" );
12require( CLASS_PATH . "/Article.php" );
13
14function handleException( $exception ) {
echo "Sorry, a problem occurred. Please try later.";
15 error_log( $exception->getMessage() );
16}
17
18set_exception_handler( 'handleException' );
19?>
20

Let's break this file down:

1. Display errors in the browser


The ini_set() line causes error messages to be displayed in the browser. This is good
for debugging, but it should be set to false on a live site since it can be a security
risk.
2. Set the timezone
As our CMS will use PHP's date() function, we need to tell PHP our server's timezone
(otherwise PHP generates a warning message). Mine is set to "Australia/Sydney"
change this value to your local timezone.
3. Set the database access details
Next we define a constant, DB_DSN, that tells PHP where to find our MySQL database.
Make sure the dbname parameter matches the name of your CMS database (cms in this
case). We also store the MySQL username and password that are used to access the
CMS database in the constants DB_USERNAME and DB_PASSWORD. Set these values to
your MySQL username and password.
4. Set the paths
We set 2 path names in our config file: CLASS_PATH, which is the path to the class files,
and TEMPLATE_PATH, which is where our script should look for the HTML template files.
Both these paths are relative to our top-level cms folder.
5. Set the number of articles to display on the homepage
HOMEPAGE_NUM_ARTICLES controls the maximum number of article headlines to display
on the site homepage. We've set this to 5 initially, but if you want more or less articles,
just change this value.
6. Set the admin username and password
The ADMIN_USERNAME and ADMIN_PASSWORD constants contain the login details for the
CMS admin user. Again, you'll want to change these to your own values.
7. Include the Article class
Since the Article class file which we'll create next is needed by all scripts in our
application, we include it here.
8. Create an exception handler
Finally, we define handleException(), a simple function to handle any
PHPexceptions that might be raised as our code runs. The function displays a generic
error message, and logs the actual exception message to the web server's error log. In
particular, this function improves security by handling any PDO exceptions that might
otherwise display the database username and password in the page. Once we've
defined handleException(), we set it as the exception handler by calling
PHP's set_exception_handler() function.
This exception handler is a bit of a quick and dirty shortcut to keep the tutorial as
simple as possible. The "proper" way to handle exceptions is to wrap all the PDO calls
within Article.php in try ... catch blocks.

Security note

In a live server environment it'd be a good idea to place config.php somewhere outside
your website's document root, since it contains usernames and passwords. While it's not
usually possible to read the source code of a PHP script via the browser, it does happen
sometimes if the web server is misconfigured.

You could also use hash() to make a hash from your admin password, and store the hash
in config.php instead of the plaintext password. Then, at login time, you can hash() the
entered password and see if it matches the hash in config.php.

Step 4: Build the Article class

You're now ready to build the Article PHP class. This is the only class in our CMS, and it
handles the nitty-gritty of storing articles in the database, as well as retrieving articles
from the database. Once we've built this class, it will be really easy for our other CMS
scripts to create, update, retrieve and delete articles.
Inside your cms folder, create a classes folder. Inside that classes folder, create a new
file called Article.php, and put the following code into it:

1 <?php
2
/**
3
* Class to handle articles
4 */
5
6 class Article
7 {
8
9 // Properties
10
/**
11 * @var int The article ID from the database
12 */
13 public $id = null;
14
15 /**
16 * @var int When the article was published
*/
17 public $publicationDate = null;
18
19 /**
20 * @var string Full title of the article
21 */
public $title = null;
22
23 /**
24 * @var string A short summary of the article
25 */
26 public $summary = null;
27
28 /**
* @var string The HTML content of the article
29 */
30 public $content = null;
31
32
33 /**
34 * Sets the object's properties using the values in the supplied array
*
35
* @param assoc The property values
36 */
37
38 public function __construct( $data=array() ) {
39 if ( isset( $data['id'] ) ) $this->id = (int) $data['id'];
40 if ( isset( $data['publicationDate'] ) ) $this->publicationDate = (int)
$data['publicationDate'];
41 if ( isset( $data['title'] ) ) $this->title = preg_replace ( "/[^\.\,\-
42 \_\'\"\@\?\!\:\$ a-zA-Z0-9()]/", "", $data['title'] );
43 if ( isset( $data['summary'] ) ) $this->summary = preg_replace (
44 "/[^\.\,\-\_\'\"\@\?\!\:\$ a-zA-Z0-9()]/", "", $data['summary'] );
45 if ( isset( $data['content'] ) ) $this->content = $data['content'];
}
46
47
48 /**
49 * Sets the object's properties using the edit form post values in the
50 supplied array
51 *
* @param assoc The form post values
52 */
53
54 public function storeFormValues ( $params ) {
55
56 // Store all the parameters
57 $this->__construct( $params );
58
59 // Parse and store the publication date
if ( isset($params['publicationDate']) ) {
60 $publicationDate = explode ( '-', $params['publicationDate'] );
61
62 if ( count($publicationDate) == 3 ) {
63 list ( $y, $m, $d ) = $publicationDate;
64 $this->publicationDate = mktime ( 0, 0, 0, $m, $d, $y );
}
65 }
66 }
67
68
69 /**
70 * Returns an Article object matching the given article ID
71 *
* @param int The article ID
72 * @return Article|false The article object, or false if the record was
73 not found or there was a problem
74 */
75
76 public static function getById( $id ) {
$conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD );
77 $sql = "SELECT *, UNIX_TIMESTAMP(publicationDate) AS publicationDate
78 FROM articles WHERE id = :id";
79 $st = $conn->prepare( $sql );
80 $st->bindValue( ":id", $id, PDO::PARAM_INT );
81 $st->execute();
$row = $st->fetch();
82 $conn = null;
83 if ( $row ) return new Article( $row );
84 }
85
86
87 /**
88 * Returns all (or a range of) Article objects in the DB
*
89 * @param int Optional The number of rows to return (default=all)
90 * @param string Optional column by which to order the articles
91 (default="publicationDate DESC")
92 * @return Array|false A two-element array : results => array, a list of
93 Article
*/
objects; totalRows => Total number of articles
94
95 public static function getList( $numRows=1000000, $order="publicationDate
96 DESC" ) {
97 $conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD );
98 $sql = "SELECT SQL_CALC_FOUND_ROWS *, UNIX_TIMESTAMP(publicationDate)
AS publicationDate FROM articles
99 ORDER BY " . mysql_escape_string($order) . " LIMIT :numRows";
100
101 $st = $conn->prepare( $sql );
102 $st->bindValue( ":numRows", $numRows, PDO::PARAM_INT );
103 $st->execute();
$list = array();
104
105 while ( $row = $st->fetch() ) {
106 $article = new Article( $row );
107 $list[] = $article;
108 }
109
110 // Now get the total number of articles that matched the criteria
$sql = "SELECT FOUND_ROWS() AS totalRows";
111 $totalRows = $conn->query( $sql )->fetch();
112 $conn = null;
113 return ( array ( "results" => $list, "totalRows" => $totalRows[0] ) );
114 }
115
116
117 /**
118 * Inserts the current Article object into the database, and sets its ID
property.
119 */
120
121 public function insert() {
122
123 // Does the Article object already have an ID?
124 if ( !is_null( $this->id ) ) trigger_error ( "Article::insert():
Attempt to insert an Article object that already has its ID property set
125(to $this->id).", E_USER_ERROR );
126
127 // Insert the Article
128 $conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD );
129 $sql = "INSERT INTO articles ( publicationDate, title, summary, content
130 ) VALUES ( FROM_UNIXTIME(:publicationDate), :title, :summary, :content )";
$st = $conn->prepare ( $sql );
131 $st->bindValue( ":publicationDate", $this->publicationDate,
132PDO::PARAM_INT );
133 $st->bindValue( ":title", $this->title, PDO::PARAM_STR );
134 $st->bindValue( ":summary", $this->summary, PDO::PARAM_STR );
$st->bindValue( ":content", $this->content, PDO::PARAM_STR );
135 $st->execute();
136 $this->id = $conn->lastInsertId();
137 $conn = null;
138 }
139
140
141 /**
* Updates the current Article object in the database.
142 */
143
144 public function update() {
145
146 // Does the Article object have an ID?
147 if ( is_null( $this->id ) ) trigger_error ( "Article::update(): Attempt
148 to update an Article object that does not have its ID property set.",
E_USER_ERROR );
149
150 // Update the Article
151 $conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD );
152 $sql = "UPDATE articles SET
153 publicationDate=FROM_UNIXTIME(:publicationDate), title=:title,
summary=:summary, content=:content WHERE id = :id";
154 $st = $conn->prepare ( $sql );
155 $st->bindValue( ":publicationDate", $this->publicationDate,
156PDO::PARAM_INT );
157 $st->bindValue( ":title", $this->title, PDO::PARAM_STR );
158 $st->bindValue( ":summary", $this->summary, PDO::PARAM_STR );
$st->bindValue( ":content", $this->content, PDO::PARAM_STR );
159 $st->bindValue( ":id", $this->id, PDO::PARAM_INT );
160 $st->execute();
161 $conn = null;
162 }
163
164
165 /**
* Deletes the current Article object from the database.
166 */
167
168 public function delete() {
169
170 // Does the Article object have an ID?
171 if ( is_null( $this->id ) ) trigger_error ( "Article::delete(): Attempt
to delete an Article object that does not have its ID property set.",
172E_USER_ERROR );
173
174 // Delete the Article
175 $conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD );
176 $st = $conn->prepare ( "DELETE FROM articles WHERE id = :id LIMIT 1" );
177 $st->bindValue( ":id", $this->id, PDO::PARAM_INT );
178 $st->execute();
$conn = null;
179 }
180
181}
182
183?>
184
185
186
187
188
189
190
191

This file is quite long, but it's fairly simple stuff when you break it down. Let's take a look
at each section of the code:

1. The class definition and properties

First, we begin to define our Article class with the code:

1class Article
2{

Everything after these lines of code up until the closing brace at the end of the file
contains the code that makes up the Article class.

After starting our class definition, we declare the properties of the


class: $id,$publicationDate, and so on. Each Article object that we create will store its
article data in these properties. You can see that the property names mirror the field
names in our articles database table.

Technically, this type of class which contains properties that map directly to the
corresponding database fields, as well as methods for storing and retrieving records from
the database follows an object-oriented design pattern known as active record.

2. The constructor

Next we create the class methods. These are functions that are tied to the class, as well as
to objects created from the class. Our main code can call these methods in order to
manipulate the data in the Article objects.

The first method, __construct(), is the constructor. This is a special method that is
called automatically by the PHP engine whenever a new Article object is created. Our
constructor takes an optional $data array containing the data to put into the new object's
properties. We then populate those properties within the body of the constructor. This
gives us a handy way to create and populate an object in one go.
$this->propertyName means: "The property of this object that has the name
"$propertyName".

You'll notice that the method filters the data before it stores them in the properties.
Theid and publicationDate properties are cast to integers using (int), since these
values should always be integers. The title and summary are filtered using a regular
expression to only allow a certain range of characters. It's good security practice to filter
data on input like this, only allowing acceptable values and characters through.

We don't filter the content property, however. Why? Well, the administrator will probably
want to use a wide range of characters, as well as HTML markup, in the article content. If
we restricted the range of allowed characters in the content then we would limit the
usefulness of the CMS for the administrator.

Normally this could be a security loophole, since a user could insert malicious JavaScript
and other nasty stuff in the content. However, since we presumably trust our site
administrator who is the only person allowed to create the content this is an
acceptable tradeoff in this case. If you were dealing with user-generated content, such as
comments or forum posts, then you would want to be more careful, and only allow "safe"
HTML to be used. A really great tool for this is HTML Purifier, which thoroughly analyses
HTML input and removes all potentially malicious code.

PHP security is a big topic, and beyond the scope of this tutorial. If you'd like to find out
more then start with Terry Chay's excellent post, Filter Input-Escape Output: Security
Principle and Practice. Also see the Wikipedia entries on secure input/output
handling, XSS, CSRF, SQL injection, and session fixation.

3. storeFormValues()

Our next method, storeFormValues(), is similar to the constructor in that it stores a


supplied array of data in the object's properties. The main difference is
thatstoreFormValues() can handle data in the format that is submitted via our New
Article and Edit Article forms (which we'll create later). In particular, it can handle
publication dates in the format YYYY-MM-DD, converting the date into the UNIX timestamp
format suitable for storing in the object.

UNIX timestamps are integer values representing the number of seconds between
midnight on 1 Jan, 1970 and the date/time in question. I generally like to handle dates and
times as UNIX timestamps in PHP, since they're easy to store and manipulate.

The purpose of this method is simply to make it easy for our admin scripts to store the
data submitted by the forms. All they have to do is call storeFormValues(), passing in
the array of form data.
All of the members (that is, the properties and methods) of our Article class have
the public keyword before their names, which means that they're available to code
outside the class. You can also create private members (which can only be used by the
class itself) and protected members (which can be used by the class and any of its
subclasses). Don't worry, I'll be covering all this in a later tutorial!

4. getById()

Now we come to the methods that actually access the MySQL database. The first of
these, getById(), accepts an article ID argument ($id), then retrieves the article record
with that ID from the articles table, and stores it in a new Article object.

Usually, when you call a method, you first create or retrieve an object, then call the
method on that object. However, since this method returns a new Article object, it would
be helpful if the method could be called directly by our calling code, and not via an
existing object. Otherwise, we would have to create a new dummy object each time we
wanted to call the method and retrieve an article.

To enable our method to be called without needing an object, we add the statickeyword
to the method definition. This allows the method to be called directly without specifying
an object:

83public static function getById( $id ) {

The method itself uses PDO to connect to the database, retrieve the article record using
a SELECT SQL statement, and store the article data in a new Article object, which is then
returned to the calling code.

PDO PHP Data Objects is an object-oriented library built into PHP that makes it easy
for PHP scripts to talk to databases.

Let's break this method down:

1. Connect to the database

84$conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD );

2. This makes a connection to the MySQL database using the login details from
theconfig.php file, and stores the resulting connection handle in $conn. This handle
is used by the remaining code in the method to talk to the database.
3. Retrieve the article record

85$sql = "SELECT *, UNIX_TIMESTAMP(publicationDate) AS publicationDate FROM


86articles WHERE id = :id";
$st = $conn->prepare( $sql );
87$st->bindValue( ":id", $id, PDO::PARAM_INT );
88$st->execute();
89$row = $st->fetch();

4. Our SELECT statement retrieves all fields (*) from the record in the articles table that
matches the given id field. It also retrieves the publicationDate field in UNIX
timestamp format instead of the default MySQL date format, so we can store it easily in
our object.
5. Rather than placing our $id parameter directly inside the SELECT string, which can be a
security risk, we instead use :id. This is known as a placeholder. In a minute, we'll call
a PDO method to bind our $id value to this placeholder.
6. Once we've stored our SELECT statement in a string, we prepare the statement by
calling $conn->prepare(), storing the resulting statement handle in a $st variable.

7. Prepared statements are a feature of most databases; they allow your database calls to
be faster and more secure.

8. We now bind the value of our $id variable that is, the ID of the article we want to
retrieve to our :id placeholder by calling the bindValue() method. We pass in the
placeholder name; the value to bind to it; and the value's data type (integer in this case)
so that PDO knows how to correctly escape the value.
9. Lastly, we call execute() to run the query, then we use fetch() to retrieve the
resulting record as an associative array of field names and corresponding field values,
which we store in the $row variable.
10. Close the connection

90$conn = null;

11. Since we no longer need our connection, we close it by assigning null to


the $connvariable. It's a good idea to close database connections as soon as possible to
free up memory on the server.
12. Return the new Article object

91 if ( $row ) return new Article( $row );


92}

13. The last thing our method needs to do is create a new Article object that stores the
record returned from the database, and return this object to the calling code. First it
checks that the returned value from the fetch() call, $row, does in fact contain data. If
it does then it creates a new Article object, passing in $row as it does so. Remember
that this calls our constructor that we created earlier, which populates the object with
the data contained in the $row array. We then return this new object, and our work here
is done.

5. getList()

Our next method, getList(), is similar in many ways to getById(). The main difference,
as you might imagine, is that it can retrieve many articles at once, rather than just 1
article. It's used whenever we need to display a list of articles to the user or administrator.

getList() accepts 2 optional arguments:

$numRows
The maximum number of articles to retrieve. We default this value to 1,000,000
(i.e. effectively all articles). This parameter allows us to display, say, just the first 5
articles on the site homepage.

$order

The sort order to use when returning the articles. We default this
to "publicationDate DESC", which means "sort by publication date, newest first".

Much of this method's code is similar to getById(). Let's look at a few lines of interest:

105$sql = "SELECT SQL_CALC_FOUND_ROWS *, UNIX_TIMESTAMP(publicationDate) AS


publicationDate FROM articles
106 ORDER BY " . mysql_escape_string($order) . " LIMIT :numRows";

Our query is a bit more complex than last time. First, notice that there's no WHEREclause
this time; this is because we want to retrieve all articles, rather than an article that matches
a specific ID.

We've added an ORDER BY clause to sort the returned records by the specified sort order.
We've also added a LIMIT clause, passing in the $numRows parameter (as a placeholder),
so that we can optionally limit the number of records returned.

Finally, the special MySQL value SQL_CALC_FOUND_ROWS tells MySQL to return the actual
number of records returned; this information is useful for displaying to the user, as well as
for other things like pagination of results.

Rather than pass the $order value to the query via a placeholder, we interpolate it directly
into the query string itself, calling mysql_escape_string() to ensure that any special
characters in the value are escaped (for security reasons). If we used a placeholder then
PDO would place quotes (') around the string value (for example,ORDER BY
'publicationDate DESC'), which is invalid syntax.

111$list = array();
112
113while ( $row = $st->fetch() ) {
114 $article = new Article( $row );
115 $list[] = $article;
116}

Since we're returning multiple rows, we create an array, $list, to hold the
corresponding Article objects. We then use a while loop to retrieve the next row
viafetch(), create a new Article object, store the row values in the object, and add the
object to the $list array. When there are no more rows, fetch() returns false and the
loop exits.

118// Now get the total number of articles that matched the criteria
119$sql = "SELECT FOUND_ROWS() AS totalRows";
120$totalRows = $conn->query( $sql )->fetch();
121$conn = null;
122return ( array ( "results" => $list, "totalRows" => $totalRows[0] ) );
Finally, we run another query that uses the MySQL FOUND_ROWS() function to get the
number of returned rows calculated by our previous SQL_CALC_FOUND_ROWS command.
This time we use the PDO query() method, which lets us quickly run a query if there are
no placeholders to bind. We call fetch() on the resulting statement handle to retrieve the
result row, then return both the list of Article objects ($list) and the total row count as
an associative array.

6. insert()

The remaining methods in our Article class deal with adding, changing and deleting
article records in the database.

insert() adds a new article record to the articles table, using the values stored in the
current Article object:

o First, the method makes sure that the object doesn't already have its $id property set.
If it does have an ID then the article presumably already exists in the database, so we
shouldn't try to insert it again.
o Then the method runs an SQL INSERT query to insert the record into
the articlestable, using placeholders to pass the property values to the database.
Note the use of the MySQL FROM_UNIXTIME() function to convert the publication date
from UNIX timestamp format back into MySQL format.
o After running the query, the method retrieves the new article record's ID using the
PDO lastInsertId() function, and stores it in the object's $id property for future
reference. Remember that we set up the articles table's id field as
anauto_increment field, so that MySQL generates a new unique ID for each new article
record.
Notice that we use PDO::PARAM_INT when binding integer values to placeholders,
and PDO::PARAM_STR when binding string values. This is so that PDO can escape the
values appropriately.

7. update()

This method is similar to insert(), except that it updates an existing article record in the
database instead of creating a new record.

First it checks that the object has an ID, since you can't update a record without knowing
its ID. Then it uses the SQL UPDATE statement to update the record's fields. Notice that we
pass the object's ID to the UPDATE statement so that it knows which record to update.

5. delete()

The delete() method is pretty self-explanatory. It uses the SQL DELETE statement to
remove the article stored in the object from the articles table, using the
object's $idproperty to identify the record in the table. For safety reasons, we add LIMIT
1 to the query to make sure that only 1 article record can be deleted at a time.

Step 5: Write the front-end index.php script


We've now created our Article class, which does the heavy lifting for our CMS. Now that's
out of the way, the rest of the code is pretty simple!

First, let's create index.php, the script that controls the display of the front-end pages of
the site. Save this file in the cms folder you created earlier, at the start of Step 4:

<?php
1
2 require( "config.php" );
3 $action = isset( $_GET['action'] ) ? $_GET['action'] : "";
4
5 switch ( $action ) {
6 case 'archive':
archive();
7 break;
8 case 'viewArticle':
9 viewArticle();
10 break;
11 default:
homepage();
12}
13
14
15function archive() {
16 $results = array();
17 $data = Article::getList();
18 $results['articles'] = $data['results'];
$results['totalRows'] = $data['totalRows'];
19 $results['pageTitle'] = "Article Archive | Widget News";
20 require( TEMPLATE_PATH . "/archive.php" );
21}
22
23function viewArticle() {
if ( !isset($_GET["articleId"]) || !$_GET["articleId"] ) {
24 homepage();
25 return;
26 }
27
28 $results = array();
29 $results['article'] = Article::getById( (int)$_GET["articleId"]
);
30 $results['pageTitle'] = $results['article']->title . " | Widget
31News";
32 require( TEMPLATE_PATH . "/viewArticle.php" );
33}
34
35function homepage() {
$results = array();
36 $data = Article::getList( HOMEPAGE_NUM_ARTICLES );
37 $results['articles'] = $data['results'];
$results['totalRows'] = $data['totalRows'];
38 $results['pageTitle'] = "Widget News";
39 require( TEMPLATE_PATH . "/homepage.php" );
40}
41
42?>
43
44
45
46
47
48

Let's break this script down:

4. Include the config file


The first line of code includes the config.php file we created earlier, so that all the
configuration settings are available to the script. We use require() rather
thaninclude(); require() generates an error if the file can't be found.
5. Grab the action parameter
We store the $_GET['action'] parameter in a variable called $action, so that we can
use the value later in the script. Before doing this, we check that
the$_GET['action'] value exists by using isset(). If it doesn't, we set the
corresponding $action variable to an empty string ("").

It's good programming practice to check that user-supplied values, such as query
string parameters, form post values and cookies, actually exist before attempting to
use them. Not only does it limit security holes, but it prevents the PHP engine raising
"undefined index" notices as your script runs.

6. Decide which action to perform


The switch block looks at the action parameter in the URL to determine which action
to perform (display the archive, or view an article). If no action parameter is in the URL
then the script displays the site homepage.
7. archive()
This function displays a list of all the articles in the database. It does this by calling
the getList() method of the Article class that we created earlier. The function then
stores the results, along with the page title, in a $results associative array so the
template can display them in the page. Finally, it includes the template file to display
the page. (We'll create the templates in a moment.)
8. viewArticle()
This function displays a single article page. It retrieves the ID of the article to display
from the articleId URL parameter, then calls the Article class's getById()method
to retrieve the article object, which it stores in the $results array for the template to
use. (If no articleId was supplied, or the article couldn't be found, then the function
simply displays the homepage instead.)
Notice that we use (int) to cast the value of the articleID query parameter to an
integer. This is a good security measure, as it prevents anything other than integers
from being passed to our code.

9. homepage()
Our last function, homepage(), displays the site homepage containing a list of up
toHOMEPAGE_NUM_ARTICLES articles (5 by default). It's much like
the archive()function, except that it passes HOMEPAGE_NUM_ARTICLES to
the getList() method to limit the number of articles returned.

Step 6: Write the back-end admin.php script

Our admin script is a bit more complex than index.php, since it deals with all the admin
functions for the CMS. The basic structure, though, is similar to index.php.

Save this file, admin.php, in the same folder as your index.php script:

1 <?php
2
3 require( "config.php" );
session_start();
4 $action = isset( $_GET['action'] ) ? $_GET['action'] : "";
5 $username = isset( $_SESSION['username'] ) ? $_SESSION['username'] : "";
6
7 if ( $action != "login" && $action != "logout" && !$username ) {
8 login();
exit;
9 }
10
11 switch ( $action ) {
12 case 'login':
13 login();
14 break;
case 'logout':
15 logout();
16 break;
17 case 'newArticle':
18 newArticle();
break;
19
case 'editArticle':
20 editArticle();
21 break;
22 case 'deleteArticle':
23 deleteArticle();
break;
24 default:
25 listArticles();
26 }
27
28
29 function login() {
30
31 $results = array();
$results['pageTitle'] = "Admin Login | Widget News";
32
33 if ( isset( $_POST['login'] ) ) {
34
35 // User has posted the login form: attempt to log the user in
36
37 if ( $_POST['username'] == ADMIN_USERNAME && $_POST['password'] ==
38 ADMIN_PASSWORD ) {
39
// Login successful: Create a session and redirect to the admin
40 homepage
41 $_SESSION['username'] = ADMIN_USERNAME;
42 header( "Location: admin.php" );
43
44 } else {
45
46 // Login failed: display an error message to the user
$results['errorMessage'] = "Incorrect username or password. Please
47 try again.";
48 require( TEMPLATE_PATH . "/admin/loginForm.php" );
49 }
50
51 } else {
52
// User has not posted the login form yet: display the form
53 require( TEMPLATE_PATH . "/admin/loginForm.php" );
54 }
55
56 }
57
58
59 function logout() {
60 unset( $_SESSION['username'] );
header( "Location: admin.php" );
61 }
62
63
64 function newArticle() {
65
66 $results = array();
67 $results['pageTitle'] = "New Article";
68 $results['formAction'] = "newArticle";
69
if ( isset( $_POST['saveChanges'] ) ) {
70
71 // User has posted the article edit form: save the new article
72 $article = new Article;
73 $article->storeFormValues( $_POST );
74 $article->insert();
header( "Location: admin.php?status=changesSaved" );
75
76 } elseif ( isset( $_POST['cancel'] ) ) {
77
78 // User has cancelled their edits: return to the article list
79 header( "Location: admin.php" );
80 } else {
81
82 // User has not posted the article edit form yet: display the form
$results['article'] = new Article;
83 require( TEMPLATE_PATH . "/admin/editArticle.php" );
84 }
85
86 }
87
88
89 function editArticle() {
90
91 $results = array();
$results['pageTitle'] = "Edit Article";
92 $results['formAction'] = "editArticle";
93
94 if ( isset( $_POST['saveChanges'] ) ) {
95
96 // User has posted the article edit form: save the article changes
97
98 if ( !$article = Article::getById( (int)$_POST['articleId'] ) ) {
header( "Location: admin.php?error=articleNotFound" );
99 return;
100 }
101
102 $article->storeFormValues( $_POST );
103 $article->update();
104 header( "Location: admin.php?status=changesSaved" );
105
} elseif ( isset( $_POST['cancel'] ) ) {
106
107 // User has cancelled their edits: return to the article list
108 header( "Location: admin.php" );
109 } else {
110
111 // User has not posted the article edit form yet: display the form
$results['article'] = Article::getById( (int)$_GET['articleId'] );
112 require( TEMPLATE_PATH . "/admin/editArticle.php" );
113 }
114
115}
116
117
118function deleteArticle() {
119
120 if ( !$article = Article::getById( (int)$_GET['articleId'] ) ) {
header( "Location: admin.php?error=articleNotFound" );
121 return;
122 }
123
124 $article->delete();
125 header( "Location: admin.php?status=articleDeleted" );
126}
127
128
function listArticles() {
129 $results = array();
130 $data = Article::getList();
131 $results['articles'] = $data['results'];
132 $results['totalRows'] = $data['totalRows'];
133 $results['pageTitle'] = "All Articles";
134 if ( isset( $_GET['error'] ) ) {
135 if ( $_GET['error'] == "articleNotFound" ) $results['errorMessage'] =
136"Error: Article not found.";
137 }
138
139 if ( isset( $_GET['status'] ) ) {
if ( $_GET['status'] == "changesSaved" ) $results['statusMessage'] =
140"Your changes have been saved.";
141 if ( $_GET['status'] == "articleDeleted" ) $results['statusMessage'] =
142"Article deleted.";
143 }
144
145} require( TEMPLATE_PATH . "/admin/listArticles.php" );
146
147
148?>
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163

Let's look at some interesting sections of this script:

10. Start a user session


Towards the top of the script we call session_start(). This PHP function starts a new
session for the user, which we can use to track whether the user is logged in or not. (If
a session for this user already exists, PHP automatically picks it up and uses it.)
Because sessions need cookies to work, and cookies are sent to the browser before
content, you should call session_start() at the top of the script, before any content
has been output.

11. Grab the action parameter and username session variable


Next we store the $_GET['action'] parameter in a variable called $action, and
the $_SESSION['username'] session variable in $username, so that we can use these
values later in the script. Before doing this, we check that these values exist by
using isset(). If a value doesn't exist then we set the corresponding variable to an
empty string ("").
12. Check the user is logged in
The user shouldn't be allowed to do anything unless they're logged in as an
administrator. So the next thing we do is inspect $username to see if the session
contained a value for the username key, which we use to signify that the user is logged
in. If $username's value is empty and the user isn't already trying to log in or out
then we display the login page and exit immediately.
13. Decide which action to perform
The switch block works much like the one in index.php: it calls the appropriate
function based on the value of the action URL parameter. The default action is to
display the list of articles in the CMS.
14. login()
This is called when the user needs to log in, or is in the process of logging in.

If the user has submitted the login form which we check by looking for
the loginform parameter then the function checks the entered username and
password against the config values ADMIN_USERNAME and ADMIN_PASSWORD. If they
match then the username session key is set to the admin username, effectively logging
them in, and we then redirect the browser back to the admin.php script, which then
displays the list of articles. If the username and password don't match then the login
form is redisplayed with an error message.

If the user hasn't submitted the login form yet then the function simply displays the
form.

15. logout()
This function is called when the user elects to log out. It simply removes
theusername session key and redirects back to admin.php.
16. newArticle()

This function lets the user create a new article. If the user has just posted the "new
article" form then the function creates a new Article object, stores the form data in
the object by calling storeFormValues(), inserts the article into the database by
calling insert(), and redirects back to the article list, displaying a "Changes Saved"
status message.

If the user has not posted the "new article" form yet then the function creates a new
empty Article object with no values, then uses the editArticle.php template to
display the article edit form using this empty Article object.

17. editArticle()

This function is similar to newArticle(), except that it lets the user edit an existing
article. When the user saves their changes then the function retrieves the existing
article using getById(), stores the new values in the Article object, then saves the
changed object by calling update(). (If the article isn't found in the database then the
function displays an error.)

When displaying the article edit form, the function again uses the getById()method to
load the current article field values into the form for editing.

Notice that the script uses the same template (editArticle.php) both for creating
new articles, and for editing existing articles. This means that we only need to create a
single HTML form. The formAction parameter is used to determine if the user is
adding or editing an article.

18. deleteArticle()
If the user has chosen to delete an article then this function first retrieves the article to
be deleted (displaying an error if the article couldn't be found in the database), then
calls the article's delete() method to remove the article from the database. It then
redirects to the article list page, displaying an "article deleted" status message.
19. listArticles()
The last function in admin.php displays a list of all articles in the CMS for editing. The
function uses the Article class's getList() method to retrieve all the articles, then it
uses the listArticles.php template to display the list. Along the way, it also checks
the URL query parameters error and status to see if any error or status message
needs to be displayed in the page. If so, then it creates the necessary message and
passes it to the template for display.
Step 7: Create the front-end templates

We've now created all the PHP code for our CMS's functionality. The next step is to create
the HTML templates for both the front-end and admin pages.

First, the front-end templates.

1. The include files

Create a folder called templates inside your cms folder. Now create a folder
calledinclude inside the templates folder. In this folder we're going to put the header
and footer markup that is common to every page of the site, to save having to put it inside
every template file.

Create a new file called header.php inside your include folder, with the following code:

1 <!DOCTYPE html>
2 <html lang="en">
3 <head>
4 <title><?php echo htmlspecialchars( $results['pageTitle'] )?></title>
<link rel="stylesheet" type="text/css" href="style.css" />
5 </head>
6 <body>
7 <div id="container">
8
9 <a href="."><img id="logo" src="images/logo.jpg" alt="Widget News"
/></a>
10

As you can see, this code simply displays the markup to start the HTML page. It uses
the$results['pageTitle'] variable passed from the main script
(index.php oradmin.php) to set the title element, and also links to a
stylesheet, style.css (we'll create this in a moment).

Notice that we've passed the value of $results['pageTitle'] through the PHP
function htmlspecialchars(). This function encodes any special HTML characters, such
as <, >, and &, into their HTML entity equivalents (&lt;, &gt;, and &amp;). Along with
filtering input which we did when we wrote the Article constructor in Step 4
encoding output is a good security habit to get into. We'll encode the majority of the data
in our templates this way.

Next, create a file called footer.php in the same folder:

1 <div id="footer">
2 Widget News &copy; 2011. All rights reserved. <a
href="admin.php">Site Admin</a>
3 </div>
4
5 </div>
6 </body>
7</html>

This markup finishes off each HTML page in the system.

2. homepage.php

Now go back up to the templates folder, and create a homepage.php template file in
there, with the following code:

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


2
<ul id="headlines">
3
4 <?php foreach ( $results['articles'] as $article ) { ?>
5
6 <li>
7 <h2>
8 <span class="pubDate"><?php echo date('j F', $article-
9 >publicationDate)?></span><a href=".?action=viewArticle&amp;articleId=<?php
echo $article->id?>"><?php echo htmlspecialchars( $article->title )?></a>
10 </h2>
11 <p class="summary"><?php echo htmlspecialchars( $article->summary
12)?></p>
13 </li>
14
15<?php } ?>
16
</ul>
17
18 <p><a href="./?action=archive">Article Archive</a></p>
19
20<?php include "templates/include/footer.php" ?>

This template displays the article headlines on the homepage as an unordered list. It loops
through the array of Article objects stored in $results['articles'] and displays each
article's publication date, title, and summary. The title is linked back to'.' (index.php),
passing action=viewArticle, as well as the article's ID, in the URL. This allows the visitor
to read an article by clicking its title.

The template also includes a link to the article archive ("./?action=archive").

Notice that this template, as well as subsequent templates, use the PHP includestatement
to include the header and footer files in the page.

3. archive.php
Now create an archive.php template file in your templates folder:

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


1
2 <h1>Article Archive</h1>
3
4 <ul id="headlines" class="archive">
5
6 <?php foreach ( $results['articles'] as $article ) { ?>
7
8 <li>
9 <h2>
<span class="pubDate"><?php echo date('j F Y', $article-
10>publicationDate)?></span><a href=".?action=viewArticle&amp;articleId=<?php
11echo $article->id?>"><?php echo htmlspecialchars( $article->title )?></a>
12 </h2>
13 <p class="summary"><?php echo htmlspecialchars( $article->summary
)?></p>
14 </li>
15
16<?php } ?>
17
18 </ul>
19
20 <p><?php echo $results['totalRows']?> article<?php echo (
21 $results['totalRows'] != 1 ) ? 's' : '' ?> in total.</p>
22
<p><a href="./">Return to Homepage</a></p>
23
24<?php include "templates/include/footer.php" ?>

This template displays the archive of all articles in the CMS. As you can see, it's almost
identical to homepage.php. It adds an archive CSS class to the unordered list so we can
style the list items a bit differently to the homepage, and it also adds the year to the article
publication dates (since the archive might go back a few years).

The page also includes a total count of the articles in the database, retrieved
via$results['totalRows']. Finally, instead of the archive link at the bottom of the page,
it includes a "Return to Homepage" link.

4. viewArticle.php

The last front-end template displays an article to the user. Create a file
calledviewArticle.php in your templates folder, and add the following markup:

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


1
2 <h1 style="width: 75%;"><?php echo htmlspecialchars(
$results['article']->title )?></h1>
3 <div style="width: 75%; font-style: italic;"><?php echo
4 htmlspecialchars( $results['article']->summary )?></div>
5 <div style="width: 75%;"><?php echo $results['article']-
6 >content?></div>
<p class="pubDate">Published on <?php echo date('j F Y',
7 $results['article']->publicationDate)?></p>
8
9 <p><a href="./">Return to Homepage</a></p>
10
<?php include "templates/include/footer.php" ?>
This template is very straightforward. It displays the selected article's title, summary and
content, as well as its publication date and a link to return to the homepage.

You might have noticed that we haven't passed $results['article']-


>contentthrough htmlspecialchars(). As explained when we created
the Articleconstructor in Step 4, the administrator will probably want to use HTML
markup, such as <p> tags, in the article content. If we encoded the content then <p> tags
would appear on the page as <p>, rather than creating paragraphs.

Step 8: Create the back-end templates

Now that we've created the templates for the front end of the site, it's time to create the 3
admin templates.

1. loginForm.php

First, create another folder called admin inside your templates folder. Inside
theadmin folder, create the first of the 3 templates, loginForm.php:

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


2 <form action="admin.php?action=login" method="post" style="width:
3 50%;">
4 <input type="hidden" name="login" value="true" />
5
6 <?php if ( isset( $results['errorMessage'] ) ) { ?>
7 <div class="errorMessage"><?php echo $results['errorMessage']
?></div>
8 <?php } ?>
9
10 <ul>
11
12 <li>
13 <label for="username">Username</label>
<input type="text" name="username" id="username"
14
placeholder="Your admin username" required autofocus maxlength="20" />
15 </li>
16
17 <li>
18 <label for="password">Password</label>
<input type="password" name="password" id="password"
19placeholder="Your admin password" required maxlength="20" />
20 </li>
21
22 </ul>
23
24 <div class="buttons">
<input type="submit" name="login" value="Login" />
25 </div>
26
27 </form>
28
29<?php include "templates/include/footer.php" ?>
30

This page contains the admin login form, which posts back to admin.php?action=login.
It includes a hidden field, login, that our login() function from Step 6 uses to check if
the form has been posted. The form also contains an area for displaying any error
messages (such as an incorrect username or password), as well as username and password
fields and a "Login" button.

We've used some HTML5 form features such


as placeholder, required, autofocusand date in our admin forms. This makes the
forms nicer to use, and also saves us having to check for required fields in our PHP code.
Since not all browsers currently support these HTML5 form features, you would probably
want to use JavaScript and/or PHP fallbacks to check for required fields in a production
system.

2. listArticles.php

Now create the second admin template in your admin folder. This one's
calledlistArticles.php:

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


2
<div id="adminHeader">
3 <h2>Widget News Admin</h2>
4 <p>You are logged in as <b><?php echo htmlspecialchars(
5 $_SESSION['username']) ?></b>. <a href="admin.php?action=logout"?>Log
6 out</a></p>
</div>
7
8 <h1>All Articles</h1>
9
10<?php if ( isset( $results['errorMessage'] ) ) { ?>
11 <div class="errorMessage"><?php echo $results['errorMessage']
12 ?></div>
13<?php } ?>
14
15
<?php if ( isset( $results['statusMessage'] ) ) { ?>
16 <div class="statusMessage"><?php echo $results['statusMessage']
17?></div>
18<?php } ?>
19
20 <table>
<tr>
21 <th>Publication Date</th>
22 <th>Article</th>
23 </tr>
24
25<?php foreach ( $results['articles'] as $article ) { ?>
26
<tr
27onclick="location='admin.php?action=editArticle&amp;articleId=<?php echo
28$article->id?>'">
29 <td><?php echo date('j M Y', $article->publicationDate)?></td>
30 <td>
<?php echo $article->title?>
31 </td>
32 </tr>
33
34<?php } ?>
35
36 </table>
37
38 <p><?php echo $results['totalRows']?> article<?php echo (
$results['totalRows'] != 1 ) ? 's' : '' ?> in total.</p>
39
40 <p><a href="admin.php?action=newArticle">Add a New Article</a></p>
41
42<?php include "templates/include/footer.php" ?>

This template displays the list of articles for the administrator to edit. After displaying any
error or status messages, it loops through the array of Article objects stored
in$results['articles'], displaying each article's publication date and title in a table
row. It also adds a JavaScript onclick event to each article's table row, so that the
administrator can click an article to edit it.

The template also includes the total article count, as well as a link to let the administrator
add a new article.

3. editArticle.php

Now save the final template, editArticle.php, in your admin folder:

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


2
<div id="adminHeader">
3 <h2>Widget News Admin</h2>
4 <p>You are logged in as <b><?php echo htmlspecialchars(
5 $_SESSION['username']) ?></b>. <a href="admin.php?action=logout"?>Log
6 out</a></p>
7 </div>
8
<h1><?php echo $results['pageTitle']?></h1>
9
10 <form action="admin.php?action=<?php echo $results['formAction']?>"
11method="post">
12 <input type="hidden" name="articleId" value="<?php echo
13$results['article']->id ?>"/>
14
15<?php if (<div
isset( $results['errorMessage'] ) ) { ?>
class="errorMessage"><?php echo $results['errorMessage']
16?></div>
17<?php } ?>
18
19 <ul>
20
21 <li>
<label for="title">Article Title</label>
22 <input type="text" name="title" id="title" placeholder="Name of
23the article" required autofocus maxlength="255" value="<?php echo
24htmlspecialchars( $results['article']->title )?>" />
25 </li>
26
<li>
27 <label for="summary">Article Summary</label>
28 <textarea name="summary" id="summary" placeholder="Brief
29description of the article" required maxlength="1000" style="height:
305em;"><?php echo htmlspecialchars( $results['article']->summary
31)?></textarea>
</li>
32
33 <li>
34 <label for="content">Article Content</label>
35 <textarea name="content" id="content" placeholder="The HTML
36 content of the article" required maxlength="100000" style="height:
30em;"><?php echo htmlspecialchars( $results['article']->content
37)?></textarea>
38 </li>
39
40 <li>
41 <label for="publicationDate">Publication Date</label>
<input type="date" name="publicationDate" id="publicationDate"
42placeholder="YYYY-MM-DD" required maxlength="10" value="<?php echo
43$results['article']->publicationDate ? date( "Y-m-d", $results['article']-
44>publicationDate ) : "" ?>" />
45 </li>
46
47
48 </ul>
49
<div class="buttons">
50 <input type="submit" name="saveChanges" value="Save Changes" />
51 <input type="submit" formnovalidate name="cancel" value="Cancel" />
52 </div>
53
</form>

<?php if ( $results['article']->id ) { ?>


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

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

This edit form is used both for creating new articles, and for editing existing articles. It
posts to either admin.php?action=newArticle or admin.php?action=editArticle,
depending on the value passed in the $results['formAction'] variable. It also contains
a hidden field, articleId, to track the ID of the article being edited (if any).

The form also includes an area for error messages, as well as fields for the article title,
summary, content, and publication date. Finally, there are 2 buttons for saving and
cancelling changes, and a link to allow the admin to delete the currently-edited article.

As usual, we pass all data through htmlspecialchars() before outputting it in the


markup. Not only is this a good security habit, but it also ensures that our form field
values are properly escaped. For example, if the title field value contained a double
quote (") that wasn't escaped then the title would be truncated, since double quotes are
used to delimit the field's value in the markup.
Note the use of the HTML5 formnovalidate attribute on the "Cancel" button. This handy
attribute tells the browser not to validate the form if the user presses "Cancel".
Step 9: Create the stylesheet and logo image
Our CMS application is basically done now, but in order to make it look a bit nicer for both
our visitors and the site administrator, we'll create a CSS file to control the look of the site.
Save this file as style.css in your cms folder:

1 /* Style the body and outer container */


2
3 body {
margin: 0;
4 color: #333;
5 background-color: #00a0b0;
6 font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
7 line-height: 1.5em;
8 }
9
#container {
10 width: 960px;
11 background: #fff;
12 margin: 20px auto;
13 padding: 20px;
-moz-border-radius: 5px;
14
-webkit-border-radius: 5px;
15 border-radius: 5px;
16 }
17
18
19 /* The logo and footer */
20
21 #logo {
display: block;
22 width: 300px;
23 padding: 0 660px 20px 0;
24 border: none;
25 border-bottom: 1px solid #00a0b0;
26 margin-bottom: 40px;
}
27
28 #footer {
29 border-top: 1px solid #00a0b0;
30 margin-top: 40px;
31 padding: 20px 0 0 0;
font-size: .8em;
32 }
33
34
35 /* Headings */
36
37 h1 {
38 color: #eb6841;
39 margin-bottom: 30px;
line-height: 1.2em;
40 }
41
42 h2, h2 a {
43 color: #edc951;
44 }
45
h2 a {
46 text-decoration: none;
47 }
48
49
50 /* Article headlines */
51
52 #headlines {
53 list-style: none;
54 padding-left: 0;
width: 75%;
55 }
56
57 #headlines li {
58 margin-bottom: 2em;
59 }
60
61 .pubDate {
font-size: .8em;
62 color: #eb6841;
63 text-transform: uppercase;
64 }
65
66 #headlines .pubDate {
display: inline-block;
67 width: 100px;
68 font-size: .5em;
69 vertical-align: middle;
70 }
71
72 #headlines.archive .pubDate {
width: 130px;
73 }
74
75 .summary {
76 padding-left: 100px;
77 }
78
#headlines.archive .summary {
79 padding-left: 130px;
80 }
81
82
83 /* "You are logged in..." header on admin pages */
84
85 #adminHeader {
86 width: 940px;
padding: 0 10px;
87 border-bottom: 1px solid #00a0b0;
88 margin: -30px 0 40px 0;
89 font-size: 0.8em;
90 }
91
92
93 /* Style the form with a coloured background, along with curved corners
and a drop shadow */
94
95 form {
96 margin: 20px auto;
97 padding: 40px 20px;
98 overflow: auto;
background: #fff4cf;
99 border: 1px solid #666;
100 -moz-border-radius: 5px;
101 -webkit-border-radius: 5px;
102 border-radius: 5px;
103 -moz-box-shadow: 0 0 .5em rgba(0, 0, 0, .8);
-webkit-box-shadow: 0 0 .5em rgba(0, 0, 0, .8);
104 box-shadow: 0 0 .5em rgba(0, 0, 0, .8);
105}
106
107
108/* Give form elements consistent margin, padding and line height */
109
110form ul {
list-style: none;
111 margin: 0;
112 padding: 0;
113}
114
115form ul li {
margin: .9em 0 0 0;
116 padding: 0;
117}
118
119form * {
120 line-height: 1em;
121}
122
123/* The field labels */
124
125label {
126 display: block;
127 float: left;
128 clear: left;
text-align: right;
129 width: 15%;
130 padding: .4em 0 0 0;
131 margin: .15em .5em 0 0;
132}
133
134
135/* The fields */
136input, select, textarea {
137 display: block;
138 margin: 0;
139 padding: .4em;
140 width: 80%;
}
141
142input, textarea, .date {
143 border: 2px solid #666;
144 -moz-border-radius: 5px;
145 -webkit-border-radius: 5px;
146 border-radius: 5px;
background: #fff;
147}
148
149input {
150 font-size: .9em;
151}
152
153select {
padding: 0;
154 margin-bottom: 2.5em;
155 position: relative;
156 top: .7em;
157}
158
textarea {
159 font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
160 font-size: .9em;
161 height: 5em;
162 line-height: 1.5em;
163}
164textarea#content {
165 font-family: "Courier New", courier, fixed;
166}
167
168
169/* Place a border around focused fields */
170
171form *:focus {
border: 2px solid #7c412b;
172
173 outline: none;
174}
175
176
/* Display correctly filled-in fields with a green background */
177
178input:valid, textarea:valid {
179 background: #efe;
180}
181
182
183/* Submit buttons */
184
185.buttons {
text-align: center;
186 margin: 40px 0 0 0;
187}
188
189input[type="submit"] {
190 display: inline;
margin: 0 20px;
191 width: 12em;
192 padding: 10px;
193 border: 2px solid #7c412b;
194 -moz-border-radius: 5px;
195 -webkit-border-radius: 5px;
border-radius: 5px;
196 -moz-box-shadow: 0 0 .5em rgba(0, 0, 0, .8);
197 -webkit-box-shadow: 0 0 .5em rgba(0, 0, 0, .8);
198 box-shadow: 0 0 .5em rgba(0, 0, 0, .8);
199 color: #fff;
background: #ef7d50;
200 font-weight: bold;
201 -webkit-appearance: none;
202}
203
204input[type="submit"]:hover, input[type="submit"]:active {
205 cursor: pointer;
background: #fff;
206 color: #ef7d50;
207}
208
209input[type="submit"]:active {
210 background: #eee;
211 -moz-box-shadow: 0 0 .5em rgba(0, 0, 0, .8) inset;
-webkit-box-shadow: 0 0 .5em rgba(0, 0, 0, .8) inset;
212 box-shadow: 0 0 .5em rgba(0, 0, 0, .8) inset;
213}
214
215
216/* Tables */
217
218table {
width: 100%;
219 border-collapse: collapse;
220}
221
222tr, th, td {
223 padding: 10px;
224 margin: 0;
text-align: left;
225}
226
227table, th {
228 border: 1px solid #00a0b0;
229}
230
th {
231 border-left: none;
232 border-right: none;
233 background: #ef7d50;
234 color: #fff;
cursor: default;
235}
236
237tr:nth-child(odd) {
238 background: #fff4cf;
239}
240
241tr:nth-child(even)
background: #fff;
{
242}
243
244tr:hover {
245 background: #ddd;
246 cursor: pointer;
}
247
248
249/* Status and error boxes */
250
251.statusMessage, .errorMessage {
252 font-size: .8em;
253 padding: .5em;
254 margin: 2em 0;
-moz-border-radius: 5px;
255 -webkit-border-radius: 5px;
256 border-radius: 5px;
257 -moz-box-shadow: 0 0 .5em rgba(0, 0, 0, .8);
258 -webkit-box-shadow: 0 0 .5em rgba(0, 0, 0, .8);
-box-shadow: 0 0 .5em rgba(0, 0, 0, .8);
259}
260
261.statusMessage {
262 background-color: #2b2;
263 border: 1px solid #080;
264 color: #fff;
}
265
266.errorMessage {
267 background-color: #f22;
268 border: 1px solid #800;
269 color: #fff;
270}
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312

I won't go into the details of the CSS, since this tutorial is about PHP and MySQL! Suffice to
say, it styles things like the page layout, colours, fonts, forms, tables and so on.

Last, but not least, our site needs a logo. Here's one I prepared earlier save it in
animages folder inside your cms folder, calling it logo.jpg (or roll your own logo):

All done!
We've finished our CMS! To try it out, open a browser and point it to the base URL of your
CMS (for example, http://localhost/cms/). Click the Site Admin link in the footer, log
in, and add some articles. Then try browsing them on the front end (click the logo to
return to the homepage).

Don't forget you can try out the demo on our server too!

In this tutorial you've built a basic content management system from the ground up, using
PHP and MySQL. You've learnt about MySQL, tables, field types, PDO, object-oriented
programming, templating, security, sessions, and lots more.

While this CMS is pretty basic, it has hopefully given you a starting point for building your
own CMS-driven websites. Some features you might want to add include:

o Pagination on the article archive (front end) and article list (back end) so that the
system can easily handle hundreds of articles
o A WYSIWYG editor for easier content editing

o An image upload facility

o A preview facility, so the admin can see how an article will look before publishing it

o Article categories and tags (I've written a follow-up tutorial on adding categories)

o Integration with Apache's mod_rewrite to create more human-friendly permalink URLs


for the articles (find out how to do this)

o A user comments system

I hope you've enjoyed this tutorial and found it useful. Happy coding!

Learn PHP With Ease!

Buy from Amazon.com

Buy from Amazon.co.uk

Written by Matt Doyle ELATED's resident Web programming expert Beginning PHP
5.3 is a complete introduction to PHP, covering everything in these tutorials and lots more
besides. Find out how to:

o Set up PHP on your computer


o Use strings, arrays, functions and objects
o Create interactive Web forms
o Handle cookies and sessions
o Work with files on the server
o Build database-driven sites with MySQL
o Send emails from your scripts
o Create images on the fly with PHP
o Work with regular expressions
o Write robust, secure PHP applications

...and lots more!

What a pleasure it's been spending hours and hours studying PHP with this
magical book. Lulio, Florida
The book is not only great for learning, but I find myself using it constantly as a
reference as well! David A. Stoltz

Buy Beginning PHP 5.3 now from Amazon.com or Amazon.co.uk.

Share This Page


o

o
o

Link to this page

<a href=
Link HTML:
http://w w w
URL only:

Follow Elated
o

Subscribe to the Elated Newsletter

Get tips, tricks and site updates once a month! Free web template when you sign up! Privacy

Your Email A Send

Related articles
o How to Add Article Categories to Your CMS

o How to Make a Slick Ajax Contact Form with jQuery and PHP

o JSON Basics: What You Need to Know

o PHP References: How They Work, and When to Use Them


o How We Redesigned Elated.com: The Build Process

o Create Nice-Looking PDFs with PHP and FPDF

o PHP For Loops

o PHP While Loops

o The PHP elseif Statement

Responses to this article

20 most recent responses (oldest first):

chrishirst
25-Apr-12 12:39
Windows I can understand because of the different folder/drive structures. Mint, I don't
know what the default paths are yet as i'm just setting up my first mint box up, Centos
is normally my weapon of choice when it comes to Linux. Is "pear" installed and on that
particular path?

[Edited by chrishirst on 25-Apr-12 12:40]


slayerscout
26-Apr-12 20:12
Hi all,

This is a great CMS, but i'm having some issue with using youtubes code for embedding
youtube videos.

I find that when I save to the database it adds "\" all the way along the embed code

<iframe width=\"640\" height=\"360\"

src=\"http://www.youtube.com/embed/1obZFdfL4TM\" frameborder=\"0\"

allowfullscreen></iframe>

Is there anyway of stoppping this from happening?

Thank all
David

[Edited by slayerscout on 26-Apr-12 20:13]


vman
02-May-12 00:50
Great tutorial.

Question about Admin login link on the webpage.

The Admin Login (site admin) link is currently on the same page as blog template.

Can I implement a seperate login page that is not viewable to the public, just to add
another level of security. Basically, when I update by webpage/blog contents, I do it via a
different personal page.
MarcusS
03-May-12 04:52
Hey, first of all, thanks for the great tutorial, it's helped me a lot, but unfortunately I'm
having one problem currently (Similar to slayerscout's)

http://justanotherrandomsite.com/ I've uploaded it to my site, but when I create links or


('s) it keeps adding "\'s" in the code.

It also messes up the links so I can't link to anywhere, because the links come out like (ex):

http://justanotherrandomsite.com\*www.vgchartz.com*\

(When linking to www.vgchartz.com

I didn't have this problem when developing on my PC, but now the website isonly and it
occurs.

Any help would be appreciated, thanks.


revjeff7
04-May-12 15:49
Hey Matt,

Thanks for the great tutorial! I am in the process of learning web programming mostly
from books and articles I am reading online. To help me in the process, I am actually
taking the time to type the code myself instead of cutting and pasting. This is what I've
done with your tutorial, and everything seems to be functioning okay except I can't get the
articles to post to my database. I've checked the code several times alongside your original
code. Everything seems to match up perfectly. Still won't post to the DB though. I either
get the success message when I use the CMS as coded in your original example, or the
"Please fill out this field" callout message appears on the upper left corner of my browser
(Firefox) when I try to integrate tinymce with the form. Any ideas on where I am going
wrong?
Thanks again!!! It always amazes me when people with your great skill take the time to
write comprehensive tutorials like this one to help teach us "newbies".
chotikarn
04-May-12 19:23
@revjeff7

hi, about your problem. it seem that you have "required" parameter on your "textarea" tag.
this should be work fine without any Text Editor such TinyMCE or CKeditor, but when you
need to use it on textarea the "required" parameter have to remove.

if you still have "required" and have Text Editor, HTML will assume that textarea has on
data on it and "Please fill this field" will appear but on my chrome 17 everything look
freezing, not have any sign of error.

PS. hope it might help you.

[Edited by chotikarn on 04-May-12 19:23]


revjeff7
04-May-12 22:28
@chotikarn

Thanks! You were right about the "required" parameter with tinymce. I guess I need to look
for a way to get around this and still be able to use the required parameter on the form.
Also, I still can't get articles to post to my database.
chotikarn
05-May-12 05:22
@revjeff7

maybe jquery form validation might help.

do you re-check your code yet? re-check your typo "__construct" functioin
"storeFormValues" and "insert" function.
ffxpwns
05-May-12 15:35
Hi, I was wondering what I would have to edit to make it so instead of just
year/month/day, I could make it year/month/day at HH:MM. I thought I had it down, but
whenever I try it, it just displays the epoch. Any ideas?
matt
10-May-12 04:39
@slayerscout @MarcusS: Search this topic for "magic quotes".

@vman: You could bookmark the link, then remove the link from the front-end template. If
you're still worried about security, you could password-protect your admin script like
this: http://www.elated.com/articles/password-protecting-your-pages-with-htaccess/

@ffxpwns: Here's the syntax for the PHP date()


function:http://php.net/manual/en/function.date.php
MarcusS
10-May-12 10:29
@ matt

Thanks for the advice, I found the solution yesterday earlier in the topic.
Farmman64
15-May-12 06:13
Dear Matt.

First off let me thank you for your wonderful, concise and clear method of teaching. I am
currently studying Networking and Administration at university and wanted to branch off
and learn some PHP. I will defiantly be sharing your website around.

Following on from http://www.elated.com/forums/topic/5114/#post20643 I have pieced


together a similar method to upload images in the editArticle.php however it appears that
once I insert

$st->bindValue( ":imageId", $this->imageId, PDO::PARAM_STR );

Into the function () Insert and update, the entire article refuses to save to the database,
and transfers me back to listArticles.php admin page without saving.

Article Class

ImageId is defined as

public $imageId = null;

In __construct, imageId is as follows

if ( isset( $_FILES['imageId']['tmp_name'])) $this->imageId =

$data['imageId'];

In the function insert () and update ()


$st->bindValue( ":imageId", $this->imageId, PDO::PARAM_STR );

if ((($_FILES["imageId"]["type"] == "image/gif")

|| ($_FILES["imageId"]["type"] == "image/jpeg")

|| ($_FILES["imageId"]["type"] == "image/pjpeg"))

&& ($_FILES["imageId"]["size"] < 500000))

if (file_exists("articleImageLocation" .

$_FILES["imageId"]["tmp_name"]))

echo $_FILES["imageId"]["tmp_name"] . " already exists. ";

else

move_uploaded_file($_FILES["imageId"]["tmp_name"], "articleImageLocation" .

$_FILES["imageId"]["tmp_name"]);

I have a feeling that the way I have gone about the code to move the uploaded file is
incorrect. Should it be under its own function INSIDE function insert/update? Should it
come before or after $st->bindValue( ":imageId", $this->imageId, PDO::PARAM_STR );

editArticle.php

Inside editArticle.php, I have added the form/multidata tag, as well as

<li>

<label for="file">Image:</label>

<input name="imageId" type="file" "<?php echo( $results['article']-

>imageId )?>" />

</li>

Inside viewArticle.php, I have added the <img> code. Which is correct?


<img src="<?php $article->imageId )?>" alt="article image">

Now what I believed this would do, is it should send the results/location to
Article::ImageID, which should then use the code in the Article class to send the image
filename and location to the database field ImageId, (varchar(255)) which would then be
fetched, along with the rest of the article, when that article is clicked on. What I believe
and what I know though are two different things.

Im not sure how much you could help me with just this code alone, I believe I am missing
some pretty crucial code, but I have no clue what. When I SELECT * from articles; in mysql,
the imageID field is blank.
Please and a massive thank you in advance if you could give me a hint as to what am doing
wrong.
jpkelly
16-May-12 11:55
Any chance of you doing a tutorial on adding images or other media to the CMS articles.
matt
17-May-12 04:58
@MarcusS: Glad you got it working!

@Farmman64 @jpkelly: I'm currently working on a followup CMS tutorial (due next week)
that shows how to upload a single image for each article.
rebelianto
19-May-12 15:18
Problem:
<a href=\\\\\\\"
In index.php insert the code:
<?php
if (get_magic_quotes_gpc() == TRUE) {
ini_set('magic_quotes_gpc','off');

Welcome the http://rotflonline.pl

[Edited by rebelianto on 19-May-12 15:18]


jpkelly
19-May-12 21:37
Great news Matt!
Looking forward to the new tutorial on adding an image to articles.
Marki
22-May-12 16:42
Hello All...!

Very nice CMS system...! Works fine here...!

But, i want to add multiple admin accounts with DB...and not calling a solo "username" and
"password" account from config.php...!

How to construct a multiple admin account based on DB using this plataform (CMS)...?

Basically this workaround is ready...but, i couldnt see a simple form to do this...!

Need some help, example or tut...simple and elegant as this tut...!

Thank you.
jpkelly
28-May-12 01:00
Any word on the progress of the adding images to articles tutorial?
Farmman64
28-May-12 07:18
Haha Kelly, im the same with the wondering. We shouldn't push and pester Matt though,
he is doing this out of the kindness of his own heart!

Looking forward to it though!


jpkelly
28-May-12 10:56
Sorry.... I don't want to pester or be pushy.

View all 315 responses

Vous aimerez peut-être aussi