PHP Cookbook/Classes and Objects

From WikiContent

Jump to: navigation, search
PHP Cookbook


Contents

Introduction

At first, PHP wasn't an object-oriented (OO) language. As it evolved, more and more object-oriented features appeared. First, you could define classes, but there were no constructors. Then, constructors appeared, but there were no destructors. Slowly but surely, as more people began to push the limits of PHP's syntax, additional features were added to satisfy the demand.

However, if you're the type of person who wishes PHP to be a true OO language, you'll probably be disappointed. At its heart, PHP is a procedural language. It isn't Java. But, if you're the type of person who wants to use some OO features in your code, PHP is probably right for you.

A class is a package containing two things: data and methods to access and modify that data. The data portion consists of variables; they're known as properties. The other part of a class is a set of functions that can alter a class' properties; they're called methods.

When we define a class, we don't define an object that can be accessed and manipulated. Instead, we define a template for an object. From this blueprint, we create malleable objects through a process known as instantiation . A program can have multiple objects of the same class, just as a person can have more than one book or many pieces of fruit.

Classes also live in a defined hierarchy. At the top of the chain, there is a generic class. In PHP, this class is named stdClass , for "standard class." Each class down the line is more specialized than its parent. For example, a parent class could be a building. Buildings can be further divided into residential and commercial. Residential buildings can be further subdivided into houses and apartment buildings, and so forth.

Both houses and apartment buildings have the same set of properties as all residential buildings, just as residential and commercial buildings share some things in common. When classes are used to express these parent-child relationships, the child class inherits the properties and methods defined in the parent class. This allows you to reuse the code from the parent class and requires you to write code only to adapt the new child to its specialized circumstances. This is called inheritance and is one of the major advantages of classes over functions. The process of defining a child class from a parent is known as subclassing or extending.

Objects play another role in PHP outside their traditional OO position. Since PHP can't use more than one namespace, the ability for a class to package multiple properties into a single object is extremely helpful. It allows clearly demarcated separate areas for variables.

Classes in PHP are easy to define and create:

class guest_book {
    var $comments;
    var $last_visitor;

    function update($comment, $visitor) {
        ...
    }

}

The class keyword defines an class, just as function defines a function. Properties are declared using the var keyword. Method declaration is identical to how functions are defined.

The new keyword instantiates an object:

$gb = new guest_book;

Object instantiation is covered in more detail in Recipe 7.2.

Inside a class, you can optionally declare properties using var. There's no requirement to do so, but it is a useful way to reveal all the class' variables. Since PHP doesn't force you to predeclare all your variables, it's possible to create one inside a class without PHP throwing an error or otherwise letting you know. This can cause the list of variables at the top of a class definition to be misleading, because it's not the same as the list of variables actually in the class.

Besides declaring a property, you can also assign it a value:

var $last_visitor = 'Donnan';

You can assign constant values only using this construct:

var $last_visitor = 'Donnan';         // okay
var $last_visitor = 9;                // okay
var $last_visitor = array('Jesse');   // okay
var $last_visitor = pick_visitor( );   // bad
var $last_visitor = 'Chris' . '9';    // bad

If you try to assign something else, PHP dies with a parse error.

To assign a non-constant value to a variable, do it from a method inside the class.

var $last_visitor;

function update($comment, $visitor) {
    if (!empty($comment)) {
        array_unshift($this->comments, $comment);
        $this->last_visitor = $visitor;
    }
}

If the visitor left a comment, you add it to the top of the array of comments and set that person as the latest visitor to the guest book. The variable $this is a special variable that refers to the current object. So, to access the $size property of an object from inside that object, refer to $this->size.

To assign nonconstant values to variables upon instantiation, assign them in the class constructor. The class constructor is a method automatically called when a new object is created, and it has the same name as your class:

class guest_book {
    var $comments;
    var $last_visitor;

    function guest_book($user) {
        $dbh  =  mysql_connect('localhost', 'username', 'password');
        $db   =  mysql_select_db('sites');
        $user =  mysql_real_escape_string($user);
        $sql  = "SELECT comments, last_visitor FROM guest_books WHERE user='$user'";
        $r    =  mysql_query($sql);

        if ($obj  = mysql_fetch_object($r)) {
            $this->comments = $obj->comments;
            $this->last_visitor = $obj->last_visitor;
        }
    }
}

