-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
NEW Add AnnotationTransformer to allow configuration through PHP docblock annotations #35
Closed
ScopeyNZ
wants to merge
2
commits into
silverstripe:1
from
creative-commoners:pulls/1.2/annotation-collector
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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,215 @@ | ||||||
<?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 | ||||||
* @param array $annotationDefinitions | ||||||
*/ | ||||||
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) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
{ | ||||||
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(trim($part, '=')); | ||||||
continue; | ||||||
} | ||||||
|
||||||
list ($key, $value) = explode('=', $part, 2); | ||||||
|
||||||
$arguments[trim($key)] = trim($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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be worth a doc block to explain the purpose of classes implementing this interface |
||
{ | ||
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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could annotate
AnnotationDefinitionInterface[]
here to make it clearer what is expected. Also worth adding a@throws
for the exception if one of the entries isn't one of these interfaces