Creating a Custom WordPress Shortcode

Creating your own WordPress shortcodes is a nifty way to build dynamic elements into your website without relying on 3rd party developers or plugins. This tutorial offers a beginner’s guide to creating your first WordPress plugin which will be referenced by a custom shortcode.

In this example we’ll be creating a basic random quote generator plugin. This can just as easily be achieved through your themes functions.php but I prefer to be able to turn items on and off in WordPress without having to edit blocks of code.

Creating the WordPress Plugin File

If you’re familiar with WordPress plugins and have installed them before, you’ve probably seen folders chock full of complicated PHP classes. This is not necessary for a working plugin; something as simple as a quote generator only needs 1 file. Create your plugin file (in this example we’ll call it gwg_quote.php) and enter the following base info:

<?php
/*
	Plugin Name: gwg Random Quote Generator
	Plugin URI: http://girlswhogeek.com/?p=154
	Description: Basic custom WordPress plugin example: random quote generator
	Version: 1.0
	Author: Girls Who Geek
	Author URI: http://girlswhogeek.com
	License: GPL2

	Copyright 2011 GirlsWhoGeek  (email: jem@girlswhogeek.com)

	This program is free software; you can redistribute it and/or modify
	it under the terms of the GNU General Public License, version 2, as
	published by the Free Software Foundation.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

This is the required plugin info which will be used to populate the WordPress plugins page, and the license information. In this instance we’re using GPL2 because a) it’s what WordPress uses and b) I am a big fan of free code :)

Creating the Plugin Function

Although WordPress supports classes (and encourages their use to prevent naming clashes) it’s too big a subject for the scope of this plugin. Creating a basic PHP function is plenty sufficient. If you’re unfamiliar with the syntax for creating a function with PHP, or have never heard of scope before, I recommend reading PHP Beginner’s Guide Part Six on my development blog (off site).

And so we add the barebones of the function underneath the previously entered licencing information:

<?php
/*
	[license info]
*/

function gwg_random_quote_generator() {

}

I’ve used a descriptive name for the function to make it easier to find/modify later on, and prefixed the function with gwg_ to avoid naming clashes briefly mentioned earlier.