$gb = new guest_book('stewart');

Constructors are covered in Recipe 7.3. Note that mysql_real_escape_string() is new as of PHP 4.3; for earlier versions, use mysql_escape_string().

Be careful not to mistakenly type $this->$size. This is legal, but it's not the same as $this->size. Instead, it accesses the property of the object whose name is the value stored in the $size variable. More often then not, $size is undefined, so $this->$size appears empty. For more on variable property names, see Recipe 6.6.

Besides using -> to access a method or member variable, you can also use ::. This syntax can access static methods in a class. These methods are identical for every instance of an class, because they can't rely on instance-specific data. For example:

class convert {
    // convert from Celsius to Fahrenheit
    function c2f($degrees) {
        return (1.8 * $degrees) + 32;
    }
}

$f = convert::c2f(100); // 212

To implement inheritance by extending an existing class, use the extends keyword:

class xhtml extends xml {

}

Child classes inherit parent methods and can optionally choose to implement their own specific versions:

class DB {
    var $result;

    function getResult() {
        return $this->result;
    }

    function query($sql) {
        error_log("query() must be overridden by a database-specific child");
        return false;
    }
}

class MySQL extends DB {
    function query($sql) {
        $this->result =  mysql_query($sql);
    }
}

The MySQL class above inherits the getResult( ) method unchanged from the parent DB class, but has its own MySQL-specific query( ) method.

Preface the method name with parent :: to explicitly call a parent method:

function escape($sql) {
    $safe_sql = mysql_real_escape_string($sql); // escape special characters
    $safe_sql = parent::escape($safe_sql); // parent method adds '' around $sql
    return $safe_sql;
}

Recipe 7.8 covers accessing overridden methods.

The underlying engine powering PHP is named Zend. PHP 4 uses Zend Engine 1; PHP 5 will use an updated version — Zend Engine 2 (ZE2). ZE2 has an entirely new object model that allows PHP to support many new object-oriented features: constructors and destructors, private methods, exception handling, cloning, and nested classes. In this chapter, we mention when there's a difference in syntax or features between PHP 4 and what's supported by ZE2, so you can plan for the future.

Instantiating Objects

Problem

You want to create a new instance of an object.

Solution

Define the class, then use new to create an instance of the class:

class user {
    function load_info($username) {
       // load profile from database
    }
}

$user = new user;
$user->load_info($_REQUEST['username']);

Discussion

You can instantiate multiple instances of the same object:

$adam = new user;
$adam->load_info('adam');

$dave = new user;
$dave->load_info('adam');

These are two independent objects that happen to have identical information. They're like identical twins; they may start off the same, but they go on to live separate lives.

See Also

Recipe 7.5 for more on copying objects; Recipe 7.6 for more on copying objects by reference; documentation on classes and objects at http://www.php.net/oop.

Defining Object Constructors

Problem

You want to define a method that is called when an object is instantiated. For example, you want to automatically load information from a database into an object when it's created.

Solution

Define a method with the same name as the class:

class user {
    function user($username, $password) {
        ...
    }
}

Discussion

If a function has the same name as its class, it acts as a constructor:

class user {
    var $username;

    function user($username, $password) { 
        if ($this->validate_user($username, $password)) {
            $this->username = $username;
        }
    }
}

$user = new user('Grif', 'Mistoffelees'); // using built-in constructor

PHP hasn't always had support for constructors. So people made pseudo-constructors by adopting a naming convention and calling that function after creation:

class user {
    ...

    init($username, $password) { ... }
}

$user = new user();
$user->init($username, $password);

If you see this, it's usually a result of legacy code.

However, having a standard name for all constructors makes it easier to call your parent's constructor (because you don't need to know the name of the parent class) and also doesn't require you to modify the constructor if you rename your class name. With Zend Engine 2, the naming conventions of constructors have been modified, and the new constructor name is _ _construct( ). However, for backwards compatibility, if this method isn't found, PHP tries to call a constructor with the same name as the class.

See Also

Recipe 7.8 for more on calling parent constructors; documentation on object constructors at http://www.php.net/oop.constructor.

Destroying an Object

