dcsimg

Reflecting on PHP 5

If you want to build a PHP class browser or a PHP debugger, or programatically discover the nitty-gritty details about an object, reflection is a great new trick to teach to your old code.

The first beta version of PHP 5 was released in the summer of 2003 to much excitement. Developers descended upon PHP 5′s long list of object-oriented features, analyzing and experimenting with the nascent capabilities. Oddly, PHP 5′s Reflection application programming interface (API) was not part of that initial release, and was absent from the overview documentation. In the following weeks, the Reflection API quietly found its way into the PHP 5 source tree without fuss or fanfare. Yet its stealthy arrival belies its importance: PHP 5′s Reflection API is to PHP what the java.lang.reflect package is to Java.

The Reflection API consists of built-in classes for analyzing properties, methods, and classes. It’s similar in some respects to existing object functions such as get_class_vars(), but is more flexible and provides much greater detail. It’s also designed to work with PHP’s newest object-oriented features, such as access control, interfaces, and abstract classes.

If you want to build a class browser, a debugger, or just determine the class of an object, the Reflection API is a great new trick to teach to your old code.

Getting Started

Let’s take a good look at the features of the Reflection API and build a small example.

The Reflection API was not part of PHP 5 Beta 1, but is included in Beta 2 (and beyond). To use the API, update your software to the latest release of PHP 5.

The Reflection API consists of classes that model language features. For example, the Reflection_Function class provides information about a given function, and Reflection_ Class yields insight about a class. Table One lists some of the classes in the API. Between them, the classes in the Reflection API provide unprecedented runtime access to information about the objects, functions and extensions in your scripts.




Table One: Some of the classes in PHP 5′s new Reflection API

CLASS DESCRIPTION

Reflection Provides a static export() method

Reflection_Class Class information and tools

Reflection_Method Class method information and tools

Reflection_Parameter Method argument information

Reflection_Property Class property information

Reflection_Function Function information and tools

Reflection_Extension PHP extension information

Reflection_Exception An error class


How is this useful? You usually know all about the objects you want to work with, especially if you’re using argument hinting (and you should be). Methods generally know the kinds of objects that are required and are sure about the interface they will be working with. Sometimes, though, you may need to work with an interface that is not prescribed so neatly.

For example, you might want to generate PHPDocumentor comments derived from an object of any type, or you might want to save object information to a database, examining an object’s accessor (“getter” and “setter”) methods to extract field names. Building a framework that invokes methods in module classes according to a naming scheme is another use of reflection.

Time to Roll Up Your Sleeves

To use the Reflection classes, you need a class to examine. Here’s one to work with:


class WorkPerson {
public static $COMPANY = “Thing Works Ltd”;
public $name;
protected $age=0;
private $boss;
function __construct( $name ) {
$this->name = $name;
}

public function getName() {
return $this->name;
}

protected function setBoss( WorkPerson
$p ) {
$this->boss = $p;
}

public function setAge( $int ) {
$this->age = (int)$int;
}
}

WorkPerson uses some PHP 5 features that can be probed with the Reflection API: it has public, private, and protected properties and methods; it has a static property called WorkPerson::$COMPANY; and argument hinting is used in the WorkPerson::setBoss() method.

First, here’s an instance of WorkPerson as examined through the built-in var_dump() function.


$person = new WorkPerson( “harry” );
$person->setAge(42);
var_dump( $person );

object(workperson)#1 (3) {
["name"]=>
string(5) “harry”
[""]=>
int(42)
[""]=>
NULL
}

As you can see, var_dump() yields little about the properties in the object, and yields nothing about its methods. You could learn more by using traditional class and object functions to probe WorkPerson, but what’s really needed is a good one-stop-shop for information — without having to instantiate an object to get it. Enter the Reflection_ Class object.

Reflection_Class provides methods that reveal information about every aspect of a given class, whether it’s a user-defined or internal class. The constructor of Reflection_ Class accepts a class name as its sole argument:


$person_class = new Reflection_Class(
‘WorkPerson’ );
Reflection::export( $person_class );

Once you’ve created a Reflection_Class object, you can use the Reflection utility class to dump information about WorkPerson. Reflection has a static export() method that outputs data on any of the Reflection classes (that is, any class that implements the Reflector interface, to be pedantic). Listing One shows the output from a call to Reflection::export().




Listing One: Sample output from Reflection::export()

