Advanced PHP V5 objects

Create reusable, extensible, and scalable PHP code

The May 2005 IBM developerWorks article "Getting started with objects with PHP V5" covered enough detail to get a reader up and running with the basics of classes and objects in PHP. This article introduces some of PHP V5's more advanced and design-oriented features. Among them are object types, which allow for decoupling the components of a system from one another, creating reusable, extensible, and scalable code.

Matt Zandstra (matt@fate16.com), Developer and Writer, Yahoo!

Matt Zandstra has been a developer, teacher, and writer specializing in Internet applications for more than a decade. He currently works as an engineer at Yahoo!, where he helps develop a core template management system. He is the author of "SAMS Teach Yourself PHP in 24 Hours" (SAMS, 2003) and "PHP 5 Objects, Patterns, and Practice" (Apress, 2004). He has written PHP articles for Linux Magazine, Zend Technologies, and BGZ Consultants, as well as his own site, getInstance.



09 August 2005

Taking the hint

Let's start by taking a look at object types and the benefits of type hinting. A class defines a type. Any object instantiated from that class belongs to that type by definition. So, you use a Car class to create Car objects. If the Car class extends a Vehicle superclass, a Car object will also be a Vehicle object. This mirrors the way we categorize things in the real world. As you will see, though, types are more than just a useful way of organizing a system's elements. They are fundamental to object-oriented programming because a type is a promise of good and consistent behavior. A lot of design magic flows from that promise.

"Getting started with objects with PHP V5" demonstrates that objects guarantee you an interface. When your system passes a Dictionary object, you can be sure that it has a $translations array and a summarize() method. By contrast, an associative array does not offer the same level of certainty. To take advantage of the clear interface offered by a class, you need to know your object is actually an instance of Dictionary and not some imposter. You can verify it manually with the instanceof operator, a handy tool introduced with PHP V5 that sits between an object instance and a class name.

$en instanceof Dictionary

The instanceof operator resolves to true if the given object is an instance of the given class. The first time you encounter a Dictionary object in a calling method, you can check its type before working with it.

if ( $en instanceof Dictionary ) {
    print $en->summarize();
}

With PHP V5, however, you can build object type checking right into the class or method declaration.

In "Getting started with objects with PHP V5," the focus was on two classes: Dictionary, which stores terms and translations, and DictionaryIO, which exports and imports Dictionary data from and to the file system. Features like these make it easy to send Dictionary files to third-party translators, who can use their own software to edit the data. You can then reimport processed files. Listing 1 is a version of the Dictionary class that accepts a DictionaryIO object and stores it for later use.

Listing 1. A version of the Dictionary class that accepts a DictionaryIO object
class Dictionary {
    public $translations = array();
    public $type ="En";
    public $dictio;

    function addDictionaryIO( $dictio ) {
        $this->dictio=$dictio;
    }

    function export() {
        if ( $this->dictio ) {
            $this->dictio->export( $this );
        }
    }
}

class DictionaryIO {
    function export( $dict ) {
        print "exporting dictionary data ".
              "($dict->type)\n";
    }
}

$en = new Dictionary();
$en->addDictionaryIO( new DictionaryIO() );
$en->export();

// output: 
// dumping dictionary data (En)

The DictionaryIO class has a single method, export(), which expects a Dictionary object and uses it to output a dummy message. Dictionary now has two new methods: addDictionaryIO(), which accepts and stores a DictionaryIO object, and export(), which uses the supplied object to export Dictionary data -- or would in a fully implemented version.

You may be wondering why the Dictionary object doesn't just instantiate its own DictionaryIO object, or even just handle import-export operations internally without recourse to a second object at all. One reason is that you may wish to have one DictionaryIO object work with multiple Dictionary objects and to store a separate reference to it. Another reason is that by passing a DictionaryIO object to Dictionary, you can take advantage of class switching, or polymorphism. In other words, you could pass an instance of a DictionaryIO child class, such as XmlDictionaryIO, to Dictionary and change the way that data is saved and retrieved at run time.

