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
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