Monday, November 14, 2011

Using Callback Functions in PHP


A commonly used technique when programming is to use callbacks. A callback is a piece of code that is passed to a function that is later executed by that function. In this article I will show you how to define and call callbacks in PHP, as well as a practical example of how callbacks can be useful.
To demonstrate how callbacks can be beneficial, assume we have a PHP script that when called will download an RSS feed from 10 different web sites. If it takes 2 seconds to download from each site, it will take 20 seconds to download from all sites.
The code to achieve this may look something like the following code. I've added a called to sleep() to demonstrate how slow this script may be.
Listing 1 listing-1.php
<?php
    // this function simulates downloading data from a site
    function downloadFromSites($sites)
    {
        $ret = array();
 
        foreach ($sites as $site) {
            sleep(2);
 
            // write the data for this site to return array
            $ret[] = array(
                'site' => $site,
                'data' => 'downloaded data here'
            );
        }
 
        return $ret;
    }
 
    // imaginary list of sites to download from
    $sites = array(
        'http://www.example1.com',
        'http://www.example2.com'
        // more sites...
    );
 
    // start downloading and store the return data
    $data = downloadFromSites($sites);
 
    // output the return data
    foreach ($data as $row) {
        echo sprintf("Finished downloading from %s\n", $row['site']);
    }
?>
If you run this code, you'll notice that it hangs until all sites have been completed. You then have access to all of the return data at once. The following figure shows the workflow of this script.
Figure 1 The workflow of a function call that doesn't use a callback
Figure 1: The workflow of a function call that doesn't use a callback
A far better solution would be to return newly-downloaded data as it becomes available. Of course, you could hard-code the required functionality in the downloadFromSites() function, but doing so makes it extremely difficult to reuse the code and use the returned data in a different way should the need arise.
The following diagram shows the workflow of the previous function modified to use callbacks. In this case, the callback function is called every time a single site has completed downloading.
Figure 2 The workflow of a function call that uses callbacks
Figure 2: The workflow of a function call that uses callbacks
PHP makes using callbacks extremely simple using the helper functions is_callable() and call_user_func(). There are three types of callbacks that can be used:
  1. A standard PHP function. This is passed as a string such as 'callbackName'
  2. A static method of a class. This is passed as an array with the the class name as the first element and the method name as the second argument. For example, array('MyClass', 'MyCallback')
  3. A method of an object. This is also passed as an array, but instead of the class name being the first element, the object is. For example, array($obj, 'MyCallback')
The following listing demonstrates how to define each of the types of callbacks and then how to reference each of them.
Listing 2 listing-2.php
<?php
    // Callback type 1: normal function
    function myCallback() {
    }
 
    $callback = 'myCallback';
 
    // Callback type 2: static method
    class MyClass {
        public static function MyCallback() {
        }
    }
 
    $callback = array('MyClass', 'MyCallback');
 
    // Callback type 3: object method
    class AnotherClass {
        public function MyCallback() {
        }
    }
 
    $obj = new AnotherClass();
    $callback = array($obj, 'MyCallback');
?>
Note: The static method callback is defined as static.
To test if a callback can be used you can use the is_callable() function. This function accepts the callback as its only argument and returns true if it's a valid callback. You can then call the callback usingcall_user_func(). The callback is passed as the first argument, and if your callback accepts any arguments you can include them as the second and subsequent arguments.
The following code demonstrates how this would be typically used. This code ensures a callback can be called, then calls it with a single argument. This script outputs Hello.
Listing 3 listing-3.php
<?php
    function myCallback($arg1) {
        echo $arg1;
    }
 
    $callback = 'myCallback';
 
    if (is_callable($callback)) {
        call_user_func($callback, 'Hello');
    }
?>
Note: In this particular example you could simply call $callback('Hello'). The problem with this is that it will only work for normal functions - you can't use a class method as a callback using this technique.
Returning to our previous example, we can now alter the downloadFromSites() function so it accepts a callback as the second argument. Now in addition to returning all of the data once all sites have been processed, the callback will be called after each site is processed.
The callback will accept the site name as its first argument and the downloaded data as the second argument.
Listing 4 listing-4.php
<?php
    // this function simulates downloading data from a site
    function downloadFromSites($sites, $callback = null)
    {
        $ret = array();
 
        foreach ($sites as $site) {
            sleep(2);
 
            $data = 'downloaded data here';
 
            // check if the callback is valid
            if (is_callable($callback)) {
                // callback is valid - call it with given arguments
                call_user_func($callback, $site, $data);
            }
 
            // write the data for this site to return array
            $ret[] = array(
                'site' => $site,
                'data' => $data
            );
        }
 
        return $ret;
    }
 
    // define a fictional class used for the callback
    class MyClass
    {
        // this is the callback method
        public static function downloadComplete($site, $data)
        {
            echo sprintf("Finished downloading from %s\n", $site);
        }
    }
 
    // imaginary list of sites to download from
    $sites = array(
        'http://www.example1.com',
        'http://www.example2.com'
        // more sites...
    );
 
    // start downloading and store the return data
    downloadFromSites($sites, array('MyClass', 'downloadComplete'));
 
    // we don't need to loop over the return data
    // now since the callback handles that instead
