PHP DevCenter
oreilly.comSafari Books Online.Conferences.

advertisement


Code As Data: Reflection in PHP

by Zachary Kessin
04/26/2007

As programmers, many of us instinctively draw a distinction between the programs we write and work with, and the data that they are meant to process. While this is often a useful thing to do, it does tend to hide one key fact: programs are themselves nothing but well-defined data. In order to run a program, some other program must parse it and turn it into an executable. This may be a compiler or an interpreter or some other tool, but it is still a program. However, for many people, the only parser that they use besides the one to actually compile or run their code is a highlighter that color-codes things in their text editor.

There is a long history of using programs to automate the writing of code in C and similar languages (including Lex, yacc, lint, ctags, and others). For C programmers, many of these tools have been around in one form or another since the 1970s, and are very well understood by the community. However, for people who learned to code in PHP, Perl, and Python (the "P" languages), the use of tools like this may never have entered their horizon. This is unfortunate, because a solid tool chain for code generation and parsing can lead to better end code and higher programmer efficiency, as I will show here.

Once we establish that we can parse our code with a tool, then two questions remain: how to do it, and what to do with the information once we have done it. It is possible to use regular expressions to parse code. For example, this is the approach taken by Emacs and other editors for their syntax highlighting. However, this can get difficult very quickly, as modern "P" languages often are quite complex, and creating a solid set of regular expressions to describe PHP or Perl would be very difficult. Many syntax highlighters have problems relating to here documents and other special cases. It would be better to let the language's own parser take care of the hard part, as it already knows how to parse itself. In PHP (version 5 and later), we can use the Reflection API to do this.

Reflection is an addition to PHP version 5 that allows a program to examine its own code. The Reflection API will tell us a lot of information about a function or object, including where is it defined (file and range of line numbers), its list of parameters, function name, doc, comments, and so on.

To use reflection, you first have to include the program code to be examined. Be sure to use include and not require, as require will cause the program to exit if there is an error in the included code. Reflection uses PHP's own parser to examine the code, so any code in the included file that is not inside a function or class will be executed. For this reason, using reflection on untrusted code is not a good idea. However this will often not be a problem, because generally this type of tool will be built on code that has been written by the programmer or his team. So while it is probable that there are bugs in the target code, it is unlikely that there is an actual harmful payload. This testing engine should not be used on unknown code.

Once a file has been included, we can use reflection to examine the classes or functions in it. A program can use this information to build various structures that depend on the code base, such as a WSDL file, or unit tests or other wrappers.

The user will have to provide the file that defines the class or classes to be examined. The get_declared_classes() function will return a list of all the classes that PHP knows about. This can be used to present a menu of classes to the user.

Once the user has selected a class to work with, reflection can be used to examine that class. In this example, I will deal with classes and objects. However, the Reflection interfaces for functions are similar to those for class methods. Reflection can then make a list of methods and properties of that class, from which we can build our test suite. Note that our test suite will not be able to call private or protected functions of the class, as there is not a way in reflection to override the access protection around an object. Here is the function that performs this task:

<?php

#$Id: parse_source.php,v 1.2 2007/04/23 11:30:14 zkessin Exp $

   $file  = $argv[1];
   $class = $argv[2];

function parse_class($file,$class)
   {
   include_once($file);
   $block   = "\n<methods fail='continue' class='$class'>\n";
   $rf      = new ReflectionClass($class);
   $methods = $rf->getMethods();
   foreach($methods as $method)
   {
   if($method->isPublic() == false)
   continue;
   $static     = $method->isStatic()?"static='true'":'';
   $final      = $method->isFinal()?"final='true'":'';
   $final      = $method->isAbstract()?"abstract='true'":'';
   
   $methodName = $method->getName();
   $className  = $method->getDeclaringClass()->getName();
   $block .= "  <method  name='$methodName' $static $final>\n";
   foreach ($method->getParameters() as $p)
   {
   $default = $p->isDefaultValueAvailable()?"default='".$p->getDefaultValue()."'":'';
   $name    = $p->getName();
   $block .= "    <parameter name='$name' $default/>\n";
   }
   
   $block .= "  </method>\n";
   $block .= "<!--********************************************************************************-->\n\n";
   }
   
   $block .= "</methods>\n";
   return $block;
   }
   file_put_contents($file . ".test.xml",parse_class($file,$class));

?>

This example is a rather limited case of a test suite, to demonstrate the idea of using reflection for this purpose. A test suite will consist of a set of one or more tests. Ideally each function or method in a PHP module should have several tests, to check all of the various conditions that might happen with the code.

One a class has been selected, the program will use reflection to find all of the methods in that class. Then the user will have a chance to create tests on the various methods of the class. For each test, the user will enter values for each parameter to the method, as well as for each parameter to the class constructor. After the tests are defined, they are written to an XML datafile, which can then be used to generate PHP code to run the actual tests.

