Skip to content

Commit

Permalink
Fix for issue doctrine#81 Traits Composed from Traits
Browse files Browse the repository at this point in the history
Use statements are now also imported for properties
and methods from traits in traits. Conflict resolution
and aliases are respected.

For backwards compatibility reasons the use statements
from the declaring class are still merged. PHP itself 
does not show this behaviour regarding to imported types
in traits.
  • Loading branch information
Hidde Boomsma committed Oct 27, 2016
1 parent e1dd28b commit 2dc9b9b
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 25 deletions.
58 changes: 38 additions & 20 deletions lib/Doctrine/Common/Annotations/AnnotationReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -344,22 +344,15 @@ private function getClassImports(ReflectionClass $class)
private function getMethodImports(ReflectionMethod $method)
{
$class = $method->getDeclaringClass();
$classImports = $this->getClassImports($class);
if (!method_exists($class, 'getTraits')) {
return $classImports;
}
$classImports = $this->getClassImports($class);

$traitImports = array();
// Since there is no way to know where a method originates
// when conflict resolution for traits has been used we fall
// back to using the filename form the \ReflectionMethod
// directly.
$methodImports = $this->phpParser->parseMethod($method);

foreach ($class->getTraits() as $trait) {
if ($trait->hasMethod($method->getName())
&& $trait->getFileName() === $method->getFileName()
) {
$traitImports = array_merge($traitImports, $this->phpParser->parseClass($trait));
}
}

return array_merge($classImports, $traitImports);
return array_merge($classImports, $methodImports);
}

/**
Expand All @@ -377,15 +370,40 @@ private function getPropertyImports(ReflectionProperty $property)
return $classImports;
}

$traitImports = array();
$traitImports = $this->getDefiningTraitImports($property, $class);

return array_merge($classImports, $traitImports);
}

foreach ($class->getTraits() as $trait) {
if ($trait->hasProperty($property->getName())) {
$traitImports = array_merge($traitImports, $this->phpParser->parseClass($trait));
/**
* Perform depth first search for trait defining the property.
* Return the imports from the defining trait.
*
* @param ReflectionProperty $property
* @param ReflectionClass $classOrTrait
*
* @return array
*/
private function getDefiningTraitImports(ReflectionProperty $property, ReflectionClass $classOrTrait)
{
$traitImports = [];

foreach ($classOrTrait->getTraits() as $trait)
{
// Depth first.
if ($traitImports === []) {
$traitImports = $this->getDefiningTraitImports($property, $trait);
} else {
// exit early if we found our leaf definition of the property.
break;
}
}

return array_merge($classImports, $traitImports);
// Do not add additional imports when going back up the recursion tree.
if ($traitImports === [] && $trait->hasProperty($property->getName())) {
return $this->phpParser->parseClass($trait);
}
}
return $traitImports;
}

/**
Expand Down
41 changes: 36 additions & 5 deletions lib/Doctrine/Common/Annotations/PhpParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,50 @@ public function parseClass(\ReflectionClass $class)
return array();
}

$content = $this->getFileContent($filename, $class->getStartLine());
return $this->parse($filename, $class->getStartLine(), $class->getNamespaceName());
}

/**
* Parses a method.
*
* @param \Reflectionmethod $method A <code>ReflectionMethod</code> object.
*
* @return array A list with use statements in the form (Alias => FQN).
*/
public function parseMethod(\ReflectionMethod $method)
{
if (method_exists($method, 'getUseStatements')) {
return $method->getUseStatements();
}

if (false === $filename = $method->getFilename()) {
return array();
}

return $this->parse($filename, $method->getStartLine(), $method->getNamespaceName());
}

/**
* Parse use statements from file.
*
* @param $filename
* @param $lineNumber
* @param $namespace
*
* @return array
*/
private function parse($filename, $lineNumber, $namespace)
{
$content = $this->getFileContent($filename, $lineNumber);

if (null === $content) {
return array();
}

$namespace = preg_quote($class->getNamespaceName());
$content = preg_replace('/^.*?(\bnamespace\s+' . $namespace . '\s*[;{].*)$/s', '\\1', $content);
$tokenizer = new TokenParser('<?php ' . $content);

$statements = $tokenizer->parseUseStatements($class->getNamespaceName());

return $statements;
return $tokenizer->parseUseStatements($namespace);
}