Problem

You want to eliminate an object.

Solution

Objects are automatically destroyed when a script terminates. To force the destruction of an object, use unset( ):

$car = new car; // buy new car
...
unset($car);    // car wreck

Discussion

It's not normally necessary to manually clean up objects, but if you have a large loop, unset( ) can help keep memory usage from spiraling out of control.

PHP 4 doesn't have destructors, however Zend Engine 2 supports them with the _ _destruct( ) method.

See Also

Documentation on unset( ) at http://www.php.net/unset.

Cloning Objects

Problem

You want to make a copy of an existing object. For instance, you have an object containing a message posting and you want to copy it as the basis for a reply message.

Solution

Use = to assign the object to a new variable:

$rabbit = new rabbit;
$rabbit->eat();
$rabbit->hop();
$baby = $rabbit;

Discussion

In PHP, all that's needed to make a copy of an object is to assign it to a new variable. From then on, each instance of the object has an independent life and modifying one has no effect upon the other:

class person {
    var $name;

    function person ($name) {
        $this->name = $name;
    }
}

$adam = new person('adam');
print $adam->name;    // adam
$dave = $adam;
$dave->name = 'dave';
print $dave->name;    // dave
print $adam->name;    // still adam

Zend Engine 2 allows explicit object cloning via a _ _clone( ) method that is called whenever an object is copied. This provides more finely-grained control over exactly which properties are duplicated.

See Also

Recipe 7.6 for more on assigning objects by reference.

Assigning Object References

Problem

You want to link two objects, so when you update one, you also update the other.

Solution

Use =& to assign one object to another by reference:

$adam = new user;
$dave =& $adam;

Discussion

When you do an object assignment using =, you create a new copy of an object. So, modifying one doesn't alter the other. But when you use =&, the two objects point at each other, so any changes made in the first are also made in the second:

$adam = new user;
$adam->load_info('adam');

$dave =& $adam;
$dave->load_info('dave');

The values in $adam are equal to those of $dave.

See Also

Recipe 7.5 for more on copying object; documentation on references at http://www.php.net/references.

Calling Methods on an Object Returned by Another Method

Problem

You need to call a method on an object returned by another method.

Solution

Assign the object to a temporary variable, and then call the method of that temporary variable:

$orange = $fruit->get('citrus');
$orange->peel( );

Discussion

This is necessary because a parse error results from:

$fruit->get('citrus')->peel( );

Zend Engine 2 supports direct dereferencing of objects returned from a method so this workaround is no longer necessary.

Accessing Overridden Methods

Problem

You want to access a method in the parent class that's been overridden in the child.

Solution

Prefix parent:: to the method name:

class shape {
    function draw( ) {
        // write to screen
    }
}

class circle extends shape {
   function draw($origin, $radius) {
      // validate data
      if ($radius > 0) {
          parent::draw( );
          return true;
      }

      return false;
   }
}

Discussion

When you override a parent method by defining one in the child, the parent method isn't called unless you explicitly reference it.

In the Solution, we override the draw( ) method in the child class, circle, because you want to accept circle specific parameters and validate the data. However, in this case, we still want to perform the generic shape::draw( ) action, which does the actual drawing, so we call parent::draw( ) inside your method if $radius is greater than 0.

Only code inside the class can use parent::. Calling parent::draw( ) from outside the class gets you a parse error. For example, if circle::draw( ) checked only the radius, but you also wanted to call shape::draw( ), this wouldn't work:[1]

$circle = new circle;
if ($circle->draw($origin, $radius)) {
    $circle->parent::draw();
}

If you want to call the constructor belonging to an object's parent but don't know the parent's class name, use get_parent_class( ) to dynamically identify the parent, then combine that with parent:: to call the parent's constructor:

class circle extends shape {
    
    function circle( ) {
        $parent = get_parent_class($this);
        parent::$parent( );
    }
}

The function get_parent_class( ) takes a class name or an object and returns the name of the object's parent. In order to maintain generality, pass $this, which is the reference to the current object. In this case, the function returns shape. Then, use parent:: to ensure PHP explicitly calls the constructor in the parent class. Calling $parent( ) without parent:: runs the risk of calling a method in circle that overrides the parent definition.

