Skip to content
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

BUPH-21 | add more Annotation Processor documentation #32

Merged
merged 9 commits into from
Dec 9, 2019
77 changes: 2 additions & 75 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
# ...
<PrimaryActorName>/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'
<PrimaryActorName>/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)
Expand Down
185 changes: 185 additions & 0 deletions docs/AnnotationProcessors.md
Original file line number Diff line number Diff line change
@@ -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`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be worth being more explicit here that if there are no Default Contents, the end of comment identifier (*/) must still be on a new line.
Eg:
Correct:

/** @neighborhoods-buphalo:annotation-processor Builder.setters
*/

Incorrect:

/** @neighborhoods-buphalo:annotation-processor Builder.setters */

While this line technically covers this case, (as I mentioned in my rescinded approval) I have lost an afternoon to this problem because the incorrect example works with one AP in a template, but once you add a second AP, things break.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that was the case, but I just tested it and got it to work if I didn't include Default Contents. It may have been fixed since then?

    public const TEST_STRING = "/** @neighborhoods-buphalo:annotation-processor TestSimpleStringAnnotationProcessor */";

plus

      TestSimpleStringAnnotationProcessor:
        processor_fqcn: \Neighborhoods\Buphalo\V1\AnnotationProcessors\SimpleString
        static_context_record:
          string: 'this is a string'

produced

    public const TEST_STRING = "this is a string";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem actually comes up when you have more than 1 annotation processor in a file. Assuming Buphalo uses that same regex matching as Bradfab, I believe the below example will break. The first AP will replace everything from the first /** after TEST_STRING = " to the last */ after ThisWillProbablyDisappearToo.

    public const TEST_STRING = "/** @neighborhoods-buphalo:annotation-processor TestSimpleStringAnnotationProcessor */";

// This will probably disappear

/** @neighborhoods-buphalo:annotation-processor ThisWillProbablyDisappearToo */

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because the incorrect example works with one AP in a template, but once you add a second AP, things break.

Huh, just re-read this, and that's very interesting. I'll see if I can replicate with some tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @jakemalachowski

I've been able to validate this with tests. Created BUPH-92 | Single-Line Annotation Tags Don't Work Well With Others and #38 for us to address.

- 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
<?php

namespace Neighborhoods\BuphaloTemplateTree;

class Builder {
/** @var array */
public $record;

public function build(): PrimaryActorNameInterface
{
$PrimaryActorName = new PrimaryActorName();

/** @neighborhoods-buphalo:annotation-processor Builder.setters
// TODO: Build the object
throw new \LogicException('Unimplemented Build Method');
*/

return $PrimaryActorName;
}
}
```

### Fabrication File Definitions
For any actor in the fabrication file, you can specify a number of Annotation Processors to use,
keyed by the `AnnotationProcessorKey` from the Annotation in the template.
Each Annotation Processor entry includes the following:
- `processor_fqcn`: **required** A string to the Fully Qualified Class Name of the Annotation Processor class
- `static_context_record`: *optional* An object or array that the Annotation Processor has access to

#### Example Fabrication File
```yaml
actors:
<PrimaryActorName>/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
<?php

namespace Neighborhoods\Buphalo\V1\AnnotationProcessors\PrimaryActorName;

use Neighborhoods\Buphalo\V1;

class Builder implements V1\AnnotationProcessorInterface
{
use V1\AnnotationProcessor\Context\AwareTrait {
getAnnotationProcessorContext as public;
}

public function getReplacement() : string
{
$context = $this->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
<?php

namespace ReplacedNamespace;

class Builder {
/** @var array */
public $record;

public function build(): FooInterface
{
$Foo = new Foo();

$Foo->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:
<PrimaryActorName>/Builder.php:
template: PrimaryActorName/Builder.php
```

Buphalo will generate the following instead (Note that the annotation was replaced with its default contents)
```php
<?php

namespace ReplacedNamespace;

class Builder {
/** @var array */
public $record;

public function build(): FooInterface
{
$Foo = new Foo();

// TODO: Build the object
throw new \LogicException('Unimplemented Build Method');

return $Foo;
}
}
```

### General Purpose Annotation Processors
For more exmaples of Annotation Processors and for processors you can use out-of-the-box,
see [General Purpose Annotation Processors][GPAP]

