Приглашаем посетить
Чернышевский (chernyshevskiy.lit-info.ru)

Overloading

Previous
Table of Contents
Next

Overloading

Let's bring together some of the techniques developed so far in this chapter and use overloading to provide a more OO-style interface to the result set. Having all the results in a single object may be a familiar paradigm to programmers who are used to using Java's JDBC database connectivity layer.

Specifically, you want to be able to do the following:

$query = "SELECT name, email FROM users";
$dbh = new DB_Mysql_Test;
$stmt = $dbh->prepare($query)->execute();
$result = $stmt->fetch();
while($result->next()) {
  print "<a href=\"mailto:$result->email\">$result->name</a>";
}

The code flow proceeds normally until after execution of the query. Then, instead of returning the rows one at a time as associative arrays, it would be more elegant to return a result object with an internal iterator that holds all the rows that have been seen.

Instead of implementing a separate result type for each database that you support through the DB_Connection classes, you can exploit the polymorphism of the statement's classes to create a single DB_Result class that delegates all its platform-specific tasks to the DB_Statement object from which it was created.

DB_Result should possess forward and backward iterators, as well as the ability to reset its position in the result set. This functionality follows easily from the techniques you've learned so far. Here is a basic implementation of DB_Result:

class DB_Result {
  protected $stmt;
  protected $result = array();
  private $rowIndex = 0;
  private $currIndex = 0;
  private $done = false;
  public function _ _construct(DB_Statement $stmt)
  {
    $this->stmt = $stmt;
  }
  public function first()
  {
    if(!$this->result) {
      $this->result[$this->rowIndex++] = $this->stmt->fetch_assoc();
    }
    $this->currIndex = 0;
    return $this;
  }
  public function last()
  {
    if(!$this->done) {
      array_push($this->result, $this->stmt->fetchall_assoc());
    }
    $this->done = true;
    $this->currIndex = $this->rowIndex = count($this->result) - 1;
    return $this;
  }
  public function next()
  {
    if($this->done) {
      return false;
    }
    $offset = $this->currIndex + 1;
    if(!$this->result[$offset]) {
      $row = $this->stmt->fetch_assoc();
      if(!$row) {
        $this->done = true;
        return false;
      }
      $this->result[$offset] = $row;
      ++$this->rowIndex;
      ++$this->currIndex;
      return $this;
    }
    else {
      ++$this->currIndex;
      return $this;
    }
  }
  public function prev()
  {
    if($this->currIndex == 0) {
      return false;
    }
    --$this->currIndex;
    return $this;
  }
}

The following are some things to note about DB_Result:

  • Its constructor uses a type hint to ensure that the variable passed to it is a DB_Statement object. Because your iterator implementations depend on $stmt complying with the DB_Statement API, this is a sanity check.

  • Results are lazy-initialized (that is, they are not created until they are about to be referenced). In particular, individual rows are only populated into DB_Result::result when the DB_Result object is iterated forward to their index in the result set; before that, no populating is performed. We will get into why this is important in Chapter 10, "Data Component Caching," but the short version is that lazy initialization avoids performing work that might never be needed until it is actually called for.

  • Row data is stored in the array DB_Result::result; however, the desired API had the data referenced as $obj->column, not $obj->result['column'], so there is still work left to do.

The difficult part in using an OO interface to result sets is providing access to the column names as properties. Because you obviously cannot know the names of the columns of any given query when you write DB_Result, you cannot declare the columns correctly ahead of time. Furthermore, because DB_Result stores all the rows it has seen, it needs to store the result data in some sort of array (in this case, it is DB_Result::result).

Fortunately, PHP provides the ability to overload property accesses via two magical methods:

  • function _ _get($varname) {} This method is called when an undefined property is accessed for reading.

  • function _ _set($varname, $value) {} This method is called when an undefined property is accessed for writing.

In this case, DB_Result needs to know that when a result set column name is accessed, that column value in the current row of the result set needs to be returned. You can achieve this by using the following _ _get function, in which the single parameter passed to the function is set by the system to the name of the property that was being searched for:

public function _ _get($varname)
{
  if(array_key_exists($value,
                      $this->result[$this->currIndex])) {
    return $this->result[$this->currIndex][$value];
  }
}

Here you check whether the passed argument exists in the result set. If it does, the accessor looks inside $this->result to find the value for the specified column name.

Because the result set is immutable (that is, you cannot change any of the row data through this interface), you don't need to worry about handling the setting of any attributes.

There are many other clever uses for property overriding abilities. One interesting technique is to use _ _get() and _ _set() to create persistent associative arrays that are tied to a DBM file (or other persistent storage). If you are familiar with Perl, you might liken this to using tie() in that language.

To make a persistent hash, you create a class called Tied that keeps an open handle to a DBM file. (DBM files are explored in depth in Chapter 10.) When a read request is initiated on a property, that value is fetched from the hash and deserialized (so that you can store complex data types). A write operation similarly serializes the value that you are assigning to the variable and writes it to the DBM. Here is an example that associates a DBM file with an associative array, making it effectively a persistent array (this is similar to a Tied hash in Perl):

class Tied {
  private $dbm;
  private $dbmFile;
  function _ _construct($file = false)
  {
    $this->dbmFile = $file;
    $this->dbm = dba_popen($this->dbmFile, "c", "ndbm");
  }
  function _ _destruct()
  {
    dba_close($this->dbm);
  }
  function _ _get($name)
  {
    $data = dba_fetch($name, $this->dbm);
    if($data) {
      print $data;
      return unserialize($data);
    }
    else {
      print "$name not found\n";
      return false;
    }
  }
  function _ _set($name, $value)
  {
    dba_replace($name, serialize($value), $this->dbm);
  }
}

Now you can have an associative array type of object that allows for persistent data, so that if you use it as:

<?
$a = new Tied("/tmp/tied.dbm");
if(!$a->counter) {
  $a->counter = 1;
}
else {
  $a->counter++;
}
print "This page has been accessed ".$a->counter." times.\n";
?>

each access increments it by one:

> php 19.php

This page has been accessed 1 times.
> php 19.php

This page has been accessed 2 times.

Overloading can also be used to provide access controls on properties. As you know, PHP variables can be of any type, and you can switch between types (array, string, number, and so on) without problems. You might, however, want to force certain variables to stay certain types (for example, force a particular scalar variable to be an integer). You can do this in your application code: You can manually validate any data before a variable is assigned, but this can become cumbersome, requiring a lot of duplication of code and allowing for the opportunity for forgetting to do so.

By using _ _get() and _ _set(), you can implement type checking on assignment for certain object properties. These properties won't be declared as standard attributes; instead, you will hold them in a private array inside your object. Also, you will define a type map that consists of variables whose types you want to validate, and you will define the function you will use to validate their types. Here is a class that forces its name property to be a string and its counter property to be an integer:

class Typed {
  private $props = array();
  static $types = array (
    "counter" => "is_integer",
    "name" => "is_string"
  );
  public function _ _get($name) {
    if(array_key_exists($name, $this->props)) {
      return $this->props[$name];
    }
  }
  public function _ _set($name,$value) {
    if(array_key_exists($name, self::$types)) {
      if(call_user_func(self::$types[$name],$value)) {
        $this->props[$name] = $value;
   }
   else {
     print "Type assignment error\n";
     debug_print_backtrace();
   }
  }
 }
}

When an assignment occurs, the property being assigned to is looked up in self::$types, and its validation function is run. If you match types correctly, everything works like a charm, as you see if you run the following code:

$obj = new Typed;
$obj->name = "George";
$obj->counter = 1;

However, if you attempt to violate your typing constraints (by assigning an array to $obj->name, which is specified of type is_string), you should get a fatal error. Executing this code:

$obj = new Typed;
$obj->name = array("George");

generates the following error:

> php 20.php
Type assignment error
#0 typed->_ _set(name, Array ([0] => George)) called at [(null):3]
#1 typed->unknown(name, Array ([0] => George)) called at [/Users/george/
Advanced PHP/examples/chapter-2/20.php:28]

SPL and Iterators

In both of the preceding examples, you created objects that you wanted to behave like arrays. For the most part, you succeeded, but you still have to treat them as objects for access. For example, this works:

$value = $obj->name;

But this generates a runtime error:

$value = $obj['name'];

Equally frustrating is that you cannot use the normal array iteration methods with them. This also generates a runtime error:

foreach($obj as $k => $v) {}

To enable these syntaxes to work with certain objects, Marcus Boerger wrote the Standard PHP Library (SPL) extension for PHP5. SPL supplies a group of interfaces, and it hooks into the Zend Engine, which runs PHP to allow iterator and array accessor syntaxes to work with classes that implement those interfaces.

The interface that SPL defines to handle array-style accesses is represented by the following code:

interface ArrayAccess {
  function offsetExists($key);
  function offsetGet($key);
  function offsetSet($key, $value);
  function offsetUnset($key);
}

Of course, because it is defined inside the C code, you will not actually see this definition, but translated to PHP, it would appear as such.

If you want to do away with the OO interface to Tied completely and make its access operations look like an arrays, you can replace its _ _get() and _ _set() operations as follows:

function offsetGet($name)
{
  $data = dba_fetch($name, $this->dbm);
  if($data) {
    return unserialize($data);
  }
  else {
    return false;
  }
}
function offsetExists($name)
{
  return dba_exists($name, $this->dbm);
}
function offsetSet($name, $value)
{
  return dba_replace($name, serialize($value), $this->dbm);
}
function offsetUnset($name)
{
  return dba_delete($name, $this->dbm);
}

Now, the following no longer works because you removed the overloaded accessors:

$obj->name = "George"; // does not work

But you can access it like this:

$obj['name'] = "George";

If you want your objects to behave like arrays when passed into built-in array functions (e.g., array map( )) you can implement the Iterator and IteratorAggregate interfaces, with the resultant iterator implementing the necessary interfaces to provide support for being called in functions which take arrays as parameters. Here's an example:

interface IteratorAggregate {
  function getIterator();
}
interface Iterator {

  function rewind();

  function valid();

  function key();

  function current();

  function next();

}

In this case, a class stub would look like this:

class KlassIterator implemnts Iterator {
  /* ... */
}

class Klass implements IteratorAggregate {
  function getIterator() {
    return new KlassIterator($this);
  }
  /* ... */
}

The following example allows the object to be used not only in foreach() loops, but in for() loop as well:

$obj = new Klass;

for($iter = $obj->getIterator(); $iter->valid(); $iter = $iter->next())
{
  // work with $iter->current()
}

In the database abstraction you wrote, you could modify DB_Result to be an iterator. Here is a modification of DB_Result that changes it's API to implement Iterator:

class DB_Result {
  protected $stmt;
  protected $result = array();
  protected $rowIndex = 0;
  protected $currIndex = 0;
  protected $max = 0;
  protected $done = false;

  function _ _construct(DB_Statement $stmt)
  {
    $this->stmt = $stmt;
  }
  function rewind() {
    $this->currIndex = 0;
  }
  function valid() {
    if($this->done && $this->max == $this->currIndex) {
      return false;
    }
    return true;
  }
  function key() {
    return $this->currIndex;
  }
  function current() {
    return $this->result[$this->currIndex];
  }
  function next() {
    if($this->done && ) {
      return false;
    }
    $offset = $this->currIndex + 1;
    if(!$this->result[$offset]) {
      $row = $this->stmt->fetch_assoc();
      if(!$row) {
        $this->done = true;
        $this->max = $this->currIndex;
        return false;
      }
      $this->result[$offset] = $row;
      ++$this->rowIndex;
      ++$this->currIndex;
      return $this;
    }
    else {
      ++$this->currIndex;
      return $this;
    }
  }
}