The call to parent::$parent( ) may look a little odd. However, PHP just substitutes in the parent class name for the $parent variable. Then, because there are ( )s after the variable, PHP knows it should make a method call.

It's possible to hardcode the call to parent::shape( ) directly into the circle constructor:

function circle( ) {
    parent::shape( );
}

However, this isn't as flexible as using get_parent_class( ). It is faster, so if you know your object hierarchy isn't going to change, that may be a trade-off you can benefit from.

Last, you can't chain the parent :: keyword to work back to a "grandparent" class, so, parent::parent::foo( ) doesn't work.

See Also

Recipe 7.3 for more on object constructors; documentation on class parents at http://www.php.net/keyword.parent and on get_parent_class( ) at http://www.php.net/get-parent-class.

Using Property Overloading

Problem

You want handler functions to execute whenever you read and write object properties. This lets you write generalized code to handle property access in your class.

Solution

Use the experimental overload extension and write _ _get( ) and _ _set( ) methods to intercept property requests.

Discussion

Property overloading allows you to seamlessly obscure from the user the actual location of your object's properties and the data structure you use to store them.

For example, the pc_user class shown in Example 7-1 stores variables in an array, $data.

Example 7-1. pc_user class

require_once 'DB.php';

class pc_user {

    var $data = array();

    function pc_user($user) {
        /* connect to database and load information on 
         * the user named $user into $this->data
         */
         
         $dsn = 'mysql://user:password@localhost/test';
         $dbh = DB::connect($dsn);
         if (DB::isError($dbh)) { die ($dbh->getMessage()); }

         $user = $dbh->quote($user);
         $sql = "SELECT name,email,age,gender FROM users WHERE user LIKE '$user'";
         if ($data = $dbh->getAssoc($sql)) {
             foreach($data as $key => $value) {
                 $this->data[$key] = $value;
             }
         }
    }

    function __get($property_name, &$property_value) {
        if (isset($this->data[$property_name])) {
            $property_value = $this->data[$property_name];
            return true;
        }

        return false;
    }

    function __set($property_name, $property_value) {
        $this->data[$property_name] = $property_value;
        return true;
    }
}

Here's how to use the pc_user class:

overload('pc_user');

$user = new pc_user('johnwood');
$name = $user->name;                // reads $user->data['name']
$user->email = 'jonathan@wopr.mil'; // sets  $user->data['email']

The class constructor connects to the users table in the database and retrieves information about the user named $user. When you set data, _ _set( ) rewrites the element inside of $data. Likewise, use _ _get( ) to trap the call and return the correct array element.

Using an array as the alternate variable storage source doesn't provide many benefits over a nonoverloaded object, but this feature isn't restricted to simple arrays. For instance, you can make $this->email return the get_name( ) method of an email object. You can also avoid pulling all the user information from the database at once and request it on demand. Another alternative is to use a more persistent storage mechanism, such as files, shared memory, or a database to hold data.

See Also

Recipe 6.8 for information on storing objects in external sources; documentation on the overload extension at http://www.php.net/overload.

Using Method Polymorphism

Problem

You want to execute different code depending on the number and type of arguments passed to a method.

Solution

PHP doesn't support method polymorphism as a built-in feature. However, you can emulate it using various type-checking functions. The following combine( ) function uses is_numeric(), is_string(), is_array(), and is_bool():

// combine() adds numbers, concatenates strings, merges arrays,
// and ANDs bitwise and boolean arguments
function combine($a, $b) {
    if (is_numeric($a) && is_numeric($b)) {
        return $a + $b;
    }

    if (is_string($a)  && is_string($b))  {
        return "$a$b";
    }

    if (is_array($a)   && is_array($b))   {
        return array_merge($a, $b);
    }

    if (is_bool($a)    && is_bool($b))    {
        return $a & $b;
    }

    return false;
}

Discussion

Because PHP doesn't allow you to declare a variable's type in a method prototype, it can't conditionally execute a different method based on the method's signature, as can Java and C++. You can, instead, make one function and use a switch statement to manually recreate this feature.

For example, PHP lets you edit images using GD. It can be handy in an image class to be able to pass in either the location of the image (remote or local) or the handle PHP has assigned to an existing image stream. Example 7-2 shows a pc_Image class that does just that.

