Docblock annotations parser for the Phalcon Framework, in pure PHP.
Phalcon v6 reads annotations as native PHP attributes through
reflection, so the framework no longer has an annotation language. This package
is for the code that has not moved yet. It parses the classic
/** @Route("/users") */ docblock syntax, so a v4/v5 application can run on v6
without rewriting every annotation into an attribute first.
It is a behavioural port of the annotations parser in cphalcon, which is written in C with a re2c scanner and a lemon grammar. The output is identical: the same nested-array AST, the same integer type codes, the same syntax-error strings.
There is no C extension to build, and no runtime dependency beyond
phalcon/traits.
- PHP
>= 8.1 < 9.0 ext-mbstringext-apcu, only for the APCu adapter
composer require phalcon/annotationsuse Phalcon\Annotations\Docblock\Reader;
$parsed = Reader::parseDocBlock('/** @Route("/users", methods={"GET"}) */');parseDocBlock() returns the raw AST, or false when the docblock contains no
annotations at all.
use Phalcon\Annotations\Docblock\Adapter\Memory;
$annotations = new Memory();
$reflection = $annotations->get(MyController::class);
$route = $annotations->getMethod(MyController::class, 'indexAction')->get('Route');
$route->getArgument(0); // "/users"
$route->getNamedArgument('methods'); // ["GET"]The adapter caches the parsed Reflection per class, so a class is parsed once
regardless of how many annotations are read from it. Memory, Stream and
Apcu adapters are provided; they share the base class, so the backend is
interchangeable.
Classes live under Phalcon\Annotations\Docblock\, not Phalcon\Annotations\.
Phalcon v6 already occupies the latter with its attribute-based component, and
both packages are installed side by side during a migration.
This package is also self-contained rather than plugging into v6's annotations
service, because that service builds its collections from ReflectionAttribute
objects, which a docblock parser does not have. Use
Phalcon\Annotations\Docblock\Adapter\* where a v5 application used
Phalcon\Annotations\Adapter\*.
The parser reproduces cphalcon's behaviour, including the surprising parts:
- Type codes are the output format.
300annotation,301integer,302double,303string,304null,305false,306true,307identifier,308array. - Numbers stay strings.
@Foo(1)resolves to"1", not1. - Strings are not unescaped.
"a\"b"keeps the backslash; only the surrounding quotes are removed. - Optional AST keys are omitted, never null. An annotation with no arguments
has no
argumentskey at all. null,trueandfalseare case-insensitive, butnullableis still an identifier.{...}and[...]both build an array, and neither has an empty form:@Foo({})is a syntax error.- Nesting fails closed past roughly 100 levels, matching lemon's fixed stack.
false, not an empty array, comes back from a docblock with nothing in it.
composer install
composer test-unit # PHPUnit
composer analyze # PHPStan, level max, no baseline
composer cs # PHP_CodeSniffer, PSR-12
composer cs-fixer-fix # php-cs-fixertests/differential.php runs a corpus of docblocks through both this package and
Phalcon\Annotations\Reader from ext-phalcon 5.x, and reports any difference in
the AST or in the thrown message. It needs the extension loaded, and exits
quietly when it is absent:
php tests/differential.phptests/generate-golden.php records the expected AST for every fixture in
tests/fixtures. Run it with ext-phalcon loaded and the goldens come from the C
parser, which is what makes FixturesTest a cross-implementation check rather
than a snapshot of this package's own behaviour.
volt and phql were ported out of cphalcon by transliterating the generated
artefacts: the re2c DFA became a PHP state machine, the lemon tables became PHP
arrays. That is the right trade for their grammars, which are 159 and 171
productions with full operator-precedence expression rules.
The annotations grammar is 25 productions over 17 terminals with no operator
precedence at all: expr is a flat nine-way alternation, and the only
recursion is structural nesting. It is LL(1) apart from one token of lookahead in
argument_item. Parse tables exist to resolve ambiguity and precedence, and
there is neither here. lemon's driver template alone would be more code than the
whole parser, so the 25 productions are written out directly, one method per
nonterminal, like Zephir's pure-PHP parser.