Class [ <user> class workperson ] {
@@ /home/php/reflection-api/WorkPerson.php 2-24

- Constants [0] {
}

- Static properties [1] {
Property [ public static $COMPANY ]
}

- Static methods [0] {
}

- Properties [3] {
Property [ <default> public $name ]
Property [ <default> protected $age ]
Property [ <default> private $boss ]
}

- Methods [4] {
Method [ <user> <ctor> public method __construct ] {
@@ /home/php/reflection-api/WorkPerson.php 9 – 11

- Parameters [1] {
Parameter #0 [ $name ]
}
}

Method [ <user> public method getName ] {
@@ /home/php/WorkPerson.php 13 – 15
}

Method [ <user> protected method setBoss ] {
@@ /home/php/reflection-api/WorkPerson.php 17 – 19

- Parameters [1] {
Parameter #0 [ workperson or NULL $p ]
}
}

Method [ <user> public method setAge ] {
@@ /home/php/reflection-api/WorkPerson.php 21 – 23

- Parameters [1] {
Parameter #0 [ $int ]
}
}
}
}

As you can see, Reflection::export() provides remarkable access to information about a class. Reflection: :export() provides summary information about almost every aspect of WorkPerson, including the access control status of properties and methods, the arguments required by every method, and the location of every method within the script document.

Examining a Class

The Reflection API can provide a great deal of useful information for debugging. Let’s see how by working directly with the Reflection classes.

You’ve already seen how to instantiate a Reflection_ Class object:


$person_class = new Reflection_Class(
‘WorkPerson’ );

Next, let’s use the Reflection_Class object to investigate WorkPerson within a script. What kind of class is it? Can an instance be created? Listing Two shows a function that answers these questions.




Listing Two: Probe the WorkPerson class

$person_class = new Reflection_Class( ‘WorkPerson’ );
print classData( $person_class );

function classData( Reflection_Class $class ) {
$details = “”;
$name = $class->getName();
if ( $class->isUserDefined() ) {
$details .= “$name is user defined\n”;
}
if ( $class->isInternal() ) {
$details .= “$name is built-in\n”;
}
if ( $class->isInterface() ) {
$details .= “$name is interface\n”;
}
if ( $class->isAbstract() ) {
$details .= “$name is an abstract class\n”;
}
if ( $class->isFinal() ) {
$details .= “$name is a final class\n”;
}
if ( $class->isInstantiable() ) {
$details .= “$name can be instantiated\n”;
} else {
$details .= “$name can not be instantiated\n”;
}
return $details;
}

The code in Listing Two creates a Reflection_Class object called $person_class by passing the name of the WorkPerson class to Reflection_Class‘s constructor. $person_class is then passed to a function called classData() that demonstrates some of the methods that can query a class.

The methods should be self-explanatory, but here’s a brief description of each one:

* Reflection_Class::getName() returns the name of the class being examined.

* The Reflection_Class::isUserDefined() method returns true if the class has been declared in PHP code, and Reflection_Class::isInternal() yields true if the class is built-in.

* You can test whether a class is abstract with Reflection_Class::isAbstract(), and whether it’s an interface with Reflection_Class::isInterface().

If you want to get an instance of the class, you can test for that with Reflection_Class::isInstantiable().

You can even examine a class’s source code. The Reflection_Class object provides access to its class’s file name, and to the start and finish lines of the class in the file.

Here’s a quick and dirty function to extract source code using Reflection_Class:


require_once( ‘WorkPerson.php’ );

class ReflectionUtil {
static function getClassSource(
Reflection_Class $class ) {
$path = $class->getFileName();
$lines = @file( $path );
$from = $class->getStartLine();
$to = $class->getEndLine();
$len = $to-$from+1;
return implode( array_slice( $lines,
$from-1, $len ));
}
}

print ReflectionUtil::getClassSource(
new Reflection_Class( ‘WorkPerson’ ) );

Here, ReflectionUtil is a simple class with a single static method, ReflectionUtil::getClassSource(). That method takes a Reflection_Class object as its only argument, and returns the referenced class’s source code. theReflection_Class::getFileName() provides the path to the class’s file as an absolute path, so the code should be able to go right ahead and open it. file() obtains an array of all the lines in the file. Reflection_Class: :getStartLine() provides the class’s start line; Reflection_ Class::getEndLine() finds the final line. From there, it’s simply a matter of using array_slice() to extract the lines of interest.

To keep things brief, this code omits error handling. In a real-world application, you’d want to check arguments and result codes.