Example 7-2. pc_Image class

class pc_Image {

    var $handle;

    function ImageCreate($image) {
        if (is_string($image)) {
            // simple file type guessing

            // grab file suffix
            $info = pathinfo($image);
            $extension = strtolower($info['extension']);
            switch ($extension) {
            case 'jpg':
            case 'jpeg':
                $this->handle = ImageCreateFromJPEG($image);
                break;
            case 'png':
                $this->handle = ImageCreateFromPNG($image);
                break;
            default:
                die('Images must be JPEGs or PNGs.');
            }
        } elseif (is_resource($image)) {
            $this->handle = $image;
        } else {
            die('Variables must be strings or resources.');
        }
    }
}

In this case, any string passed in is treated as the location of a file, so we use pathinfo() to grab the file extension. Once we know the extension, we try to guess which ImageCreateFrom( ) function accurately opens the image and create a handle.

If it's not a string, we're dealing directly with a GD stream, which is of type resource. Since there's no conversion necessary, we assign the stream directly to $handle. Of course, if you're using this class in a production environment, you'd be more robust in your error handling.

Method polymorphism also encompasses methods with differing numbers of arguments. The code to find the number of arguments inside a method is identical to how you process variable argument functions using func_num_args( ). This is discussed in Recipe 6.6.

See Also

Recipe 6.6 for variable argument functions; documentation on is_string( ) at http://www.php.net/is-string, is_resource( ) at http://www.php.net/is-resource, and pathinfo( ) at http://www.php.net/pathinfo.

Finding the Methods and Properties of an Object

Problem

You want to inspect an object to see what methods and properties it has, which lets you write code that works on any generic object, regardless of type.

Solution

Use get_class_methods( ) and get_class_vars( ) to probe an object for information:

// learn about cars
$car_methods = get_class_methods('car');
$car_vars    = get_class_vars('car');

// act on our knowledge
if (in_array('speed_away', $car_methods)) {
    $getaway_van = new car;
    $getaway_van->speed_away( );
}

Discussion

It's rare to have an object and be unable to examine the actual code to see how it's described. Still, these functions can be useful for projects you want to apply to a whole range of different classes, such as creating automated class documentation, generic object debuggers, and state savers, like serialize( ).

Both get_class_methods( ) and get_class_vars( ) return an array of values. In get_class_methods( ), the keys are numbers, and the values are the method names. For get_class_vars( ), both variable names and default values (assigned using var) are returned, with the variable name as the key and the default value, if any, as the value.

Another useful function is get_object_vars( ) . Unlike its sister function get_class_vars( ), get_object_vars( ) returns variable information about a specific instance of an object, instead of a generic newly created object.

As a result, you can use it to check the status of an object as it currently exists in a program:

$clunker = new car;
$clunker_vars = get_object_vars($clunker); // we pass the object, not the class

Since you want information about a specific object, you pass the object and not its class name. But, get_object_vars( ) returns information in the same format as get_class_vars( ).

This makes it easy to write quick scripts to see if you're adding new class variables:

$new_vars = array_diff(array_keys(get_object_vars($clunker)),
                       array_keys(get_class_vars('car')));

You extract the variable names using array_keys( ) . Then, with the help of array_diff( ), you find which variables are in the $clunker object that aren't defined in the car class.

If you just need a quick view at an object instance, and don't want to fiddle with get_class_vars( ), use either var_dump( ) , var_export( ), or print_r( ) to print the object's values. Each of these three functions prints out information in a slightly different way; var_export( ) can optionally return the information, instead of displaying it.

See Also

Recipe 5.9 for more on printing variables; documentation on get_class_vars( ) at http://www.php.net/get-class-vars, get_class_methods( ) at http://www.php.net/get-class-methods, get_object_vars( ) at http://www.php.net/get-object-vars, var_dump( ) at http://www.php.net/var-dump, var_export( ) at http://www.php.net/var-export, and print_r( ) at http://www.php.net/print-r.

Adding Properties to a Base Object

Problem

You want to create an object and add properties to it, but you don't want to formally define it as a specific class. This is useful when you have a function that requires an object with certain properties, such as what's returned from mysql_fetch_object( ) or imap_header( ).

