PHP programming tutorial




      8. File I/O

      You often need a way to save data between successive requests to your scripts. One way of doing this is storing files on the filesystem and performing reads and writes on them as necessary.

      For most file I/O operations you first need to open a file pointer with fopen() – fopen() can be called with up to four arguments, but is normally called with just two. The first is the name of the file to be opened – this filename supports various wrappers that allow you to open files on remote servers and in zip files among other things. The second argument is a mode parameter that specifies the type of access you’d like to have to the file or stream.

      Mode Description
      ‘r’ Open for reading only; place the file pointer at the beginning of the file.
      ‘r+’ Open for reading and writing; place the file pointer at the beginning of the file.
      ‘w’ Open file for writing only; place the file pointer at the beginning of the file and truncate the file to zero length. If the file does not exist, attempt to create it.
      ‘w+’ Open for reading and writing; place the file pointer at the beginning of the file and truncate the file to zero length. If the file does not exist, attempt to create it.
      ‘a’ Open for writing only; place the file pointer at the end of the file. If the file does not exist, attempt to create it.
      ‘a+’ Open for reading and writing; place the file pointer at the end of the file. If the file does not exist, attempt to create it.
      ‘x’ Create and open for writing only; place the file pointer at the beginning of the file. If the file already exists, the fopen() call will fail by returning FALSE. If the file does not exist, attempt to create it.
      ‘x+’ Create and open for reading and writing; place the file pointer at the beginning of the file. If the file already exists, the fopen() call will fail by returning FALSE. If the file does not exist, attempt to create it.

      For portability all modes should be combined with ‘b’ as the last character of the mode string so that the file is opened in binary mode. Here’s an example opening a file and writing to it:

      1.  
      2. <?php
      3. $fp = fopen('count.txt', 'wb');
      4. $c = 1;
      5. fwrite($fp, $c . "\n");
      6. fclose($fp);
      7. ?>
      8.  

      First we open the file in binary write mode, then we assign the integer 1 to $c. When we call fwrite() on the file pointer, we’re passing it a string containing the number 1 followed by a newline. $c is implictly converted from an integer to a string when we concatenate it. We can load that same file back up and read the number out like this:

      1.  
      2. <?php
      3. $fp = fopen('count.txt', 'rb');
      4. $c = fgets($fp);
      5. fclose($fp);
      6. echo $c;
      7. ?>
      8.  

      This time we open the file in binary read mode and call the fgets() function to read a single line from the file. $c is now a string containing the number 1 followed by a newline. It’s not difficult to put those two fragments of code together and produce a basic hit counter. Here’s the basic hit counter example:

      1.  
      2. <?php
      3. $countfile = 'count.txt';
      4. if (!file_exists($countfile)) {
      5. $c = 0;
      6. } else {
      7. $fp = fopen($countfile, 'rb');
      8. $c = fgets($fp);
      9. fclose($fp);
      10. $c = (int) $c;
      11. }
      12. echo "Number of hits: " . $c . "<br />\n";
      13. $c++;
      14. $fp = fopen($countfile, 'wb');
      15. fwrite($fp, $c . "\n");
      16. fclose($fp);
      17. ?>
      18.  

      You can see the output of this script if you click here – try refreshing a few times and watching the counter go up. We’ve introduced the file_exists() function – first we check if the counter file exists – if it doesn’t exist, we set $c to 0, otherwise we open up the file and read the counter from it. Notice when we do ‘$c = (int) $c;’ we’re type casting into an integer from a string, we need to do this so that we can increment it – you cannot increment a string. When you typecast a string containing a number into an integer, PHP will automatically try to parse that string and make the correct conversion.

      The end part of the script simply outputs the counter, increments $c and outputs the new value for $c to the file. Again, $c is implicitly converted to a string when we concatenate it.

      However, this counter example isn’t finished yet – we need to add error checking to the fopen() call. Whenever we call a function that may fail, we should check its result to ensure it was successful. fopen() is one such function that can fail, so we need to check its return value is a valid file pointer. If we modify the above counter example with proper error checking we get this counter program:

      1.  
      2. <?php
      3. $countfile = 'count.txt';
      4. if (!file_exists($countfile)) {
      5. $c = 0;
      6. } else {
      7. $fp = fopen($countfile, 'rb');
      8. if (!$fp) {
      9. $c = 0;
      10. } else {
      11. $c = fgets($fp);
      12. fclose($fp);
      13. $c = (int) $c;
      14. }
      15. }
      16. echo "Number of hits: " . $c . "<br />\n";
      17. $c++;
      18. $fp = fopen($countfile, 'wb');
      19. if (!$fp) {
      20. echo "Error opening counter file for writing.";
      21. }
      22. fwrite($fp, $c . "\n");
      23. fclose($fp);
      24. ?>
      25.  

      The first time we call fopen() to read the file, we check if the fopen() failed, and if it did we just set the counter to 0 and do nothing. Then when we come to try and write the file, if the fopen fails, we output an error message that the file could not be opened. Although it’s unlikely the fopen() calls will ever fail in this example, proper error checking should permeate all of your code!

      You rearrange the code a bit so that the file is only written-to when a form is submitted, as you can see in the case of this example – a rudimentary comments system:

      1.  
      2. <?php
      3. $cfile = 'comments.txt';
      4. if (isset($_POST['name']) && strlen($_POST['name'])>2 &&
      5. strlen($_POST['comment']) > 2) {
      6.  
      7. $fp = fopen($cfile, 'ab');
      8. if (!$fp) {
      9. echo "Failed to save comment.<br \>\n";
      10. } else {
      11. fwrite($fp, strip_tags($_POST['comment']) . " (" .
      12. strip_tags($_POST['name']) . ")<br />\n");
      13. fclose($fp);
      14. echo "Comment saved.<br \>\n";
      15. }
      16. }
      17. ?>
      18. <form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post">
      19. Name:<input type="text" name="name"> Comment:<input type="text" name="comment"><br \>
      20. <input type="submit" value="Comment">
      21. </form>
      22. <?php
      23. if (file_exists($cfile) && $fp = fopen($cfile, 'rb')) {
      24. ?>
      25. <h3>Comments:</h3>
      26. <?php
      27.  
      28. while (!feof($fp)) {
      29. $line = fgets($fp);
      30. echo $line;
      31. }
      32. fclose($fp);
      33. }
      34. ?>
      35.  

      You can see a live version of this script by clicking here. We start by checking if the form is being submitted – if it is and we have at least 3 characters in both the name and the comments fields. If we do, we open the comments file and save the comment to it. We’re also using the strip_tags() function to remove any HTML tags that are given in either of the fields. This is to prevent the comments system being overrun with link spammers.

      After the form, we check if the comments file exists and if it does, we try opening it. If the file is successfully opened, we read it one line at a time and echo it to the browser. The correct way to loop over a file reading it a line at a time as you see above is to do a while loop on !feof() – you’re saying ‘continue looping while we’re not at the end of the file’. This is a little bit different to how some other languages do it but you can see the correct structure in the example above.

      If we combine what we’ve learned about File I/O with what we learned in chapter 6 ‘User Input’, you can come up with a simple guestbook fairly easily, here’s an example of that:

      1.  
      2. <?php
      3. $gfile = 'guestbook.txt';
      4. function validate() {
      5. $ret = TRUE;
      6. if (strlen($_POST['name']) < 3) {
      7. echo "Please enter your name.<br />\n";
      8. $ret = FALSE;
      9. }
      10. if (strlen($_POST['message']) < 3) {
      11. echo "Please enter your message.<br />\n";
      12. $ret = FALSE;
      13. }
      14. if (strlen($_POST['subject']) < 2) {
      15. echo "Please enter a subject.<br />\n";
      16. $ret = FALSE;
      17. }
      18. if (!preg_match(
      19. '/^([a-z0-9])(([-a-z0-9._])*([a-z0-9]))*\@([a-z0-9])' .
      20. '(([a-z0-9-])*([a-z0-9]))+' .
      21. '(\.([a-z0-9])([-a-z0-9_-])?([a-z0-9])+)+$/i', $_POST['email'])) {
      22. echo "Please enter a valid e-mail address.<br />\n";
      23. $ret = FALSE;
      24. }
      25. return $ret;
      26. }
      27. if (isset($_POST['name']) && validate()) {
      28.  
      29. $fp = fopen($gfile, 'ab');
      30. if (!$fp) {
      31. echo "Failed to save entry.<br \>\n";
      32. } else {
      33. fwrite($fp, "<table>\n");
      34. fwrite($fp, "<tr><td>Name:</td><td>" .
      35. strip_tags($_POST['name']) .
      36. "</td></tr>\n");
      37. fwrite($fp, "<tr><td>Subject:</td><td>" .
      38. strip_tags($_POST['subject']) .
      39. "</td></tr>\n");
      40. fwrite($fp, "<tr><td>Message:</td><td>" .
      41. strip_tags($_POST['message']) .
      42. "</td></tr>\n");
      43. fwrite($fp, "</table><br />\n");
      44. fclose($fp);
      45. echo "Entry saved. <a href=\"" .
      46. $_SERVER['PHP_SELF'] .
      47. "\">Click here to return.</a><br \>\n";
      48. }
      49. }
      50. ?>
      51. <form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post">
      52. <table>
      53. <tr><td>Enter your name:</td><td><input type="text" name="name" value="<?php
      54. echo htmlentities($_POST['name']); ?>"></td></tr>
      55. <tr><td>Enter your e-mail address:</td><td><input type="text" name="email" value="<?php
      56. echo htmlentities($_POST['email']); ?>"></td></tr>
      57. <tr><td>Subject:</td><td><input type="text" name="subject" value="<?php
      58. echo htmlentities($_POST['subject']); ?>"></td></tr>
      59. <tr><td>Message:</td><td><textarea name="message"><?php
      60. echo htmlentities($_POST['message']); ?></textarea></td></tr>
      61. <tr><td colspan="2"><input type="submit" value="Submit"></td></tr>
      62. </table>
      63. </form>
      64. <?php
      65. if (file_exists($gfile)) {
      66. ?>
      67. <h3>Entries:</h3>
      68. <?php
      69. readfile($gfile);
      70. }
      71. ?>
      72.  

      You can test this program here and see the file it creates here. We’ve copied the validate function directly from the form mailer example in chapter 6, we’ve also copied the HTML form from the same example. Now we have fields for name, email, subject and message, and a handy function to validate them. The only thing that we’re doing differently is to save the results of the form to the end of the guestbook file rather than send them in an e-mail.

      After the form we check if the guestbook file exists, but this time rather than open it and read the file line-by-line, we just call the readfile() function on the file directly – this just outputs the entire contents of the file directly to the browser. You can also use file_get_contents() to read the entire contents of a file into a string variable.

      There are a number of limitations to this approach – we’re saving the HTML table with each entry into the data file. What happens when we want to change the layout of the page? – if we use the code above we have to write a program to modify our data file and replace all the occurances of our table layout with whatever the new layout code is. This is completely unnecessary and it’s much better to be able to just change the layout in a single segment of PHP code. We can achieve this by separating the layout from the data and storing just the data in CSV format. Here’s a CSV guestbook example:

      1. <?php
      2. $gfile = 'csvbook.txt';
      3. function validate() {
      4. $ret = TRUE;
      5. if (strlen($_POST['name']) < 3) {
      6. echo "Please enter your name.<br />\n";
      7. $ret = FALSE;
      8. }
      9. if (strlen($_POST['message']) < 3) {
      10. echo "Please enter your message.<br />\n";
      11. $ret = FALSE;
      12. }
      13. if (strlen($_POST['subject']) < 2) {
      14. echo "Please enter a subject.<br />\n";
      15. $ret = FALSE;
      16. }
      17. if (!preg_match(
      18. '/^([a-z0-9])(([-a-z0-9._])*([a-z0-9]))*\@([a-z0-9])' .
      19. '(([a-z0-9-])*([a-z0-9]))+' .
      20. '(\.([a-z0-9])([-a-z0-9_-])?([a-z0-9])+)+$/i', $_POST['email'])) {
      21. echo "Please enter a valid e-mail address.<br />\n";
      22. $ret = FALSE;
      23. }
      24. return $ret;
      25. }
      26. if (isset($_POST['name']) && validate()) {
      27.  
      28. $fp = fopen($gfile, 'ab');
      29. if (!$fp) {
      30. echo "Failed to save entry.<br \>\n";
      31. } else {
      32. fwrite($fp, '"' . htmlentities(strip_tags($_POST['name'])) . '","' .
      33. htmlentities(strip_tags($_POST['subject'])) . '","' .
      34. htmlentities(strip_tags($_POST['message'])) . "\n");
      35. fclose($fp);
      36. echo "Entry saved. <a href=\"" .
      37. $_SERVER['PHP_SELF'] .
      38. "\">Click here to return.</a><br \>\n";
      39. }
      40. }
      41. ?>
      42. <form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post">
      43. <table>
      44. <tr><td>Enter your name:</td><td><input type="text" name="name" value="<?php
      45. echo htmlentities($_POST['name']); ?>"></td></tr>
      46. <tr><td>Enter your e-mail address:</td><td><input type="text" name="email" value="<?php
      47. echo htmlentities($_POST['email']); ?>"></td></tr>
      48. <tr><td>Subject:</td><td><input type="text" name="subject" value="<?php
      49. echo htmlentities($_POST['subject']); ?>"></td></tr>
      50. <tr><td>Message:</td><td><textarea name="message"><?php
      51. echo htmlentities($_POST['message']); ?></textarea></td></tr>
      52. <tr><td colspan="2"><input type="submit" value="Submit"></td></tr>
      53. </table>
      54. </form>
      55. <?php
      56. if (file_exists($gfile) && $fp = fopen($gfile, 'rb')) {
      57. ?>
      58. <h3>Entries:</h3>
      59. <?php
      60. while (!feof($fp)) {
      61. $elements = fgetcsv($fp);
      62. if (count($elements) != 3) {
      63. continue;
      64. }
      65. echo "<table>\n";
      66. echo "<tr><td>Name:</td><td>" . $elements[0] . "</td></tr>\n";
      67. echo "<tr><td>Subject:</td><td>" . $elements[1] . "</td></tr>\n";
      68. echo "<tr><td>Message:</td><td>" . $elements[2] . "</td></tr>\n";
      69. echo "</table><br />\n";
      70. }
      71. fclose($fp);
      72. }
      73. ?>
      74.  

      You can test this example by clicking here. You can see the CSV file it creates by clicking here. This time rather than writing an entire block of HTML to the data file, we just write the individual fields surrounded by double-quotes and separated by commas. We use the htmlentities() function to ensure the strings don’t contain any double-quotes themselves. Later in the program when we read the comments back out, we use PHP’s fgetcsv() function to get an array of the elements on each line of the CSV file and apply the HTML table layout to this array. By separating the layout from the underlying data in this way it makes things much easier later when you come to change how you want the data to be displayed.

      CSV files are a very crude way of storing a table of data. In the next chapter we’ll look at a much better way.

      Next chapter… (9. MySQL)