Examining Methods

Reflection_Class is used to examine a class; similarly, a Reflection_Method object can examine a method.

You can acquire a Reflection_Method in two ways: you can get an array of Reflection_Method objects from Reflection_Class::getMethods(), or if you need to work with a specific method, Reflection_Class: :getMethod() accepts a method name and returns the relevant Reflection_Method object.

Listing Three uses Reflection_Class::getMethods() to put some of Reflection_Method‘s own methods through their paces.




Listing Three: Some of Reflection_Method’s
techniques to discover methods

$person_class = new Reflection_Class(
‘WorkPerson’ );
$methods = $person_class->getMethods();

foreach ( $methods as $method ) {
print methodData( $method );
print “\n—-\n”;
}

function methodData( Reflection_Method $method ) {
$details = “”;
$name = $method->getName();
if ( $method->isUserDefined() ) {
$details .= “$name is user defined\n”;
}
if ( $method->isInternal() ) {
$details .= “$name is built-in\n”;
}
if ( $method->isAbstract() ) {
$details .= “$name is abstract\n”;
}
if ( $method->isPublic() ) {
$details .= “$name is public\n”;
}
if ( $method->isProtected() ) {
$details .= “$name is protected\n”;
}
if ( $method->isPrivate() ) {
$details .= “$name is private\n”;
}
if ( $method->isStatic() ) {
$details .= “$name is static\n”;
}
if ( $method->isFinal() ) {
$details .= “$name is final\n”;
}
if ( $method->isConstructor() ) {
$details .= “$name is the constructor\n”;
}
if ( $method->returnsReference() ) {
$details .= “$name returns a reference (as
opposed to a value)\n”;
}
return $details;
}

The code uses Reflection_Class::getMethods() to get an array of Reflection_Method objects, and then loops through the array, passing each object to methodData().

The names of the methods used in methodData() reflect their intent: the code checks whether the method is user-defined, built-in, abstract, public, protected, static, or final. You can also check whether the method is the constructor for its class, and whether or not it returns a reference.

One caveat: Reflection_Method::returnsReference() doesn’t return true if the tested method simply returns an object, even though objects are passed and assigned by reference in PHP 5. Instead, Reflection_Method: :returnsReference() only returns true if the method in question has been declared to return a reference (by placing an ampersand character in front of the method name).

As you might expect, you can access a method’s source code using a technique similar to the one used previously with Reflection_Class:


class ReflectionUtil {
static function getMethodSource(
Reflection_Method $method ) {
$path = $method->getFileName();
$lines = @file( $path );
$from = $method->getStartLine();
$to = $method->getEndLine();
$len = $to-$from+1;
return implode( array_slice( $lines, $from-1, $len ));
}
}

$class = new Reflection_Class( ‘WorkPerson’ );
$method = $class->getMethod( ‘setBoss’ );
print ReflectionUtil::getMethodSource(
$method );

Because Reflection_Method provides us with get FileName(), getStartLine(), and getEndLine() methods, it’s a simple matter to extract the method’s source code.

Examining Method Arguments

Prior to PHP 5, all method arguments were mixed. If you wanted to demand a specific type, you’d have to add type-checking code to the body of the method. PHP 5 introduces type hinting for objects. By adding a class name to an argument varable in the method declaration, you force the caller of the method to supply an object of that type.

Here’s an example:


function setBoss( WorkPerson $p ) {
// …
}

This code declares that setBoss() requires a WorkPerson object as its argument. If anything other than a WorkPerson is passed to the method, the script engine throws a fatal error. By the way, hinting only works with objects; primitives need to be checked in the method body as before.

Now that method signatures can constrain the types of object arguments, the ability to examine the arguments declared in a method signature becomes immensely useful. The Reflection API provides the Reflection_Parameter class just for this purpose. To get a Reflection_Parameter object, you need the help of a Reflection_Method object. The Reflection_Method::getParameters() method returns an array of Reflection_Parameter objects.

Reflection_Parameter can tell you the name of an argument, whether the variable is passed by reference (that is, with a preceding ampersand in the method declaration), the class required by argument hinting, and whether the method will accept a null value for the argument.

Listing Four shows an example of the Reflection_ Parameter‘s methods. Using the Reflection_Class: :getMethod() method, the code acquires a Reflection_ Method object. It then uses Reflection_Method: :getParameters() to get an array of Reflection_ Parameter objects. The argData() function uses the Reflection_Parameter object it was passed to acquire information about the argument.