Data Storage

For each PHP class, the code generates a XML file to store its data. XML has the advantage that it is a text format, so users can edit the data if they want to do something that an interface is not yet written for. It also can be checked into CVS, or another source code control system. For each public method, we store the names of each parameter, along with the default values if any. Then we store as many tests as are needed. Each test has a name, possibly some setup code, a call to the method with defined parameters, and an assertion. Assertions can test the return value, or an exception. A test passes if and only if the assertion is met. The code can be configured to stop after the first test failure, or to keep running, depending on which makes sense for the programmer.

<?xml version="1.0"?>
   <methods fail="continue" class="testClass">
 <method name="execute">
 <parameter name="uid"/>
 <test name="test2"><parameter name="uid"
 value="12345"/><assertion type="exception" value='exception'/></test>
 <test name="test3"><parameter name="uid"
 value="test"/><assertion type='exception' value='exception'/></test>
 <test name="test4"><parameter name="uid"
 value="12345"/><assertion type='value' value='true/></test>

 </method>
 <!--********************************************************************************-->

</methods>

Building the Tests

An interface must be built to allow the user to define each test. This could be done in a number of ways, but implementing it with an Ajax object seems to make most sense. The user can be presented with a three-level menu of classes, methods, and tests. He can then choose to edit an existing test or to add a new one. Using the JavaScript prototype library makes building an Ajax application to do this reasonably simple. We just need to create a PHP backend that can encode the data in a useful format (in this case, raw HTML), and then some JavaScript code for the user interface.

Once the user has selected a class and method to work with, he will be presented with a list of tests that have been defined. He may edit an existing test, or create a new one, in each case the procedure is the same. A HTML form will be presented to allow entry of each of the parameters and the expected results. This data will then be sent to the server, to be written in the XML datafile. Ideally, the user should be able to delete, clone, and reorder tests. A more complete implementation would allow the user to set parameters for the object constructor, as well as set session, get, and post options.

This program uses a simple command-line program to manage an XML file, showing the files to be included. Then the programmer can work with the data in an Ajax-based application. The user interface will allow the user to select from a list of classes to be worked on, and then pick a method to test and create the test scenario.

The Ajax frontend will use the reflection API to communicate with the PHP backend. In some cases, the data is sent as raw HTML fragments, and in other cases it is sent as a JSON structure. In the cases where the frontend will have to replace some HTML, it is easiest to create it on the server and then send it as HTML. The JavaScript prototype library makes this reasonably easy, because in the default mode it will allow an Ajax call in which the returned HTML is placed into an HTML element.

Here is the frontend JavaScript:

/* $Id: ajaxInterface.js,v 1.3 2007/04/23 11:30:14 zkessin Exp $ */

function loadClasses()
   {
   new Ajax.Updater('classes',
   'list_classes.php',
   {asynchronous: true, 
   evalScripts:  true, 
   onComplete:   function(request, json){},
   }
   );
   }
   
function loadMethods(className)
   {
   new Ajax.Updater('methods',
   'list_methods.php',
   {asynchronous: true, 
   evalScripts:  true, 
   onComplete:   function(request, json){},
   parameters:   {class: className}});
   }

function loadTests(className,methodName)
   {
   new Ajax.Updater('tests',
   'list_tests.php',
   {asynchronous: true, 
   evalScripts:  true, 
   onComplete:   function(request, json){},
   parameters:   {class: className,method:methodName}});
   }
     
function editTests(className,methodName,testname)
   {
   var target;
   
   target = 'new_test.php';
   new Ajax.Updater('test_form',
   target,
   {asynchronous: true, 
   evalScripts:  true, 
   onComplete:   function(request, json)
   {
   },
   parameters:   {class: className,method:methodName,test:testname}});
   }

function addTest(button) 
   {
   form = button.form;
   console.log(form);
   var className  = form.class.value;
   var methodName = form.method.value;
   new Ajax.Updater('result',
   'add_test.php',
   {asynchronous: true, 
   evalScripts:  true, 
   onComplete:   function(request, json)
   {
   loadTests(className,methodName);
   },
   parameters:   Form.serialize(form)});
   return false;
}

And here is the XML file with the list of files:

 <?xml version="1.0"?>
   <classes>
       <class source="UnitTestBuilder.class.php"/>
   </classes>

Pages: 1, 2

Next Pagearrow




Valuable Online Certification Training

Online Certification for Your Career
Earn a Certificate for Professional Development from the University of Illinois Office of Continuing Education upon completion of each online certificate program.

PHP/SQL Programming Certificate — The PHP/SQL Programming Certificate series is comprised of four courses covering beginning to advanced PHP programming, beginning to advanced database programming using the SQL language, database theory, and integrated Web 2.0 programming using PHP and SQL on the Unix/Linux mySQL platform.

Enroll today!


Sponsored by: