PULLING DATA FROM MULTIPLE TABLES 421 <option value="<?php echo $row['image_id']; ?>" <?php if ($row['image_id'] == $image_id) { echo 'selected'; } ?>><?php echo $row['filename']; ?></option> <?php } ?> </select> </p> The first <option> tag is hard-coded with the label Select image, and its value is set to an empty string. The remaining <option> tags are populated by a while loop that extracts each record to an array called $row. A conditional statement checks whether the current image_id is the same as the one already stored in the articles table. If it is, selected is inserted into the <option> tag so that it displays the correct value in the drop-down menu. Make sure you dont omit the third character in the following line: ?>><?php echo $row['filename']; ?></option> Its the closing angle bracket of the <option> tag, sandwiched between two PHP tags. 3. Save the page, and load it into a browser. You should be automatically redirected to blog_list_mysqli.php. Select one of the EDIT links, and make sure that your page looks like Figure 15-4. Check the browser source code view to verify that the value attributes of the <option> tags contain the primary key of each image. 4. The final stage is to add the image_id to the UPDATE query. Because some blog entries might not be associated with an image, you need to create alternative prepared statements like this: // if form has been submitted, update record if (isset($_POST ['update'])) { // prepare update query if (!empty($_POST['image_id'])) { $sql = 'UPDATE blog SET image_id = ?, title = ?, article = ? WHERE article_id = ?'; $stmt->prepare($sql); $stmt->bind_param('issi', $_POST['image_id'], $_POST['title'], $_POST['article'], $_POST['article_id']); } else { $sql = 'UPDATE blog SET image_id = NULL, title = ?, article = ? WHERE article_id = ?'; $stmt->prepare($sql); $stmt->bind_param('ssi', $_POST['title'], $_POST['article'], $_POST['article_id']); } $stmt->execute(); $done = $stmt->affected_rows; } CHAPTER 15 422 If $_POST['image_id'] has a value, you add it to the SQL as the first parameter with a placeholder question mark. Since it must be an integer, you add i to the beginning of the first argument of bind_param(). However, if $_POST['image_id'] doesnt contain a value, you need to create a different prepared statement to set the value of image_id to NULL in the SQL query. Because it has an explicit value, you dont add it to bind_param(). 5. Test the page again, select a filename from the drop-down menu, and click Update Entry. You can verify whether the foreign key has been inserted into the articles table by refreshing Browse in phpMyAdmin or by selecting the same article for updating. This time, the correct filename should be displayed in the drop-down menu. 6. Check your code against blog_update_mysqli_04.php in the ch15 folder, if necessary. The PDO version is in blog_update_pdo_04.php in the ch15 folder. Selecting records from multiple tables There are several ways to link tables in a SELECT query, but the most common is to list the table names separated by INNER JOIN. On its own, INNER JOIN produces all possible combinations of rows (a Cartesian join). To select only related values, you need to specify the primary/foreign-key relationship. For example, to select articles and their related images from the blog and images table, you can use a WHERE clause like this: SELECT title, article, filename, caption FROM blog INNER JOIN images WHERE blog.image_id = images.image_id The title and article columns exist only in the blog table. Likewise, filename and caption exist only in the images table. Theyre unambiguous and dont need to be qualified. However, image_id exists in both tables, so you need to prefix each reference with the table name and a period. For many years, it was common practice to use a comma in place of INNER JOIN like this: SELECT title, article, filename, caption FROM blog, images WHERE blog.image_id = images.image_id This is no longer recommended practice because of changes made to the way joins are handled in MySQL 5.0.12. Using a comma to join tables can result in SQL syntax errors that can be difficult to resolve. Use INNER JOIN instead. Instead of a WHERE clause, you can use ON like this: SELECT title, article, filename, caption FROM blog INNER JOIN images ON blog.image_id = images.image_id Download from Wow! eBook <www.wowebook.com> PULLING DATA FROM MULTIPLE TABLES 423 When both columns have the same name, you can use the following syntax: SELECT title, article, filename, caption FROM blog INNER JOIN images USING (image_id) This last method of matching the primary and foreign keys is my personal preference. However, if the columns you are matching have different names, you must use ON or a WHERE clause. PHP Solution 15-3: Building the details page This PHP solution shows how to join the blog and images tables to display a selected article with its associated photo. 1. Copy details_01.php from the ch15 folder to the phpsols site root, and rename it details.php. Do not update the links if your editing environment prompts you to do so. Make sure that footer.inc.php and menu.inc.php are in the includes folder, and load the page in a browser. It should look like Figure 15-5. Figure 15-5. The details page contains a placeholder image and text. 2. Load blog_list_mysqli.php or blog_list_pdo.php into a browser, and update the following three articles by assigning the image filename as indicated: Basin of Contentment: basin.jpg Tiny Restaurants Crowded Together: menu.jpg Trainee Geishas Go Shopping: maiko.jpg 3. Check that the foreign keys have been registered by navigating to the blog table in phpMyAdmin and clicking the Brows e tab. At least one article should have NULL as the value for image_id, as shown in Figure 15-6. CHAPTER 15 424 Figure 15-6. The foreign key of the article not associated with an image is set to NULL. 4. In details.php, include utility_funcs.inc.php from the previous chapter (if necessary, copy it from the ch14 folder to the includes folder). Then include the database connection file, create a read-only connection, and prepare the SQL query inside a PHP code block above the DOCTYPE declaration like this: require_once('./includes/utility_funcs.inc.php'); require_once('./includes/connection.inc.php'); // connect to the database $conn = dbConnect('read'); // check for article_id in query string if (isset($_GET['article_id']) && is_numeric($_GET['article_id'])) { $article_id = (int) $_GET['article_id']; } else { $article_id = 0; } $sql = "SELECT title, article, DATE_FORMAT(updated, '%W, %M %D, %Y') AS updated, filename, caption FROM blog INNER JOIN images USING (image_id) WHERE blog.article_id = $article_id"; $result = $conn->query($sql); $row = $result->fetch_assoc(); The code checks for article_id in the URL query string. If it exists and is numeric, its assigned to $article_id using the (int) casting operator to make sure its an integer. Otherwise, $article_id is set to 0. You could choose a default article instead, but leave it at 0 for the moment because I want to illustrate an important point. The SELECT query retrieves the title, article, and updated columns from the blog table, and the filename and caption columns from the images table. The value of updated is formatted using the DATE_FORMAT() function and an alias as described in Chapter 14. Because only one record is being retrieved, using the original column name as the alias doesnt cause a problem with the sort order. The tables are joined using INNER JOIN and a USING() clause that matches the values in the image_id columns in both tables. The WHERE clause selects the article identified by $article_id. Since the data type of $article_id has been checked, its safe to use in the query. Theres no need to use a prepared statement. PULLING DATA FROM MULTIPLE TABLES 425 Note that the query is wrapped in double quotes so that the value of $article_id is interpreted. To avoid conflicts with the outer pair of quotes, single quotes are used around the format string passed as an argument to DATE_FORMAT(). 5. The rest of the code displays the results of the SQL query in the main body of the page. Replace the placeholder text in the <h2> tags like this: <h2><?php if ($row) { echo $row['title']; } else { echo 'No record found'; } ?> </h2> If the SELECT query finds no results, $row will be empty, which PHP interprets as false. So this displays the title, or No record found if the result set is empty. 6. Replace the placeholder date like this: <p><?php if ($row) { echo $row['updated']; } ?></p> 7. Immediately following the date paragraph is a <div> containing a placeholder image. Even if the result set isnt empty, not all articles are associated with an image, so the <div> needs to be wrapped in a conditional statement that also checks that $row['filename'] contains a value. Amend the <div> like this: <?php if ($row && !empty($row['filename'])) { $filename = "images/{$row['filename']}"; $imageSize = getimagesize($filename); ?> <div id="pictureWrapper"> <img src="<?php echo $filename; ?>" alt="<?php echo $row['caption']; ?>" <?php echo $imageSize[3];?>> </div> <?php } ?> This uses code that was described in Chapter 12, so I wont go into it again. 8. Finally, you need to display the article. Delete the paragraph of placeholder text, and add the following code between the closing curly brace and closing PHP tag at the end of the final code block in the previous step: <?php } if ($row) { echo convertToParas($row['article']); } ?> This uses the convertToParas() function in utility_funcs.inc.php to wrap the blog entry in <p> tags and replace sequences of new line characters with closing and opening tags (see “Displaying paragraphs” in Chapter 14). 9. Save the page, and load blog.php into a browser. Click the More link for an article that has an image assigned through a foreign key. You should see details.php with the full article and CHAPTER 15 426 image laid out as shown in Figure 15-7. Check your code, if necessary, with details_mysqli_01.php or details_pdo_01.php in the ch15 folder. Figure 15-7. The details page pulls the article from one table and the image from another. 10. Click the link back to blog.php, and test the other items. Each article that has an image associated with it should display correctly. Click the More link for the article that doesnt have an image. This time you should see the result shown in Figure 15-8. Figure 15-8. The lack of an associated image causes the SELECT query to fail. PULLING DATA FROM MULTIPLE TABLES 427 You know that the article is in the database because the first two sentences wouldnt be displayed in blog.php otherwise. To understand this sudden “disappearance,” see Figure 15-16. The value of image_id is NULL for the record that doesnt have an image associated with it. Because all the records in the images table have a primary key, the USING() clause cant find a match. The solution is to use LEFT JOIN instead of INNER JOIN, as explained in the next section. Finding records that dont have a matching foreign key Take the SELECT query from PHP Solution 15-3, and remove the condition that searches for a specific article, which leaves this: SELECT title, article, DATE_FORMAT(updated, '%W, %M %D, %Y') AS updated, filename, caption FROM blog INNER JOIN images USING (image_id) If you run this query in the SQL tab of phpMyAdmin, it produces the result shown in Figure 15-9. Figure 15-9. INNER JOIN finds only records that have a match in both tables. With INNER JOIN, the SELECT query succeeds only if there is a full match. However, if you use LEFT JOIN, the result includes records that have a match in the left table, but not in the right one. Left and right refer to the order in which you perform the join. Rewrite the SELECT query like this: SELECT title, article, DATE_FORMAT(updated, '%W, %M %D, %Y') AS updated, filename, caption FROM blog LEFT JOIN images USING (image_id) When you run it in phpMyAdmin, you get all four articles as shown in Figure 15-10. Figure 15-10. LEFT JOIN includes records that dont have a match in the right table. As you can see, the empty fields from the right table (images) are displayed as NULL. CHAPTER 15 428 If the column names are not the same in both tables, use ON like this: FROM table_1 LEFT JOIN table_1 ON table_1.col_name = table_2.col_name So, now you can rewrite the SQL query in details.php like this: $sql = "SELECT title, article, DATE_FORMAT(updated, '%W, %M %D, %Y') AS updated, filename, caption FROM blog LEFT JOIN images USING (image_id) WHERE blog.article_id = $article_id"; If you click the More link to view the article that doesnt have an associated image, you should now see the article correctly displayed as shown in Figure 15-11. The other articles should still display correctly, too. The finished code is in details_mysqli_02.php, and details_pdo_02.php. Figure 15-11. LEFT JOIN also retrieves articles that dont have a matching foreign key. Creating an intelligent link The link at the bottom of details.php goes straight back to blog.php. Thats fine with only four items in the blog table, but once you start getting more records in a database, you need to build a navigation system as I showed you in Chapter 12. The problem with a navigation system is that you need a way to return visitors to the same point in the result set that they came from. PHP Solution 15-4: Returning to the same point in a navigation system This PHP solution checks whether the visitor arrived from an internal or external link. If the referring page was within the same site, the link returns the visitor to the same place. If the referring page was an external site, or if the server doesnt support the necessary superglobal variables, the script substitutes a standard link. It is shown here in the context of details.php, but it can be used on any page. PULLING DATA FROM MULTIPLE TABLES 429 1. Locate the back link in the main body of details.php. It looks like this: <p><a href="blog.php">Back to the blog</a></p> 2. Place your cursor immediately to the right of the first quotation mark, and insert the following code highlighted in bold: <p><a href=" <?php // check that browser supports $_SERVER variables if (isset($_SERVER['HTTP_REFERER']) && isset($_SERVER['HTTP_HOST'])) { $url = parse_url($_SERVER['HTTP_REFERER']); // find if visitor was referred from a different domain if ($url['host'] == $_SERVER['HTTP_HOST']) { // if same domain, use referring URL echo $_SERVER['HTTP_REFERER']; } } else { // otherwise, send to main page echo 'blog.php'; } ?>">Back to the blog</a></p> $_SERVER['HTTP_REFERER'] and $_SERVER['HTTP_HOST'] are superglobal variables that contain the URL of the referring page and the current hostname. You need to check their existence with isset() because not all servers support them. Also, the browser might block the URL of the referring page. The parse_url() function creates an array containing each part of a URL, so $url['host'] contains the hostname. If it matches $_SERVER['HTTP_HOST'], you know that the visitor was referred by an internal link, so the full URL of the internal link is inserted in the href attribute. This includes any query string, so the link sends the visitor back to the same position in a navigation system. Otherwise, an ordinary link is created to the target page. The finished code is in details_mysqli_03.php, and details_pdo_3.php in the ch15 folder. Chapter review Retrieving information stored in multiple tables is relatively simple with INNER JOIN and LEFT JOIN. The key to working successfully with multiple tables lies in structuring the relationship between them so that complex relationships can always be resolved as 1:1, if necessary through a cross-reference (or linking) table. The next chapter continues the exploration of working with multiple tables, showing you how to deal with foreign key relationships when inserting, updating, and deleting records. CHAPTER 15 430 . ?'; $stmt->prepare($sql); $stmt->bind_param('ssi', $_POST['title'], $_POST['article'], $_POST['article_id']); } $stmt->execute();. folder to the phpsols site root, and rename it details .php. Do not update the links if your editing environment prompts you to do so. Make sure that footer.inc .php and menu.inc .php are in the. echo 'selected'; } ?>>< ?php echo $row['filename']; ?></option> < ?php } ?> </select> < /p& gt; The first <option> tag is hard-coded