Skip to content

Commit c010bb9

Browse files
authored
Merge pull request #124 from wol-soft/attributes
Attribute support
2 parents 350ae8d + 32008bb commit c010bb9

File tree

11 files changed

+503
-0
lines changed

11 files changed

+503
-0
lines changed

docs/source/generator/custom/customPostProcessor.rst

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ What can you do inside your custom post processor?
2828

2929
* Add additional traits and interfaces to your models
3030
* Add additional methods and properties to your models
31+
* Add PHP attributes to the generated class or to individual properties (see `PHP Attributes`_ below)
3132
* Hook via **SchemaHooks** into the generated source code and add your snippets at defined places inside the model:
3233

3334
* Implement the **ConstructorBeforeValidationHookInterface** to add code to the beginning of your constructor
@@ -44,3 +45,61 @@ What can you do inside your custom post processor?
4445
This behaviour also applies also to properties changed via the *populate* method added by the `PopulatePostProcessor <#populatepostprocessor>`__ and the *setAdditionalProperty* method added by the `AdditionalPropertiesAccessorPostProcessor <#additionalpropertiesaccessorpostprocessor>`__
4546

4647
To execute code before/after the processing of the schemas override the methods **preProcess** and **postProcess** inside your custom post processor.
48+
49+
PHP Attributes
50+
--------------
51+
52+
You can attach PHP 8.0 `attributes <https://www.php.net/manual/en/language.attributes.overview.php>`__ to the generated class and to individual properties using **PHPModelGenerator\\Model\\PhpAttribute**.
53+
54+
**Class-level attribute** (placed before the ``class`` keyword in the generated file):
55+
56+
.. code-block:: php
57+
58+
use PHPModelGenerator\Model\PhpAttribute;
59+
use PHPModelGenerator\SchemaProcessor\PostProcessor\PostProcessor;
60+
61+
class ORM_EntityPostProcessor extends PostProcessor
62+
{
63+
public function process(Schema $schema, GeneratorConfiguration $generatorConfiguration): void
64+
{
65+
// No arguments
66+
$schema->addAttribute(new PhpAttribute(\Doctrine\ORM\Mapping\Entity::class));
67+
68+
// With positional arguments (pre-rendered PHP expression strings)
69+
$schema->addAttribute(new PhpAttribute(\Doctrine\ORM\Mapping\Table::class, ["'users'"]));
70+
71+
// With named arguments
72+
$schema->addAttribute(new PhpAttribute(
73+
\Doctrine\ORM\Mapping\Table::class,
74+
['name' => "'users'", 'schema' => "'public'"],
75+
));
76+
}
77+
}
78+
79+
**Property-level attribute** (placed before the property declaration in the generated file):
80+
81+
.. code-block:: php
82+
83+
use PHPModelGenerator\Model\PhpAttribute;
84+
use PHPModelGenerator\SchemaProcessor\PostProcessor\PostProcessor;
85+
86+
class ORM_ColumnPostProcessor extends PostProcessor
87+
{
88+
public function process(Schema $schema, GeneratorConfiguration $generatorConfiguration): void
89+
{
90+
foreach ($schema->getProperties() as $property) {
91+
$property->addAttribute(new PhpAttribute(
92+
\Doctrine\ORM\Mapping\Column::class,
93+
['name' => "'" . $property->getName() . "'"],
94+
));
95+
}
96+
}
97+
}
98+
99+
The constructor of **PhpAttribute** accepts:
100+
101+
* ``$fqcn`` — the fully-qualified class name of the attribute (e.g. ``'Doctrine\\ORM\\Mapping\\Column'``)
102+
* ``$arguments`` — an optional array of pre-rendered PHP expression strings:
103+
104+
* **Integer keys** → positional arguments (rendered as-is)
105+
* **String keys** → named arguments (rendered as ``key: value``)