Figure 1 shows the Dictionary and DictionaryIO classes and their use relationship.

Figure 1. The Dictionary and DictionaryIO classes
Dictionary and DictionaryIO classes

As it stands, nothing is stopping a coder from passing an entirely random object to addDictionaryIO(). You would catch an error like that only when you ran export() and found that the object you had been storing in $dictio does not, in fact, have an export() method. Using PHP V4, you would have to test the argument types in this example to be absolutely sure that the coder passes around the right kind of objects. Using PHP V5, you can deploy argument hints to enforce object type. Simply add the object type you require to the argument variable in the method declaration, as shown in Listing 2:

Listing 2. Adding the object type to the argument variable in the method declaration
    function addDictionaryIO( DictionaryIO $dictio ) {
        $this->dictio=$dictio;
    }

    function export() {
        if ( $this->dictio ) {
            $this->dictio->export( $this );
        }
    }

Now if a client coder attempts to pass the wrong kind of object to addDictionaryIO(), the PHP engine will throw a fatal error. Type hinting, therefore, makes for safer code. Unfortunately, hinting works only for objects, so you cannot require a string or an integer in your argument lists. You must test manually for primitives such as these.

Even though you can guarantee that addDictionaryIO() will get the correct object type, you can't guarantee that the method is called in the first place. The export() method tests for the existence of the $dictio property in the export() method, which prevents errors. You may want to be stricter than this, though, and and make the constructor method require a DictionaryIO object in its argument list, thus ensuring that $dictio is always populated.


Calling overridden methods

In Listing 3, XmlDictionaryIO extends DictionaryIO. While DictionaryIO writes and reads serialized data, XmlDictionaryIO works with XML, which can be shared with third-party applications. XmlDictionaryIO can override its parent methods (import() and export()), or it can choose not to provide its own implementation (path()). If a client calls upon the path() method in an XmlDictionaryIO object, the path() method implemented in DictionaryIO is invoked.

In fact, you can have it both ways. You can override a method and invoke the parent implementation. To do this, use the new keyword parent. Use parent with the scope resolution operator and the name of the method in question. Imagine, for example, that you need XmlDictionaryIO to use a directory called xml in the current working directory if one is available; otherwise, it should use the default path generated by the parent DictionaryIO class, as shown in Listing 3:

Listing 3. XmlDictionaryIO uses the xml directory or default path generated by DictionaryIO class
class XmlDictionaryIO extends DictionaryIO {

    function path( Dictionary $dictionary, $ext ) {
        $sep = DIRECTORY_SEPARATOR;
        if ( is_dir( ".{$sep}xml"  ) ) {
            return ".{$sep}xml{$sep}{$dictionary->getType()}.$ext";
        }
        return parent::path( $dictionary, $ext );
    }
    // ...

As you can see, this method checks for a local xml directory. If that test fails, it delegates to the parent method using the parent keyword.

Child classes and constructor methods

The parent keyword is particularly important in constructor methods. If you don't define a constructor in a child class, the parent constructor is transparently invoked on your behalf. If you do create a constructor method in a child class, however, it becomes your responsibility to invoke the parent class's constructor, passing on any arguments, as shown in Listing 4:

Listing 4. Invoking the parent class's constructor
class SpecialDictionary extends Dictionary { 
    function __construct( $type, 
                          DictionaryIO $dictio,
                          $additional ) {
        // do something with $additional
        parent::__construct( $type, $dictio );
    }
}

Abstract classes and methods

Although providing a default behavior in a parent class is perfectly legal, it is probably not the neatest way of doing things. For starters, you must rely on the authors of child classes to understand that they must implement both import() and export() so as not to create classes in a broken state. Also, the DictionaryIO classes are really siblings, rather than parent and child. XmlDictionaryIO is not a specialization of DictionaryIO; rather, it is an alternative implementation.

PHP V5 allows you to define a partially implemented class whose main role is to prescribe a core interface for its children. Such classes must be declared abstract.

abstract class DictionaryIO {
}

An abstract class cannot be instantiated. You must subclass (that is, create a class that extends it) and create an instance of the child class. You can declare standard and abstract methods in an abstract class, as shown in Listing 5. Abstract methods must be qualified by the abstract keyword and must consist of a method signature only. That means that they should include the abstract keyword, optionally a visibility modifier, the function keyword, and an optional list of arguments in parentheses. There should be no method body.

Listing 5. Declaring an abstract class
abstract class DictionaryIO {

    protected function path( Dictionary $dictionary, 
                             $ext ) {
        $path  = Dictionary::getSaveDirectory();
        $path .= DIRECTORY_SEPARATOR;
        $path .= $dictionary->getType().".$ext";
        return $path;
    }
    
    abstract function import( Dictionary $dictionary ); 
    abstract function export( Dictionary $dictionary ); 
}

Notice that the path() function is now declared protected. This allows access from child classes, but not from outside of the DictionaryIO type. Any class that extends DictionaryIO must implement the import() and export() methods or risk a fatal error.

Any class that declares an abstract method must itself be declared abstract. A child class that extends an abstract class must implement all abstract methods in its parent or itself be declared abstract.

Listing 6 shows the concrete DictionaryIO classes, with the actual implementations omitted for the sake of brevity.

Listing 6. Concrete DictionaryIO classes
class SerialDictionaryIO extends DictionaryIO {

    function export( Dictionary $dictionary ) {
        // implementation
    }

    function import( Dictionary $dictionary ) {
        // implementation
    }
}

class XmlDictionaryIO extends DictionaryIO {

    protected function path( Dictionary $dictionary, 
                             $ext ) {
        $path = strtolower( 
                parent::path( $dictionary, $ext ) ); 
        return $path;
    }

    function export( Dictionary $dictionary ) {
        // implementation
    }

    function import( Dictionary $dictionary ) {
        // implementation
    }
}

The Dictionary class defines a constructor method, which requires a DictionaryIO object, but it neither knows nor cares whether the object in question is an instance of XmlDictionaryIO or SerialDictionaryIO. All it knows is that the given object extends DictionaryIO and can, therefore, be guaranteed to support import() and export() methods. This switching of classes at run time is a common feature of object-oriented programming, known as polymorphism.

Figure 2 shows the DictionaryIO classes. Notice that an abstract class is denoted by italicized text, as are abstract methods. This diagram is a good illustration of polymorphism. It shows that the DictionaryIO class's defined relationship is with DictionaryIO, but that either SerialDictionaryIO or XmlDictionaryIO will implement that relationship.

Figure 2. The abstract DictionaryIO class and its concrete children
The abstract DictionaryIO class and its concrete children

Interfaces

Like Java™ programming language applications, PHP supports only single inheritance. This means that a class can directly extend only one parent (though, of course, it may indirectly extend many ancestors). While this makes for clean design, sometimes you may need to define more than one set of capabilities for a class.

One benefit of working with objects is the guarantee of functionality that a type can give you. A Dictionary object always has a get() method, whether an instance of Dictionary itself or of a child class. Another characteristic of Dictionary is its support for export(). Imagine that you need to make a number of other classes in your system similarly exportable. You could give all these disparate classes their own export() methods, then aggregate instances, cycling through all of them and calling export() on each one when you want to save the state of your system to a file. Listing 7 shows a second class that implements an export() method.

Listing 7. A second class that implements an export() method
class ThirdPartyNews {
    // ... 
}  

class OurNews extends ThirdPartyNews {     
    // ...
    function export() {
        print "OurNews export\n";
    }
}

Notice that this example includes the constraint that the new class OurNews extends an external class called ThirdPartyNews.

Listing 8 shows a class that aggregates instances of classes equipped with export() methods.

Listing 8. A class that aggregates instances of classes equipped with export() methods
class Exporter {
    private $exportable = array();
    function add( $obj ) {
        $this->exportable[] = $obj;
    }