Listing Four: Discover parameters with Reflection_Parameter

$person_class = new Reflection_Class( WorkPerson );
$method = $person_class->getMethod( “setBoss” );
$params = $method->getParameters();

foreach ( $params as $param ) {
print argData( $param );
}

function argData( Reflection_Parameter $arg ) {
$details = “”;
$name = $arg->getName();
$class = $arg->getClass();

if ( ! empty( $class ) ) {
$classname = $class->getName();
$details .= “\$$name must be a $classname
object\n”;
}
if ( $arg->allowsNull() ) {
$details .= “\$$name can be null\n”;
}
if ( $arg->isPassedByReference() ) {
$details .= “\$$name is passed by reference\n”;
}
return $details;
}

First, it gets the argument’s variable name with Reflection_Parameter::getName(). The Reflection_ Parameter::getClass() method returns a Reflection_Class object if a hint’s been provided. Finally the code checks whether the argument can be null with allowsNull(), and whether it’s a reference with isPassedByReference().

An Example

With the basics of the Reflection API under your belt, you can now put the API to work. (The following example is modeled very loosely upon work already undertaken for a port of the Java tool Ant to PHP.)

Imagine that you’re creating a class that calls Module objects dynamically. To implement dynamism, you’d likely define an execute() method in the Module interface or abstract base class, forcing all child classes to define an implementation. You’d also likely allow the users of your system to list the Module classes to call in an external XML configuration file. Given those requirements, implementing your new class is a simple matter of using this information to aggregate a number of Module objects before calling execute() on each one.

However, what happens if each Module requires different information to do its job? In that case, the XML file can provide property keys and values for each Module, and the creator of each Module can provide setter methods for each property name. Given that foundation, it’s up to your code to ensure that the correct setter method is called for the correct property name.

Here’s some groundwork for the Module interface and a couple of implementing classes.


require_once( ‘WorkPerson.php’ );

interface Module {
function execute();
}

class FtpModule implements Module {
function setHost( $host ) {
print “FtpModule::setHost(): $host\n”;
}

function setUser( $user ) {
print “FtpModule::setUser(): $user\n”;
}

function execute() {
// do things
}
}

class PersonModule implements Module {
function setPerson( WorkPerson $person ) {
print “PersonModule::setPerson():
“.$person->getName().”\n”
}

function execute() {
// do things
}
}

Here, PersonModule and FtpModule both provide empty implementations of the execute() method. Each class also implements setter methods that do nothing but report that they were invoked. By convention, all setter methods must either expect a single mixed argument (a string), or an object that can be instantiated with a single string argument.

To work with PersonModule and FtpModule, the next step is to create a ModuleRunner class. It will use a multi-dimensional array indexed by module name to represent configuration information provided in the XML file. Here’s that code:


class ModuleRunner {
private $configData = array(
“PersonModule” =>
array( ‘person’=>’bob’ ),
“FtpModule” =>
array( ‘host’ =>’example.com’,
‘user’ =>’anon’ )
);
private $modules = array();
// …
}

The ModuleRunner::$configData property contains references to the two Module classes. For each module element, the code maintains a subarray containing a set of properties. ModuleRunner‘s init() method is responsible for creating the correct Module objects, as shown in Listing Five.




Listing Five: ModuleRunner creates other Module objects

class ModuleRunner {
// …

function init() {
try {
foreach ( $this->configData as $modulename =>
$params ) {
$module_class = new Reflection_Class(
$modulename );

// $interface = new Reflection_Class(‘Module’);
// if ( ! $module_class->isSubclassOf(
$interface ) ) {
// throw new Exception( “unknown module
type: $modulename” );
// }

if ( ! my_is_subclass( $modulename, ‘module’ )
) {
throw new Exception( “unknown module type:
$modulename” );
}
$module = $module_class->newInstance();
foreach ( $module_class->getMethods() as
$method ) {
$this->handleMethod( $module, $method,
$params );
}
array_push( $this->modules, $module );
}
} catch ( Exception $e ) {
die( $e->getMessage() );
}
}

//…
}

$test = new ModuleRunner();
$test->init();

The init() method loops through the ModuleRunner: :$configData array and attempts to create a Reflection_ Class object for each module element. An exception is generated when Reflection_Class‘s constructor is invoked with the name of a non-existent class, so most of the init() method is in a trycatch block.