?>
When you run this code, you will see that the "finished downloading" message is displayed as each site is completed rather than all at the end.

PHP A to ZCE: Databases and SQL


Databases are an extremely useful tool in web development as they allow you to store data about users, customers, e-commerce products and orders, or anything else. In this article I will cover the basics of using databases in PHP, including how to manage data using SQL.
  • There are many different database servers that can be used, each offering varying features and licensing options
  • PHP has support for many different database types, each of which is documented in the manual (PHP Database Extensions)
  • The most commonly used database servers with PHP are MySQL, PostgreSQL and Sqlite. This is primary because they (usually) free and they work on servers that run Apache and PHP
  • Each type of database has its own PHP functions. For instance, to perform a query on MySQL, PostgreSQL and Sqlite the mysql_query()pg_query() and sqlite_query() functions are used respectively.
  • Because of this, database and/or SQL abstraction is encouraged. See the section on Database Abstraction below.

Database Design

  • A database is made up of a series of tables
  • Typically each table is used to store different types of data. For instance, you might have one table to store information about your users and another to store information about your products.
  • Each table can have zero or more records. If you have a table to store user information, a single user would correspond to a single record.
  • Each table is made up of one or more columns. Each column has its own name and type. For example, one column might a string that holds the user's name, and another might be a date column to hold their date of birth.
  • Each table can have a primary key, used to distinguish each row. This allows you to retrieve a specific column (or columns) as required
  • Tables can have relationships with other tables. For example, if you have a table that holds e-commerce orders it may have a relationship with the users table so you know which user placed the order.
  • When creating a table you can index columns. This allows searching of that column quickly.
The following figure is a basic representation of how a users and orders tables may be designed. In the real-world there would be more columns to hold additional data (not to mention more tables, such as one to hold product data).
Figure 1 A simple example of how database tables relate to each other
Figure 1: A simple example of how database tables relate to each other
Note: The arrows indicate a one-to-many relationship. That is, a single user can have many orders, but a single order only has one user.

Structured Query Language (SQL)

In order to manage the data in a database, Structured Query Language, or SQL, is used.
  • There are four basic operations in SQL: Selecting records, inserting records, updating records, deleting records
  • This is also known as CRUD (create, read, update, delete)
  • For the most part SQL is the same between each database server type, but there are some differences (for instance, the syntax to limit the number of rows returns differs slightly between MySQL and PostgreSQL).

Creating Tables

  • Tables are created using a CREATE TABLE statement
  • This statement lists all columns and keys that should be created for the table
Listing 1 listing-1.txt
CREATE TABLE users (
    user_id     serial          not null,
    name        varchar(255)    not null,
    country     varchar(2)      not null,

    primary key (user_id)
);
Note: In MySQL and PostgreSQL, the serial column type defines an integer column that auto-increments when a new row is inserted.

Inserting Data

  • Data is inserted using an INSERT statement
  • You specify a list of columns and values to insert into
  • If a column is not defined as NOT NULL you don't need to specify it when inserting.
Listing 2 listing-2.txt
INSERT INTO users (name, country) VALUES ('Quentin', 'AU');

Retrieving Data

  • Data is retrieved using a SELECT statement
  • There are several key clauses to a select statement:
    • List of columns to retrieve. You can use * to mean every column
    • List of tables those columns belong to
    • Criteria for filtering results, such as returning only users with a country of AU (the WHERE clause)
    • Criteria for sorting the returned data, such as alphabetical by name (the ORDER BY clause
  • There are other clauses that can be included, but these are the most important
  • Only the list of columns and tables are required
  • Clauses must appear in the correct order (columns, tables, where, order, limit).
Listing 3 listing-3.txt
mysql> SELECT name, country FROM users WHERE country = 'AU' ORDER BY 'name';
+---------+---------+
| name    | country |
+---------+---------+
| Quentin | AU      |
+---------+---------+

Updating Data

  • Data is updated using an UPDATE statement. This statement operates on a single table.
  • This statement contains a list of the columns you want to update and corresponding values to update
  • You must also specify a WHERE clause - if you don't, every row will be updated!
The following SQL statement updates the name of all matching users.
Listing 4 listing-4.txt
mysql> UPDATE users SET name = 'Peter' WHERE name = 'Quentin';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT name, country FROM users WHERE country = 'AU' ORDER BY 'name';
+-------+---------+
| name  | country |
+-------+---------+
| Peter | AU      |
+-------+---------+
1 row in set (0.00 sec)
Caution: It is strongly recommended that you test your update statements by creating a select statement using the same WHERE clause. This will help you avoid updating incorrect rows.

Deleting Data

  • Data is removed using a DELETE statement. This statement operates on a single table.
  • You must specify a WHERE clause - if you don't, every row will be deleted!
Caution: It is strongly recommended that you test your delete statements by creating a select statement using the same WHERE clause. This will help you avoid updating incorrect rows.
The following SQL statement deletes all users for the given country code.
Listing 5 listing-5.txt
DELETE FROM users WHERE country = 'AU';

Using MySQL With PHP

The general flow of using MySQL is as follows
  1. Connect to database server with mysql_connect()
  2. Select a database to operate on using mysql_select_db()
  3. Perform select, insert, update or delete query using mysql_query()
  4. If performing a select statement you can used the returned result to determine the number of rows using mysql_num_rows(), and you can loop over rows using mysql_fetch_array()
  5. Close the connection using mysql_close().
Listing 6 listing-6.php
<?php
    // connect to the database server
    $db = mysql_connect('localhost', 'myUsername', 'myPassword');
 
    // select the database
    mysql_select_db('myDatabase', $db);
 
    // perform a query
    $query = 'select * from users';
    $result = mysql_query($query, $db);
 
    // output the number of rows
    echo sprintf('%d rows found', mysql_num_rows($db));
 
    // loop over the rows and output data
    while ($row = mysql_fetch_array($result)) {
        echo sprintf('%s is from %s', $row['name'], $row['country']);
    }
 
    // close the connection
    mysql_close($db);
?>
Note: Typically you'd make the connection in a bootstrap file so the connection is available from all PHP scripts in your site.
  • Whenever you use a PHP variable in a where clause you should call mysql_real_escape_string() to prevent SQL injection
  • This is especially true for user-submitted data.
Listing 7 listing-7.php
<?php
    $name = $_POST['name'];
 
    $query = sprintf(
        "select * from users where name = '%s'",
        mysql_real_escape_string($name)
    );
?>

Database Abstraction

Because each database has its own set of functions, database abstraction is commonly used. This is a layer between your PHP script and the database-specific PHP functions.
The following figure demonstrates how database abstraction works. Once you use database abstraction you only need to know the functions of the abstraction layer - you don't need to care about the specific functions (although you should still learn them!)
Figure 2 Database abstraction gives a single code-entry point regardless of the database server type
Figure 2: Database abstraction gives a single code-entry point regardless of the database server type
Note that this diagram doesn't mention a specific abstraction layer (there are many to choose from). One good example is Zend_Db_Adapter. You can also use Zend_Db_Select for SQL abstraction (that is, build SQL statements that are compatible with multiple database servers).

Summary

This article touches on a number of different subjects, but it's really just a primer on SQL and using MySQL in PHP. There's a lot of things to learn when it comes to databases.

Shortening URLs for goo.gl with Google's URL Shortener API


In 2010 Google released its own URL shortener, which allows you to shorten URLs to use the goo.gl domain. In this article I will show you how to easily create your own short URLs using their new URL shortener API.
Note: This API is still in labs, meaning the API is subject to change without notice.
The Google URL Shortener API allows you to do the following:
  • Create a new short URL
  • Retrieve info about a short URL (such as the full URL and various analytics)
  • Retrieve a list of shortened URLs for a given user
Note: To retrieve a list of shortened URLs an OAuth token is required to authenticate the user. Additionally, you must be authenticated in the same way when creating the URL in order for it to be linked to the given account. To simplify this article we will not be covering this aspect of the API.
We will use the cURL library to perform the required HTTP requests to make use of this API. Additionally, JSON data is extensively used for both requests and responses, so we will use the json_encode() andjson_decode() functions accordingly.

Creating an API Key

To use the Google URL Shortener API you must have an API key. To get an API, key, follow these instructions:
  1. Visit the Google APIs console
  2. Create a project
  3. Activate the URL Shortener API
  4. Click Keys in the left navigation. You can then copy and paste the key from this page
Note: You can perform a limited number of API calls without a key. This may be useful during development.

Creating a Short URL

To create a shortened URL, post to https://www.googleapis.com/urlshortener/v1/url?key=key.
Rather than posting traditional form fields, we post JSON data instead. As such, we must also set the correct content type header for the request. Usually for post requests this is application/x-www-form-urlencoded, but we're posting JSON data so we use application/json.
To begin, let's define the API key and endpoint URL. The call we're making isn't the only API call available, so we define the base URL endpoint so it can be used for other calls too.
Listing 1 Defining the API key and API endpoint (listing-1.php)
<?php
    define('GOOGLE_API_KEY', '[insert your key here]');
    define('GOOGLE_ENDPOINT', 'https://www.googleapis.com/urlshortener/v1');
?>
Next, we define the shortenUrl function. This function accepts the long URL you want shortened and returns an array which holds the long and short URLs.
Listing 2 Defining the shortenUrl() function and initializing the cURL connection (listing-2.php)
<?php
    function shortenUrl($longUrl)
    {
        // initialize the cURL connection
        $ch = curl_init(
            sprintf('%s/url?key=%s', GOOGLE_ENDPOINT, GOOGLE_API_KEY)
        );
 
        // tell cURL to return the data rather than outputting it
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
 
        // ... more code will go here
    }
?>
Here we initialize the cURL connection for our HTTP request. We build the URL from the endpoint and our API key and pass it as the first argument to curl_init().
Additionally, we set the CURLOPT_RETURNTRANSFER option to true. If we don't do this, we can't decode the returned JSON data. Instead, it will be output directly.
Next we must build the request data. As mentioned previously, the request must be a POST request which contains JSON data as the post body.
To create a new shortened URL, a single parameter called longUrl is required. The corresponding value should the URL to shorten. The following list demonstrates how we set the request to post JSON data.
Listing 3 Setting the cURL connection to post JSON data (listing-3.php)
<?php
    function shortenUrl($longUrl)
    {
        // ... other code
 
        // create the data to be encoded into JSON
        $requestData = array(
            'longUrl' => $longUrl
        );
 
        // change the request type to POST
        curl_setopt($ch, CURLOPT_POST, true);
 
        // set the form content type for JSON data
        curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-type: application/json'));
 
        // set the post body to encoded JSON data
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestData));
 
        // ... other code
    }
?>
At this point, the cURL request is all set-up and ready to go. We can now excecute the request. The following listing demonstrates how to achieve this.
Listing 4 Performing the cURL request and decoding the response (listing-4.php)
<?php
    function shortenUrl($longUrl)
    {
        // ... other code
 
        // perform the request
        $result = curl_exec($ch);
        curl_close($ch);
 
        // decode and return the JSON response
        return json_decode($result, true);
    }
?>
Since this web service returns JSON data, we can turn it into a native PHP array using json_docode(). The second argument of true instructs PHP to create an array (instead of an object).
Note: This code does not perform any error handling. Read about Google URL Shortener API error messages.
Finally, we can make use of the shortenUrl() function simply by passing it a URL. We can then output the returned shortened URL. The following listing demonstrates how this is achieved.
Listing 5 Using shortenUrl() and outputting the results (listing-5.php)
<?php
    $response = shortenUrl('http://phpriot.com');
 
    echo sprintf(
        '%s was shortened to %s',
        $response['longUrl'],
        $response['id']
    );
?>

The Complete Code Listing

The complete listing we constructed in this article is as follows. Remember to substitute in your own API key.
Listing 6 Complete code listing (listing-6.php)
<?php
    define('GOOGLE_API_KEY', '[insert your key here]');
    define('GOOGLE_ENDPOINT', 'https://www.googleapis.com/urlshortener/v1');
 
    function shortenUrl($longUrl)
    {
        // initialize the cURL connection
        $ch = curl_init(
            sprintf('%s/url?key=%s', GOOGLE_ENDPOINT, GOOGLE_API_KEY)
        );
 
        // tell cURL to return the data rather than outputting it
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
 
        // create the data to be encoded into JSON
        $requestData = array(
            'longUrl' => $longUrl
        );
 
        // change the request type to POST
        curl_setopt($ch, CURLOPT_POST, true);
 
        // set the form content type for JSON data
        curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-type: application/json'));
 
        // set the post body to encoded JSON data
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestData));
 
        // perform the request
        $result = curl_exec($ch);
        curl_close($ch);
 
        // decode and return the JSON response
        return json_decode($result, true);
    }
 
    $response = shortenUrl('http://phpriot.com');
 
    echo sprintf(
        '%s was shortened to %s',
        $response['longUrl'],
        $response['id']
    );
?>

Summary

In this article I showed you how to make use of the new Google URL Shortener API to generate your own short links on the goo.gl domain.
This API also allows you retrieve information and analytics about your shortened URLs. This requires only basic modifications to this script.