/**
Expand Down
31 changes: 31 additions & 0 deletions tests/Doctrine/Tests/Common/Annotations/AnnotationReaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\DocParser;
use Doctrine\Tests\Common\Annotations\Fixtures\Annotation\Autoload;
use Doctrine\Tests\Common\Annotations\Fixtures\Annotation\Version;
use Doctrine\Tests\Common\Annotations\Fixtures\IgnoredNamespaces\AnnotatedAtClassLevel;
use Doctrine\Tests\Common\Annotations\Fixtures\IgnoredNamespaces\AnnotatedAtMethodLevel;
use Doctrine\Tests\Common\Annotations\Fixtures\IgnoredNamespaces\AnnotatedAtPropertyLevel;
Expand Down Expand Up @@ -52,6 +54,35 @@ public function testPropertyAnnotationFromTrait()
$this->assertInstanceOf('Doctrine\Tests\Common\Annotations\Fixtures\Annotation\Autoload', $annotations[0]);
}

public function testPropertyAnnotationFromTraitThatUsesAnotherTrait()
{
$reader = $this->getReader();
$ref =
new \ReflectionClass('Doctrine\Tests\Common\Annotations\Fixtures\ClassThatUsesTraitThatUsesAnotherTrait');

$annotations = $reader->getPropertyAnnotations($ref->getProperty('route'));
$this->assertInstanceOf(Version::class, $annotations[0]);

$annotations = $reader->getPropertyAnnotations($ref->getProperty('intermediate'));
$this->assertInstanceOf(Autoload::class, $annotations[0]);
}

public function testMethodAnnotationFromTraitThatUsesAnotherTrait()
{
$reader = $this->getReader();
$ref =
new \ReflectionClass('Doctrine\Tests\Common\Annotations\Fixtures\ClassThatUsesTraitThatUsesAnotherTrait');

$annotations = $reader->getMethodAnnotations($ref->getMethod('secretAction'));
$this->assertInstanceOf('Doctrine\Tests\Common\Annotations\Fixtures\Annotation\Route', $annotations[0]);

$annotations = $reader->getMethodAnnotations($ref->getMethod('conflict'));
$this->assertInstanceOf('Doctrine\Tests\Common\Annotations\Fixtures\Annotation\Template', $annotations[0]);

$annotations = $reader->getMethodAnnotations($ref->getMethod('noConflict'));
$this->assertInstanceOf('Doctrine\Tests\Common\Annotations\Fixtures\Annotation\Autoload', $annotations[0]);
}

public function testOmitNotRegisteredAnnotation()
{
$parser = new DocParser();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Doctrine\Tests\Common\Annotations\Fixtures\Traits;

use Doctrine\Tests\Common\Annotations\Fixtures\Annotation\Template;

trait ConflictTraitA
{
/**
* @Template
*/
public function conflict(){}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Doctrine\Tests\Common\Annotations\Fixtures\Traits;

use Doctrine\Tests\Common\Annotations\Fixtures\Annotation\Route;

trait ConflictTraitB
{
/**
* @Route
*/
public function conflict(){}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Doctrine\Tests\Common\Annotations\Fixtures\Traits;

use Doctrine\Tests\Common\Annotations\Fixtures\Annotation\Autoload;

trait ConflictTraitC
{
/**
* @Autoload
*/
public function conflict(){}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@

use Doctrine\Tests\Common\Annotations\Fixtures\Annotation\Template;
use Doctrine\Tests\Common\Annotations\Fixtures\Annotation\Route;
use Doctrine\Tests\Common\Annotations\Fixtures\Annotation\Version as Property;

trait SecretRouteTrait
{
/**
* @Property
*/
private $route;

/**
* @Route("/secret", name="_secret")
* @Template()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,20 @@

namespace Doctrine\Tests\Common\Annotations\Fixtures\Traits;

use Doctrine\Tests\Common\Annotations\Fixtures\Annotation\Autoload as In;

trait TraitThatUsesAnotherTrait
{
use EmptyTrait;
use SecretRouteTrait;
use ConflictTraitA, ConflictTraitB, ConflictTraitC {
ConflictTraitA::conflict insteadof ConflictTraitB;
ConflictTraitA::conflict insteadOf ConflictTraitC;
ConflictTraitC::conflict as protected noConflict;
}

/**
* @In
*/
private $intermediate;
}

0 comments on commit 2dc9b9b

Please sign in to comment.