Before you can invoke the execute() method of each Module, an instance has to be created. That’s the purpose of method Reflection_Class::newInstance(). That method accepts any number of arguments, which it passes on to the relevant class’s constructor method. If all’s well, it returns an instance of the class. (For production code, be sure to code defensively: check that the constructor method for each Module object doesn’t require arguments before creating an instance.)

You may notice that some of the code in Listing Five is commented out. The Reflection_Class::isSubClassOf() method should accept a Reflection_Class object and return true if the argument references a parent class. You could use this to check that the class is a Module class before creating an instance.

At the time of writing, however, this method was broken. Instead, the code invokes a function called my_is_subclass() that accepts two strings: the name of a child class and that of a parent. If the child class implements or extends the parent, the function returns true. my_is_subclass() creates a Reflection_Class object and loops through implemented interfaces and extended super-classes to check the provenance of the given class. You probably won’t need this workaround function, but it is included below for the sake of completeness.


function my_is_subclass( $class, $parent ) {
$test_class = new Reflection_Class(
$class );
$interfaces = $test_class-
>getInterfaces();
foreach ( $interfaces as $int ) {
if ( $int->getName() == $parent ) {
return true;
}
}

while ( $test_class = $test_class-
>getParentClass() ) {
if ( $test_class->getName() == $parent
) {
return true;
}
}
return false;
}




Listing Six: Run a number of Modules, setting parameters for each one

class ModuleRunner {
// …
function handleMethod( Module $module, Reflection_Method $method, $params ) {
$name = $method->getName();
$args = $method->getParameters();
if ( count( $args ) != 1 ||
substr( $name, 0, 3 ) != “set” ) {
return false;
}
$property = strtolower( substr( $name, 3 ));
if ( ! isset( $params[$property] ) ) {
return false;
}
$arg_class = $args[0]->getClass();
if ( empty( $arg_class ) ) {
$method->invoke( $module, $params[$property] );
} else {
$method->invoke( $module,
$arg_class->newInstance( $params[$property] ) );
}
}
}

Reflection_Class::getMethods() returns an array of all Reflection_Method objects available for the class. For each element in the array, the code invokes the ModuleRunner::handleMethod() method, passing it a Module instance, the Reflection_Method object, and an array of properties to associate with the Module. handle Method() verifies and invokes the Module object’s setter methods.

handleMethod() first checks that the method is a valid setter. In the code, a valid setter method must be named setXXXX(), and must declare one and only one argument.

Assuming that the argument checks out, the code then extracts a property name from the method name by removing set from the beginning of the method name and converting the resulting substring to lowercase characters. That string is used to test the $paramsarray argument. This array contains the user-supplied properties that are to be associated with the Module object.

If the $params array doesn’t contain the property, the code gives up and returns false.

Otherwise, it can go ahead and invoke the setter method. To do that, the code must check the type of the first (and only) required argument of the setter method. The Reflection_ Parameter::getClass() method provides this information. If the method returns an empty value, the setter expects a primitive of some kind; otherwise it expects an object.

To call the setter method, you need a new Reflection API method. Reflection_Method::invoke() requires an object and any number of method arguments to pass on to the relevant method. Reflection_Method::invoke() throws an exception if the provided object is wrong for the class.

If the setter method doesn’t require an object argument, you call Reflection_Method::invoke() with the user-supplied property string. If the method requires an object, use the property string to instantiate an object of the correct type, which is then pass to the setter.

The example assumes that the required object can be instantiated with a single string argument to its constructor. It’s best, of course, to check this before calling Reflection_ Class::newInstance().

By the time the ModuleRunner::init() method has run its course, the object has a store of Module objects, all primed with data. The class can now be given a method to loop through the Module objects, calling execute() on each one.

Reflection Perfection

While space for this article was limited, you’ve nonetheless seen a lot of the Reflection API’s core features. Check the documentation for more information on all of the classes designed to help you investigate functions, properties, and PHP extensions.

The addition of the Reflection API to Zend 2 represents another sign that PHP 5 is shaping up as a serious language for object-oriented design. Expect to see powerful tools that use the API, including documentation generation, automatic database persistence, and automated class diagrams.



Matt Zandstra is a programmer and writer. His company, Corrosive, develops enterprise applications and provides technical consultancy. He is the author of SAMS Teach Yourself PHP in 24 Hours, and is currently working on a book about PHP 5 and design patterns for Apress. You can reach Matt at triffid@corrosive.co.uk.

Comments are closed.