PHP中的注解有什么用处?

47

注解在PHP中有什么用处?我指的不是通用的PHPDoc。

我只是想要一个现实世界的例子或者其他什么东西。


根据@Max的回答:注解通过一行特定的PHPDoc实现了与抽象工厂相同的功能。- hopeseekr 0秒前编辑


7
你可能需要更具体一些,我猜。 - Scott M.
2
...或者至少提供一个具体的示例链接。取自您在这里的回答:http://code.google.com/p/addendum/wiki/ShortTutorialByExample - deceze
如果我问ORM有什么用处,我可能会得到无数个回答。我看了一些注解的例子,但还没完全明白。它究竟能为我们做什么呢?懒加载、动态编码调试起来也挺麻烦的。 - Theodore R. Smith
@hopeseekr 当我早些时候看到有关注释库的问题时,我也有同样的想法。似乎这是一种更慢、更麻烦的方法来处理对象属性,而不是直接使用对象属性。我真的在等待一个好的答案。 - deceze
所以它基本上是对现有(和可能是新颖的)设计模式的另一种实现方式。 - Theodore R. Smith
4个回答

62

Rob Olmos正确地解释了这个概念:

注解基本上允许您注入行为并促进解耦。

我认为这些注解在反射方面尤其有价值,因为您可以收集(额外的)关于正在检查的类/方法/属性的元数据。

另一个例子是依赖注入框架,代替ORM。例如,即将推出的FLOW3框架使用docComments/annotations来标识从DI容器创建的实例中注入哪些对象,而不是在XML配置文件中指定。

以下是一个简单的例子:

您有两个类,一个Soldier类和一个Weapon类。一个Weapon实例被注入到一个Soldier实例中。看一下这两个类的定义:

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

class Soldier {
    private $weapon;

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

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

如果您想使用这个类并手动注入所有依赖项,那么您需要像这样操作:

$weapon = new Weapon();

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

好的,这是很多样板代码(请耐心等待,我马上会解释注解有什么用)。依赖注入框架可以为您抽象创建这些组合对象并自动注入所有依赖项,您只需执行以下操作:
$soldier = Container::getInstance('Soldier');
$soldier->fight(); // ! weapon is already injected

没错,但是Container必须知道Soldier类需要哪些依赖。因此,大多数常见的框架使用XML作为配置格式。例如配置:

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

但是,FLOW3使用的不是XML,而是直接在PHP代码中使用注释来定义这些依赖关系。在FLOW3中,您的Soldier类将如下所示(仅作为示例的语法):

class Soldier {
    ...

    // ---> this

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

    ...

因此,在DI容器中,不需要使用XML标记SoldierWeapon的依赖关系。
FLOW 3还在AOP的上下文中使用这些注释,以标记应该被“编织”(在方法之前或之后注入行为)的方法。
据我所知,对于这些注释的实用性我并不确定。我不知道将这种依赖和设置隐藏在PHP代码中而不是使用单独的文件是否会使事情更容易或更糟。
例如,我曾经在Spring.NET、NHibernate以及基于XML配置文件的DI框架(PHP中不是FLOW3)上工作过,但我不能说它们太困难了。维护这些设置文件也还可以。
但也许未来与FLOW3的项目证明注释是正确的方式。

2
因此,注释通过一行专门的PHPDoc实现了与抽象工厂相同的功能。 - Theodore R. Smith
1
我选择了这个答案,因为它提供了一个具体的测试用例,展示了注解的潜在用途之一。 - Theodore R. Smith
“behavior” 的意思是传统的“四人组行为设计模式”还是指更宽泛的概念? - Snowcrash

8

它有什么好处?

注解基本上允许您注入行为并促进解耦。一个例子是Doctrine ORM。由于使用了注解,您不必继承自特定于Doctrine的类,而Propel ORM则需要。

难以调试的惰性加载动态编码?

不幸的是,这是解耦的大多数/所有操作(如设计模式、数据转换等)的副作用。

嗯,我的大脑还没有理解它。 - hopeseekr

如果您没有继承Doctrine类,则很可能必须使用其他元数据规范,例如配置文件,来指定特定属性是记录的ID。在这种情况下,它将与注释(元数据)描述的语法相去甚远。


5
为了完整起见,这里提供一个工作示例,展示如何同时使用注释以及如何扩展PHP语言来支持它们,全部在一个文件中。
这些是“真正的”注释,意味着它们声明在语言级别,而不是隐藏在注释中。使用这样的“Java”风格注释的优点是,它们无法被忽略掉注释的解析器所忽略。
__halt_compiler(); 之前的顶部部分是处理器,使用简单的方法注释扩展了PHP语言,用于缓存方法调用。
底部的类是使用 @cache 注释的示例。
(此代码最好从下往上阅读)。
<?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();

以依赖注入容器为例(与注解基本无关) ,上述方法也可用于修改类构造函数以处理注入的任何依赖项,使组件的使用完全透明。在代码评估之前修改源代码的方法大致相当于自定义Java Classloaders中的“字节码插装”(我提到Java,因为据我所知,这是注解首次引入的地方)。
这个特定示例的有用性在于,您可以将一个方法标记为需要缓存,而不必手动为每个方法编写缓存代码,从而减少了重复工作量并使代码更加清晰。此外,任何地方的任何注解效果都可以在运行时打开和关闭。

你如何使IDE支持这个? - tonix

1
phpDocumentor和现代IDE使用注释来确定方法参数类型(@param)、返回值(@return)等。
PhpUnit测试使用注释来分组测试,定义依赖关系。

这是我们使用它的唯一原因——从我们的代码库自动产生文档。 - Bryan Green

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接