Skip to content

Commit

Permalink
Merge pull request #186 from kitsunet/gerrit-34903
Browse files Browse the repository at this point in the history
FEATURE: Trait introduction via AOP

This allows to introduce traits in generated proxy classes
via AOP. You can use the Introduce annotation with the
argument traitName to introdue the given trait into
generated proxy classes matching the pointcut.

For example::

@flow\Introduce(
traitName="Vendor\Package\Service\Traits\ExampleTrait"
pointcutExpression="class(TYPO3\Flow\Mvc\Controller\ActionController)"
)

Would introduce the trait \Vendor\Package\Service\Traits\ExampleTrait
into all proxies that either are the class or extend the class
TYPO3\Flow\Mvc\Controller\ActionController.

Resolves: FLOW-123
  • Loading branch information
kitsunet committed Apr 17, 2016
2 parents 2a4a963 + c670191 commit 1d80d2d
Show file tree
Hide file tree
Showing 7 changed files with 253 additions and 10 deletions.
10 changes: 10 additions & 0 deletions TYPO3.Flow/Classes/TYPO3/Flow/Annotations/Introduce.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ final class Introduce
*/
public $interfaceName;

/**
* The trait name to introduce
*
* @var string
*/
public $traitName;

/**
* @param array $values
* @throws \InvalidArgumentException
Expand All @@ -46,5 +53,8 @@ public function __construct(array $values)
if (isset($values['interfaceName'])) {
$this->interfaceName = $values['interfaceName'];
}
if (isset($values['traitName'])) {
$this->traitName = $values['traitName'];
}
}
}
43 changes: 37 additions & 6 deletions TYPO3.Flow/Classes/TYPO3/Flow/Aop/AspectContainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ class AspectContainer
*/
protected $propertyIntroductions = array();

/**
* An array of \TYPO3\Flow\Aop\TraitIntroduction objects
*
* @var array
*/
protected $traitIntroductions = array();

/**
* An array of explicitly declared \TYPO3\Flow\Pointcut objects
* @var array
Expand Down Expand Up @@ -120,6 +127,16 @@ public function getPropertyIntroductions()
return $this->propertyIntroductions;
}

/**
* Returns the trait introductions which were defined in the aspect
*
* @return array Array of \TYPO3\Flow\Aop\TraitIntroduction objects
*/
public function getTraitIntroductions()
{
return $this->traitIntroductions;
}

/**
* Returns the pointcuts which were declared in the aspect. This
* does not contain the pointcuts which were made out of the pointcut
Expand All @@ -135,36 +152,47 @@ public function getPointcuts()
/**
* Adds an advisor to this aspect container
*
* @param \TYPO3\Flow\Aop\Advisor $advisor The advisor to add
* @param Advisor $advisor The advisor to add
* @return void
*/
public function addAdvisor(\TYPO3\Flow\Aop\Advisor $advisor)
public function addAdvisor(Advisor $advisor)
{
$this->advisors[] = $advisor;
}

/**
* Adds an introduction declaration to this aspect container
*
* @param \TYPO3\Flow\Aop\InterfaceIntroduction $introduction
* @param InterfaceIntroduction $introduction
* @return void
*/
public function addInterfaceIntroduction(\TYPO3\Flow\Aop\InterfaceIntroduction $introduction)
public function addInterfaceIntroduction(InterfaceIntroduction $introduction)
{
$this->interfaceIntroductions[] = $introduction;
}

/**
* Adds an introduction declaration to this aspect container
*
* @param \TYPO3\Flow\Aop\PropertyIntroduction $introduction
* @param PropertyIntroduction $introduction
* @return void
*/
public function addPropertyIntroduction(\TYPO3\Flow\Aop\PropertyIntroduction $introduction)
public function addPropertyIntroduction(PropertyIntroduction $introduction)
{
$this->propertyIntroductions[] = $introduction;
}

/**
* Adds an introduction declaration to this aspect container
*
* @param TraitIntroduction $introduction
* @return void
*/
public function addTraitIntroduction(TraitIntroduction $introduction)
{
$this->traitIntroductions[] = $introduction;
}

/**
* Adds a pointcut (from a pointcut declaration) to this aspect container
*
Expand Down Expand Up @@ -194,6 +222,9 @@ public function reduceTargetClassNames(Builder\ClassNameIndex $classNameIndex)
foreach ($this->propertyIntroductions as $propertyIntroduction) {
$result->applyUnion($propertyIntroduction->getPointcut()->reduceTargetClassNames($classNameIndex));
}
foreach ($this->traitIntroductions as $traitIntroduction) {
$result->applyUnion($traitIntroduction->getPointcut()->reduceTargetClassNames($classNameIndex));
}
$this->cachedTargetClassNameCandidates = $result;
return $result;
}
Expand Down
49 changes: 45 additions & 4 deletions TYPO3.Flow/Classes/TYPO3/Flow/Aop/Builder/ProxyClassBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
*/

