MANAGING CONTENT 381 You can rewrite it like this: $result = @ $conn->query($sql); if (!$result) { // redirect to custom error page } You should also remove the conditional statements surrounding MySQLi prepared statements once you have verified that they dont generate SQL syntax errors. For example, your development code might look like this: if ($stmt->prepare($sql)) { $stmt->bind_param('s', $searchterm); $stmt->bind_result($image_id, $filename, $caption); $stmt->execute(); $stmt->store_result(); $numRows = $stmt->num_rows; } else { echo $stmt->error; } To deploy it on a live website, change it to this: $stmt->prepare($sql); $stmt->bind_param('s', $searchterm); $stmt->bind_result($image_id, $filename, $caption); $stmt->execute(); $stmt->store_result(); $numRows = $stmt->num_rows; Chapter review Content management with a database involves inserting, selecting, updating, and deleting records. Each records primary key plays a vital role in the update and delete processes. Most of the time, generating the primary key is handled automatically by MySQL when a record is first created. Thereafter, finding a records primary key is simply a matter of using a SELECT query, either by displaying a list of all records, or by searching for something you know about the record, such as a title or words in an article. MySQLi and PDO prepared statements make database queries more secure by removing the need to ensure that quotes and control characters are properly escaped. They also speed up your application if the same query needs to be repeated during a script using different variables. Instead of validating the SQL every time, the script needs do it only once with the placeholders. Although this chapter has concentrated on content management, the same basic techniques apply to most interaction with a database. Of course, theres a lot more to SQL—and to PHP. In the next chapter, Ill address some of the most common problems, such as displaying only the first sentence or so of a long text field and handling dates. Then, in Chapter 15 and 16, well explore working with more than one table in a database. CHAPTER 13 382 Download from Wow! eBook <www.wowebook.com> 383 Chapter 14 Formatting Text and Dates We have some unfinished business left over from the previous chapter. Figure 13-1 in Chapter 13 shows content from the blog table with just the first two sentences of each article displayed and a link to the rest of the article. However, I didnt show you how it was done. There are several ways to extract a shorter piece of text from the beginning of a longer one. Some are rather crude and usually leave you with a broken word at the end. In this chapter, youll learn how to extract complete sentences. The other piece of unfinished business is that full list of articles in blog_list_mysqli.php and blog_list_pdo.php displays the MySQL timestamp in its raw state, which isnt very elegant. You need to reformat the date to look user friendlier. Handling dates can be a major headache because MySQL and PHP use completely different methods of storing them. This chapter guides you through the minefield of storing and displaying dates in a PHP/MySQL context. Youll also learn about the powerful new date and time features introduced in PHP 5.2 and 5.3, which make complex date calculations, such as finding the second Tuesday of each month, childs play. In this chapter, youll learn about the following: • Extracting the first section of a longer text item • Using an alias in a SQL query • Displaying text retrieved from a database as paragraphs • Formatting dates with MySQL • Selecting records based on temporal criteria • Using the PHP DateTime, DateTimeZone, DateInterval, and DatePeriod classes Displaying a text extract There are many ways to extract the first few lines or characters from a longer piece of text. Sometimes, you need just the first 20 or 30 characters to identify an item. At other times, its preferable to show complete sentences or paragraphs. CHAPTER 14 384 Extracting a fixed number of characters You can extract a fixed number of characters from the beginning of a text item either with the PHP substr() function or with the LEFT() function in a SQL query. Using the PHP substr() function The substr() function extracts a substring from a longer string. It takes three arguments: the string you want to extract the substring from, the starting point (counted from 0), and the number of characters to extract. The following code displays the first 100 characters of $row['article']: echo substr($row['article'], 0, 100); The original string remains intact. If you omit the third argument, substr() extracts everything to the end of the string. This makes sense only if you choose a starting point other than 0. Using the MySQL LEFT() function The MySQL LEFT() function extracts characters from the beginning of a column. It takes two arguments: the column name and the number of characters to extract. The following retrieves article_id, title, and the first 100 characters from the article column of the blog table: SELECT article_id, title, LEFT(article, 100) FROM blog ORDER BY created DESC Whenever you use a function in a SQL query like this, the column name no longer appears in the result set as article, but as LEFT(article, 100) instead. So its a good idea to assign an alias to the affected column using the AS keyword. You can either reassign the columns original name as the alias or use a descriptive name as in the following example (the code is in blog_left_mysqli.php and blog_left_pdo.php in the ch14 folder): SELECT article_id, title, LEFT(article, 100) AS first100 FROM blog ORDER BY created DESC If you process each record as $row, the extract is in $row['first100']. To retrieve both the first 100 characters and the full article, simply include both in the query like this: SELECT article_id, title, LEFT(article, 100) AS first100, article FROM blog ORDER BY created DESC Taking a fixed number of characters produces a very crude result, as Figure 14-1 shows. Figure 14-1. Selecting the first 100 characters from an article chops words in half. FORMATTING TEXT AND DATES 385 Ending an extract on a complete word To end an extract on a complete word, you need to find the final space and use that to determine the length of the substring. So, if you want the extract to be a maximum of 100 characters, use either of the preceding methods to start with, and store the result in $extract. Then you can use the PHP string functions strrpos() and substr() to find the last space and end the extract like this (the code is in blog_word_mysqli.php and blog_word_pdo.php): $extract = $row['first100']; // find position of last space in extract $lastSpace = strrpos($extract, ' '); // use $lastSpace to set length of new extract and add echo substr($extract, 0, $lastSpace) . ' '; This produces the more elegant result shown in Figure 14-2. It uses strrpos(), which finds the last position of a character within another string. Since youre looking for a space, the second argument is a pair of quotes with a single space between them. The result is stored in $lastSpace, which is passed as the third argument to substr(), finishing the extract on a complete word. Finally, add a string containing three dots and a space, and join the two with the concatenation operator (a period or dot). Figure 14-2. Ending the extract on a complete word produces a more elegant result. Extracting the first paragraph Assuming that you have entered your text in the database using the Enter or Return key to indicate new paragraphs, this is very easy. Simply retrieve the full text, use strpos() to find the first new line character, and use substr() to extract the first section of text up to that point. The following SQL query is used in blog_para_mysqli.php, and blog_para_pdo.php: SELECT article_id, title, article FROM blog ORDER BY created DESC The following code is used to display the first paragraph of article: echo substr($row['article'], 0, strpos($row['article'], PHP_EOL)); If that makes your head spin, then lets break it up and take a look at the third argument on its own: strpos($row['article'], PHP_EOL) This locates the first end of line character in $row['article'] in a cross-platform way using the PHP_EOL constant (see Chapter 7). You could rewrite the code like this: $newLine = strpos($row['article'], PHP_EOL); echo substr($row['article'], 0, $newLine); CHAPTER 14 386 Both sets of code do exactly the same thing, but PHP lets you nest a function as an argument passed to another function. As long as the nested function returns a valid result, you can frequently use shortcuts like this. Using the PHP_EOL constant eliminates the problem of dealing with the different characters used by Linux, Mac OS X, and Windows to insert a new line. Displaying paragraphs Since were on the subject of paragraphs, many beginners are confused by the fact that all the text retrieved from a database is displayed as a continuous block, with no separation between paragraphs. HTML ignores whitespace, including new lines. To get text stored in a database displayed as paragraphs, you have the following options: • Store your text as HTML. • Convert new lines to <br /> tags. • Create a custom function to replace new lines with paragraph tags. The first option involves installing an HTML editor, such as CK Editor (http://ckeditor.com/) or TinyMCE (http://tinymce.moxiecode.com/) in your content management forms. Mark up your text as you insert or update it. The HTML is stored in the database, and the text displays as intended. Installing one of these editors is beyond the scope of this book. The simplest option is to pass your text to the nl2br() function before displaying it like this: echo nl2br($row['article']); Voilà!—paragraphs. Well, not really. The nl2br() function converts new line characters to <br /> tags. As a result, you get fake paragraphs. Its a quick and dirty solution, but not ideal. The nl2br() function inserts the slash before the closing angle bracket for compatibility with XHTML. The trailing slash is optional in HTML5, so your code remains valid even if youre not using XHTML-style markup. To display text retrieved from a database as genuine paragraphs, wrap the database result in a pair of paragraph tags, and then use the preg_replace() function to convert consecutive new line characters to a closing </p> tag immediately followed by an opening <p> tag like this: <p><?php echo preg_replace('/[\r\n]+/', '</p><p>', $row['article']); ?></p> The regular expression used as the first argument matches one or more carriage returns and/or newline characters. You cant use the PHP_EOL constant here because you need to match all consecutive newline characters and replace them with a single pair of paragraph tags. Remembering the pattern for a regex can be difficult, so you can easily convert this into a custom function like this: function convertToParas($text) { $text = trim($text); return '<p>' . preg_replace('/[\r\n]+/', '</p><p>', $text) . '</p>'; } FORMATTING TEXT AND DATES 387 This trims whitespace, including newline characters from the beginning and end of the text, adds a <p> tag at the beginning, replaces internal sequences of newline characters with closing and opening tags, and appends a closing </p> tag at the end. You can then use the function like this: <?php echo convertToParas($row['article']); ?> The code for the function definition is in utility_funcs.inc.php in the ch14 folder. You can see it being used in blog_ptags_mysqli.php and blog_ptags_pdo.php. Extracting complete sentences PHP has no concept of what constitutes a sentence. Counting periods means you ignore all sentences that end with an exclamation point or question mark. You also run the risk of breaking a sentence on a decimal point or cutting off a closing quote after a period. To overcome these problems, I have devised a PHP function called getFirst() that identifies the punctuation at the end of a normal sentence: • A period, question mark, or exclamation point • Optionally followed by a single or double quote • Followed by one or more spaces The getFirst() function takes two arguments: the text from which you want to extract the first section and the number of sentences you want to extract. The second argument is optional; if its not supplied, the function extracts the first two sentences. The code looks like this (its in utility_funcs.inc.php): function getFirst($text, $number=2) { // use regex to split into sentences $sentences = preg_split('/([.?!]["\']?\s)/', $text, $number+1, PREG_SPLIT_DELIM_CAPTURE); if (count($sentences) > $number * 2) { $remainder = array_pop($sentences); } else { $remainder = ''; } $result = array(); $result[0] = implode('', $sentences); $result[1] = $remainder; return $result; } All you really need to know about this function is that it returns an array containing two elements: the extracted sentences and any text thats left over. You can use the second element to create a link to a page containing the full text. If youre interested in how the function works, read on. The line highlighted in bold uses a regex to identify the end of each sentence—a period, question mark, or exclamation point, optionally followed by a double or single quotation mark and a space. This is passed as the first argument to preg_split(), which uses the regex to split the text into an array. The second argument is the target text. The third argument determines the maximum number of chunks to split the text into. You want one more than the number of sentences to be extracted. Normally, preg_split() discards the characters matched by the regex, but using PREG_SPLIT_DELIM_CAPTURE as the fourth argument together with a pair of capturing parentheses CHAPTER 14 388 in the regex preserves them as separate array elements. In other words, the elements of the $sentences array consist alternately of the text of a sentence followed by the punctuation and space like this: $sentences[0] = '"Hello, world'; $sentences[1] = '!" '; Its impossible to know in advance how many sentences there are in the target text, so you need to find out if theres anything remaining after extracting the desired number of sentences. The conditional statement uses count() to ascertain the number of elements in the $sentences array and compares the result with $number multiplied by 2 (because the array contains two elements for each sentence). If theres more text, array_pop() removes the last element of the $sentences array and assigns it to $remainder. If theres no further text, $remainder is an empty string. The final stage of the function uses implode() with an empty string as its first argument to stitch the extracted sentences back together and then returns a two-element array containing the extracted text and anything thats left over. Dont worry if you found that explanation hard to follow. The code is quite advanced. It took a lot of experimentation to build the function, and I have improved it gradually over the years. PHP Solution 14-1: Displaying the first two sentences of an article This PHP solution shows how to display an extract from each article in the blog table using the getFirst() function described in the preceding section. If you created the Japan Journey site earlier in the book, use blog.php. Alternatively, use blog_01.php from the ch14 folder, and save it as blog.php in the phpsols site root. You also need footer.inc.php, menu.inc.php, title.inc.php, and connection.inc.php in the includes folder. 1. Copy utility_funcs.inc.php from the ch14 folder to the includes folder, and include it in the PHP code block above the DOCTYPE declaration. Also include the MySQL connection file, and create a connection to the database. This page needs read-only privileges, so use read as the argument passed to dbConnect() like this: require_once('./includes/connection.inc.php'); require_once('./includes/utility_funcs.inc.php'); // create database connection $conn = dbConnect('read'); 2. Prepare a SQL query to retrieve all records from the blog table like this: $sql = 'SELECT * FROM blog ORDER BY created DESC'; 3. For MySQLi, use this: $result = $conn->query($sql); Theres no need to submit the query at this stage for PDO. 4. Create a loop inside the maincontent <div> to display the results. For MySQLi, use this: <div id="maincontent"> <?php while ($row = $result->fetch_assoc()) { FORMATTING TEXT AND DATES 389 ?> <h2><?php echo $row['title']; ?></h2> <p><?php $extract = getFirst($row['article']); echo $extract[0]; if ($extract[1]) { echo '<a href="details.php?article_id=' . $row['article_id'] . '"> More</a>'; } ?></p> <?php } ?> </div> The code is the same for PDO, except for this line: while ($row = $result->fetch_assoc()) { Replace it with this: foreach ($conn->query($sql) as $row) { The main part of the code is inside the <p> tags. The getFirst() function processes $row['article'] and stores the result in $extract. The first two sentences of article in $extract[0] are immediately displayed. If $extract[1] contains anything, it means there is more to display. So the code inside the if statement displays a link to details.php with the articles primary key in a query string. 5. Save the page, and test it in a browser. You should see the first two sentences of each article displayed as shown in Figure 14-3. Figure 14-3. The first two sentences have been extracted cleanly from the longer text. 6. Test the function by adding a number as a second argument to getFirst() like this: $extract = getFirst($row['article'], 3); This displays the first three sentences. If you increase the number so that it equals or exceeds the number of sentences in an article, the More link wont be displayed. You can compare your code with blog_mysqli.php and blog_pdo.php in the ch14 folder. Well look at details.php in Chapter 15. Before that, lets tackle the minefield presented by using dates in a dynamic website. CHAPTER 14 390 Lets make a date Dates and time are so fundamental to modern life that we rarely pause to think how complex they are. There are 60 seconds to a minute and 60 minutes to an hour, but 24 hours to a day. Months range between 28 and 31 days, and a year can be either 365 or 366 days. The confusion doesnt stop there, because 7/4 means July 4 to an American or Japanese, but 7 April to a European. To add to the confusion, PHP and MySQL handle dates differently. Time to bring order to chaos . . . How MySQL handles dates In MySQL, dates and time are always expressed in descending order from the largest unit to the smallest: year, month, date, hour, minutes, seconds. Hours are always measured using the 24-hour clock with midnight expressed as 00:00:00. Even if this seems unfamiliar to you, its the recommendation laid down by the International Organization for Standardization (ISO). If you attempt to store a date in any other format than year, month, date, MySQL inserts 0000-00-00 in the database. MySQL allows considerable flexibility about the separator between the units (any punctuation symbol is OK), but there is no argument about the order—its fixed. Ill come back later to the way you insert dates into MySQL, because its best to validate them and format them with PHP. First, lets take a look at some of the things you can do with dates once theyre stored in MySQL. MySQL has many date and time functions, which are listed with examples at http://dev.mysql.com/doc/refman/5.1/en/date-and-time-functions.html. One of the most useful functions is DATE_FORMAT(), which does exactly what its name suggests. Formatting dates in a SELECT query with DATE_FORMAT() The syntax for DATE_FORMAT() is as follows: DATE_FORMAT( date, format ) Normally, date is the table column to be formatted, and forma t is a string composed of formatting specifiers and any other text you want to include. Table 14-1 lists the most common specifiers, all of which are case-sensitive. Table 14-1. Frequently used MySQL date format specifiers Period Specifier Description Example %Y Four-digit format 2006 Year %y Two-digit format 06 %M Full name January, September %b Abbreviated name, three letters Jan, Sep Month %m Number with leading zero 01, 09 . blog .php. Alternatively, use blog_01 .php from the ch14 folder, and save it as blog .php in the phpsols site root. You also need footer.inc .php, menu.inc .php, title.inc .php, and connection.inc .php. < /p& gt; tag immediately followed by an opening < ;p& gt; tag like this: < ;p& gt;< ?php echo preg_replace('/[
]+/', '< /p& gt;< ;p& gt;', $row['article']);. blog_word_mysqli .php and blog_word_pdo .php) : $extract = $row['first100']; // find position of last space in extract $lastSpace = strrpos($extract, ' '); // use $lastSpace to set