开发者

How is annotation useful in PHP?

开发者 https://www.devze.com 2023-01-15 00:14 出处:网络
How is annotation useful in PHP? and I don\'t mean PHPDoc generically. I just want a real-world example or something, I guess.

How is annotation useful in PHP? and I don't mean PHPDoc generically.

I just want a real-world example or something, I guess.


So, according to @Max's answer: Annotations accomplish the same thing as Abstract Factories, only via one line of specialized P开发者_如何转开发HPDoc. – hopeseekr 0 secs ago edit


Rob Olmos explained it right:

Annotations basically let you inject behavior and can promote decoupling.

In my words I'd say that these annotations are valuable especially in context of reflection where you gather (additional) metadata about the class/method/property you are inspecting.

Another example instead of ORM: Dependency Injection frameworks. The upcoming FLOW3 framework for example uses docComments/annotations to identify which objects are injected in an instance created from a DI container instead of specifying it in an XML configuration file.

Oversimplified example following:

You have two classes, one Soldier class and a Weapon class. A Weapon instance gets injected in a Soldier instance. Look at the definition of the two classes:

class Weapon {
    public function shoot() {
        print "... shooting ...";
    }
}

class Soldier {
    private $weapon;

    public function setWeapon($weapon) {
        $this->weapon = $weapon;
    }

    public function fight() {
        $this->weapon->shoot();
    }
}

If you would use this class and inject all dependencies by hand, you´d do it like this:

$weapon = new Weapon();

$soldier = new Soldier();
$soldier->setWeapon($weapon); 
$soldier->fight();

All right, that was a lot of boilerplate code (bear with me, I am coming to explain what annotations are useful for pretty soon). What Dependency Injection frameworks can do for you is to abstract the creation such composed objects and inject all dependencies automatically, you simply do:

$soldier = Container::getInstance('Soldier');
$soldier->fight(); // ! weapon is already injected

Right, but the Container has to know which dependencies a Soldier class has. So, most of the common frameworks use XML as configuration format. Example configuration:

<class name="Soldier">
    <!-- call setWeapon, inject new Weapon instance -->
    <call method="setWeapon">
        <argument name="Weapon" />
    </call>
</class>

But what FLOW3 uses instead of XML is annotations directly in the PHP code in order to define these dependencies. In FLOW3, your Soldier class would look like this (syntax only as an example):

class Soldier {
    ...

    // ---> this

    /**
     * @inject $weapon Weapon
     */
    public function setWeapon($weapon) {
        $this->weapon = $weapon;
    }

    ...

So, no XML required to mark the dependency of Soldier to Weapon for the DI container.

FLOW 3 uses these annotations also in the context of AOP, to mark methods which should be "weaved" (means injecting behaviour before or after a method).


As far as I am concerned, I am not too sure about the usefulness about these annotations. I dont know if it makes things easier or worse "hiding" this kind of dependencies and setup in PHP code instead of using a separate file.

I worked e. g. in Spring.NET, NHibernate and with a DI framework (not FLOW3) in PHP both based on XML configuration files and cant say it was too difficult. Maintaining these setup files was ok, too.

But maybe a future project with FLOW3 proves the opposite and annotations are the real way to go.


Exactly what is it good for?

Annotations basically let you inject behavior and can promote decoupling. One example would be the Doctrine ORM. Because of the use of annotations you do not have to inherit from a Doctrine-specific class unlike the Propel ORM.

Hard to debug lazy loading dynamic coding?

Unfortunately that is a side effect like most/all actions of decoupling such as design patterns, data translations, etc.

Hmm. My brain still isn't groking it. – hopeseekr

If you didn't inherit from a Doctrine class, you'd most likely have to use some other metadata specification, like a configuration file, to specify that a particular property is the ID of the record. In that case, it would be too far removed from the syntax that the annotation (metadata) describes.


For completeness sake, here's a working example of both using annotations aswell as how to extend the PHP language to support them, all in a single file.

These are 'real' annotations, meaning, declared at the language level, and not hidden in comments. The advantage of using 'Java' style annotations like these is that they cannot be overlooked by parsers ignoring comments.

The top part, before __halt_compiler(); is the processor, extending the PHP language with a simple method annotation that caches method calls.

The class at the bottom is an example of using the @cache annotation on a method.

(this code is best read bottom-up).

<?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) ) . "</pre>";

// modify the tokens, processing annotations
$newtokens = process_tokens( $tokens );

// format the new source code
$newcode = fmt( $newtokens );
echo "<pre style='background-color:black;color:#ddd'>" . htmlentities($newcode) . "</pre>";

// execute modified code
eval($newcode);

// stop processing this php file so we can have data at the end
__halt_compiler();

class AnnotationExample {

  @cache
  private function foo( $arg = 'default' ) {
    echo "<b>(timeconsuming code)</b>";
    return $arg . ": 1";
  }

  public function __construct() {
    echo "<h1 style='color:red'>".get_class()."</h1>";
    echo $this->foo("A")."<br/>";
    echo $this->foo("A")."<br/>";
    echo $this->foo()."<br/>";
    echo $this->foo()."<br/>";
  }
}

new AnnotationExample();

Staying with the example of a DI Container (which has basically nothing whatsoever to do with annotations), the above approach can also be used to modify class constructors to take care of injecting any dependencies, which makes the use of components completely transparent. The approach of modifying the source code before it is evaluated is roughly equivalent to 'bytecode instrumentation' in custom Java Classloaders. (I mention Java since AFAIK this is where annotations were first introduced).

The usefulness of this particular example is that instead of manually having to write caching code for each method, you can simply mark a method as having to be cached, reducing the amount of repetitive work, and making the code clearer. Also, the effects of any annotation anywhere can be turned on and off at runtime.


phpDocumentor and modern IDEs use annotations to determine method parameter types (@param), return values (@return) and so on.

PhpUnit Testing use annotation to group tests, define dependencies.

0

精彩评论

暂无评论...
验证码 换一张
取 消