src/Model/AttributesTrait.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PHPModelGenerator\Model;
6+
7+
trait AttributesTrait
8+
{
9+
/** @var PhpAttribute[] */
10+
private array $phpAttributes = [];
11+
12+
public function addAttribute(PhpAttribute $attribute): static
13+
{
14+
$this->phpAttributes[] = $attribute;
15+
16+
return $this;
17+
}
18+
19+
/**
20+
* @return PhpAttribute[]
21+
*/
22+
public function getAttributes(): array
23+
{
24+
return $this->phpAttributes;
25+
}
26+
}

src/Model/PhpAttribute.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PHPModelGenerator\Model;
6+
7+
final class PhpAttribute
8+
{
9+
/**
10+
* @param string $fqcn Fully-qualified class name of the attribute.
11+
* @param array $arguments Pre-rendered PHP expression strings.
12+
* String keys → named arguments (key: value)
13+
* Integer keys → positional arguments (value)
14+
*/
15+
public function __construct(
16+
private readonly string $fqcn,
17+
private readonly array $arguments = [],
18+
) {}
19+
20+
public function getFqcn(): string
21+
{
22+
return $this->fqcn;
23+
}
24+
25+
/**
26+
* Render the attribute body — the content that appears inside #[...].
27+
* Uses the simple (unqualified) class name; the FQCN is added via use-import separately.
28+
*/
29+
public function render(): string
30+
{
31+
$parts = explode('\\', $this->fqcn);
32+
$shortName = end($parts);
33+
34+
if (empty($this->arguments)) {
35+
return $shortName;
36+
}
37+
38+
$args = [];
39+
foreach ($this->arguments as $key => $value) {
40+
$args[] = is_string($key) ? "$key: $value" : $value;
41+
}
42+
43+
return $shortName . '(' . implode(', ', $args) . ')';
44+
}
45+
}

src/Model/Property/AbstractProperty.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
namespace PHPModelGenerator\Model\Property;
66

77
use PHPModelGenerator\Exception\SchemaException;
8+
use PHPModelGenerator\Model\AttributesTrait;
9+
use PHPModelGenerator\Model\PhpAttribute;
810
use PHPModelGenerator\Model\SchemaDefinition\JsonSchema;
911
use PHPModelGenerator\Model\SchemaDefinition\JsonSchemaTrait;
1012
use PHPModelGenerator\Utils\NormalizedName;
@@ -19,6 +21,7 @@ abstract class AbstractProperty implements PropertyInterface
1921
{
2022
use JsonSchemaTrait;
2123
use ResolvableTrait;
24+
use AttributesTrait;
2225

2326
protected string $attribute;
2427

src/Model/Property/PropertyInterface.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace PHPModelGenerator\Model\Property;
66

7+
use PHPModelGenerator\Model\PhpAttribute;
78
use PHPModelGenerator\Model\Schema;
89
use PHPModelGenerator\Model\SchemaDefinition\JsonSchema;
910
use PHPModelGenerator\Model\Validator;
@@ -145,4 +146,11 @@ public function getNestedSchema(): ?Schema;
145146
* Get the JSON schema used to set up the property
146147
*/
147148
public function getJsonSchema(): JsonSchema;
149+
150+
public function addAttribute(PhpAttribute $attribute): static;
151+
152+
/**
153+
* @return PhpAttribute[]
154+
*/
155+
public function getAttributes(): array;
148156
}

src/Model/Property/PropertyProxy.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace PHPModelGenerator\Model\Property;
66

77
use PHPModelGenerator\Exception\SchemaException;
8+
use PHPModelGenerator\Model\PhpAttribute;
89
use PHPModelGenerator\Model\SchemaDefinition\JsonSchema;
910
use PHPModelGenerator\Model\SchemaDefinition\ResolvedDefinitionsCollection;
1011
use PHPModelGenerator\Model\Schema;
@@ -247,4 +248,22 @@ public function isInternal(): bool
247248
{
248249
return $this->getProperty()->isInternal();
249250
}
251+
252+
/**
253+
* @inheritdoc
254+
*/
255+
public function addAttribute(PhpAttribute $attribute): static
256+
{
257+
$this->getProperty()->addAttribute($attribute);
258+
259+
return $this;
260+
}
261+
262+
/**
263+
* @inheritdoc
264+
*/
265+
public function getAttributes(): array
266+
{
267+
return $this->getProperty()->getAttributes();
268+
}
250269
}

