-
Notifications
You must be signed in to change notification settings - Fork 0
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
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
e061b42
BUPH-21 | add more Annotation Processor documentation
jpmarcotte e9a96a8
BUPH-21 | add page for General Purpose Annotation Processors
jpmarcotte a023611
BUPH-21 | clarify newline requirements in Annotation tags
jpmarcotte 20da600
BUPH-21 | clarify newline in annotations
jpmarcotte 2da3f62
BUPH-83 | add Symfony Expression AP
jpmarcotte fa69e4b
Merge pull request #33 from neighborhoods/BUPH-83-symfony-expression-ap
jpmarcotte ca900d9
Merge branch 'master' into BUPH-21-ap-docs
jpmarcotte d39d0f3
BUPH-21 | add test for multiple annotations in a file
jpmarcotte 1d9ab5f
BUPH-21 | clarify newline requirements in Annotation Tags
jpmarcotte 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
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,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 | ||
<?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 |
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,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 |
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,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() | ||
] | ||
); | ||
} | ||
} |
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 @@ | ||
fab/* |
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.
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:
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.
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.
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?
plus
produced
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.
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
/**
afterTEST_STRING = "
to the last*/
afterThisWillProbablyDisappearToo
.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.
Huh, just re-read this, and that's very interesting. I'll see if I can replicate with some tests.
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.
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.