Solution

Use the built-in base class, stdClass :

$pickle = new stdClass;
$pickle->type = 'fullsour';

Discussion

Just as array( ) returns an empty array, creating an object of the type stdClass provides you with an object without properties or methods.

Like objects belonging to other classes, you can create new object properties, assign them values, and check those properties:

$guss = new stdClass;

$guss->location = 'Essex';
print "$guss->location\n";
$guss->location = 'Orchard';
print "$guss->location\n";
Essex
               Orchard
            

Methods, however, can't be defined after an object is instantiated.

It is useful to create objects of stdClass when you have a function that takes a generic object, such as one returned from a database fetching function, but you don't want to actually make a database request. For example:

function pc_format_address($obj) {
    return "$obj->name <$obj->email>";
}

$sql = "SELECT name, email FROM users WHERE id=$id";
$dbh = mysql_query($sql);
$obj = mysql_fetch_object($dbh);
print pc_format_address($obj);
David Sklar <david@example.com>
            

The pc_print_address( ) function takes a name and email address and converts it to a format as you might see in the To and From fields in an email program. Here's how to call this function without calling mysql_fetch_object( ):

$obj = new stdClass;
$obj->name = 'Adam Trachtenberg';
$obj->email = 'adam@example.com';
print pc_format_address($obj);
Adam Trachtenberg <adam@example.com>
            

Creating a Class Dynamically

Problem

You want to create a class, but you don't know everything about it until your code is executed.

Solution

Use eval( ) with interpolated variables:

eval("class van extends $parent_class {
    function van() {
        \$this->$parent_class();
    }
};");

$mystery_machine = new van;

Discussion

While it's okay in PHP to use variable names to call functions or create objects, it's not okay to define functions and classes in a similar manner:

$van( );                     // okay
$van = new $parent_class    // okay
function $van( ) {};         // bad
class $parent_class {};     // bad

Trying to do either of the last two examples results in a parser error because PHP expects a string, and you supplied a variable.

So, if you want to make a class named $van and you don't know beforehand what's going to be stored in $van, you need to employ eval( ) to do your dirty work:

eval("class $van {};");

There is a performance hit whenever you call eval( ), so high traffic sites should try to restructure their code to avoid this technique when possible. Also, if you're defining your class based on input from users, be sure to escape any potentially dangerous characters.

See Also

Recipe 7.14 to instantiate an object dynamically; documentation on eval( ) at http://www.php.net/eval.

Instantiating an Object Dynamically

Problem

You want to instantiate an object, but you don't know the name of the class until your code is executed. For example, you want to localize your site by creating an object belonging to a specific language. However, until the page is requested, you don't know which language to select.

Solution

Use a variable for your class name:

$language = $_REQUEST['language'];
$valid_langs = array('en_US' => 'US English', 
                     'en_GB' => 'British English', 
                     'es_US' => 'US Spanish',
                     'fr_CA' => 'Canadian French');

if (isset($valid_langs[$language]) && class_exists($language)) {
    $lang = new $language;
}

Discussion

Sometimes you may not know the class name you want to instantiate at runtime, but you know part of it. For instance, to provide your class hierarchy a pseudo-namespace, you may prefix a leading series of characters in front of all class names; this is why we often use pc_ to represent PHP Cookbook or PEAR uses Net_ before all Networking classes.

However, while this is legal PHP:

$class_name = 'Net_Ping';
$class = new $class_name;               // new Net_Ping

This is not:

$partial_class_name = 'Ping';
$class = new "Net_$partial_class_name"; // new Net_Ping

This, however, is okay:

$partial_class_name = 'Ping';
$class_prefix = 'Net_';

$class_name = "$class_prefix$partial_class_name";
$class = new $class_name;               // new Net_Ping

So, you can't instantiate an object when its class name is defined using variable concatenation in the same step. However, because you can use simple variable names, the solution is to preconcatenate the class name.

See Also

Recipe 6.5 for more on variable variables; Recipe 7.13 for more on defining a class dynamically; documentation on class_exists( ) at http://www.php.net/class-exists.

Notes

  1. In fact, it fails with the error unexpected T_PAAMAYIM_NEKUDOTAYIM, which is Hebrew for "double-colon."
Personal tools