Annotations can be introduced by extending the abstract class
recess.lang.Annotation. User defined
annotation classes must end in the word 'Annotation'. For example: to have
a !Protected annotation the classname must be
ProtectedAnnotation. There are four
abstract methods which must be implemented by custom annotations:
usage, isFor,
validate, and expand.
These methods are used by the framework to make working with user-defined
annotations feel like a first-class PHP construct. Following are
descriptions of the purpose for each annotation:
usage - Returns a string representation of the intended usage of an annotation.
isFor - Returns an integer representation of the type(s) of PHP language constructs the annotation is applicable to. Use the Annotation::FOR_* consts to return the desired result.
Example 6.8. Examples of Annotation's isFor
Method
// Only valid on classes function isFor() { return Annotation::FOR_CLASS; } // Valid on methods or properties function isFor() { return Annotation::FOR_METHOD | Annotation::FOR_PROPERTY; }
validate($class) - Validate is called just
before expansion. Because there may be multiple constraints of an
annotation the implementation of validate should append any error messages
to the protected $errors property. Commonly used
validations helper methods are provided as protected methods on the
Annotation class.
expand($class, $reflection, $descriptor) -
The expansion step of an annotation gives it an opportunity to manipulate
a class' descriptor by introducing additional metadata, attach methods,
and wrap methods. Parameters: $class the classname
the annotation is applied to. $reflection The PHP
Reflection(Class|Method|Property) object the annotation is located on.
$descriptor is the ClassDescriptor that the
annotation is being expanded on.
The power of annotations comes in the
expand step. An annotation can modify a class'
descriptor which allows for methods to be attached to a class and
available on all instances of a class, as well as "wrap" methods of a
class marked as !Wrappable
To demonstrate authoring a custom annotation, let's create an
annotation for methods on a Controller that
should be protected by a protected cookie key of "secret" and value of
"password". Our goal is to be able to place
!CookieProtected on a method in a controller and
redirect users to an optionally specified path (else, "/") if they do
not have the necessary cookie. We'll craft the annotation in two-steps,
first we'll implement the abstract methods of
Annotation, then we'll take combine the
annotation with the previously discussed
WrappedMethods to finish off the functionality of
the CookieProtectedAnnotation by wrapping a
Controller's serve
method.
Example 6.9. The Beginnings of the !CookieProtected
Annotation
<?php Library::import('recess.lang.Annotation'); class CookieProtectedAnnotation extends Annotation { public function usage() { return '!CookieProtected [optional/redirect/path]'; } public function isFor() { return Annotation::FOR_METHOD; } protected function validate($class) { $this->minimumParameterCount(0); $this->maximumParameterCount(1); $this->validOnSubclassesOf($class, Controller::CLASSNAME); } protected function expand($class, $reflection, $descriptor) {} } ?>
Let's walk through each method. We can see
usage simply returns a string demonstrating
how to properly use an annotation in code.
isFor shows that this annotation can only
be applied to methods, so Recess with throw an error if the
annotation is placed on a class or a property of a class. This is
what we want as we are trying to protect methods of a controller.
validate is making use of helper methods
defined in Annotation for validating the
formation of the annotation. These validations are used to check
things like the number of parameters, which classes the annotation
is meaningful for (in this case, only on subclasses of
'Controller'), etc.
The full list of helper methods for validate is:
acceptedKeys,
requiredKeys,
acceptedKeylessValues,
acceptedIndexedValues,
acceptedValuesForKey,
acceptsNoKeylessValues,
acceptsNoKeyedValues,
validOnSubclassesOf,
minimumParameterCount,
maximumParameterCount,
exactParameterCount. To understand the meaning
of 'keys', 'indexed values', 'keyless values', you must understand the
way recess interprets annotations. Recess parses an annotation by doing
the following: 1) find an annotation by looking for an !Bang followed by
whitespace, "!Bang". 2) Convert everything after the
start of an annotation into a parameters array, !Bang foo, key:
bar becomes array("foo", "key" =>
"bar"). 3) Instantiate the class named
BangAnnotation and pass the parameters into the
BangAnnotationinstance's
init method. Thus, every subclass of
Annotation has a $parameters
property which is this array. The helper methods provide convenient ways
of checking the contents of this array during
validate.
Once validate has been run, if errors are
found in the annotation's $errors property a
RecessErrorException is thrown. If no errors are
found, then the final step before expand is
called is to make all keyed values in the $parameters
array properties of the annotation instance and to append all keyless
values of $parameters to the $values property. For
example, the annotation !Bang foo, Key: bar becomes:
$bang = new BangAnnotation(); $bang->init(array('foo', 'Key'
=> 'bar')); Finally, after Recess calls
expandAnnotation (which, in turn, calls your
user-defined expand), your
expand can reference
$this->values[0] to find
'foo' and $this->key to get
'bar'.
During the init method of
Annotation all array keys are converted to
lowercase.