• Lecture 19 - PHP III: Web Services with File I/O and JSON

    Code Quality in JS

    Talk with your neighbor: Try to list at least 5 Code Quality standards we expect to achieve for your JS in this course (e.g. as specific as "line lengths < 100 characters" or "good variable descriptive identifier names")

    Did they include some of the following?

    • Using proper naming conventions (camelCase and ALL_CAPS) for JS
    • Good indentation and curly brace style
    • Using descriptive JSdoc to comment functions
    • Use the module pattern and "use strict;"
    • Localizing variables, not overusing module globals
    • Using functions to capture functionality
    • Refactoring common code and sub-operations into function
    • Do not mix HTML/CSS/JS

    We will see how these transfer to PHP as we develop our web services

    CSE 154

    Lecture 19 - PHP III: Web Services with File I/O and JSON

    Agenda

    Important Course Details

    • No exploration session this week. Next week on React
    • Reminder: CP4 out.
    • HW4 is out, due May 24th, 11:00pm

    Web Services with JSON

    PHP File I/O

    Web Services with File I/O

    Web Services with File I/O and JSON

    Reminder: Associative Arrays

    Associative arrays are arrays that have keys with assigned values (similar to Maps in Java, dictionaries in Python, or JSON objects in JS)

    $tas = array("AA" => "Daniel Hsu", "AB" => "Chao Hsu Lin",
                 "AC" => "Jack Venberg", "AD" => "Sandy Yang",
                 "AE" => "Ann Shan", "AF" => "Manchen Jin",
                 "AG" => "Hudson Gilmore", "AH" => "Manny Munoz",
                 "AI" => "Will Bigelow", "AJ" => "Zach Wu");
    $tas["ZZ"] = "Jeremy Zhang";
    

    PHP

    $ages = array(); # empty array (length 0)
    $ages["Whitney"] = 18; # stores 17 at the location where "Whitney" is stored
    

    PHP

    foreach and associative arrays

    foreach ($array_name as $key => $value)  {
      ...
    }

    PHP (template)

    $tas = array("AA" => "Daniel Hsu", ...);
    foreach ($tas as $section => $ta)  {
      echo "{$ta} leads section {$section}.\n";
    }
    

    PHP (example)

    Daniel Hsu leads section AA.
    Chao Hsu Lin leads section AB.
    Jack Venberg leads section AC.
    Sandy Yang Chen leads section AD.
    ...

    Output

    in_array vs array_key_exists

    in_array tests whether a value is in an array

    array_key_exists tests whether a key is in an array

    $tas = array("AA" => "Daniel Hsu", ...);
    in_array("AA", $tas);           # false
    in_array("Jeremy Zhang", $tas); # true
    array_key_exists("AA", $tas);   # true
    

    PHP (example)

    Web services with JSON

    A simple case study: JSONtest

    For this example let's write a simple web service jsontest.php that takes no query parameters and returns a JSON like the one below.

    This result will be used in a web page jsontest.html, jsontest.css, and jsontest.js.

    Sample code

    {
      "name": "Miranda",
      "hobbies": [
        "pottery",
        "softball",
        "cycling",
        "watching youtube"
      ]
    }

    JSON

    Look down for HTML and JS...

    JSONtest HTML

    <body>
    <header>
      <h1>JSON Test</h1>
    </header>
    <section>
      <button id="test-btn">Get JSON data.</button>
      <h2>Response</h2>
      <p id="response"></p>
    </section>
    </body>

    HTML

    JSONtest screen

    JSONtest Fetch load data handler

    function loadData(data) {
      let response = id("response");
      response.innerText = data["name"] + "'s Hobbies";
      let ul = document.createElement("ul");
      response.appendChild(ul);
      for(let i = 0; i < data["hobbies"].length; i++) {
        let li = document.createElement("li");
        li.innerText = data["hobbies"][i];
        ul.appendChild(li);
      }
    }

    JS

    Reminder: Setting Content Type with header

    header("Content-type: type/subtype");

    PHP (template)

    Most of the time we'll use header("Content-type: text/plain"); or header("Content-type: application/json");

    Also remember that in error cases we want to send back the Invalid Request header header("HTTP/1.1 400 Invalid Request");

    Remember that you can not print or echo before a header statement but it doesn't have to be the first line of code.

    Helpful Slides from Tuesday's section on what header information in the Network's tab for Text and HTML look like are here, look below for "application/json"

    Inspecting Header Information in Networks Tab

    This example shows the result of setting Content-Type as application/json

    and the JSONs in the response

    Building a simple service that returns JSON

    Try using the following steps in jsontest.php

    1. Create an array and assign it to a variable ($output = array() for example )
    2. Gather your data in the associative array ($output) with the key/value pairs you want to return in your JSON object
    3. Use json_encode($output) to "stringify" your associative array into a JSON formatted string.
    4. Ensure you have set your header correctly before you print anything!
      (e.g. header("Content-Type: application/json");)
    5. Print the JSON formatted string!

    NOTE: we can also use json_decode to convert JSON strings into PHP arrays.

    Example: JSONtest server code

    <?php
      header("Content-Type: application/json");
    
      $output = array();
      $output["name"] = "Miranda";
      $output["hobbies"] = array("pottery", "softball",
                                 "cycling", "watching youtube");
    
      print(json_encode($output));
    ?>

    Produces:

    {
      "name":"Miranda",
      "hobbies":["pottery","softball", "cycling", "watching youtube"]
    }

    PHP

    Now let's build a web service

    New case study: Points Of Interest

    <backstory>
    It's week 7 and I'm ready for a vacation, but I can't decide where to go. So I want a tool that will randomly pick from a list of "Points of Interests" (vacation spots) and give me some guidance. I know I have already built the randomizer and JSONTest - so I want something similar...
    </backstory>

    To start we will use a modified version of JSONtest as a prototype

    Sample code

    Step 1: Define the API

    Let's first determine how the users will interact with the API. Questions we can ask might be:

    • What data might a user need?
    • What might be the easiest format for the user to use this data in?

    For Points of Interest we're looking for:

    • A city that one might want to travel to
    • A list of Points of Interest (sites) for that city, including for each:
      • The name of the site
      • An image of the site
      • A review of the site

    Example: Points of Interest sample output

    The general format of the response could look like this for all of our cities:

    {
      "cities" : [
        {
          "name": "Seattle",
          "sites": [
            {
              "name": "Space Needle",
              "image": "....",
              "review": ".... "
            }
          ]
        },
        {
          "name": "New York",
          "sites": [
            {
              "name": "Ellis Island",
              "image": "....",
              "review": ".... "
            },
            {
              "name": "Statue of Liberty",
              "image": "....",
              "review": ".... "
            }
          ]
        }
        ...
      ]
    }

    JSON

    Step 2: Determine how to view the data

    Given the information we may store in the server, how might we want to view it?

    • Is the data simple enough to return as plain text (so the end user doesn't have to split to view different parts)?
    • Where might I need more structured information that requires responding with JSON encoded data?
    • What "filters" might I want on that data? Do I want all of it? Some of it? Just one at a time?

    Example: Points of Interest requests

    For our Points of Interest API we want three types of data:

    • All of the cities we have with at least one point of interest each. (city=all)
    • A random city with the point of interest(s) for that city (city=random)
    • The Point(s) of Interest for a city that is specified by the user (city={cityname})

    For each Point of Interest we want:

    • The name (e.g. Space Needle)
    • An image
    • A review

    Step 3: Write your server code to handle the requests

    • Think of the conditionals you need to handle your request
    • Remember to check if the $_GET and $_POST indices for those requests are set (using isset), and handle the error correctly if not.
    • You can use conditional statements to figure out which query parameter to handle.
    • Remember to be careful about using $_GET vs $_POST where needed (for now we're using $_GET).
    • Oh and don't forget to handle other error cases as well.
    • Put the correct headers in each part of the conditional... then look to see if there is any refactoring you can do!

    Step 4: Write code to handle each request type

    How will your data be stored on the server?

    How will you convert the data into the right format for sending back to the caller?

    Example: Points of Interest Web Service, simple version

    Write a simple web service that accepts a city and outputs an attraction that you should visit in that city

    http://example.com/pointsofinterest.php?city=Seattle

    GET request

    The best place to visit in Seattle is Space Needle.

    Output

    See below for sample code

    Solution
    <?php
      header("Content-type: text/plain");
      if (isset($_GET["city"])) {
        $attractions = get_list();
        $city = $_GET["city"];
        echo "The best place to visit in " . $city .  " is " .
              $attractions[$city] . ".\n";
      }
    
      function get_list() {
        $attractions = array("Seattle" => "Space Needle",
                             "New York" => "Ellis Island",
                              ...
                             "London" => "Big Ben");
      }
    ?>

    PHP

    Example: Points of Interest Web Service

    What if we want to return all of the cities/points of interest?

    http://example.com/pointsofinterest.php?city=all

    GET request

    {
        "Seattle":"Space Needle",
        "New York":"Ellis Island",
        ...
        "San Franciso":"Fisherman's Warf"
    }

    JSON Output

    See below for sample code

    Solution

    <?php
        if (isset($_GET["city"])) {
          $attractions = get_list();
          $city = $_GET["city"];
    
          if (strcmp($city, "all") === 0) {
            header("Content-type: application/json");
            print_r(json_encode($attractions));
          }
        }
    
        function get_list() {
          $attractions = array("Seattle" => "Space Needle",
                               ...);
        }
    ?>

    PHP

    That's all great, but I don't want to have my PHP file get bloated with the associative array. There has to be a better way to store this information.

    Besides, I'm still missing the images, and multiple Points of Interest per city!

    Let's explore storing the data in a different way

    PHP File I/O

    spice rack

    From Wikipedia

    PHP I/O Functions

    function name(s) category
    file, file_get_contents, file_put_contents reading/writing entire files
    basename, file_exists, filesize, fileperms, filemtime, is_dir, is_readable, is_writable, disk_free_space asking for information
    copy, rename, unlink, chmod, chgrp, chown, mkdir, rmdir manipulating files and directories
    glob, scandir reading directories

    Reading Files - IMPORTANT SLIDE!

    contents of foo.txt file("foo.txt") file_get_contents("foo.txt")
    Hello
      how r u?
    
      I'm fine
    
                    
    array("Hello\n", #0
      "how r u?\n",    #1
      "\n",            #2
      "I'm fine\n"     #3
      )
                    
    "Hello\n
      how r u\n    # a single
      \n           # string
      I'm fine\n"
                    

    The file function returns lines of a file as an array (\n at end of each).

    file_get_contents returns entire contents of a file as a single string.

    file_put_contents writes a string into a file.

    Sidebar: Default Parameter Values

    A quick note because you will see this in some of the PHP documentation

    function name(parameterName=value, ..., parameterName=value) {
      statements;
    }

    PHP (template)

    function print_separated($str, $separator=", ") {
      if (strlen($str) < 0) {
        echo $str[0];
        for ($i = 1; $i < strlen($str); $i++) {
          echo $separator . $str[$i];
        }
      }
    }
    print_separated("hello");      # h, e, l, l, o
    print_separated("hello", "-"); # h-e-l-l-o

    PHP (example)

    If no value is passed, the default will be used (defaults must come last)

    The file Function

    filereturns the lines of a file as an array of strings.

    But each ends with \n; to strip it, use an optional second parameter:

    $cities = file("cities.txt");
    foreach ($cities as $city) { # for ($i = 0; $i < count($cities); $i++)
      echo $city;                # no newline needed here!
    }

    PHP

    $lines = file("cities.txt", FILE_IGNORE_NEW_LINES);
    foreach ($cities as $city) {
      echo "{$city}\n";              # a newline is now needed!
    }

    PHP

    Common idiom: foreach or for loop over lines of file

    Reading/Writing an Entire File

    # reverse a file
    $text = file_get_contents("poem.txt");
    $text = strrev($text);
    file_put_contents("poem.txt", $text);

    PHP

    file_get_contents returns entire contents of a file as a string

    • if the file doesn't exist, you will get a warning and an empty return string

    file_put_contents writes a string into a file, replacing its old contents

    • if the file doesn't exist, it will be created

    Appending to a File

    # add a new line to a file
    $new_text = "P.S. ILY, GTG TTYL!~";
    file_put_contents("poem.txt", $new_text, FILE_APPEND);

    PHP

    old contents new contents
    Roses are red
    Violets are blue
    All my base
    Are belong to you.
    Roses are red
    Violets are blue
    All my base
    Are belong to you.
    P.S. ILY, GTG TTYL!~

    file_put_contents can be called with an optional third parameter to append (add to end) rather than overwrite.

    Splitting/Joining Strings

    $arr = explode(delimiter, string);
    $str = implode(delimiter, array);

    PHP (template)

    $s = "CSE 154 A";
    $a = explode(" ", $s);     # ("CSE", "154", "A")
    $s2 = implode("...", $a);  # "CSE...154...A"  

    PHP (example)

    explode and implode convert between strings and arrays.

    For more complex strings, you can use regular expressions.

    Example with explode

    Seattle, Space Needle
    New York, Ellis Island
    Boston, Boston Harbor
    Philadelphia, Valley Forge

    contents of cities.txt

    foreach (file("cities.txt", FILE_IGNORE_NEW_LINES) as $city) {
      $tokens = explode(",", $city);
      echo "The best place to visit in " . $tokens[0] .
             " is " . $tokens[1] . ".\n";
    }

    PHP

    The best place to visit in Seattle is Space Needle.
    The best place to visit in New York is Ellis Island.
    The best place to visit in Boston is Boston Harbor.
    The best place to visit in Philadelphia is Valley Forge.

    output

    Unpacking an array: list

    The list function "unpacks" an array into a set of variables.

    When you now a file or line's exact length/format, use file and list to unpack it

    list($var1, ..., $varN) = array;

    PHP (template)

    Kyle Thayer
    (206) 154 2017
    17-154-0123

    contents of personal.txt

    list($name, $phone, $ssn) = file("personal.txt");
    ...
    list($area_code, $prefix, $suffix) = explode(" ", $phone);  

    PHP

    Reading Directories - IMPORTANT SLIDE!

    function description
    glob returns an array of all file names that match a given pattern (returns a file path and name, such as "foo/bar/myfile.txt")
    scandir returns an array of all file names in a given directory (returns just the file names, such as "myfile.txt")

    Can accept a general path with the * wildcard (more powerful).

    Example: Points of Interests in Files

    directory structure for cities

    Suppose we store information about each Point of Interest in a file in a city subdirectory.

    The name of the file is the name of the Point of Interest

    Each file contains 2 lines

    • The URL of an image of the Point of Interest
    • The review of the Point of Interest

    glob Example

    # Get all of the information from the cities
    $cities = glob("cities/*/*");
    foreach ($cities as $city) {
      $text = file_get_contents($city);
      echo (basename($city) . " : " . $text . "\n");
    }

    PHP

    glob can match a wildcard path with the * character

    • glob("foo/bar/*.doc") returns all .doc files in the foo/bar subdirectory
    • glob("food*") returns all files whose names begin with "food"

    The basename function strips any leading directory from a file path

    • basename("foo/bar/baz.txt") returns "baz.txt"

    scandir Example

    foreach (scandir("cities/") as $city) {
      echo "I found a city: {$city}\n";
    }

    PHP

    I found a city: .

    I found a city: ..

    I found a city: Ann_Arbor

    I found a city: Beijing

    ...

    output

    scandir includes current directory (".") and parent ("..") in the array.

    Don't need basename with scandir; returns file names only without directory

    Putting it all together: File Based Points of Interest

    The pointsofinterest.zip example has been put together for you to look explore (note that it is not yet finished, and also could use some refactoring to meet the Code Quality Guide!)

    Code Quality in a Web Service

    Where did you see adherence to the similar code quality guidlines as for JS?

    What was similar?

    What was different?