Для полноты картины приведем рабочий пример использования аннотаций, а также того, как расширить язык PHP для их поддержки, все в одном файле.
Это «настоящие» аннотации, то есть объявленные на уровне языка и не скрытые в комментариях. Преимущество использования таких аннотаций в стиле «Java» заключается в том, что они не могут быть пропущены парсерами, игнорирующими комментарии.
Верхняя часть, перед __halt_compiler();
- это процессор, расширяющий язык PHP простой аннотацией метода, которая кэширует вызовы метода.
Класс внизу является примером использования аннотации @cache
для метода.
(этот код лучше всего читать снизу вверх).
<code><?php
// parser states
const S_MODIFIER = 0; // public, protected, private, static, abstract, final
const S_FUNCTION = 1; // function name
const S_SIGSTART = 2; // (
const S_SIGEND = 3; // )
const S_BODYSTART = 4; // {
const S_BODY = 5; // ...}
function scan_method($tokens, $i)
{
$state = S_MODIFIER;
$depth = 0; # {}
$funcstart = $i;
$fnameidx;
$funcbodystart;
$funcbodyend;
$sig_start;
$sig_end;
$argnames=array();
$i--;
while ( ++$i < count($tokens) )
{
$tok = $tokens[$i];
if ( $tok[0] == T_WHITESPACE )
continue;
switch ( $state )
{
case S_MODIFIER:
switch ( $tok[0] )
{
case T_PUBLIC:
case T_PRIVATE:
case T_PROTECTED:
case T_STATIC:
case T_FINAL:
case T_ABSTRACT: # todo: handle body-less functions below
break;
case T_FUNCTION:
$state=S_FUNCTION;
break;
default:
return false;
}
break;
case S_FUNCTION:
$fname = $tok[1];
$fnameidx = $i;
$state = S_SIGSTART;
break;
case S_SIGSTART:
if ( $tok[1]=='(' )
{
$sig_start = $i;
$state = S_SIGEND;
}
else return false;
case S_SIGEND:
if ( $tok[1]==')' )
{
$sig_end = $i;
$state = S_BODYSTART;
}
else if ( $tok[0] == T_VARIABLE )
$argnames[]=$tok[1];
break;
case S_BODYSTART:
if ( $tok[1] == '{' )
{
$funcbodystart = $i;
$state = S_BODY;
}
else return false;
#break; # fallthrough: inc depth
case S_BODY:
if ( $tok[1] == '{' ) $depth++;
else if ( $tok[1] == '}' )
if ( --$depth == 0 )
return (object) array(
'body_start' => $funcbodystart,
'body_end' => $i,
'func_start' => $funcstart,
'fnameidx' => $fnameidx,
'fname' => $fname,
'argnames' => $argnames,
'sig_start' => $sig_start,
'sig_end' => $sig_end,
);
break;
default: die("error - unknown state $state");
}
}
return false;
}
function fmt( $tokens ) {
return implode('', array_map( function($v){return $v[1];}, $tokens ) );
}
function process_annotation_cache( $tokens, $i, $skip, $mi, &$instructions )
{
// prepare some strings
$args = join( ', ', $mi->argnames );
$sig = fmt( array_slice( $tokens, $mi->sig_start, $mi->sig_end - $mi->sig_start ) );
$origf = fmt( array_slice( $tokens, $mi->func_start, $mi->body_start - $mi->func_start ) );
// inject an instruction to rename the cached function
$instructions[] = array(
'action' => 'replace',
'trigger' => $i,
'arg' => $mi->sig_end -$i -1,
'tokens' => array( array( "STR", "private function __cached_fn_$mi->fname$sig" ) )
);
// inject an instruction to insert the caching replacement function
$instructions[] = array(
'action' => 'inject',
'trigger' => $mi->body_end + 1,
'tokens' => array( array( "STR", "
$origf
{
static \$cache = array();
\$key = join('#', func_get_args() );
return isset( \$cache[\$key] ) ? \$cache[\$key]: \$cache[\$key] = \$this->__cached_fn_$mi->fname( $args );
}
" ) ) );
}
function process_tokens( $tokens )
{
$newtokens=array();
$skip=0;
$instructions=array();
foreach ( $tokens as $i=>$t )
{
// check for annotation
if ( $t[1] == '@'
&& $tokens[$i+1][0]==T_STRING // annotation name
&& $tokens[$i+2][0]==T_WHITESPACE
&& false !== ( $methodinfo = scan_method($tokens, $i+3) )
)
{
$skip=3; // skip '@', name, whitespace
$ann_method = 'process_annotation_'.$tokens[$i+1][1];
if ( function_exists( $ann_method ) )
$ann_method( $tokens, $i, $skip, $methodinfo, $instructions );
# else warn about unknown annotation
}
// process instructions to modify the code
if ( !empty( $instructions ) )
if ( $instructions[0]['trigger'] == $i ) // the token index to trigger at
{
$instr = array_shift( $instructions );
switch ( $instr['action'] )
{
case 'replace': $skip = $instr['arg']; # fallthrough
case 'inject': $newtokens=array_merge( $newtokens, $instr['tokens'] );
break;
default:
echo "<code style='color:red'>unknown instruction '{$instr[1]}'</code>";
}
}
if ( $skip ) $skip--;
else $newtokens[]=$t;
}
return $newtokens;
}
// main functionality
$data = file_get_contents( __FILE__, null, null, __COMPILER_HALT_OFFSET__ );
$tokens = array_slice( token_get_all("<"."?php ". $data), 1 );
// make all tokens arrays for easier processing
$tokens = array_map( function($v) { return is_string($v) ? array("STR",$v) : $v;}, $tokens );
echo "<pre style='background-color:black;color:#ddd'>" . htmlentities( fmt($tokens) ) . "
";
// модифицируем токены, обрабатываем аннотации
$ newtokens = process_tokens ($ tokens);
// форматировать новый исходный код
$ newcode = fmt ($ newtokens);
echo "
". htmlentities ($ newcode). "
";
// выполнить модифицированный код
Eval ($ newcode);
// прекращаем обработку этого php-файла, чтобы мы могли получить данные в конце
__HALT_COMPILER ();
class AnnotationExample {
@cache
приватная функция foo ($ arg = 'default') {
echo "
(трудоемкий код) ";
вернуть $ arg. «: 1»;
}
публичная функция __construct () {
echo "
". get_class (). "
";
echo $ this-> foo ("A"). "
";
echo $ this-> foo ("A"). "
";
echo $ this-> foo (). "
";
echo $ this-> foo (). "
";
}
}
новый AnnotationExample ();
Оставаясь на примере DI-контейнера (который практически не имеет ничего общего с аннотациями), описанный выше подход также можно использовать для изменения конструкторов классов, чтобы позаботиться о внедрении любых зависимостей, что делает использование компонентов полностью прозрачным ,
Подход изменения исходного кода до его оценки примерно эквивалентен «инструментарию байт-кода» в пользовательских загрузчиках классов Java. (Я упоминаю Java начиная с AFAIK, именно здесь впервые были представлены аннотации).
Полезность этого конкретного примера заключается в том, что вместо того, чтобы вручную писать код кэширования для каждого метода, вы можете просто пометить метод как необходимый для кэширования, уменьшив объем повторяющихся операций и сделав код более понятным. Кроме того, эффекты любой аннотации в любом месте можно включать и выключать во время выполнения.