Additionally, you need to modify MysqlStatement to be an IteratorAggregate, so that it can be passed into foreach() and other array-handling functions. Modifying MysqlStatement only requires adding a single function, as follows:

class MysqlStatement implements IteratorAggregate {
  function getIterator() {
    return new MysqlResultIterator($this);
  }
}

If you don't want to create a separate class to be a class's Iterator, but still want the fine-grain control that the interface provides, you can of course have a single class implement both the IteratorAggregate and Iterator interfaces.

For convenience, you can combine the Iterator and Array Access interfaces to create objects that behave identically to arrays both in internal and user-space functions. This is ideal for classes like Tied that aimed to pose as arrays. Here is a modification of the Tied class that implements both interfaces:

class Tied implements ArrayAccess, Iterator {
  private $dbm;
  private $dbmFile;
  private $currentKey;
  function _ _construct($file = false)
  {
    $this->dbmFile = $file;
    $this->dbm = dba_popen($this->dbmFile, "w", "ndbm");
  }
  function _ _destruct()
  {
    dba_close($this->dbm);
  }
  function offsetExists($name)
  {
    return dba_exists($name, $this->dbm);
  }
  function offsetGet($name)
  {
    $data = dba_fetch($name, $this->dbm);
    if($data) {
      return unserialize($data);
    }
    else {
      return false;
    }
  }
  function offsetSet($name, $value)
  {
    return dba_replace($name, serialize($value), $this->dbm);
  }
  function offsetUnset($name)
  {
    return dba_delete($name, $this->dbm);
  }
  function rewind() {
        $this->current = dba_firstkey($this->dbm);
  }
  function current()
 {
    $key = $this->currentKey;
    if($key !== false) {
      return $this->_ _get($key);
    }
  }
  function next()
 {
    $this->current = dba_nextkey($this->dbm);
  }
  function has_More() {
    return ($this->currentKey === false)?false:true;
  }
  function key()
 {
    return $this->currentKey;
  }
}

To add the iteration operations necessary to implement Iterator, Tied uses dba_firstkey() to rewind its position in its internal DBM file, and it uses dba_nextkey() to iterate through the DBM file.

With the following changes, you can now loop over a Tied object as you would a normal associative array:

$obj = new Tied("/tmp/tied.dbm");
$obj->foo = "Foo";
$obj->bar = "Bar";
$obj->barbara = "Barbara";

foreach($a as $k => $v) {
        print "$k => $v\n";
}

Running this yields the following:

foo => Foo
counter => 2
bar => Bar
barbara => Barbara

Where did that counter come from? Remember, this is a persistent hash, so counter still remains from when you last used this DBM file.

_ _call()

PHP also supports method overloading through the _ _call() callback. This means that if you invoke a method of an object and that method does not exist, _ _call() will be called instead. A trivial use of this functionality is in protecting against undefined methods. The following example implements a _ _call() hook for a class that simply prints the name of the method you tried to invoke, as well as all the arguments passed to the class:

class Test {
  public function _ _call($funcname, $args)
  {
    print "Undefined method $funcname called with vars:\n";
    print_r($args);
  }
}

If you try to execute a nonexistent method, like this:

$obj = new Test;
$obj->hello("george");

you will get the following output:

Undefined method hello called with vars:
Array
(
     [0] => george
)

_ _call() handlers are extremely useful in remote procedure calls (RPCs), where the exact methods supported by the remote server are not likely to know when you implement your client class. RPC methods are covered in depth in Chapter 16, "RPC: Interacting with Remote Services." To demonstrate their usage here briefly, you can put together an OO interface to Cisco routers. Traditionally, you log in to a Cisco router over Telnet and use the command-line interface to configure and maintain the router. Cisco routers run their own proprietary operating system, IOS. Different versions of that operating system support different feature sets and thus different command syntaxes. Instead of programming a complete interface for each version of IOS, you can use _ _call() to automatically handle command dispatching.

