diff --git a/README.md b/README.md index ccaa73b..21d70ad 100644 --- a/README.md +++ b/README.md @@ -225,82 +225,9 @@ Neighborhoods_Buphalo_V1_TemplateTree_Map_Builder_FactoryInterface__TemplateTree * In order to be efficient, Buphalo will only fabricate files that do not exist in `src` since anything in `src` will override what exists in `fab`. ### Annotation Processors -* Annotation Processors allow user space to define dynamic template content before tokenization or compilation of the template. -* Annotation Processors are optional. -* Providing static context to the Annotation Processor is optional. -* If the `static_context_record` key is provided, it MUST resolve to a PHP `array`. -* Default annotation replacement is accomplished by using the contents of the annotation. -* Annotation Processors MUST implement `\Neighborhoods\Buphalo\V1\AnnotationProcessorInterface`. -* Annotation Processors are not shared services. -```php -namespace Neighborhoods\Buphalo\V1; +Annotation Processors (APs) are optional configuration tools that allow user space to define dynamic template content. -use Neighborhoods\Buphalo\V1\AnnotationProcessor\ContextInterface; - -interface AnnotationProcessorInterface -{ - public function setAnnotationProcessorContext(ContextInterface $Context); - - public function getAnnotationProcessorContext(): ContextInterface; - - public function getReplacement(): string; -} -``` -* Currently, annotation processors have access to the static context, the annotation contents, and the Fabrication File by accessing the injected `\Neighborhoods\Buphalo\V1\AnnotationProcessor\ContextInterface` object. - -### Example Annotation Processors -* Annotation Processor Tag: `@neighborhoods-buphalo:annotation-processor` -* Annotation Processor Keys: - * `Neighborhoods\BuphaloTemplateTree\PrimaryActorName\Builder.build1` - * `Neighborhoods\BuphaloTemplateTree\PrimaryActorName\Builder.build2` -* The keys above are named according to a collision avoidance convention. However, there is no requirement on the key name except for uniqueness. -```php -// template-tree/V1/PrimaryActorName/Builder.php - public function build(): PrimaryActorNameInterface - { - $PrimaryActorName = $this->getPrimaryActorNameFactory()->create(); - /** @neighborhoods-buphalo:annotation-processor Neighborhoods\BuphaloTemplateTree\PrimaryActorName\Builder.build1 - */ - /** @neighborhoods-buphalo:annotation-processor Neighborhoods\BuphaloTemplateTree\PrimaryActorName\Builder.build2 - // @TODO - build the object. - throw new \LogicException('Unimplemented build method.'); - */ - - return $PrimaryActorName; - } -``` -```yml -# src/V2/Toe.fabrication.yml -actors: -# ... - /Builder.php: - template: PrimaryActorName/Builder.php - annotation_processors: - Neighborhoods\BuphaloTemplateTree\PrimaryActorName\Builder.build1: - processor_fqcn: \VENDOR\PRODUCT\AnAnnotationProcessor - Neighborhoods\BuphaloTemplateTree\PrimaryActorName\Builder.build2: - processor_fqcn: \VENDOR\PRODUCT\AnotherAnnotationProcessor - static_context_record: - head: 'shoulders' - knees: 'toes' - /Builder.service.yml: - template: PrimaryActorName/Builder.service.yml -# ... -``` -* If no annotation processors are defined then `\Neighborhoods\Buphalo\V1\AnnotationProcessor` is used and the above compiles as -```php -// src/V2/Toe/Builder.php - public function build(): ToeInterface - { - $Toe = $this->getToeFactory()->create(); - - - // @TODO - build the object. - throw new \LogicException('Unimplemented build method.'); - - return $Toe; - } -``` +See [AnnotationProcessors](docs/AnnotationProcessors.md) for more information. ## References * [Symfony Finder Component](https://symfony.com/doc/current/components/finder.html) diff --git a/docs/AnnotationProcessors.md b/docs/AnnotationProcessors.md new file mode 100644 index 0000000..3a93550 --- /dev/null +++ b/docs/AnnotationProcessors.md @@ -0,0 +1,185 @@ +# Annotation Processors +Annotation Processors (APs) are optional configuration tools that allow user space to define dynamic template content. + +## How Annotation Processors Work +When a template includes an appropriate [annotation][Annotations] (a special comment with a particular tag), +Buphalo will replace that comment with the results of the specified Annotation Processor. +If no Annotation Processor is specified, Buphalo uses a default AP that replaces the annotation with the contents of +the comment (sans annotation tag) + +### Template Annotations +The annotations in a template use the following form with two variables: +```php +/** @neighborhoods-buphalo:annotation-processor AnnotationProcessorKey +Default Contents +*/ +``` +- `AnnotationProcessorKey`: Also used in the fabrication file to tie the annotation to the specific AP. +- `Default Contents`: If no AnnotationProcessor is specified in the fabrication file, this is used instead +- There MUST be a newline between the `AnnotationProcessorKey` and `Default Contents` + - If no Default Contents exist, there SHOULD be a newline after the `AnnotationProcessorKey`. + - If more than one annotation is used in a template, there MUST be a newline after the `AnnotationProcessorKey` + +#### Example Template +```php +/Builder.php: + template: PrimaryActorName/Builder.php + annotation_processors: + Builder.setters: # The AnnotationProcessorKey + processor_fqcn: \Neighborhoods\Buphalo\V1\AnnotationProcessors\Actor\Builder + static_context_record: + properties: + - record_key: 'bar' + set_method: 'setBar' + - record_Key: 'baz' + set_method: 'setBaz' +``` + + +### Annotation Processors +Each Annotation Processor MUST `implement \Neighborhoods\Buphalo\V1\AnnotationProcessorInterface`. +This will require implementing the following methods: +```php +public function setAnnotationProcessorContext(ContextInterface $Context); +public function getAnnotationProcessorContext(): ContextInterface; +public function getReplacement(): string; +``` +- `setAnnotationProcessorContext`: Used to inform the Annotation Processor of the context that it can use. + - The `ContextInterface` includes a `getStaticContextRecord()` method +- `getAnnotationProcessorContext`: Currently required to set the Default Contents +- `getReplacement`: Returns the text that will replace the annotation in the template + + +#### Example Annotation Processor +```php +getAnnotationProcessorContext()->getStaticContextRecord(); + $calls = []; + + foreach ($context['properties'] as $property) { + $calls[] = sprintf( + ' $PrimaryActorName->%s($this->record[\'%s\']);', + $property['set_method'], + $property['record_key'] + ); + } + + return implode(PHP_EOL, $calls); + } +} +``` + +### Putting it all together +If the above examples are used in `Foo.buphalo.v1.fabrication.yml`, +Buphalo will generate the following to `Foo/Builder.php`: +```php +setBar($this->record['bar']); + $Foo->setBaz($this->record['baz']); + + return $Foo; + } +} +``` + +If The `Builder.setters` Annotation Processor is not specified in `Foo.buphalo.v1.fabrication.yml`, _e.g._ +```yaml +actors: + /Builder.php: + template: PrimaryActorName/Builder.php +``` + +Buphalo will generate the following instead (Note that the annotation was replaced with its default contents) +```php +getAnnotationProcessorContext()->getStaticContextRecord(); + + + $expressionLanguage = new ExpressionLanguage(); + return (string) $expressionLanguage->evaluate( + $context['expression'], + [ + 'context' => $this->getAnnotationProcessorContext() + ] + ); + } +} diff --git a/tests/v1/MultipleAPs/.gitignore b/tests/v1/MultipleAPs/.gitignore new file mode 100644 index 0000000..44f14e1 --- /dev/null +++ b/tests/v1/MultipleAPs/.gitignore @@ -0,0 +1 @@ +fab/* diff --git a/tests/v1/MultipleAPs/control/MultipleAnnotations.php b/tests/v1/MultipleAPs/control/MultipleAnnotations.php new file mode 100644 index 0000000..f43505e --- /dev/null +++ b/tests/v1/MultipleAPs/control/MultipleAnnotations.php @@ -0,0 +1,11 @@ +.php: + template: PrimaryActorName.php + annotation_processors: + FirstAnnotation: + processor_fqcn: \Neighborhoods\Buphalo\V1\AnnotationProcessors\EmptyString + SecondAnnotation: + processor_fqcn: \Neighborhoods\Buphalo\V1\AnnotationProcessors\SimpleString + static_context_record: + string: "public const SECOND = 'second';" diff --git a/tests/v1/MultipleAPs/templates/PrimaryActorName.php b/tests/v1/MultipleAPs/templates/PrimaryActorName.php new file mode 100644 index 0000000..8ce0870 --- /dev/null +++ b/tests/v1/MultipleAPs/templates/PrimaryActorName.php @@ -0,0 +1,13 @@ +.php: + template: PrimaryActorName.php + annotation_processors: + TestSymfonyExpressionAnnotationProcessor: + processor_fqcn: \Neighborhoods\Buphalo\V1\AnnotationProcessors\SymfonyExpression + static_context_record: + expression: 'context.getFabricationFile().getFileName() ~ " " ~ context.getStaticContextRecord()["word"]' + word: 'Language' diff --git a/tests/v1/SymfonyExpressionAP/templates/PrimaryActorName.php b/tests/v1/SymfonyExpressionAP/templates/PrimaryActorName.php new file mode 100644 index 0000000..94da6c8 --- /dev/null +++ b/tests/v1/SymfonyExpressionAP/templates/PrimaryActorName.php @@ -0,0 +1,9 @@ +