ONLamp.com    
 Published on ONLamp.com (http://www.onlamp.com/)
 See this if you're having trouble printing code examples


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>

Code Generation

Once the test cases have been created via the user interface, all that is left is to actually build the code. This is a fairly simple operation in terms of setting up the code for each test and then running it. If the method returns the expected result, then the test passes; if not, then it fails. The tests are run in order by a test framework. For each test, a simple function must be created to create the object, call the method, check the result, and then do any cleanup that is needed. Each test will be a simple function with no parameters. The test functions will return PASS or FAIL.

The tests are built by a command-line tool. The tool reads in the XML file for the tests to be generated, and then outputs a PHP file containing the tests, which may then be run with the test runner script.

Each test is implemented as a PHP function with no parameters. The test runner will take a list of those functions and run them in order. If a test passes it will return the constant "PASS"; if it fails, it will return "FAIL." The test runner script can then iterate over all of the tests and return the data to the user. By default, it will run at a command line with the test name followed by "PASS" or "FAIL." It may also be useful to log how long each test took to run.

The basic format of each test is to call the class constructor (with any supplied parameters), then check that the constructor returned a valid object of the correct type. If it did not, the test will fail (unless that is the expected outcome). Then it calls the method to be tested. The test will then check that all of the assertions are true.

There are three types of possible assertions. The first is a return value, which can be true, false, null, a string, numeric value, a specific result, or anything else that can be returned by a PHP function. Any of these can be tested for. The test will PASS if the return value is what was expected, and FAIL otherwise.

The second type is to check whether a class value is set. A class property assertion can test for the same types of values as a return value assertion. In some cases, there will be more than one class property assertion, or a return value assertion and a property assertion. In this case, they both must be satisfied.

The third type of assertion is an exception. This can be a general Exception or a specific type of Exception. In this case, a test will pass if the expected Exception happens, and fail otherwise.

If there are several assertions for a test, then all of them must be satisfied for the test to PASS.

The test runner will have an ordered list of all the test functions. It will then run them in order. If a test fails, the runner will stop and report the error.

Here is the code to generate the tests:

  
  public function buildClassTests($class)
  {
    
    $methods = $class->xpath('method');
    $tests   = array();
    foreach($methods as $method)
      {
    $tests[] = build_Tests($method);
      }
    $filename  = $class['name'] . ".tests.php";
    $test_file =  join('',$tests);
    
    file_put_contents($filename, $test_file);
    
  }

  private function build_tests($method)
  {
    $tests      = $method->xpath('test');
    $test_name  = $method['name'];
    foreach($test as $test)
      {
    $test_name = $test['name'];
    
    $parameters = $test->xpath('cons_parameter');
    
    foreach($parameter as $param)
      {
        if($param['value'] == 'DEFAULT')
          break;
        $cons_p[] = "'".$param['value'] .".";
      }
    
    $parameters = $test->xpath('parameter');
    $p = array();
      
    foreach($parameter as $param)
    {
      if($param['value'] == 'DEFAULT')
        break;
      $p[] = "'" . $param['value'] ."'";
    }
    $p = join(',',$p);
    $assert     = array();

    $exception        = 'Exception';
    $exceptionReturn  = 'FAIL';
    
    $assertions = $test->xpath('assertion');
    /* Current code only supports one assertion, but it may sometimes be useful to have more than one*/
    foreach($assertions  as $a)
      {
        switch($a['type'])
          {
          case 'return':
        $assert[] = sprintf("%11s%20s%4s%20s","",'$result',$a['cond'],$a['value']);
        break;
          case 'exception':
        $exception = $a['exception'];
        $exceptionReturn = 'PASS';
          }
      }
    
    if($exception != 'Exception')
      $exep           = "           catch (Exception $e) \n".
        "           {\n".
        "               return FAIL;".
        "         \n}\n";
    else
      $exep = "";

    if($assert)
      {
        $assert_block = "           if(\n". join("&&\n",$assert) . "             )\n";
        $assert_block = "              return PASS\n";
        $assert_block = "           else\n";
        $assert_block = "              return FAIL\n";
        
      }
    

      $code = <<<CODE
    function test_$test_name()
    {
      try
      {
            /* this is outputting php code, so the $ signs need to be escaped */
        \$class  = new $ClassName();
        \$result = \$class->$name($p);
$assert
      }
      catch ($exception \$e)
      {
        return $exceptionReturn
      }
      $excp
      
CODE;
      $tests[] = $code;
    }
      return join("\n\n\n",$tests);
    }

This is a simple test case. If the method returns true, it passes; if the method returns false or throws an exception, it fails.

    function test_test4()
    {
      try
      {
           
         $class  = new testClass();
         $result = $class->execute('12345');
             if($result == true)
               return PASS;
             else 
               return FAIL;
      }
      catch (Exception $e)
      {
        return FAIL;
      }

Here is the generated code for the listing code runner:

<?php
$tests = array(list of tests);
$i     = 0;
$fail  = false;
foreach ($tests as $test)
{
  $i++;
  $result = $test();
  if($result == PASS)
   echo "$i Test $test PASS<br/>";
  else
  {
    echo "<span class='fail'>$i Test $test FAIL</span><br/>";
    $fail = true;
    break;
  }

} 
if($fail)
 echo "SOME TESTS FAILED";
else 
 echo "ALL TESTS PASSED";

?>

Zachary Kessin has worked with free software and web development for more than ten years. He is a frequent speaker at Jerusalem.pm and has spoken at YAPC::Israel.


Return to PHP DevCenter.

Copyright © 2009 O'Reilly Media, Inc.