use TYPO3\Flow\Annotations as Flow;
use TYPO3\Flow\Aop\AspectContainer;
use TYPO3\Flow\Aop\PropertyIntroduction;
use TYPO3\Flow\Reflection\ClassReflection;
use TYPO3\Flow\Reflection\PropertyReflection;
use TYPO3\Flow\Aop\TraitIntroduction;

/**
* The main class of the AOP (Aspect Oriented Programming) framework.
Expand Down Expand Up @@ -360,13 +362,21 @@ protected function buildAspectContainer($aspectClassName)
}
$introduceAnnotation = $this->reflectionService->getClassAnnotation($aspectClassName, \TYPO3\Flow\Annotations\Introduce::class);
if ($introduceAnnotation !== null) {
if ($introduceAnnotation->interfaceName === null) {
throw new \TYPO3\Flow\Aop\Exception('The interface introduction in class "' . $aspectClassName . '" does not contain the required interface name).', 1172694761);
if ($introduceAnnotation->interfaceName === null && $introduceAnnotation->traitName === null) {
throw new \TYPO3\Flow\Aop\Exception('The introduction in class "' . $aspectClassName . '" does neither contain an interface name nor a trait name, at least one is required.', 1172694761);
}
$pointcutFilterComposite = $this->pointcutExpressionParser->parse($introduceAnnotation->pointcutExpression, $this->renderSourceHint($aspectClassName, $introduceAnnotation->interfaceName, \TYPO3\Flow\Annotations\Introduce::class));
$pointcut = new \TYPO3\Flow\Aop\Pointcut\Pointcut($introduceAnnotation->pointcutExpression, $pointcutFilterComposite, $aspectClassName);
$introduction = new \TYPO3\Flow\Aop\InterfaceIntroduction($aspectClassName, $introduceAnnotation->interfaceName, $pointcut);
$aspectContainer->addInterfaceIntroduction($introduction);

if ($introduceAnnotation->interfaceName !== null) {
$introduction = new \TYPO3\Flow\Aop\InterfaceIntroduction($aspectClassName, $introduceAnnotation->interfaceName, $pointcut);
$aspectContainer->addInterfaceIntroduction($introduction);
}

if ($introduceAnnotation->traitName !== null) {
$introduction = new \TYPO3\Flow\Aop\TraitIntroduction($aspectClassName, $introduceAnnotation->traitName, $pointcut);
$aspectContainer->addTraitIntroduction($introduction);
}
}

foreach ($this->reflectionService->getClassPropertyNames($aspectClassName) as $propertyName) {
Expand All @@ -381,6 +391,7 @@ protected function buildAspectContainer($aspectClassName)
if (count($aspectContainer->getAdvisors()) < 1 &&
count($aspectContainer->getPointcuts()) < 1 &&
count($aspectContainer->getInterfaceIntroductions()) < 1 &&
count($aspectContainer->getTraitIntroductions()) < 1 &&
count($aspectContainer->getPropertyIntroductions()) < 1) {
throw new \TYPO3\Flow\Aop\Exception('The class "' . $aspectClassName . '" is tagged to be an aspect but doesn\'t contain advices nor pointcut or introduction declarations.', 1169124534);
}
Expand All @@ -398,6 +409,7 @@ public function buildProxyClass($targetClassName, array &$aspectContainers)
{
$interfaceIntroductions = $this->getMatchingInterfaceIntroductions($aspectContainers, $targetClassName);
$introducedInterfaces = $this->getInterfaceNamesFromIntroductions($interfaceIntroductions);
$introducedTraits = $this->getMatchingTraitNamesFromIntroductions($aspectContainers, $targetClassName);

$propertyIntroductions = $this->getMatchingPropertyIntroductions($aspectContainers, $targetClassName);

Expand All @@ -418,6 +430,7 @@ public function buildProxyClass($targetClassName, array &$aspectContainers)
}

$proxyClass->addInterfaces($introducedInterfaces);
$proxyClass->addTraits($introducedTraits);

/** @var $propertyIntroduction PropertyIntroduction */
foreach ($propertyIntroductions as $propertyIntroduction) {
Expand Down Expand Up @@ -663,6 +676,34 @@ protected function getMatchingPropertyIntroductions(array &$aspectContainers, $t
return $introductions;
}

/**
* Traverses all aspect containers and returns an array of trait
* introductions which match the target class.
*
* @param array &$aspectContainers All aspects to take into consideration
* @param string $targetClassName Name of the class the pointcut should match with
* @return array array of trait names
*/
protected function getMatchingTraitNamesFromIntroductions(array &$aspectContainers, $targetClassName)
{
$introductions = [];
/** @var AspectContainer $aspectContainer */
foreach ($aspectContainers as $aspectContainer) {
if (!$aspectContainer->getCachedTargetClassNameCandidates()->hasClassName($targetClassName)) {
continue;
}
/** @var TraitIntroduction $introduction */
foreach ($aspectContainer->getTraitIntroductions() as $introduction) {
$pointcut = $introduction->getPointcut();
if ($pointcut->matches($targetClassName, null, null, uniqid())) {
$introductions[] = '\\' . $introduction->getTraitName();
}
}
}

return $introductions;
}

/**
* Returns an array of interface names introduced by the given introductions
*
Expand Down
82 changes: 82 additions & 0 deletions TYPO3.Flow/Classes/TYPO3/Flow/Aop/TraitIntroduction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php
namespace TYPO3\Flow\Aop;

/*
* This file is part of the TYPO3.Flow package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/


/**
* Implementation of the trait introduction declaration.
*
*/
class TraitIntroduction
{
/**
* Name of the aspect declaring this introduction
* @var string
*/
protected $declaringAspectClassName;

/**
* Name of the introduced trait
* @var string
*/
protected $traitName;

/**
* The pointcut this introduction applies to
* @var \TYPO3\Flow\Aop\Pointcut\Pointcut
*/
protected $pointcut;

/**
* Constructor
*
* @param string $declaringAspectClassName Name of the aspect containing the declaration for this introduction
* @param string $traitName Name of the trait to introduce
* @param \TYPO3\Flow\Aop\Pointcut\Pointcut $pointcut The pointcut for this introduction
*/
public function __construct($declaringAspectClassName, $traitName, \TYPO3\Flow\Aop\Pointcut\Pointcut $pointcut)
{
$this->declaringAspectClassName = $declaringAspectClassName;
$this->traitName = $traitName;
$this->pointcut = $pointcut;
}

/**
* Returns the name of the introduced trait
*
* @return string Name of the introduced trait
*/
public function getTraitName()
{
return $this->traitName;
}

/**
* Returns the pointcut this introduction applies to
*
* @return \TYPO3\Flow\Aop\Pointcut\Pointcut The pointcut
*/
public function getPointcut()
{
return $this->pointcut;
}

/**
* Returns the object name of the aspect which declared this introduction
*
* @return string The aspect object name
*/
public function getDeclaringAspectClassName()
{
return $this->declaringAspectClassName;
}
}
34 changes: 34 additions & 0 deletions TYPO3.Flow/Tests/Functional/Aop/Fixtures/Introduced01Trait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php
namespace TYPO3\Flow\Tests\Functional\Aop\Fixtures;

/*
* This file is part of the TYPO3.Flow package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

/**
* A trait which is introduced into TargetClass01
*/
trait Introduced01Trait
{
/**
* @return string
*/
public function sayHello()
{
return 'Hello from trait';
}

/**
* @return string
*/
public function introducedTraitMethod()
{
return 'I\'m the traitor';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php
namespace TYPO3\Flow\Tests\Functional\Aop\Fixtures;

/*
* This file is part of the TYPO3.Flow package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use TYPO3\Flow\Annotations as Flow;

/**
* An aspect for testing trait introduction
*
* @Flow\Introduce("class(TYPO3\Flow\Tests\Functional\Aop\Fixtures\TargetClass01)", traitName="TYPO3\Flow\Tests\Functional\Aop\Fixtures\Introduced01Trait")
* @Flow\Aspect
*/
class TraitIntroductionTestingAspect
{
}
21 changes: 21 additions & 0 deletions TYPO3.Flow/Tests/Functional/Aop/FrameworkTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,27 @@ public function interfaceWithMethodCanBeIntroduced()
$this->assertTrue(method_exists($targetClass, 'introducedMethodWithArguments'));
}

/**
* @test
*/
public function traitWithNewMethodCanBeIntroduced()
{
$targetClass = new Fixtures\TargetClass01();

$this->assertEquals('I\'m the traitor', call_user_func(array($targetClass, 'introducedTraitMethod')));
}

/**
* @test
*/
public function introducedTraitMethodWontOverrideExistingMethods()
{
$targetClass = new Fixtures\TargetClass01();

$this->assertNotEquals('Hello from trait', $targetClass->sayHello());
$this->assertEquals('Hello World', $targetClass->sayHello());
}

/**
* Public and protected properties can be introduced.
*
Expand Down

0 comments on commit 1d80d2d

Please sign in to comment.