    function exportAll() {
        foreach ( $this->exportable as $obj ) {
            $obj->export();
        }
    }
}

The Exporter class defines two methods: add(), which accepts objects for storage, and exportAll(), which loops through the stored objects calling export() on each one. This design's weakness should be apparent: add() doesn't check the type of the provided object, and so exportAll() takes a grave risk in so blithely calling export().

What would really help here is some type hinting in the add() method signature. But Dictionary and OurNews specialize different roots. You could fall back on type checking inside the add() method, but that is not elegant and inflexible. Every time you created a new type that supported export(), you would need to create a new type check.

Interfaces save the day in situations like this. As the name suggests, an interface defines functionality without implementation. You declare an interface with the interface keyword.

interface Exportable {
    public function export();
}

As with abstract classes, you can define any number of method signatures. A child class must provide an implementation for each method. Unlike an abstract class, however, an interface cannot contain any concrete methods at all (that is, no methods with any features apart from their signatures).

A class implements an interface with the implements keyword, as shown in Listing 9.

Listing 9. A class that implements an interface with the implements keyword
class OurNews extends ThirdPartyNews 
              implements Exportable {
    // ...
    function export() {
        print "OurNews export\n";
    }
}

class Dictionary implements Exportable, Iterator {
    function export() {
        //...
    }
}

You can implement as many interfaces as you want by using a comma-separated list after implements. You must implement all methods declared in each interface or declare your implementing class abstract.

So what do you gain by this? Dictionary and OurNews objects now share a type. All such objects are also Exportable. You can check them with type hinting and instanceof tests. Listing 10 shows the amended Exporter::add() method.

Listing 10. The amended Exporter::add() method
class Exporter {
    private $exportable = array();
    function add( Exportable $obj ) {
        $this->exportable[] = $obj;
    }
    //...

Interfaces are a hard concept to grasp. After all, they don't actually provide any useful code. The trick is to remember the importance of type in object-oriented programming. An interface is like a contract. It lends a class a name that gets it into places, and, in return, that class guarantees certain methods will be available. What's more, a class that uses an Exportable object neither knows nor cares what happens once export() is called. It just knows that it can safely call the method.

Figure 3 shows the relationship between the Exportable interface and its implementing classes. Notice that the Exporter is in a use relationship with the Exportable interface, not with the concrete implementations. Interface relationships are denoted with broken lines and an open arrow.

Figure 3. An interface and its implementing classes
An interface and implementing classes

Summary

This article supports the value of using type in PHP V5. Object types allow you to separate (decouple) the components of a system from one another, which makes for reusable, extensible, and scalable code. Abstract classes and interfaces help you to design a system around class types.

Client classes can be coded to require only the abstract type, leaving the strategies and outcomes of implementation to the concrete class instances passed to them at run time. Dictionary, in other words, is wedded to neither serialized data nor XML. If a new format must be supported, Dictionary will require no further development. It is entirely independent of the mechanisms by which its data is saved and loaded to and from the file system. Dictionary knows only that it must have a DictionaryIO object, and that it is therefore guaranteed export() and import() functionality.

If classes guarantee an interface, you must be able to guarantee a class. The instanceof function provides a good way of checking type, but you can also roll object type checking into the method signature itself by using type hints in the argument list.

Resources

Learn

  • Read "Getting started with objects with PHP V5" (developerWorks, May 2005) for an introduction to PHP's classes and objects.
  • You can find an essential overview of all of PHP's object-oriented features in Chapter 19. Classes and Objects (PHP 5) in the official PHP Manual.
  • For a fantastic resource for anyone interested in object-oriented design, visit the C2 Wiki.
  • Visit the developerWorks Open source zone for extensive how-to information, tools, and project updates to help you develop with open source technologies and use them with IBM's products.

Get products and technologies

  • Innovate your next open source development project with IBM trial software, available for download or on DVD.

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Open source on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Open source
ArticleID=91346
ArticleTitle=Advanced PHP V5 objects
publish-date=08092005