Because the router must be accessed via Telnet, you can extend PEAR's Net_Telnet class to provide that layer of access. Because the Telnet details are handled by the parent class, you only need two real functions in the class. The first, login(), handles the special case of login. login() looks for the password prompt and sends your login credentials when it sees the password prompt.

PEAR

PHP Extension and Application Repository (PEAR) is a project that is loosely associated with the PHP group. Its goal is to provide a collection of high-quality, OO, reusable base components for developing applications with PHP. Throughout this book, I use a number of PEAR classes. In both this book and my own programming practice, I often prefer to build my own components. Especially in performance-critical applications, it is often easiest to design a solution that fits your exact needs and is not overburdened by extra fluff. However, it can sometimes be much easier to use an existing solution than to reinvent the wheel.

Since PHP 4.3, PHP has shipped with a PEAR installer, which can be executed from the command line as follows:

   > pear

To see the full list of features in the PEAR installer you can simply type this:

   > pear help

The main command of interest is pear install. In this particular case, you need the Net_Telnet class to run this example. To install this class, you just need to execute this:

   > pear install Net_Telnet

You might need to execute this as root. To see a complete list of PEAR packages available, you can run this:

   > pear list-all

or visit the PEAR Web site, at http://pear.php.net.


The second function you need in the Net_Telnet class is the _ _call() handler. This is where you take care of a couple details:

  • Many Cisco IOS commands are multiword commands. For example, the command to show the routing table is show ip route. You might like to support this both as $router->show_ip_route() and as $router->show("ip route"). To this end, you should replace any _ in the method name with a space and concatenate the result with the rest of the arguments to make the command.

  • If you call a command that is unimplemented, you should log an error.

    (Alternatively, you could use die() or throw an exception. Chapter 3 covers good error-handling techniques in depth.)

Here is the implementation of Cisco_RPC; note how short it is, even though it supports the full IOS command set:

require_once "Net/Telnet.php";
class Cisco_RPC extends Net_Telnet {
  protected $password;
  function _ _construct($address, $password,$prompt=false)
  {
    parent::_ _construct($address);
    $this->password = $password;
    $this->prompt = $prompt;
  }
  function login()
  {
    $response = $this->read_until("Password:");
    $this->_write($this->password);
    $response =  $this->read_until("$this->prompt>");
  }
  function _ _call($func, $var) {
    $func = str_replace("_", " ", $func);
    $func .= " ".implode(" ", $var);
    $this->_write($func);
    $response = $this->read_until("$this->prompt>");
    if($response === false || strstr($response, "%Unknown command")) {
      error_log("Cisco command $func unimplemented", E_USER_WARNING);
    }
    else {
      return $response;
    }
  }
}

You can use Cisco_RPC quite easily. Here is a script that logs in to a router at the IP address 10.0.0.1 and prints that router's routing table:

$router = new Cisco_RPC("10.0.0.1", "password");
$router->login();
print $router->show("ip route");

_ _autoload()

The final magic overloading operator we will talk about in this chapter is _ _autoload(). _ _autoload() provides a global callback to be executed when you try to instantiate a nonexistent class. If you have a packaging system where class names correspond to the files they are defined in, you can use _ _autoload() to do just-in-time inclusion of class libraries.

If a class you are trying to instantiate is undefined, your _ _autoload() function will be called, and the instantiation will be tried again. If the instantiation fails the second time, you will get the standard fatal error that results from a failed instantiation attempt.

If you use a packaging system such as PEAR, where the class Net_Telnet is defined in the file Net/Telnet.php, the following _ _autoload() function would include it on-the-fly:

function _ _autoload($classname) {
  $filename = str_replace("_","/", $classname). '.php';
  include_once $filename;
}

All you need to do is replace each _ with / to translate the class name into a filename, append .php, and include that file. Then if you execute the following without having required any files, you will be successful, as long as there is a Net/Telnet.php in your include path:

<?php
$telnet = new Net_Telnet;
? >


Previous
Table of Contents
Next