It’s worth noting at this point that if we wanted to create a WordPress shortcode via which we’d pass arguments, e.g. [foo id=#], we’d need to pass an array of attributes to the function. We don’t need this functionality, and as the shortcode will work without it, I’ve left it out for simplicity’s sake. Moving swiftly on…

Inside our new (currently empty) function we place the code which powers the functionality we wish to achieve when we use our shortcode; our random quote generator (you may recognise the code from an old tutorialtastic tutorial…)

<?php
/*
	[license info]
*/

function gwg_random_quote_generator() {
	// define our array
	$quotes = array();

	// populate with quotes
	$quotes[] = "A superior man is modest in his speech, but exceeds in his actions.";
	$quotes[] = "Do not impose on others what you yourself do not desire.";
	$quotes[] = "An ounce of practice is worth more than tons of preaching.";

	// generate a random number between 0 and the total count of $quotes minus 1
	// we minus 1 from the total quotes because array indices start at 0 rather than 1 by default
	$r = rand(0,count($quotes)-1);

	// return the quote in the array with an indices of $r - our random number
	return $quotes[$r];
}

Now that we have the meat of the plugin in place, we can use the WordPress add_shortcode to specify our custom shortcode: add_shortcode( 'randomquotes', 'gwg_random_quote_generator'); This enables us to use An ounce of practice is worth more than tons of preaching. in posts and pages, which WordPress will apply the gwg_random_quote_generator function too.

Altogether now:

<?php
/*
	Plugin Name: gwg Random Quote Generator
	Plugin URI: http://girlswhogeek.com/?p=154
	Description: Basic custom WordPress plugin example: random quote generator
	Version: 1.0
	Author: Girls Who Geek
	Author URI: http://girlswhogeek.com
	License: GPL2

	Copyright 2011 GirlsWhoGeek  (email: jem@girlswhogeek.com)

	This program is free software; you can redistribute it and/or modify
	it under the terms of the GNU General Public License, version 2, as
	published by the Free Software Foundation.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

function gwg_random_quote_generator() {
	// define our array
	$quotes = array();

	// populate with quotes
	$quotes[] = "A superior man is modest in his speech, but exceeds in his actions.";
	$quotes[] = "Do not impose on others what you yourself do not desire.";
	$quotes[] = "An ounce of practice is worth more than tons of preaching.";

	// generate a random number between 0 and the total count of $quotes minus 1
	// we minus 1 from the total quotes because array indices start at 0 rather than 1 by default
	$r = rand(0,count($quotes)-1);

	// return the quote in the array with an indices of $r - our random number
	return $quotes[$r];
}

add_shortcode( 'randomquotes', 'gwg_random_quote_generator');
?>

Save to the gwg_quote.php file you created earlier and upload to /wp-content/plugins/. Once the plugin is enabled via your WordPress admin panel, test it out by popping [randomquotes] into a post or page, and Bob’s your uncle:

A superior man is modest in his speech, but exceeds in his actions.

One basic WordPress plugin with shortcode.

Build a Blog – Tips and Tricks

A few tips and tricks shared by users on the old CodeGrrl forums over the years, to help you customise your blog.

Jump to:

Please note: The Build-A-Blog series is an introduction to creating a simple blog script using PHP. These tutorials are meant to help you to learn PHP and MySQL and to use these to fetch and store data and display it on a web page. These tutorials should not be used ‘as is’ on a production website – especially if you are new to PHP and do not understand what you are doing. We would recommend that you try the B-A-B series on a safe, development environment – such as an offline installation of PHP and MySQL – so you can learn how everything works.

GWG and its staffers accept no responsibility for anything that may (or may not) happen to your site or server as a result of you using these tutorials – you do so AT YOUR OWN RISK.

Archive by month

Change this part in your archive list page:

$result = mysql_query("SELECT FROM_UNIXTIME(timestamp, '%Y') AS get_year, COUNT(*) AS entries FROM php_blog GROUP BY get_year");

To this:

$result = mysql_query("SELECT FROM_UNIXTIME(timestamp, '%M %Y') AS get_month, COUNT(*) AS entries FROM php_blog GROUP BY get_month ORDER BY timestamp ASC"); 

(Note: If you want your most recent entries first, change ASC to DESC in the query above)

Next, change this:

while ($row = mysql_fetch_array($result)) {
    $get_year = $row['get_year'];
    $entries = $row['entries'];

    echo "<a href=\"archives.php?year=" . $get_year . "\">Entries from " . $get_year . "</a> (" . $entries . ")<br />";
}

To this:

while ($row = mysql_fetch_array($result)) {
	$get_month = $row['get_month'];
	$entries = $row['entries'];
	echo "<a href=\"archives.php?month=" . $get_month . "\">Entries from " . $get_month . "</a> (" . $entries . ")<br />";
}

Then, in your archive display page, change this:

$result = mysql_query("SELECT timestamp, id, title FROM php_blog WHERE FROM_UNIXTIME(timestamp, '%Y') = '$year' ORDER BY id DESC");

To this:

$result = mysql_query("SELECT timestamp, id, title FROM php_blog WHERE FROM_UNIXTIME(`timestamp`, '%M %Y') = '" . mysql_real_escape_string($month) . "' ORDER BY id DESC")

And this:

if (!isset($_GET['year'])) {
    die("Invalid year specified.");
}
else {
    $year = (int)$_GET['year'];
}

To this:

if (!isset($_GET['month'])) {
    die("Invalid month specified.");
}
else {
    $month = htmlentities(strip_tags($_GET['month']));
}

Remember to change any other parts that say $year in your page to $month too.

Spam protection

Tip 1: Based on Jem’s original BellaBook spam protection.

In your individual entries page, put this just after the comment box, but before the submit button:

<script type="text/javascript">
<!--
document.write('<input type="hidden" name="spamtest" id="spamtest" value="SPAMWORD" />');
//-->
</script>

Change SPAMWORD to something random, no one will have to remember this so make it as complicated as you like.

Then, in process.php, find this part (should be right at the top):

<?php
if (isset($_POST['submit_comment'])) {

Add this underneath:

if (!isset($_POST['spamtest']) || $_POST['spamtest'] != "SPAMWORD") {
   exit("<p>JavaScript must be enabled to comment here due to spam restrictions in place.</p>");
}

Again, replace SPAMWORD with the word you used earlier.

Tip 2: Simple maths problem

Add a new field to your comments form with a sum of your choice, e.g. 1+2.

1+2 = <input type="text" size="5" name="sumtest" id="sumtest" />

Then, in process.php, add this in the same place as for tip 1 (under the first two lines):

if (!isset($_POST['sumtest']) || $_POST['sumtest'] != 3) {
    exit('<p>Sorry, the answer to the maths problem was not correct.</p>');
}

You can change the sum whenever you want, just remember to change the answer (in this case 3) in process.php.

Note that these are extremely simple spam prevention measures and spammers may learn to get around them eventually. You can use both tips together – it doesn’t matter which bit comes first in process.php as long as they are both there.

Remove redirection after updating a post

If you would like to go back to the edit page after saving an entry, instead of to the entry itself, try the following:

Find this line:

header("Location: journal.php?id=" . $id);

Change it to:

header("Location: update.php?id=" . $id);

Where update.php is the name of your update page. Note: the page will appear to have just refreshed when you save your entry, but it will have been saved (check the contents of your entry to make sure!).

Comment notification emails

Find this line in process.php:

$result = mysql_query("INSERT INTO php_blog_comments (entry, timestamp, name, email, url, comment) VALUES ('$entry','$timestamp','$name','$email','$url','$comment')");

Add this underneath:

    $youraddress = "YOU@YOURSITE.COM";
    $emailsubject = "New comment on entry #" . $entry;
    $bodyemail = "A new comment was posted on entry #" . $entry . " of your blog. The comment was made by " . $name . " (" . $email . "; " . $url. ") on " . date('d.m.y \a\\t H:i',$timestamp) . " and their message was "" . $comment . "".";
    $extra = "From: YOUR SITE NAME <" . $youraddress . ">\r\n" . "X-Mailer: PHP/" . phpversion();
    mail($youraddress, $emailsubject, $bodyemail, $extra);

Change items in uppercase, such as the email address and YOUR SITE NAME. If you would like to change the date/time format, change this part:

date('d.m.y \a\\t H:i',$timestamp)

Change the letters in the first part (i.e. d.m.y \a\\t H:i – leave the rest as it is or it won’t work) using the guide on php.net.

Show an excerpt of a post with read more link

Open up the main page of your blog, find the following code:

    $title = stripslashes($row['title']);
    $entry = stripslashes($row['entry']);
    $password = $row['password'];
    $id = $row['id'];

And beneath that paste this code:

if (strlen($entry) > 90) {
    $entry = substr($entry, 0, 90);
    $entry = "$entry... <br /><br /><a href=\"journal.php?id=" . $id . "\">read more</a>";
}

This is set to show an excerpt of 90 characters of the post. If you would like to show more (or less), just change 90 to another number.

Emoticons and BBCode

Note: this is for comments only.

Find this part in journal.php:

$sql = "SELECT * FROM php_blog_comments WHERE entry='$id' ORDER BY timestamp";
$result = mysql_query ($sql) or print ("Can't select comments from table php_blog_comments.<br />" . $sql . "<br />" . mysql_error());
while($row = mysql_fetch_array($result)) {
    $timestamp = date("l F d Y", $row['timestamp']);

Add this below:

$bbcode = array('[b]', '[i]', '[u]', '[/b]', '[/i]', '[/u]');
$bbcode_replace = array('<b>', '<i>', '<u>', '</b>', '</i>', '</u>');

$emoticons = array(":)", ":(", ":'(", ";)", ":P", ":o", ":D");
$emoticons_replace = array(
    '<img src="smile.gif" alt=":)" />',
    '<img src="sad.gif" alt=":(" />',
    '<img src="cry.gif" alt=":\'(" />',
    '<img src="wink.gif" alt=";)" />',
    '<img src="tongue.gif" alt=":P" />',
    '<img src="shocked.gif" alt=":o" />',
    '<img src="happy.gif" alt=":D" />'
);
$formatted_comment = str_replace($bbcode, $bbcode_replace, stripslashes($row['comment']));
$formatted_comment = str_replace($emoticons, $emoticons_replace, $formatted_comment);

Replace the parts in the <img src=""> parts with the correct URLs to your images (note you will need to have images already, we can’t provide these for you!). Feel free to add more emoticons, just add more options to the $emoticons part (before the ‘);’ – make sure to separate each one by commas and and enclose them in quotes). Add its corresponding image in a new line to the $emoticons_replace part, before the ‘);’ at the bottom. Each line should have a comma at the end EXCEPT the last line, and each line should be surrounded by quotes (I have used single quotes so as not to interfere with the double quotes used in the HTML). Same goes for the BBCode – feel free to add more items you want to allow, such as [code] or [quote]. Remember to add the closing tags as well, or your comments page will look very messy!

Note: it is very important that you specify the images and replacement HTML code in the same order that you specified the image and bbcode codes, otherwise you will get the wrong image/HTML replacement in your comments!

When you have done that, find this part in journal.php:

print("<p>" . stripslashes($row['comment']) . "</p>");

Change it to:

print("<p>" . $formatted_comment . "</p>");

External Link Hit Counter

If you own a resource website with a links page you may want to keep track of the links people are visiting so you can adjust your content and the links you add to suit. This simple external link hit counter will be able to help.

First, we need to create a database, and table in which we can store the links the referrers. We’ll do this with MySQL. Create a file called create.php and place the following code in it:

<?php
$connect = mysql_connect("localhost", "MYSQL_USER", "MYSQL_PASS");

$createdb = "CREATE DATABASE `linksdb`";
$createtbl = "CREATE TABLE `links` (
   `id` INT NOT NULL AUTO_INCREMENT,
   `siteurl` VARCHAR(255) NOT NULL,
   `sitename` VARCHAR(150) NOT NULL,
   PRIMARY KEY (`id`))";

$createtbl2 = "CREATE TABLE `linksref` (
   `id` INT NOT NULL AUTO_INCREMENT,
   `linkid` INT NOT NULL,
   `referrer` VARCHAR(255) NOT NULL,
   `hitsout` INT NOT NULL,
   PRIMARY KEY (`id`))";

if (mysql_query($createdb, $connect)) {
   echo "<p>Database created.</p>";

   mysql_select_db("linksdb", $connect);
   if (mysql_query($createtbl, $connect) && mysql_query($createtbl2, $connect)) {
      echo "<p>Tables created.</p>";
   } else {
      echo mysql_error();
   }
} else {
   echo mysql_error();
}
?>

Before you run the page in your browser, edit “MYSQL_USER” and “MYSQL_PASS” to match your MySQL username and password. If you don’t know your details and don’t know how to get them, get in touch with your host and they should be able to help.

Let’s go through the code that we’ve just pasted above. First things first we ‘open’ our PHP and declare the connection string — this will tell our mini script where to connect to MySQL, and who to connect as. The next bit is pretty self-explanatory: create a database called “linksdb” (those slanty apostrophe lookie-likies can be achieved by clicking the key next to the number ’1′ (usually)). After that we create the table that’ll store our links with the fields: id, siteurl, sitename and hitsout; and the table that’ll store our referrers: referrer and hitsout.

The next line of code is an if statement, this checks whether or not the create database statement we typed above can actually run. If it can, it’ll echo a success sentence and then continue on to create the table. If that is successful, it will also echo a success statement. If the database creation string cannot be created you’ll get an error, and the table creation won’t even be attempted. Anyone getting errors with the creation can e-mail the error/s to me at jem@jemjabella.co.uk.

Once the database and table has been created, you can delete create.php and create links.php. In links.php we’ll ‘print’ the links we’ll be storing in our database and link to the file that will handle adding to the clicks out count. Let’s start by adding one of those mysql connection strings again:

<?php
$connect = mysql_connect("localhost", "MYSQL_USER", "MYSQL_PASS");
mysql_select_db("linksdb", $connect);

?>

Next we’ll pull our links using a mysql_query() and use while() plus the handy built in mysql_fetch_assoc() function which will put the entries from our table (our links) into an array to make echo-ing easier. When we do echo the data, we don’t want to show a link directly to the site/s that we’re actually linking to because we’d not be able to count the hits out. Instead, we link to a new file that we’re going to create in a moment using the unique id stored with each link to identify it and therefore that means links.php should now look like this:

<ul>
<?php
$connect = mysql_connect("localhost", "MYSQL_USER", "MYSQL_PASS");
mysql_select_db("linksdb", $connect);

$query = mysql_query("SELECT `links`.`id`, `links`.`sitename`, SUM(`linksref`.`hitsout`) AS `hitsout` FROM `links` LEFT JOIN `linksref` ON `links`.`id` = `linksref`.`linkid` GROUP BY `linksref`.`linkid`", $connect);
while($row = mysql_fetch_assoc($query)) {
   echo '<li><a href="linkcount.php?id='.$row['id'].'">'.$row['sitename'].'</a> ('.$row['hitsout'].')</li> ';
}
?>
</ul>

That’s actually it for the links.php file. Of course, if you don’t want them in an unordered list you could swap <ul> [...] </ul> for <table> [...] </table> and <li> [...] </li> for <td> [...] </td>. Now, onto the important bit. Create linkcount.php and paste the following code in:

<?php
$connect = mysql_connect("localhost", "MYSQL_USER", "MYSQL_PASS");
mysql_select_db("linksdb", $connect);

if (isset($_GET['id']) && is_numeric($_GET['id'])) {
	$id = (int)$_GET['id'];

	$strip = array("http://", "www.");
	$referrer = str_replace($strip, "", rtrim($_SERVER['HTTP_REFERRER'], "/"));

	$findLink = mysql_query("SELECT `links`.`siteurl` FROM `links` WHERE `links`.`id` = ".(int)$id." LIMIT 1");
	if (mysql_num_rows($findLink) == 0) {
	   header("Location: links.php");
	   exit;
	} else {
		$url = mysql_result($findLink, 0, 0);

		$findReferrer = mysql_query("SELECT `id` FROM `linksref` WHERE `linkid` = ".(int)$id." AND `referrer` = '".mysql_real_escape_string($referrer)."' LIMIT 1");
		if (mysql_num_rows($findReferrer) == 1) {
			$id = mysql_result($findReferrer, 0, 0);
			mysql_query("UPDATE `linksref` SET `hitsout` = `hitsout` + 1 WHERE `id` = ".(int)$id);
		} else {
			mysql_query("INSERT INTO `linksref` (`linkid`, `referrer`, `hitsout`) VALUES (".(int)$id.", '".mysql_real_escape_string($referrer)."', 1)");
		}

		header("Location: ".$url);
		exit;
	}
} else {
	exit;
}
?>

I’ve copied the code in one go to prevent mistakes that might occur if it was pasted bit by bit as this is one of the most important parts of our script. Briefly explained: the script connects to the database; makes sure that the id submitted via the URL is in fact a number (integer); returns you to the links page if the id isn’t a number or doesn’t exist in the database; adds to the hitsout count if the link does exists and last but not least, redirects the user to the requested URL.

Once you’ve uploaded the files (don’t forget to delete ‘create.php’ when you’ve run it) and added the links to your database, you’re ready to roll. To add links you will have to use PHPMyAdmin, until I can get ’round to writing a second tutorial on creating a link management panel.

Basic HTML: Image Tags and alt Attributes

Once you’ve created the basic structure of your web page and added some text in paragraphs and headers you’ll probably want to spice things up with an image or two, and maybe a few links to your favourite websites.

Images

Images are used one web pages for two primary reasons: 1) to illustrate an example or show something relevant to the text of the web page, and 2) to make the web page look pretty. Although in the name of semantics, images used purely to improve the presentation of a page should be referenced via CSS (which we’ll learn about later), it is important to get the most basic way of adding an image right first.

To display an image, we use the <img> tag. Like <p> for paragraphs, <img> is easy to remember because it’s a shortened version of “image”. So that the browser knows which image we want to display, we can specify the path to the image with the src attribute. Confused? Read on..

What’s a path?

The “path”, in website terms, is the location of the file you’re working with. There are two types of path: relative, and absolute.

An absolute path is the full path from the very bottom directory of your webserver. This is mostly used in scripting/web programming and if you’re using freewebs (which doesn’t support the likes of PHP on free accounts) probably won’t be important to you. On a paid host, most absolute paths start with /home/username/public_html/ (where username is the name of your account).

The relative path, which is more commonly used in HTML, is the path to the file relative to the folder you’re in. If you’re in the main/root directory in which your index.htm resides, and have an images folder with a file called “me.jpg” inside, the path to that file would be images/me.jpg

What is an attribute?

An attribute is typically a property of an object. In HTML, an attribute is used to specify a property in an element (tag) to control its behaviour. Attributes are included inside of the tag, separated from the tag name with a space, with the value of the attribute inside quotation marks. It all sounds more complicated than it really is!

As you may have guessed, the “src” attribute is short for “source”, i.e. the source of the image. Going back to our example of a relative path, let’s assume you have the image (images/me.jpg) mentioned. You’d display that image like so:

<img src="images/me.jpg">

(If you’re following this tutorial in Freewebs, now would be a good time to create a folder. Using the File Manager tool, click ‘add a folder’ and call it images.)

Because we haven’t actually uploaded an image called me.jpg yet, nothing will display on your web page (or a red cross, if you’re using Internet Explorer). Sometimes even when images are online, they don’t display — either because of server problems, browser settings or preferences — or a person is visually impaired and cannot see the image. To ensure a complete browsing experience, it is good practise to provide a text equivalent in case of that scenario, and we can do that with the alt attribute.

Note: alt is not a tag

You may have heard about alt before, often incorrectly called the “alt tag”. alt is not a tag, it is an attribute. It in no way behaves like a tag so there is no reason to call it such. Back to our image…

When specifying an alt attribute, it is worth bearing in mind that it is an alternative to the image. If the image is purely decorational, some sort of graphical flourish or drop shadow for example, the page is likely to remain usable without alternative text and therefore the alt attribute should remain blank. However, in the case of “me.jpg” we’re referencing a picture of you and so a brief description is best. For example:

<img src="images/me.jpg" alt="my school picture, wearing a red jumper">

Now that we’ve got the hang of alt attributes and our <img> tag, we can upload a picture named “me.jpg” to our images folder and see the result:

first web page with pic

Checklist

Things you should now know:

  • We put images on our pages with the image tag
  • We use the src and alt attributes to show our image, and alternate text
  • alt is not a tag, but it is important
  • Relative paths (e.g. images/me.jpg) are most commonly used in HTML

Advanced Image Rotation

Although my original tutorial on image rotations still works, it’s major flaw is the dependency on the script creator to enter a list of images to rotate. While this is not a problem if you only want to rotate a few images, and don’t intend on adding to it after creation, it will become a chore if it’s an ever-updated thing.

The solution to this problem is to create a directory for your images, and use a built in PHP function called glob() to go through that directory and pick up the images for rotation. That way, when you wish to add new images to the rotation it’s as simple as uploading them to the correct directory.

glob() reads the contents of a directory into an array, based on a given pattern. As we’re creating an image rotator, it is likely that we’ll want to pick up any images in our directory with a file extension of .jpg, .gif or .png. We can use the wildcard operator to find all files of these types and put them in our array, using the “GLOB_BRACE” flag to match all 3 possibilities, like so:

$files = glob('{directory/*.png,directory/*.gif,directory/*.jpg}', GLOB_BRACE);

The next step is to simply create a random number using PHP, match it to a key in the array and echo the array value (the image). Because array keys start from 0 (so an array with 10 entries will have the keys 0 through to 9) it is important that when we calculate the maximum random number we can have, we take 1 off to match key value. The full code:

$files = glob('{directory/*.png,directory/*.gif,directory/*.jpg}', GLOB_BRACE);
$image = rand(0, (count($files) - 1));
echo '<img src="'.$files[$image].'" alt="random image" />';

Note: the file path is included in the glob() result array, so you do not need to specify “directory/” in the image path.

Working With CSV Flat Files: Storage

If your server supports PHP but not MySQL — or perhaps you don’t want to learn SQL syntax — you may be despairing of finding an effective storage solution? Don’t lose hope; you can store all sorts of content in plain text files as simple as pie. (Mmm, pie!)

What’s a “CSV”?

CSV stands for “Comma Separated Values” and is a flat file format. Its usage stretches back to the early days of computing, so finding programs to open and edit CSVs isn’t hard and there’s no reason why that compatibility can’t be extended to the web. CSV files are commonly used in place of more complex database solutions, where one CSV file is equivalent to one table in a standard database. Each entry in the CSV is the same as a row in a database.

Although the “Comma” in CSV usually indicates values are separated by exactly that (a comma: “,”) any separator can be used. However, there are usually no spaces left between separators. E.g. 23,John,Doe would be an acceptable way to store Age, First Name, Last Name, but 23, John, Doe would not.

Sometimes you may need to use the separator inside one of the field values. So that the separator is not processed as a field deliminator, the entire field must be wrapped in quotation marks, like so: 23,John,Doe,"Likes: chocolate, pie".

Creating a CSV

Although you can actually save files with a .csv extension to indicate a CSV file, it is just as feasible to save them as .txt or any other file type. As long as the contents remain consistent and follow the CSV “rules” as laid out above, the filetype is not important in this context. I prefer to use .txt

Creating a .txt for the purpose of a CSV is exactly the same as creating a normal .txt

Storing Data in our CSV

For the purpose of this tutorial I’ve decided to use a website survey system, just like the MySQL Storage tutorial. This will allow you to see the similarities between a flat file database and a MySQL database.

The Form

For the sake of brevity, I’m recycling the form code from the MySQL Storage tutorial. This is as follows (and should be saved as form.php or something similar):

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">

<html>
<head>
    <title>Visitor Survey Form</title>
</head>
<body>
    <form method="post" action="form-process.php">

    Name:<br>
    <input type="text" name="name"><br>
    E-mail:<br>
    <input type="text" name="email"><br>
    Country:<br>
    <select name="country">
        <option>United Kingdom</option>
        <option>United States</option>
        <option>Other</option>
    </select><br>
    Overall Score:<br>
    <select name="score">
        <option>1</option>
        <option>2</option>
        <option>3</option>
        <option>4</option>
        <option>5</option>
    </select><br>
    Comments:<br>
    <textarea name="comments" rows="10" cols="30"></textarea><br>

    <input type="submit" name="submit">

    </form>
</body>
</html>

We now know our fields are as follows: Name, E-mail, Country, Score and Comments — this will help us form a structure for our CSV.

Processing the Data

The next step is to take the data and process it ready to store. This “processing” involves cleaning the data of any HTML, checking for common exploits and just making sure everything is hunky dory. To save time and to prevent me from needlessly repeating myself, you can read the PHP Mail Form tutorial for detailed information on cleaning the data and implementing spam protection. This code is pretty generic and will work well in any script that deals with fetching data from forms. In short, the code we need is as follows:

<?php
if (!isset($_POST['submit']) || $_SERVER['REQUEST_METHOD'] != "POST") {
    exit("<p>You did not press the submit button; this page should not be accessed directly.</p>");
} else {
    $exploits = "/(content-type|bcc:|cc:|document.cookie|onclick|onload|javascript|alert)/i";
    $profanity = "/(beastial|bestial|blowjob|clit|cock|cum|cunilingus|cunillingus|cunnilingus|cunt|ejaculate|fag|felatio|fellatio|fuck|fuk|fuks|gangbang|gangbanged|gangbangs|hotsex|jism|jiz|kock|kondum|kum|kunilingus|orgasim|orgasims|orgasm|orgasms|phonesex|phuk|phuq|porn|pussies|pussy|spunk|xxx)/i";
    $spamwords = "/(viagra|phentermine|tramadol|adipex|advai|alprazolam|ambien|ambian|amoxicillin|antivert|blackjack|backgammon|texas|holdem|poker|carisoprodol|ciara|ciprofloxacin|debt|dating|porn)/i";
    $bots = "/(Indy|Blaiz|Java|libwww-perl|Python|OutfoxBot|User-Agent|PycURL|AlphaServer)/i";

    if (preg_match($bots, $_SERVER['HTTP_USER_AGENT'])) {
        exit("<p>Known spam bots are not allowed.</p>");
    }
    foreach ($_POST as $key => $value) {
        $value = trim($value);

        if (empty($value)) {
            exit("<p>Empty fields are not allowed. Please go back and fill in the form properly.</p>");
        } elseif (preg_match($exploits, $value)) {
            exit("<p>Exploits/malicious scripting attributes aren't allowed.</p>");
        } elseif (preg_match($profanity, $value) || preg_match($spamwords, $value)) {
            exit("<p>That kind of language is not allowed through our form.</p>");
        }

		// add slashes to prevent quotation marks/etc breaking our CSV
        if (!get_magic_quotes_gpc()) $value = addslashes($value);
        $$key = strip_tags($value);
    }

    if (!ereg("^[_a-z0-9-]+(.[_a-z0-9-]+)*@[a-z0-9-]+(.[a-z0-9-]+)*(.[a-z]{2,6})$",strtolower($email))) {
        exit("<p>That e-mail address is not valid, please use another.</p>");
    }

	// Data is checked and cleaned, we can store it now...

}
?>

We now have a gap in which we’ll be placing our storage code. PHP has lots of built in file manipulation functions which allow us to easily interact with the .txt file we created earlier. In an ideal world, we’d use one line of code and sort it all with file_put_contents() but that is only supported in PHP5 and above. While a lot of hosts rely on PHP4 we need to stick to the most compatible code; that is, using fopen(), fwrite() and fclose() successively.

fopen(), fwrite() and fclose()

To manipulate a file in PHP, we first need to “open” it for reading and writing. In our case we’d want to write to it to store our survey information, so we use the write mode (“a”). We assign the open file to a variable so that PHP knows which file we’re working with further down. Sometimes, if the file is not available or if the permissions are incorrect, fopen() will return an error; to prevent wouldbe malicious users from using any information returned to crack our system, we can customise the error message too:

$file = fopen("surveyfile.txt", "a") or die("File couldn't be opened");

Once the file is open, we can prepare our data and write it to the file. In true and “proper” CSV form, we shall use commas to separate data, and just in case the Comments contain commas we’ll wrap that field in quotation marks (escaped with backslashes, to prevent PHP thinking we’re done with the variable too early):

$data = "$name,$email,$country,"$comments"";
fwrite($file, $data);

To warn us in case of data writing failure, we could make things a bit more complex and change the simple fwrite() to include an if statement, but otherwise it’s a very simple procedure.

Once done, we close the file again to free it up for other users to write to (and that’s the simplest part of all)

fclose($file);

All together, including in our form processing we have:

<?php
if (!isset($_POST['submit']) || $_SERVER['REQUEST_METHOD'] != "POST") {
    exit("<p>You did not press the submit button; this page should not be accessed directly.</p>");
} else {
    $exploits = "/(content-type|bcc:|cc:|document.cookie|onclick|onload|javascript|alert)/i";
    $profanity = "/(beastial|bestial|blowjob|clit|cock|cum|cunilingus|cunillingus|cunnilingus|cunt|ejaculate|fag|felatio|fellatio|fuck|fuk|fuks|gangbang|gangbanged|gangbangs|hotsex|jism|jiz|kock|kondum|kum|kunilingus|orgasim|orgasims|orgasm|orgasms|phonesex|phuk|phuq|porn|pussies|pussy|spunk|xxx)/i";
    $spamwords = "/(viagra|phentermine|tramadol|adipex|advai|alprazolam|ambien|ambian|amoxicillin|antivert|blackjack|backgammon|texas|holdem|poker|carisoprodol|ciara|ciprofloxacin|debt|dating|porn)/i";
    $bots = "/(Indy|Blaiz|Java|libwww-perl|Python|OutfoxBot|User-Agent|PycURL|AlphaServer)/i";

    if (preg_match($bots, $_SERVER['HTTP_USER_AGENT'])) {
        exit("<p>Known spam bots are not allowed.</p>");
    }
    foreach ($_POST as $key => $value) {
        $value = trim($value);

        if (empty($value)) {
            exit("<p>Empty fields are not allowed. Please go back and fill in the form properly.</p>");
        } elseif (preg_match($exploits, $value)) {
            exit("<p>Exploits/malicious scripting attributes aren't allowed.</p>");
        } elseif (preg_match($profanity, $value) || preg_match($spamwords, $value)) {
            exit("<p>That kind of language is not allowed through our form.</p>");
        }

		// add slashes to prevent quotation marks/etc breaking our CSV
        if (!get_magic_quotes_gpc()) $value = addslashes($value);
        $$key = strip_tags($value);
    }

    if (!ereg("^[_a-z0-9-]+(.[_a-z0-9-]+)*@[a-z0-9-]+(.[a-z0-9-]+)*(.[a-z]{2,6})$",strtolower($email))) {
        exit("<p>That e-mail address is not valid, please use another.</p>");
    }

	// Data is checked and cleaned, we can store it now...
	$file = fopen("surveyfile.txt", "a") or die("File couldn't be opened");
	$data = "$name,$email,$country,"$comments"";
	if (fwrite($file, $data))
		echo '<p>Survey data successfully added :)</p>';
	fclose($file);
}
?>

Save the code as form-process.php, and that is that. You now have a simple script which stores survey data in a small text file, all ready for displaying on your web page!