FORMATTING TEXT AND DATES 411 Format character Description Examples %m Months, no leading zero 2, 11 %D Days with leading zero 03, 24 %d Days, no leading zero 3, 24 %a * Total number of days 15, 231 %H Hours with leading zero 03, 23 %h Hours, no leading zero 3, 23 %I Minutes with leading zero 05, 59 %i Minutes, no leading zero 5, 59 %S Seconds with leading zero 05, 59 %s Seconds, no leading zero 5, 59 %R Display minus when negative, plus when positive -, + %r Display minus when negative, no sign when positive - %% Percentage sign % * A bug verified in PHP 5.3.3 produces an incorrect result for the total number of days on Windows. Hopefully, this will be fixed in a subsequent release. The following example in date_interval_03.php shows how to get the difference between the current date and the American Declaration of Independence using diff() and displaying the result with the format() method: <p><?php $independence = new DateTime('7/4/1776'); $now = new DateTime(); $interval = $now->diff($independence); echo $interval->format('%Y years %m months %d days'); ?> since American independence.</p> If you load date_interval_03.php into a browser, you should see something similar to the following screenshot (of course, the actual period will be different). CHAPTER 14 412 The format characters follow a logical pattern. Uppercase characters always produce at least two digits with a leading zero if necessary. Lowercase characters have no leading zero. What might not be immediately obvious is that, with the exception of %a, which represents the total number of days, the format characters represent only specific parts of the overall interval. For example, if you change the format string to $interval->format('%m months'), it shows only the number of whole months that have elapsed since last July 4. It does not show the total number of months since July 4, 1776. Calculating recurring dates with the DatePeriod class Working out recurring dates, such as the second Tuesday of each month, is now remarkably easy thanks to the DatePeriod class. It works in conjunction with a DateInterval object and is available only in PHP 5.3 or later. The DatePeriod constructor is unusual in that it accepts arguments in three different ways. The first way of creating a DatePeriod object is to supply the following arguments: • A DateTime object representing the start date • A DateInterval object representing the recurring interval • An integer representing the number of recurrences • The DatePeriod::EXCLUDE_START_DATE constant (optional) The second way of creating a DatePeriod object is to replace the number of recurrences in the third argument with a DateTime object representing the end date. The third way uses a single argument: a string formatted according to the ISO 8601 recurring time interval standard (see http://en.wikipedia.org/wiki/ISO_8601#Repeating_intervals). Once you have created a DatePeriod object, you can display the recurring dates in a foreach loop using the DateTime format() method. Lets take a quick look at the three ways of creating a DatePeriod object. First, using an integer to represent the number of occurrences: The code in date_interval_04.php uses the following code to display the date of the second Tuesday of each month in 2011: $start = new DateTime('12/31/2010'); $interval = DateInterval::createFromDateString('second Tuesday of next month'); $period = new DatePeriod($start, $interval, 12, DatePeriod::EXCLUDE_START_DATE); foreach ($period as $date) { echo $date->format('l, F jS, Y') . '<br>'; } Download from Wow! eBook <www.wowebook.com> FORMATTING TEXT AND DATES 413 It produces the output shown in Figure 14-10. Figure 14-10. Calculating a recurring date is remarkably easy with the DatePeriod class. The first line of PHP code sets the start date as December 31, 2010. The next line uses the DateInterval static method createFromDateString() to set the interval at the second Tuesday of next month. Both values are passed to the DatePeriod constructor, together with 12 as the number of recurrences and the DatePeriod::EXCLUDE_START_DATE constant. The constants name is self-explanatory. Finally, a foreach loop displays the resulting dates using the DateTime format() method. The code in date_interval_05.php has been amended to create a DatePeriod object the second way, using a DateTime object as the third argument to indicate the end date. It looks like this: $start = new DateTime('12/31/2010'); $interval = DateInterval::createFromDateString('second Tuesday of next month'); $end = new DateTime('12/31/2011'); $period = new DatePeriod($start, $interval, $end, DatePeriod::EXCLUDE_START_DATE); foreach ($period as $date) { echo $date->format('l, F jS, Y') . '<br>'; } This produces exactly the same output as shown in Figure 14-10. The third way of creating a DatePeriod object using the ISO 8601 recurring time interval standard is perhaps not as user,friendly, mainly because of the need to construct a string in the correct format, which looks like this: R n /YYYY-MM-DDTHH:MM:SS tz /P interval R n is the letter R followed by the number of recurrences; tz is the time zone offset from UTC (or Z for UTC, as shown in the following example); and P interval uses the same format as the DateInterval class. The code in date_interval_06.php shows an example of how to use DatePeriod with an ISO 8601 recurring interval. It looks like this: CHAPTER 14 414 $period = new DatePeriod('R5/2011-02-05T00:00:00Z/P10D'); foreach ($period as $date) { echo $date->format('l, F j, Y') . '<br>'; } The ISO recurring interval sets five recurrences from midnight UTC on February 5, 2011 at an interval of 10 days. The recurrences are subsequent to the original date, so the preceding example produces six dates, as shown in the following output. Chapter review A large part of this chapter has been devoted to the powerful date and time features introduced in PHP 5.2 and 5.3. You dont need them every day, but theyre extremely useful and represent a major improvement on the original PHP date and time functions. MySQLs date and time functions also make it easy to format dates and execute queries based on temporal criteria. Perhaps the biggest problem with dates is deciding whether to use MySQL or PHP to handle the formatting and/or calculations. A useful feature of the PHP DateTime class is that the constructor accepts a date stored in the MySQL format, so you can use an unformatted MySQL date or timestamp to create DateTime objects. However, unless you need to perform further calculations, its more efficient to use the MySQL DATE_FORMAT() function as part of a SELECT query. This chapter has also provided you with three utility functions for formatting text and dates. In the next chapter, youll learn how to store and retrieve related information in multiple database tables. 415 Chapter 15 Pulling Data from Multiple Tables As I explained in Chapter 11, one of the major strengths of a relational database is the ability to link data in different tables by using the primary key from one table as a foreign key in another table. The phpsols database has two tables: images and blog. Its time to add some more and join them, so you can assign categories to blog entries and associate images with individual articles. You dont join multiple tables physically, but through SQL. Often, you can join tables by identifying a direct relationship between primary and foreign keys. In some cases, though, the relationship is more complex and needs to go through a third table that acts as a cross reference between the other two. In this chapter, youll learn how to establish the relationship between tables and insert the primary key from one table as a foreign key in another table. Although it sounds difficult conceptually, its actually quite easy—you use a database query to look up the primary key in the first table, save the result, and use it in another query to insert it in the second table. In particular, youll learn about the following: • Understanding the different types of table relationships • Using a cross-reference table for many-to-many relationships • Altering a tables structure to add new columns or an index • Storing a primary key as a foreign key in another table • Linking tables with INNER JOIN and LEFT JOIN Understanding table relationships The simplest type of relationship is one-to-one (often represented as 1:1). This type of relationship is often found in databases that contain information only certain people should see. For example, companies often store details of employees salaries and other confidential information in a separate table from the more widely accessible staff list. Storing the primary key of each staff members record as a foreign key in the salaries table establishes a direct relationship between the tables, allowing the accounts department to see the full range of information, while restricting others to the public information. CHAPTER 15 416 Theres no confidential information in the phpsols database, but you might create a one-to-one relationship between a single photo in the images table with an article in the blog table, as illustrated by Figure 15-1. Figure 15-1. A one-to-one relationship links one record directly with another. This is the simplest way of creating a relationship between the two tables, but its not ideal. As more articles are added, the nature of the relationship is likely to change. The photo associated with the first article in Figure 15-1 shows maple leaves floating on the water, so it might be suitable to illustrate an article about the changing seasons or autumn hues. The crystal-clear water, bamboo water scoop, and bamboo pipe also suggest other themes that the photo could be used to illustrate. So you could easily end up with the same photo being used for several articles, or a one-to-many (or 1:n) relationship, as represented by Figure 15-2. Figure 15-2. A one-to-many relationship links one record with several others. As you have already learned, a primary key must be unique. So, in a 1:n relationship, you store the primary key from the table on the 1 side of the relationship (the primary or parent table) as a foreign key in the table on the n side (the secondary or child table). In this case, the image_id from the images table needs to be stored as a foreign key in the blog table. Whats important to understand about a 1:n relationship is that its also a collection of 1:1 relationships. Reading Figure 15-2 from right to left, each article has a relationship with a single image. Without this one-on-one relationship, you wouldnt be able to identify which image is associated with a particular article. What happens if you want to associate more than one image to each article? You could create several columns in the blog table to hold the foreign keys, but this rapidly becomes unwieldy. You might start off with image1, image2, and image3, but if most articles have only one image, two columns are redundant for PULLING DATA FROM MULTIPLE TABLES 417 much of the time. And are you going add an extra column for that extra-special article that requires four images? When faced with the need to accommodate many-to-many (or n:m) relationships, you need a different approach. The images and blog tables dont contain sufficient records to demonstrate n:m relationships, but you could add a categories table to tag individual articles. Most articles are likely to belong to multiple categories, and each category will be related with several articles. The way to resolve complex relationships is through a cross-reference table (sometimes called a linking table), which establishes a series of one-to-one relationships between related records. This is a special table containing just two columns, both of which are declared a joint primary key. Figure 15-3 shows how this works. Each record in the cross-reference table stores details of the relationship between individual articles in the blog and categories tables. To find all articles that belong to the Kyoto category, you match cat_id 1 in the categories table with cat_id 1 in the cross-reference table. This identifies the records in the blog table with the article_id 2, 3, and 4 as being associated with Kyoto. Figure 15-3. A cross-reference table resolves many-to-many relationships as 1:1. Establishing relationships between tables through foreign keys has important implications for how you update and delete records. If youre not careful, you end up with broken links. Ensuring that dependencies arent broken is known as maintaining referen tial integrity. Well tackle this important subject in the next chapter. First, lets concentrate on retrieving information stored in separate tables linked through a foreign key relationship. Linking an image to an article To demonstrate how to work with multiple tables, lets begin with the straightforward scenarios outlined in Figures 15-1 and 15-2: relations that can be resolved as 1:1 through the storage of the primary key from one table (the parent table) as a foreign key in a second table (the child or dependent table). This involves adding an extra column in the child table to store the foreign key. Altering the structure of an existing table Ideally, you should design your database structure before populating it with data. However, relational databases, such as MySQL, are flexible enough to let you add, remove, or change columns in tables even when they already contain records. To associate an image with individual articles in the phpsols database, you need to add an extra column to the blog table to store image_id as a foreign key. CHAPTER 15 418 PHP Solution 15-1: Adding an extra column to a table This PHP solution shows how to add an extra column to an existing table using phpMyAdmin. It assumes that you created the blog table in the phpsols database in Chapter 13. 1. Launch phpMyAdmin, select the phpsols database, and click the link for the blog table in the left-hand navigation frame. 2. Below the blog table structure in the main frame is a form that allows you to add extra columns. You want to add only one column, so the default value in the Add field(s) text box is fine. Its normal practice to put foreign keys immediately after the tables primary key, so select the After radio button, and make sure the drop-down menu is set to article_id, as shown in the following screenshot. Then click Go. This opens the screen for you to define column attributes. Use the following settings: Field : image_id Type: INT Attributes: UNSIGNED Null: Selected Inde x: INDEX Do not select AUTO_INCREMENT. The Null check box has been set to selected because not all articles will necessarily be associated with an image. Click Save. You will be returned to the blog table structure, which should now look like this: PULLING DATA FROM MULTIPLE TABLES 419 3. If you click the Browse tab at the top left of the screen, you will see that the value of image_id is NULL in each record. The challenge now is to insert the correct foreign keys without the need to look up the numbers manually. Well tackle that next. Inserting a foreign key in a table The basic principle behind inserting a foreign key in another table is quite simple: you query the database to find the primary key of the record that you want to link to the other table. You can then use an INSERT or UPDATE query to add the foreign key to the target record. To demonstrate the basic principle, youll adapt the update form from Chapter 13 to add a drop-down menu that lists images already registered in the images table (see Figure 15-4). Figure 15-4. A dynamically generated drop-down menu inserts the appropriate foreign key. The menu is dynamically generated by a loop that displays the results of a SELECT query. Each images primary key is stored in the value attribute of the <option> tag. When the form is submitted, the selected value is incorporated into the UPDATE query as the foreign key. To focus on the structure and PHP logic, the instructions in this chapter and the next one cover only MySQLi. The only difference in the PDO version lies in the commands used to submit the SQL queries to the database and to display the results. Fully commented PDO files are in the ch15 and ch16 folders. CHAPTER 15 420 PHP Solution 15-2: Adding the image foreign key This PHP solution shows how to update records in the blog table by adding the primary key of a selected image as a foreign key. It adapts admin/blog_update_mysqli.php from Chapter 13. Use the version that you created in Chapter 13. Alternatively, copy blog_update_mysqli_03.php from the ch13 folder to the admin folder, and remove _03 from the filename. 1. The existing SELECT query that retrieves details of the article to be updated needs to be amended so that it includes the foreign key, image_id, and the result needs to be bound to a new result variable, $image_id. You then need to run a second SELECT query to get the details of the images table, but before you can do so, you need to free the database resources by applying the free_result() method on the prepared statement ($stmt). Add the following code highlighted in bold to the existing script: if (isset($_GET['article_id']) && !$_POST) { // prepare SQL query $sql = 'SELECT article_id, image_id, title, article FROM blog WHERE article_id = ?'; $stmt->prepare($sql); // bind the query parameter $stmt->bind_param('i', $_GET['article_id']); // bind the results to variables $stmt->bind_result($article_id, $image_id, $title, $article); // execute the query, and fetch the result $OK = $stmt->execute(); $stmt->fetch(); // free the database resources for the second query $stmt->free_result(); } Notice that the conditional statement wrapping the call to the prepare() method and subsequent code has been removed. You dont need it after verifying that the prepared statement doesnt contain any syntax errors. 2. Inside the form, you need to display the filenames stored in the images table. Since the second SELECT statement doesnt rely on external data, its simpler to use the query() method instead of a prepared statement. Add the following code after the article text area (its all new code, but the PHP sections are highlighted in bold for ease of reference): <p> <label for="image_id">Uploaded image:</label> <select name="image_id" id="image_id"> <option value="">Select image</option> <?php // get the list images $getImages = 'SELECT image_id, filename FROM images ORDER BY filename'; $images = $conn->query($getImages); while ($row = $images->fetch_assoc()) { ?> . date_interval_06 .php shows an example of how to use DatePeriod with an ISO 8601 recurring interval. It looks like this: CHAPTER 14 414 $period = new DatePeriod('R5/201 1-0 2-0 5T00:00:00Z /P1 0D');. column to an existing table using phpMyAdmin. It assumes that you created the blog table in the phpsols database in Chapter 13. 1. Launch phpMyAdmin, select the phpsols database, and click the. 5, 59 %R Display minus when negative, plus when positive -, + %r Display minus when negative, no sign when positive - %% Percentage sign % * A bug verified in PHP 5.3.3 produces an incorrect