Source Code Annotations (Attributes)

Once again, I’m experimenting with the translation of some of the principles from my work with ASP.NET MVC 2 and C# to PHP.

Attributes are an awesome language feature - basically, you can annotate your source code (classes, methods, properties, etc.) using custom Attributes, and inspect them at runtime.

This is used for things like validation and adding filters to actions.

To do something equivalent in PHP, we have to invent a syntax for it. Some of you may cringe at a the thought of this already, but bear with me :wink:

As an example, you might use an attribute to specify that a particular action requires a POST - which might look like this:




<?php


class MyController extends Controller

{

  #[HttpPost()]

  public function actionTest()

  {

    // ...

  }

}



Apart from the pound sign (#) this is identical to the syntax used in C#.

This is not a complete implementation by any means, but here’s a basic implementation in PHP:

[size="4"]This code sample has been superseded by a full library currently in development - see below[/size]




<?php


/**

 * This interface must be implemented by Attributes

 */

interface IAttribute {

  public function initAttribute($properties);

}


/**

 * This class implements run-time Attribute inspection

 */

abstract class Attributes

{

  /**

   * This method is the public entry-point for Attribute inspection.

   *

   * @param Reflector a ReflectionClass, ReflectionMethod or ReflectionProperty instance

   * @return array an array of Attribute instances - or an empty array, if no Attributes were found

   */

  public static function of(Reflector $r)

  {

    if ($r instanceof ReflectionMethod)

      return self::ofMethod($r);

    else if ($r instanceof ReflectionProperty)

      return self::ofProperty($r);

    else if ($r instanceof ReflectionClass)

      return self::ofClass($r);

    else

      throw new Exception("Attributes::of() : Unsupported Reflector");

  }

  

  /**

   * Inspects class Attributes

   */

  protected static function ofClass(ReflectionClass $r)

  {

    return self::loadAttributes($r->getFileName(), $r->getStartLine());

  }

  

  /**

   * Inspects Method Attributes

   */

  protected static function ofMethod(ReflectionMethod $r)

  {

    return self::loadAttributes($r->getFileName(), $r->getStartLine());

  }

  

  /**

   * Inspects Property Attributes

   */

  protected static function ofProperty(ReflectionProperty $r)

  {

    return self::loadAttributes($r->getDeclaringClass()->getFileName(), method_exists($r,'getStartLine') ? $r->getStartLine() : self::findStartLine($r));

  }

  

  /**

   * Helper method, replaces the missing ReflectionProperty::getStartLine() method

   */

  protected static function findStartLine(ReflectionProperty $r)

  {

    $c = $r->getDeclaringClass();

    $code = explode("\n", file_get_contents($c->getFileName()));

    $start = $c->getStartLine();

    $length = $c->getEndLine()-$start;

    foreach (array_slice($code,$start,$length) as $i=>$line)

      if (preg_match('/(?:public|private|protected|var)\s+\$'.preg_quote($r->getName()).'/', $line))

        return $i+$start+1;

  }

  

  /**

   * Create and initialize Attributes from source code

   */

  protected static function loadAttributes($path, $linenum)

  {

    $attributes = array();


    $code = explode("\n", file_get_contents($path));

    

    $linenum -= 1;

    while (($linenum-->=0) && ($attribute = self::parseAttribute($code[$linenum])))

      $attributes[] = $attribute;

    

    return array_reverse($attributes);

  }

  

  /**

   * Parse an Attribute from a line of source code

   */

  protected static function parseAttribute($code)

  {

    if (preg_match('/\s*\#\[(\w+)\((.*)\)\]/', $code, $matches))

    {

      $params = eval('return array('.$matches[2].');');

      return self::createAttribute($matches[1], $params);

    }

    else

      return false;

  }

  

  /**

   * Create and initialize an Attribute

   */

  protected static function createAttribute($name, $params)

  {

    $class = $name.'Attribute';

    $attribute = new $class;

    

    $attribute->initAttribute($params);

    

    return $attribute;

  }

}


/**

 * Sample Attribute

 */

class NoteAttribute implements IAttribute

{

  public $note;

  

  public function initAttribute($params)

  {

    if (count($params)!=1)

      throw new Exception("NoteAttribute::init() : The Note Attribute requires exactly 1 parameter");

    

    $this->note = $params[0];

  }

}


/**

 * A sample class with Note Attributes applied to the source code:

 */


#[Note("Applied to the Test class")]

class Test

{

  #[Note("Applied to a property")]

  public $hello='World';

  

  #[Note("First Note Applied to the run() method")]

  #[Note("And a second Note")]

  public function run()

  {

    var_dump(array(

      'class' => Attributes::of(new ReflectionClass(__CLASS__)),

      'method' => Attributes::of(new ReflectionMethod(__CLASS__, 'run')),

      'property' => Attributes::of(new ReflectionProperty(__CLASS__, 'hello')),

    ));

  }

}


// Perform a test:


header('Content-type: text/plain');


$test = new Test;

$test->run();




[s]Unfortunately, two missing features are currently forcing me to comment out the portion of this code that would implement support for property annotations.

Until this is added, property annotations would only be possible by means of a very ugly/bulky work-around.[/s]

Edit: I added a simple fix for the missing ReflectionProperty::getStartLine() method - if they ever fix it, it will default to using the real method, in the mean-time, it’ll use this simple work-around.

So this isn’t by any means a recommendation to implement this feature - more like an experiment for discussion, since PHP currently doesn’t fully provide support for completing this implementation…

Feedback/ideas/flames welcome :slight_smile:

This is interesting. Do you think there is a way to implement a good method overloading with reflection?

It’s possible to read PhpDoc for methods / properties with getDocComment() and then parse it for annotations. There are some ready to use implementations: http://code.google.com/p/addendum/

Also there is an implementation in Recess framework http://www.recessframework.org/page/recess-models-at-a-glance

Overall I don’t really like this idea. In C# and Java annotations are language level constructs and have both good runtime and IDE syntax/error checking. Also these are adding more magic.

I’m aware that some people would feel that way :wink:

The PhpDoc implementation is very simplistic, since it doesn’t even name the annotations - I think the real power of C# annotations, is that they are actually classes.

I just took a look at the Recess implementation, and I can’t say I like that either - it’s too remote from PHP syntax.

In my example, everything between the parantheses is inserted into an array() statement, so you can not only name your attributes, you can also name their properties, using PHP syntax - for example:




<?php


class Test

{

  [SomeAttribute('a'=>123, 'b'=>456)]

  public $whatever;

}



Since this pattern is actually a method for annotation of code, I also think the idea of integrating with the Reflection API, an integral part of the PHP language, makes more sense than relying on something like PhpDoc, an extraneous stand-alone specification - which also happens to require a big, bulky parser.

Of course, in the end, you could argue against attributes because they don’t really provide any functionality that doesn’t already exist - for example, you could “annotate” your code with “attributes” by implementing an interface defining a call-back method, like, say:




class Test implements Attributes

{

  public $hello='World';

  

  public function run()

  {

    var_dump(array(

      'class' => Attributes::of(new ReflectionClass(__CLASS__)),

      'method' => Attributes::of(new ReflectionMethod(__CLASS__, 'run')),

      //'property' => Attributes::of(new ReflectionProperty(__CLASS__, 'hello')),

    ));

  }


  public function getAttributes($type, $name)

  {

    static $attributes = array(

      'class' => array(

        new NoteAttribute("Applied to the Test Class"),

      ),

      'property'=>array(

        'hello'=>array(

          new NoteAttribute("Applied to the property"),

        ),

      ),

      'method'=>array(

        'run'=>array(

          new NoteAttribute("First Note Applied to the run() method"),

          new NoteAttribute("And a second Note"),

        ),

      ),

    );

    return $type=='class' ? $attributes[$type] : $attributes[$type][$name];

  }

}



This achieves the same thing, it just isn’t very elegant, since your attributes are removed from the code they apply to, and you’re forced to specify the name of every property and method again.

There’s also a performance consideration - since source code annotations are typically only needed for specific tasks, why add the overhead of having PHP load and parse all that code? Comments add no overhead.

This may be outweighed by the fact that this implementation needs to use eval() for the attribute values - but for something like schema annotations, which would be loaded once in a blue moon (e.g. a deploy script), this might be a real benefit…

By the way,

I don’t like “magic” either - but magic to me implies something happens “magically”, automatically, without your writing code to invoke a particular function.

Source code annotations are not magic by my definition, since your code has to explicitly ask for them - they are not somehow magically constructed or run without your intervention.

In case anyone is interested, I added a work-around for the missing property annotations - code updated above.

I think that’s a topic for a different discussion? But no, I don’t think reflection can help here. There is a library called RunKit in PHP, but it has to be compiled in, and it’s not included in the standard distributions - it will do overloading and many other runtime hacks. It’s been in the making for years though, like so many cool PHP features - I wonder if it will ever officially see the light of day…

Yes it’s a different thing, sorry for hijacking. I will take a look at runkit, thanks.

Early days (pre-alpha) for this library still, but I worked hard on my annotations library this weekend, and it is now functional!

You can browse the code, or check out a copy, from this repository:

http://svn.mindplay.dk/annotations/trunk/

I still need to write unit tests and add more error checking, but the library is basically functional.

It also needs documentation (although the source code is well-documented), and the standard library of annotations (DataAnnotations.php, DisplayAnnotations.php and ValidationAnnotations.php) is currently all stubs.

I welcome you to review these stubs at this point, and submit ideas/requests for missing useful annotations to include in the standard library. I’m including three categories of standard annations: data annotations to describe data-relationships in models, display annotations to describe formatting and form generation, and validation annotations to describe simple validations, validation methods, and type checking.

Of course you’re also welcome to review the overall architecture of the library itself, but please hold bug reports for now - I am aware of several minor issues that still need to be resolved, and as said, I still need to write unit tests to assist with the final round of debugging.

This is work in progress. But I made good progress this weekend :slight_smile:

By the way, I contacted the author of the native PHP extension for annotations, and after receiving his reply, I decided to redouble my efforts to complete my own implementation.

For the record, here are the comments I submitted:

For the sake of privacy, I’m not going to post his reply - but the author pretty much rejected every single comment I posted above. That’s not to say he doesn’t have his own reasons for thinking these features are unimportant or impractical - I just don’t agree with any of them.

To me, these are all real and important concerns - and even with IDE support for an official annotation feature in PHP, I don’t think this extension is going to be (even remotely) what I had hoped for.

So I’m redoubling my efforts to complete my own implementation - and while it may never see IDE support, perhaps it may serve as evidence to demonstrate the importance of the concerns listed above.

Note that the concerns listed in my e-mail are (loosely) based on what I referenced (or inferred) from the C# annotations feature set, using these features, and seeing how other third-party libraries leverage these features to greatly simplify many aspects of development and maintenance.

C# has the best support for annotations I’ve seen, and I don’t want to put up with less. Although my library is not a port of C# annotations by any means, I believe I’ve referenced the parts that are relevant to PHP - PHP being a dynamic language of a very different nature.

I would hate to see PHP distributed with another premature language feature - by the time everyone realizes why this feature doesn’t cut it, it’ll be too late, due to the usual backwards compatibility concerns. Abstraction layers, class-extensions and “semi-official” libraries of annotations (PEAR etc.) will start to emerge, and the whole thing ends up being much less useful (and fun!) than it could have been.

As much as I like PHP, I don’t want to see another addition to a list of shortcomings that it’s “too late” to expand upon - with an open-source codebase as huge as PHP’s, breaking backwards compatibility is pretty much out of the question. It almost never happens. Once a feature is in the official distribution, it’s pretty much set in stone. This feature is too important to end up on the “too late” list.

Here is the discussion you can be interested in:

http://news.php.net/php.internals/49580

Thanks, samdark - I’m keeping taps on this extension. I have contacted the reviewer at Zend and made him aware of the problems with this extension, and it seems he is already aware of some of the same issues.

The standard annotations library for my own implementation is still all stubs, but the class library itself is now fairly complete, debugged and tested, with a simple unit test to demonstrate that it works as intended.

http://svn.mindplay.dk/annotations/trunk/

I welcome any feedback and comments at this point - feel free to take it for a spin.

As said, if anyone is interested in reviewing the proposed standard annotations library, that would be helpful too.

Huh?

mindplay

Another spam wave ;)

This project now has an official home here:

http://code.google.com/p/php-annotations/

I released the first feature-complete and stable version of the annotation framework itself, last night - please see the page for more information.

Note that there’s still a lot of work to do - mainly documentation, examples, the standard library of annotations, and eventually integration with other frameworks like Yii.

But it’s definitely a milestone! :slight_smile: