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.

No comments:

Post a Comment