Runtime PHP Annotations. What a tease.

Posted: May 30th, 2008 | Author: Joey | Filed under: Coding | 7 Comments »

Today at work I found myself on the tedious side of software development. I mainly develop in PHP, and PHP lacks many things that would otherwise make it an enjoyable language to use. It seems like the mantra of the PHP developers and community as a whole is “half-assed” (in all due respect, of course ;) ).

Let me explain. If you want to use a library, tool, or feature in a way that the original developer didn’t care about or think about, then odds are that the documentation will claim it will work, but it won’t. It will work for the one case the developer tested, but that is it. Of course there are also many projects and features out there that will prove me wrong, and kudos to those ones.

I am getting sidetracked. Today I wanted annotations in PHP. I wanted to provide meta data for the methods and classes I was writing.

It would be easy enough to write a code generator that parses the source, handles the annotations, and spits out the desired code. In fact, many things already do this, like documenters and style checkers. However, that is not what I want. I want it done at runtime and I want it to change the behavior of the code.

Sometimes I have to back up my complaints with some action, so today I implemented this feature. In an entirely proof-of-concept way, but a real solution for runtime PHP annotations nonetheless.

Since examples speak louder than words, here we go.

<?php
 
class Pi {
 
  /**
   * Use the monte carlo method of estimating PI
   * @return float
   */
  public static function estimate () {
    $rounds = 1000000;
    $hits = 0;
    $max = mt_getrandmax();
    for ($i=0; $i<$rounds; $i++) {
      $x = mt_rand() * 1.0 / $max;
      $y = mt_rand() * 1.0 / $max;
      $hits += (($x*$x + $y*$y) <= 1.0)? 1: 0;
    }
    return 4.0 * $hits / $rounds;
  }
 
}
 
// Consistent return value
mt_srand(0xDEAD);
echo Pi::estimate(), "\n";
mt_srand(0xDEAD);
echo Pi::estimate(), "\n";

Does anyone else enjoy naming a php variable ‘hit’ or ‘hits’? Anyway, for this example I wanted a function that would take some time to compute. Admittedly contrived, but I didn’t ask you to read this post. Using the monte carlo method, this static class method estimates the value of PI and is called twice.

time php Pi.php 
3.141196
3.141196
 
real    0m2.186s

Obviously, if many calls are made to this method, the time adds up quickly. No one in their right mind would actually release code like this. I said right mind. Something I find myself doing often is caching the result before returning it. I’m not going to show that example since it is trivial to implement. Clearly all subsequent calls to the time-consuming method would return immediately.

I don’t like it. I do it, but I don’t like it. The method starts out very clean. All it does is estimate the value of PI (or search a graph, or prune a tree, whatever). When you add caching into it, suddenly the method is doing two things. And two completely unrelated things at that. This should bother you at least a little bit. Off the top of my head, I can avoid this approach by:

  • Relying on consumer to cache the results. I don’t think so.
  • Extending the class with a new “Caching class”. Yuck. Not to mention the example above uses a static method.
  • Moving the two routines into to separate methods: public getEstimate() and private calculateEstimate(). getEstimate() would do the caching and call calculateEstimate() internally. Ok, but pretty tedious and causes the code to grow quickly.
  • Use annotations to modify the runtime behavior of the method.

What I really want to do is annotate the method, indicating that the operation is idempotent and that the results can be cached. I don’t want to design a whole framework right now, I’ll leave that for another day. To see if it can or can’t be done, I just want to implement this single feature for now.

<?php
 
class Pi {
 
  /**
   * Use the monte carlo method of estimating PI.
   * @cache
   * @return float
   */
  public static function estimate () {
    $rounds = 1000000;
    $hits = 0;
    $max = mt_getrandmax();
    for ($i=0; $i<$rounds; $i++) {
      $x = mt_rand() * 1.0 / $max;
      $y = mt_rand() * 1.0 / $max;
      $hits += (($x*$x + $y*$y) <= 1.0)? 1: 0;
    }
    return 4.0 * $hits / $rounds;
  }
 
}
 
require_once 'Annotations.php';
Annotations::annotate('Pi');
 
// Consistent return value
mt_srand(0xDEAD);
echo Pi::estimate(), "\n";
mt_srand(0xDEAD);
echo Pi::estimate(), "\n";

Ideally, I’d like to add just that one line to the documentation comment above the method. Of course I’d need some way to trigger the parsing, so I put that at the end for the sake of the example. The code of the method remains solely dedicated to estimating the value of PI. I’ve only hinted that at runtime, it’d be great if the results were only calculated once.

It took less than twenty lines of code to implement this annotation. The methods of the class are retrieved, the annotation is searched for, and if found, the code of the method is altered to cache the results. (Actually I created a new class method that does the same thing, and rewrote the original one to do the caching only, calling the new method for the calculated value.)

Then I ran it, and sure enough it runs in half the time.

time php Pi.3.php 
3.141196
3.141196
 
real    0m1.175s

By the way, there are many attempts at a solution out there, none of which do what I want from what I can tell in 30 seconds of reading their summaries. The PHP reflection API seems to be “look but don’t touch”. I want to grope. I want a [@deprecated] method to raise a warning when the method is called. Likewise for a [@testing] method. I want my [@cache]. I want annotations for parameter validation… maybe that is a little too far, but you get the idea. The solutions I have seen let you poke and prod, but they don’t let you add/change functionality to the running code. Implementing these types of annotations would be pretty straightforward to do with the method I used above.

Since I made it work, and claim it was easy, why am I complaining? The problem is that the features I had to use to get it working are marked as unstable, unmaintained, subject to change, and risky to rely on for critical production code. The runkit extension doesn’t even compile for newer versions of PHP and some features even segfault.

So close. Half-assed.