Skip to content

Commit 2a389a7

Browse files
committed
enum documentation, enum name normalization
1 parent 826ff77 commit 2a389a7

File tree

9 files changed

+216
-42
lines changed

9 files changed

+216
-42
lines changed

docs/source/complexTypes/enum.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ Enum
33

44
Enums can be used to define a set of constant values a property must accept.
55

6+
.. hint::
7+
8+
If you define constraints via `enum` you may want to use the `EnumPostProcessor <../generator/postProcessor.html#enumpostprocessor>`__ to generate PHP enums.
9+
610
.. code-block:: json
711
812
{

docs/source/conf.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@
2020
# -- Project information -----------------------------------------------------
2121

2222
project = u'php-json-schema-model-generator'
23-
copyright = u'2020, Enno Woortmann'
23+
copyright = u'2023, Enno Woortmann'
2424
author = u'Enno Woortmann'
2525

2626
# The short X.Y version
27-
version = u'0.19'
27+
version = u'0.24'
2828
# The full version, including alpha/beta/rc tags
29-
release = u'0.19.0'
29+
release = u'0.24.0'
3030

3131

3232
# -- General configuration ---------------------------------------------------

docs/source/generator/postProcessor.rst

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,102 @@ The added method **getPatternProperties** can be used to fetch a list of all pro
204204
205205
.. note::
206206

207-
If you want to add or remove pattern properties to your object after the object instantiation you can use the `AdditionalPropertiesAccessorPostProcessor <generator/postProcessor.html#additionalpropertiesaccessorpostprocessor>`__ or the `PopulatePostProcessor <generator/postProcessor.html#populatepostprocessor>`__
207+
If you want to modify your object by adding or removing pattern properties after the object instantiation you can use the `AdditionalPropertiesAccessorPostProcessor <postProcessor.html#additionalpropertiesaccessorpostprocessor>`__ or the `PopulatePostProcessor <postProcessor.html#populatepostprocessor>`__
208+
209+
EnumPostProcessor
210+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
211+
212+
.. warning::
213+
214+
Requires at least PHP 8.1
215+
216+
.. code-block:: php
217+
218+
$generator = new ModelGenerator();
219+
$generator->addPostProcessor(new EnumPostProcessor(__DIR__ . '/generated/enum/', '\\MyApp\\Enum'));
220+
221+
The **EnumPostProcessor** generates a `PHP enum <https://www.php.net/manual/en/language.enumerations.basics.php>`_ for each `enum <../complexTypes/enum.html>`__ found in the processed schemas.
222+
Enums which contain only integer values or only string values will be rendered into a `backed enum <https://www.php.net/manual/en/language.enumerations.backed.php>`_.
223+
Other enums will provide the following interface similar to the capabilities of a backed enum:
224+
225+
.. code-block:: php
226+
227+
public static function from(mixed $value): self;
228+
public static function tryFrom(mixed $value): ?self;
229+
230+
public function value(): mixed;
231+
232+
Let's have a look at the most simple case of a string-only enum:
233+
234+
.. code-block:: json
235+
236+
{
237+
"$id": "offer",
238+
"type": "object",
239+
"properties": {
240+
"state": {
241+
"enum": ["open", "sold", "cancelled"]
242+
}
243+
}
244+
}
245+
246+
The provided schema will generate the following enum:
247+
248+
.. code-block:: php
249+
250+
enum OfferState: string {
251+
case Open = 'open';
252+
case Sold = 'sold';
253+
case Cancelled = 'cancelled';
254+
}
255+
256+
The type hints and annotations of the generated class will be changed to match the generated enum:
257+
258+
.. code-block:: php
259+
260+
/**
261+
* @param OfferState|string|null $state
262+
*/
263+
public function setState($state): self;
264+
public function getState(): ?OfferState;
265+
266+
Mapping
267+
~~~~~~~
268+
269+
Each enum which is not a string-only enum must provide a mapping in the **enum-map** property, for example an integer-only enum:
270+
271+
.. code-block:: json
272+
273+
{
274+
"$id": "offer",
275+
"type": "object",
276+
"properties": {
277+
"state": {
278+
"enum": [0, 1, 2],
279+
"enum-map": {
280+
"open": 0,
281+
"sold": 1,
282+
"cancelled": 2
283+
}
284+
}
285+
}
286+
}
287+
288+
The provided schema will generate the following enum:
289+
290+
.. code-block:: php
291+
292+
enum OfferState: int {
293+
case Open = 0;
294+
case Sold = 1;
295+
case Cancelled = 2;
296+
}
297+
298+
If an enum which requires a mapping is found a **SchemaException** will be thrown.
299+
300+
.. note::
301+
302+
By enabling the *$skipNonMappedEnums* option of the **EnumPostProcessor** you can skip enums which require a mapping but don't provide a mapping. Those enums will provide the default `enum <../complexTypes/enum.html>`__ behaviour.
208303

209304
Custom Post Processors
210305
----------------------

src/Model/Property/AbstractProperty.php

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PHPModelGenerator\Exception\SchemaException;
88
use PHPModelGenerator\Model\SchemaDefinition\JsonSchema;
99
use PHPModelGenerator\Model\SchemaDefinition\JsonSchemaTrait;
10+
use PHPModelGenerator\Utils\NormalizedName;
1011
use PHPModelGenerator\Utils\ResolvableTrait;
1112

1213
/**
@@ -70,33 +71,6 @@ public function getAttribute(bool $variableName = false): string
7071
*/
7172
protected function processAttributeName(string $name): string
7273
{
73-
$attributeName = preg_replace_callback(
74-
'/([a-z][a-z0-9]*)([A-Z])/',
75-
static function (array $matches): string {
76-
return "{$matches[1]}-{$matches[2]}";
77-
},
78-
$name
79-
);
80-
81-
$elements = array_map(
82-
static function (string $element): string {
83-
return ucfirst(strtolower($element));
84-
},
85-
preg_split('/[^a-z0-9]/i', $attributeName)
86-
);
87-
88-
$attributeName = lcfirst(join('', $elements));
89-
90-
if (empty($attributeName)) {
91-
throw new SchemaException(
92-
sprintf(
93-
"Property name '%s' results in an empty attribute name in file %s",
94-
$name,
95-
$this->jsonSchema->getFile()
96-
)
97-
);
98-
}
99-
100-
return $attributeName;
74+
return lcfirst(NormalizedName::from($name, $this->jsonSchema));
10175
}
10276
}

src/Model/Property/Property.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public function getType(bool $outputType = false): ?PropertyType
9292
public function setType(
9393
PropertyType $type = null,
9494
PropertyType $outputType = null,
95-
$reset = false
95+
bool $reset = false
9696
): PropertyInterface {
9797
if ($reset) {
9898
$this->typeHintDecorators = [];

src/SchemaProcessor/PostProcessor/EnumPostProcessor.php

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
use PHPModelGenerator\Model\Property\PropertyInterface;
1313
use PHPModelGenerator\Model\Property\PropertyType;
1414
use PHPModelGenerator\Model\Schema;
15+
use PHPModelGenerator\Model\SchemaDefinition\JsonSchema;
1516
use PHPModelGenerator\Model\Validator;
1617
use PHPModelGenerator\Model\Validator\EnumValidator;
1718
use PHPModelGenerator\Model\Validator\FilterValidator;
1819
use PHPModelGenerator\ModelGenerator;
1920
use PHPModelGenerator\PropertyProcessor\Filter\FilterProcessor;
2021
use PHPModelGenerator\Utils\ArrayHash;
22+
use PHPModelGenerator\Utils\NormalizedName;
2123

2224
/**
2325
* Generates a PHP enum for enums from JSON schemas which are automatically mapped for properties holding the enum
@@ -82,7 +84,13 @@ public function process(Schema $schema, GeneratorConfiguration $generatorConfigu
8284
if (!isset($this->generatedEnums[$enumSignature])) {
8385
$this->generatedEnums[$enumSignature] = [
8486
'name' => $enumName,
85-
'fqcn' => $this->renderEnum($generatorConfiguration, $enumName, $values, $json['enum-map'] ?? null),
87+
'fqcn' => $this->renderEnum(
88+
$generatorConfiguration,
89+
$schema->getJsonSchema(),
90+
$enumName,
91+
$values,
92+
$json['enum-map'] ?? null
93+
),
8694
];
8795
} else {
8896
if ($generatorConfiguration->isOutputEnabled()) {
@@ -207,14 +215,21 @@ static function ($item): string {
207215

208216
private function renderEnum(
209217
GeneratorConfiguration $generatorConfiguration,
218+
JsonSchema $jsonSchema,
210219
string $name,
211220
array $values,
212221
?array $map
213222
): string {
214223
$cases = [];
215224

216225
foreach ($values as $value) {
217-
$cases[ucfirst($map ? array_search($value, $map, true) : $value)] = var_export($value, true);
226+
$caseName = ucfirst(NormalizedName::from($map ? array_search($value, $map, true) : $value, $jsonSchema));
227+
228+
if (preg_match('/^\d/', $caseName) === 1) {
229+
$caseName = "_$caseName";
230+
}
231+
232+
$cases[$caseName] = var_export($value, true);
218233
}
219234

220235
$backedType = null;

src/Utils/NormalizedName.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PHPModelGenerator\Utils;
6+
7+
use PHPModelGenerator\Exception\SchemaException;
8+
use PHPModelGenerator\Model\SchemaDefinition\JsonSchema;
9+
10+
class NormalizedName
11+
{
12+
public static function from(string $name, JsonSchema $jsonSchema): string
13+
{
14+
$attributeName = preg_replace_callback(
15+
'/([a-z][a-z0-9]*)([A-Z])/',
16+
static function (array $matches): string {
17+
return "{$matches[1]}-{$matches[2]}";
18+
},
19+
$name
20+
);
21+
22+
$elements = array_map(
23+
static function (string $element): string {
24+
return ucfirst(strtolower($element));
25+
},
26+
preg_split('/[^a-z0-9]/i', $attributeName)
27+
);
28+
29+
$attributeName = join('', $elements);
30+
31+
if (empty($attributeName)) {
32+
throw new SchemaException(
33+
sprintf(
34+
"Name '%s' results in an empty name in file %s",
35+
$name,
36+
$jsonSchema->getFile()
37+
)
38+
);
39+
}
40+
41+
return $attributeName;
42+
}
43+
}

tests/Basic/BasicSchemaGenerationTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ public function testPropertyNamesAreNormalized(): void
289289
public function testEmptyNormalizedPropertyNameThrowsAnException(): void
290290
{
291291
$this->expectException(SchemaException::class);
292-
$this->expectExceptionMessage("Property name '__ -- __' results in an empty attribute name");
292+
$this->expectExceptionMessage("Name '__ -- __' results in an empty name");
293293

294294
$this->generateClassFromFile('EmptyNameNormalization.json');
295295
}

tests/PostProcessor/EnumPostProcessorTest.php

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ public function testMappedStringOnlyEnum(): void
117117

118118
$className = $this->generateClassFromFileTemplate(
119119
'EnumPropertyMapped.json',
120-
['["Hans", "Dieter"]', '{"CEO": "Hans", "CTO": "Dieter"}'],
120+
['["Hans", "Dieter"]', '{"Ceo": "Hans", "Cto": "Dieter"}'],
121121
(new GeneratorConfiguration())->setImmutable(false)->setCollectErrors(false),
122122
false
123123
);
@@ -126,11 +126,11 @@ public function testMappedStringOnlyEnum(): void
126126

127127
$object = new $className(['property' => 'Hans']);
128128
$this->assertSame('Hans', $object->getProperty()->value);
129-
$this->assertSame('CEO', $object->getProperty()->name);
129+
$this->assertSame('Ceo', $object->getProperty()->name);
130130

131131
$object->setProperty('Dieter');
132132
$this->assertSame('Dieter', $object->getProperty()->value);
133-
$this->assertSame('CTO', $object->getProperty()->name);
133+
$this->assertSame('Cto', $object->getProperty()->name);
134134

135135
$object->setProperty(null);
136136
$this->assertNull($object->getProperty());
@@ -142,17 +142,16 @@ public function testMappedStringOnlyEnum(): void
142142
$this->assertSame('string', (new ReflectionEnum($enum))->getBackingType()->getName());
143143

144144
$this->assertEqualsCanonicalizing(
145-
['CEO', 'CTO'],
145+
['Ceo', 'Cto'],
146146
array_map(function (BackedEnum $value): string { return $value->name; }, $enum::cases())
147147
);
148148
$this->assertEqualsCanonicalizing(
149149
['Hans', 'Dieter'],
150150
array_map(function (BackedEnum $value): string { return $value->value; }, $enum::cases())
151151
);
152152

153-
$object->setProperty($enum::CEO);
153+
$object->setProperty($enum::Ceo);
154154
$this->assertSame('Hans', $object->getProperty()->value);
155-
$this->assertSame('CEO', $object->getProperty()->name);
156155
}
157156

158157
/**
@@ -532,6 +531,50 @@ public function testRequiredEnum(): void
532531
$object->setProperty(null);
533532
}
534533

534+
/**
535+
* @requires PHP >= 8.1
536+
*/
537+
public function testEmptyNormalizedCaseNameThrowsAnException(): void
538+
{
539+
$this->addPostProcessor();
540+
541+
$this->expectException(SchemaException::class);
542+
$this->expectExceptionMessage("Name '__ -- __' results in an empty name");
543+
544+
$this->generateClassFromFileTemplate('EnumProperty.json', ['["__ -- __"]'], null, false);
545+
}
546+
547+
/**
548+
* @dataProvider normalizedNamesDataProvider
549+
* @requires PHP >= 8.1
550+
*/
551+
public function testNameNormalization(string $name, string $expectedNormalizedName): void
552+
{
553+
$this->addPostProcessor();
554+
555+
$className = $this->generateClassFromFileTemplate('EnumProperty.json', [sprintf('["%s"]', $name)], null, false);
556+
557+
$this->includeGeneratedEnums(1);
558+
559+
$object = new $className();
560+
561+
$returnType = $this->getReturnType($object, 'getProperty');
562+
$enum = $returnType->getName();
563+
564+
$this->assertSame(
565+
[$expectedNormalizedName],
566+
array_map(function (BackedEnum $value): string { return $value->name; }, $enum::cases())
567+
);
568+
}
569+
570+
public function normalizedNamesDataProvider(): array
571+
{
572+
return [
573+
'includes spaces' => ['not available', 'NotAvailable'],
574+
'includes non alphanumeric characters' => ['not-available', 'NotAvailable'],
575+
'numeric' => ['100', '_100'],
576+
];
577+
}
535578
private function addPostProcessor(): void
536579
{
537580
$this->modifyModelGenerator = static function (ModelGenerator $generator): void {

0 commit comments

Comments
 (0)