-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
NEW Add AnnotationTransformer to allow configuration through PHP docb…
…lock annotations
- Loading branch information
Showing
12 changed files
with
537 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
language: php | ||
|
||
php: 5.6 | ||
php: 7.1 | ||
|
||
before_script: | ||
- composer validate | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,214 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace SilverStripe\Config\Transformer; | ||
|
||
use InvalidArgumentException; | ||
use ReflectionClass; | ||
use SilverStripe\Config\Collections\MutableConfigCollectionInterface; | ||
use SilverStripe\Config\Transformer\AnnotationTransformer\AnnotationDefinitionInterface; | ||
|
||
class AnnotationTransformer implements TransformerInterface | ||
{ | ||
/** | ||
* Set a callable that can be used to resolve the classes to collect annotations from | ||
* | ||
* @var callable | ||
*/ | ||
protected $classResolver; | ||
|
||
/** | ||
* An array of @see AnnotationDefinitionInterface that indicate what annotations are collected from doc blocks and | ||
* how those annotations are converted to config | ||
* | ||
* @var AnnotationDefinitionInterface[] | ||
*/ | ||
protected $annotationDefinitions; | ||
|
||
/** | ||
* The list of resolved class names once the resolver has been called | ||
* | ||
* @var array|null | ||
*/ | ||
protected $classes; | ||
|
||
/** | ||
* AnnotationTransformer constructor. | ||
* @param callable $classResolver | ||
*/ | ||
public function __construct(callable $classResolver, array $annotationDefinitions) | ||
{ | ||
$this->classResolver = $classResolver; | ||
|
||
foreach ($annotationDefinitions as $annotationDefinition) { | ||
if (!$annotationDefinition instanceof AnnotationDefinitionInterface) { | ||
throw new InvalidArgumentException(sprintf( | ||
'Annotation definitions provided to %s must implement %s', | ||
__CLASS__, | ||
AnnotationDefinitionInterface::class | ||
)); | ||
} | ||
} | ||
|
||
$this->annotationDefinitions = $annotationDefinitions; | ||
} | ||
|
||
/** | ||
* This is responsible for parsing a single yaml file and returning it into a format | ||
* that Config can understand. Config will then be responsible for turning thie | ||
* output into the final merged config. | ||
* | ||
* @param MutableConfigCollectionInterface $collection | ||
* @return MutableConfigCollectionInterface | ||
* @throws \ReflectionException | ||
*/ | ||
public function transform(MutableConfigCollectionInterface $collection) | ||
{ | ||
if (empty($this->annotationDefinitions)) { | ||
return $collection; | ||
} | ||
|
||
foreach ($this->getClasses() as $className) { | ||
// Skip classes that don't exist | ||
if (!class_exists($className)) { | ||
continue; | ||
} | ||
|
||
$config = []; | ||
|
||
foreach ($this->annotationDefinitions as $definition) { | ||
// Check if this class should be affected at all | ||
if (!$definition->shouldCollect($className)) { | ||
continue; | ||
} | ||
|
||
$classReflector = new ReflectionClass($className); | ||
$scopes = $definition->defineCollectionScopes(); | ||
|
||
$config = []; | ||
|
||
// Collect from class docblocks | ||
if ( | ||
$scopes & AnnotationDefinitionInterface::COLLECT_CLASS | ||
&& ($doc = $classReflector->getDocComment()) | ||
) { | ||
$config = $this->augmentConfigForBlock( | ||
$config, | ||
$doc, | ||
$definition, | ||
AnnotationDefinitionInterface::COLLECT_CLASS | ||
); | ||
} | ||
|
||
// Collect from constructors separately to other methods | ||
if ( | ||
$scopes & AnnotationDefinitionInterface::COLLECT_CONSTRUCTOR | ||
&& ($constructor = $classReflector->getConstructor()) | ||
&& ($doc = $constructor->getDocComment()) | ||
) { | ||
$config = array_merge($config, $this->augmentConfigForBlock( | ||
$config, | ||
$doc, | ||
$definition, | ||
AnnotationDefinitionInterface::COLLECT_CONSTRUCTOR | ||
)); | ||
} | ||
|
||
// Collect from methods | ||
if ($scopes & AnnotationDefinitionInterface::COLLECT_METHODS) { | ||
foreach ($classReflector->getMethods() as $method) { | ||
if ( | ||
$method->isConstructor() | ||
|| !$definition->shouldCollectFromMethod($classReflector, $method) | ||
|| !($docBlock = $method->getDocComment()) | ||
) { | ||
continue; | ||
} | ||
|
||
$config = array_merge($config, $this->augmentConfigForBlock( | ||
$config, | ||
$docBlock, | ||
$definition, | ||
AnnotationDefinitionInterface::COLLECT_METHODS, | ||
$method->getName() | ||
)); | ||
} | ||
} | ||
} | ||
|
||
// Add the config to the collection | ||
foreach ($config as $name => $item) { | ||
$collection->set($className, $name, $item); | ||
} | ||
} | ||
|
||
return $collection; | ||
} | ||
|
||
/** | ||
* Returns the list of classnames - executing the class resolver for them if it has not yet been called | ||
* | ||
* @return array | ||
*/ | ||
protected function getClasses(): array | ||
{ | ||
if (!$this->classes) { | ||
$this->classes = call_user_func($this->classResolver) ?: []; | ||
} | ||
|
||
return $this->classes; | ||
} | ||
|
||
/** | ||
* Parse a string of arguments added to an annotation (eg. @Annotation(something=1,b)) into an array | ||
* | ||
* @param string $arguments | ||
* @return array | ||
*/ | ||
protected function parseAnnotationArguments(string $arguments): array | ||
{ | ||
if (empty($arguments)) { | ||
return []; | ||
} | ||
|
||
$parts = explode(',', $arguments); | ||
|
||
$arguments = []; | ||
foreach ($parts as $part) { | ||
if (!strpos($part, '=')) { | ||
// Trim in the case of `=arg` (0th index `=`) | ||
$arguments[] = trim($part, '='); | ||
continue; | ||
} | ||
|
||
list ($key, $value) = explode('=', $part, 2); | ||
|
||
$arguments[$key] = $value; | ||
} | ||
|
||
return $arguments; | ||
} | ||
|
||
protected function augmentConfigForBlock( | ||
array $config, | ||
string $docBlock, | ||
AnnotationDefinitionInterface $definition, | ||
int $context, | ||
?string $contextDetail = null | ||
): array { | ||
$annotationMatches = implode('|', array_map('preg_quote', $definition->getAnnotationStrings())); | ||
$pattern = '/^\s*\*\s*@(' . $annotationMatches . ')(?:\(([^)]+)\))?\s*$/m'; | ||
|
||
$matchCount = preg_match_all($pattern, $docBlock, $matches); | ||
|
||
for ($i = 0; $i < $matchCount; $i++) { | ||
$config = array_merge_recursive($config, $definition->createConfigForAnnotation( | ||
$matches[1][$i], | ||
$this->parseAnnotationArguments($matches[2][$i]), | ||
$context, | ||
$contextDetail | ||
)); | ||
} | ||
|
||
return $config; | ||
} | ||
} |
60 changes: 60 additions & 0 deletions
60
src/Transformer/AnnotationTransformer/AnnotationDefinitionInterface.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace SilverStripe\Config\Transformer\AnnotationTransformer; | ||
|
||
use ReflectionClass; | ||
use ReflectionMethod; | ||
|
||
interface AnnotationDefinitionInterface | ||
{ | ||
const COLLECT_CLASS = 1; | ||
const COLLECT_CONSTRUCTOR = 2; | ||
const COLLECT_METHODS = 4; | ||
|
||
/** | ||
* Return a bitwise integer combining COLLECT_* constants indicating what doc blocks to collect annotations from | ||
* | ||
* @return int | ||
*/ | ||
public function defineCollectionScopes(): int; | ||
|
||
/** | ||
* Indicates whether annotations should be collected from the given class | ||
* | ||
* @param string $className | ||
* @return bool | ||
*/ | ||
public function shouldCollect(string $className): bool; | ||
|
||
/** | ||
* Indicates whether annotations should be collected from the given method within the given class | ||
* | ||
* @param ReflectionClass $class | ||
* @param ReflectionMethod $method | ||
* @return bool | ||
*/ | ||
public function shouldCollectFromMethod(ReflectionClass $class, ReflectionMethod $method): bool; | ||
|
||
/** | ||
* Get an array of annotations to look for. For example 'Foo' would indicate that '@Foo' should be matched | ||
* | ||
* @return array | ||
*/ | ||
public function getAnnotationStrings(): array; | ||
|
||
/** | ||
* Create config from a matched annotation. | ||
* | ||
* @param string $annotation The annotation string that was matched (defined in @see getAnnotationStrings) | ||
* @param array $arguments An array of strings that were passed as arguments (eg. @Foo(argument1,argument2) | ||
* @param int $context A COLLECT_* constant that indicates what context this annotation was found in | ||
* @param string|null $contextDetail A method name, provided the context of the annotation was a method | ||
* @return array | ||
*/ | ||
public function createConfigForAnnotation( | ||
string $annotation, | ||
array $arguments, | ||
int $context, | ||
?string $contextDetail = null | ||
): array; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<?php | ||
|
||
namespace SilverStripe\Config\Tests\AnnotationTransformer; | ||
|
||
/** | ||
* @package FooPackage | ||
* @Foo | ||
* @Bar(123) | ||
*/ | ||
class TestClass | ||
{ | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace SilverStripe\Config\Tests\AnnotationTransformer; | ||
|
||
class TestClassConstructor | ||
{ | ||
/** | ||
* @ignore some fake annotation | ||
* @Foo | ||
* @Bar(123) | ||
*/ | ||
public function __construct() | ||
{ | ||
} | ||
} |
Oops, something went wrong.