As mentioned in our previous post, the very first step towards an annotation based AOP framework is a reflection API that is aware of annotations and is therefore more powerful than the standard reflection API of PHP. The API shall be able to read annotations on classes, class methods and class properties. At first, we have to think about how annotations will look like. This is pretty obvious if we bear in mind that we want take Java as a model.

The annotations are stored in a DocBlock, just like in the following example:

The task for our extended reflection API is to recognize that @Foo  is an annotation and not just a simple piece of text, while My test class is in fact just a string that has no further meaning for our application.

At this point we assume that an annotation has to be defined within a single line. So an annotation cannot be wrapped to another line. Likewise, it is not allowed to define multiple annotations within one DocBlock line.

There is a really good PHP library out there which already provides annotations support for PHP5, called Addendum. You may also browse some of the forks, i.e. (which is very comprehensive) and

We want to keep our framework as simple as possible and don’t want to introduce too many external dependencies. So we implement our own annotation parser/reader following Addendum’s approach. In the example above, the annotation @Foo  has no parameters. However, annotations may contain any number of parameters like an associative array (or map).

Eventually we’d like to support the following kinds of annotations:

  • Simple annotations with no value: @Foo  which is the same as @Foo()
  • Single-value annotations: @Foo('val') , @Foo("val") , @Foo(123) , @Foo({1,7}) and so on
  • Multi-value annotations, comma-separated: @Foo(a='b', c=123, d=true, e={1, 6})

Our reflection API should recognize whether an annotation has a single value, multiple values or none at all. Parameters can be any of the following types:

  • string/text (single or double quoted)
  • number (integer or float)
  • array (bounded by curly brackets; it actually is a list of values that will be converted to an indexed array)
  • constant (true, false, null; these are string values, but without quotes)

Now let’s bring all pieces together and sketch how an annotation is structured in general. This will help us building the parser for all kinds of annotations and their parameters: AnnotationStructure

In this picture, the blue squares are text/characters just as they have to be parsed by the annotation parser. Yellow squares stand for regular expressions which will parse annotation/parameter names or values.

This diagram makes clear how our annotation parsing is going to work. Our parser – let’s call it AnnotationParser – will check the DocBlocks line by line and try to match each comment line against the annotation structure described above. If the line cannot be matched, it is not recognized as an annotation.

To achieve this, we have to write a lot of matcher classes (for single or separated values, parameter/annotation names and so on) and make them play together. In other words, each square in our diagram is a matcher class which extracts a particular part of the annotation.

There is another dimension that has to be considered while parsing. On the one hand, there are annotation parts that are placed one after another; you can identify them by the “followed by” arrows, e.g. a key-value-pair consists of a key (or parameter name) which is followed by the equal sign (=) which in turn is followed by the value.

On the other hand, we have components that have to be parsed by several matchers because they can have different structures. For example, a value can be a string, a number, a constant, or an array. We have to recognize these different types while reading an annotation. Therefore it is reasonable to parse them in parallel and see if one of the matchers can read the component without errors.

Now let’s code

We start with an interface that describes the functionality of all matchers. All classes implementing this interfaces will return the parsed result string:

Note that we pass the parameter $str by reference. That is because the matchers not only parse the string but also remove the parsed part of the string from the original, so the original string (or the rest of it) can be parsed by the next matcher.

If the string could not be parsed by a matcher, the matcher throws an exception (see below). In this case the original string remains unchanged.

Now let’s have a look at our initial set of matchers.

String Matcher

The simplest matcher is the StringMatcher:

A StringMatcher checks if the given string starts with the expected string that was provided during instantiation as a constructor parameter. For example, we expect the opening parentheses to be the first character in a string:

After execution, an empty array is returned because the StringMatcher does not extract any information from $str, it just made sure that the string starts with opening parentheses.

The important thing is the fact that $str is now reduced to 123) , since the first character was removed as expected. In case the matcher can not parse the string an exception is thrown:

In the example above $str remains unchanged since it could not be parsed.

Regex Matcher

This matcher parses values on the basis of a regular expression, e.g. for parsing single quoted, double quoted or number value. During instantiation, a pattern must be provided against which the string is matched:

Unlike StringMatcher , RegexMatcher returns the extracted value for further processing.

Sequential Matcher

For more complex conditions we have to implement matchers which parse multiple strings sequentially (remember the “followed by” arrows in the diagram). SequentialMatcher receives a list of matchers (which are encapsulated within the AbstractMatcher class) and checks if a string can be parsed using all these matchers in the given order:

Parallel Matcher

Like SequentialMatcher, ParallelMatcher receives a list of matchers. Unlike SequentialMatcher it checks whether one of the matchers can parse the string successfully:

This way we can implement the ValueMatcher which is able to parse single values (string, constants, numbers and arrays). In fact, this is a ParallelMatcher that includes a set of more simple matchers for every value type:

For example, if the string to parse is a constant, the ParallelMatcher would catch StringNotMatchedException from ArrayMatcher and QuotedValueMatcher (which by the way are both RegexMatchers), but it would also ignore the exception until the ConstantsMatcher returns the parsed constant without errors.

Take a look at other matcher classes and at the class AnnotationsMatcher which is the starting point for our reflection API. At the top level, the AnnotationsMatcher returns an array with the two keys:

  • name – annotation name
  • parameters – associative array with annotation parameters

You can visit the package with all annotation matchers on GitHub:

Annotation Parser

The matchers allow us to parse annotations from strings and return an array with the annotation name and its parameters.

It is time to connect this functionality to the reflection API. We start with an interface that extends the native PHP reflection API (ReflectionClass, ReflectionMethod and ReflectionProperty):

Since the annotations are stored within DocBlocks and the annotation structure is identical for classes, methods and properties, we can encapsulate the reading functionality within a separate helper object that implements ReflectionInterface methods (we call it AnnotationContainer) and integrate it into our reflection API using the delegate pattern.

This is what the extended reflection API would look like for methods:

The AnnotationContainer would read annotations using AnnotationParser:

AnnotationParser is a singleton class with a static function readAnnotations which extracts annotation from DocBlocks. This is how it works:

The function gets a target object (ReflectionClass , ReflectionMethod or ReflectionProperty). In a first step, the comment lines are extracted. Then, the parser iterates over each line and checks if the current line contains an annotation definition (without parsing it):

In the next step, the annotation name is extracted:

Note that $annotationName can be changed for further processing. Now our AnnotationsMatcher comes into play (it is already instantiated within the constructor self::$matcher = new AnnotationsMatcher()):

All annotation data is now stored within $annotationName and $parameters.

Now we are able to create an annotation (or interceptor) object which would encapsulate AOP functionality. You can see the whole class here: Since there are many things to consider while creating such objects – e.g. if the annotation is actually allowed at a particular position – we will dedicate the next post to it.

Continue with Part 3: Instantiating Annotation Objects

Image courtesy of Death To The Stock Photo

Stay in the loop

Join 1,000+ subscribers and get a new article every week