src/Model/RenderJob.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use PHPModelGenerator\Exception\FileSystemException;
1010
use PHPModelGenerator\Exception\RenderException;
1111
use PHPModelGenerator\Exception\ValidationException;
12+
use PHPModelGenerator\Model\PhpAttribute;
1213
use PHPModelGenerator\Model\Validator\AbstractComposedPropertyValidator;
1314
use PHPModelGenerator\SchemaProcessor\Hook\SchemaHookResolver;
1415
use PHPModelGenerator\SchemaProcessor\PostProcessor\PostProcessor;
@@ -146,10 +147,22 @@ protected function renderClass(GeneratorConfiguration $generatorConfiguration):
146147
*/
147148
protected function getUseForSchema(GeneratorConfiguration $generatorConfiguration, string $namespace): array
148149
{
150+
$attributeFqcns = array_map(
151+
static fn(PhpAttribute $attr): string => $attr->getFqcn(),
152+
$this->schema->getAttributes(),
153+
);
154+
155+
foreach ($this->schema->getProperties() as $property) {
156+
foreach ($property->getAttributes() as $attr) {
157+
$attributeFqcns[] = $attr->getFqcn();
158+
}
159+
}
160+
149161
return RenderHelper::filterClassImports(
150162
array_unique(
151163
array_merge(
152164
$this->schema->getUsedClasses(),
165+
$attributeFqcns,
153166
$generatorConfiguration->collectErrors()
154167
? [$generatorConfiguration->getErrorRegistryClass()]
155168
: [ValidationException::class],

src/Model/Schema.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use PHPModelGenerator\Interfaces\JSONModelInterface;
88
use PHPModelGenerator\Model\Property\PropertyInterface;
9+
use PHPModelGenerator\Model\AttributesTrait;
910
use PHPModelGenerator\Model\SchemaDefinition\JsonSchema;
1011
use PHPModelGenerator\Model\SchemaDefinition\JsonSchemaTrait;
1112
use PHPModelGenerator\Model\SchemaDefinition\SchemaDefinitionDictionary;
@@ -26,6 +27,7 @@
2627
class Schema
2728
{
2829
use JsonSchemaTrait;
30+
use AttributesTrait;
2931

3032
/** @var string */
3133
protected $description;

src/Templates/Model.phptpl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,17 @@ declare(strict_types = 1);
2323
* If you need to implement something in this class use inheritance. Else you will lose your changes if the classes
2424
* are re-generated.
2525
*/
26+
{% foreach schema.getAttributes() as attribute %}
27+
#[{{ attribute.render() }}]
28+
{% endforeach %}
2629
class {{ schema.getClassName() }} {% if schema.getInterfaces() %}implements {{ viewHelper.joinClassNames(schema.getInterfaces()) }}{% endif %}
2730
{
2831
{% if schema.getTraits() %}use {{ viewHelper.joinClassNames(schema.getTraits()) }};{% endif %}
2932

3033
{% foreach schema.getProperties() as property %}
34+
{% foreach property.getAttributes() as attribute %}
35+
#[{{ attribute.render() }}]
36+
{% endforeach %}
3137
/**{% if viewHelper.getTypeHintAnnotation(property, true) %} @var {{ viewHelper.getTypeHintAnnotation(property, true) }}{% endif %}{% if property.getDescription() %} {{ property.getDescription() }}{% endif %} */
3238
{% if property.isInternal() %}private{% else %}protected{% endif %} ${{ property.getAttribute(true) }}{% if not viewHelper.isNull(property.getDefaultValue()) %} = {{ property.getDefaultValue() }}{% endif %};
3339
{% endforeach %}

0 commit comments

Comments
 (0)