### Known Issues
- Each Annotation Processor can only be used once.
- `getAnnotationProcessorContext` is required to be public
- Newlines MUST be present after the `AnnotationProcessorKey` even with no `Default Contents`

## References
- [General Purpose Annotation Processors][GPAP]
- [PHP Annotations][Annotations]
- [RFC 2119: Keywords](https://tools.ietf.org/html/rfc2119)

[Annotations]: https://php-annotations.readthedocs.io/en/latest/UsingAnnotations.html
[GPAP]: GeneralPurposeAnnotationProcessors.md
91 changes: 91 additions & 0 deletions docs/GeneralPurposeAnnotationProcessors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# General Purpose Annotation Processors
Buphalo provides a number of out-of-the box General Purpose Annotation Processors.

Each General Purpose Annotation Processor listed below has a number of similar qualities:
- Description: A brief description of the Annotation Processor's behavior
- FQCN: The fully qualified class name to use in the `processor_fqcn` field in the [fabrication file][Fabrication File]
- Contract: The expected format of the `static_context_record` field in the [fabrication file][Fabrication File]
- Example: An example template with annotation, fabrication file snippet, and the value after replacement

## Simple String Replacement

**Description:** Replaces the annotation with a specified `string`

**FQCN:** `\Neighborhoods\Buphalo\V1\AnnotationProcessors\SimpleString`

**Contract:** A single object with a `string` key.

**Example:**
```php
/** @neighborhoods-buphalo:annotation-processor annotation1
*/
```
```yaml
annotation1:
processor_fqcn: \Neighborhoods\Buphalo\V1\AnnotationProcessors\SimpleString
static_context_record:
string: 'this is a string'
```
```
this is a string
```

## Empty String Replacement

**Description:** Removes the annotation. Useful for when you do not want to include the Default Contents

**FQCN:** `\Neighborhoods\Buphalo\V1\AnnotationProcessors\EmptyString`

**Contract:** None

**Example:**
```php
Before
/** @neighborhoods-buphalo:annotation-processor annotation1
Default Content
*/
After
```
```yaml
annotation1:
processor_fqcn: \Neighborhoods\Buphalo\V1\AnnotationProcessors\EmptyString
```
```php
Before

After
```

## Symfony Expression Language
**Description:** Uses [Symfony Expression Language][Symfony EL] to generate the replacement.
Includes access to the `AnnotationProcessorContext` under the `context` alias.

**FQCN:** `\Neighborhoods\Buphalo\V1\AnnotationProcessors\SymfonyExpression`

**Contract:** A single object with an `expression` key.
May include other keys used by the expression.
Other keys can be accessed via `context.getStaticContextRecord()["key"]`

**Example:**
```php
/** @neighborhoods-buphalo:annotation-processor annotation1
*/
```
```yaml
# Expression.buphalo.v1.fabrication.yml
annotation1:
processor_fqcn: \Neighborhoods\Buphalo\V1\AnnotationProcessors\SymfonyExpression
static_context_record:
expression: 'context.getFabricationFile().getFileName() ~ " " ~ context.getStaticContextRecord()["word"]'
word: 'Language'
```
```php
Expression Language
```

## References
- [Annotation Processors][Annotation Processors]

[Annotation Processors]: AnnotationProcessors.md
[Fabrication File]: AnnotationProcessors.md#fabrication-file-definitions
[Symfony EL]: https://symfony.com/doc/current/components/expression_language.html
29 changes: 29 additions & 0 deletions src/V1/AnnotationProcessors/SymfonyExpression.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);

namespace Neighborhoods\Buphalo\V1\AnnotationProcessors;

use Neighborhoods\Buphalo\V1;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;

/** @noinspection PhpSuperClassIncompatibleWithInterfaceInspection Because PHPStorm doesn't register the alias*/
class SymfonyExpression implements V1\AnnotationProcessorInterface
{
use V1\AnnotationProcessor\Context\AwareTrait {
getAnnotationProcessorContext as public;
}

public function getReplacement(): string
{
$context = $this->getAnnotationProcessorContext()->getStaticContextRecord();


$expressionLanguage = new ExpressionLanguage();
return (string) $expressionLanguage->evaluate(
$context['expression'],
[
'context' => $this->getAnnotationProcessorContext()
]
);
}
}
1 change: 1 addition & 0 deletions tests/v1/MultipleAPs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fab/*
Loading