Skip to content

Commit 9779bff

Browse files
authored
Merge pull request #14 from open-code-modeling/feature/class-generator
Generate full classes from JSON schema - Close #13
2 parents 6785e95 + 0112043 commit 9779bff

26 files changed

+2424
-75
lines changed

composer.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,13 @@
3333
},
3434
"require": {
3535
"php": "^7.4 || ^8.0",
36-
"open-code-modeling/json-schema-to-php": "^0.1.0 || dev-master",
37-
"open-code-modeling/php-code-ast": "^0.8.6 || dev-master"
36+
"open-code-modeling/json-schema-to-php": "^0.2.0 || 0.3.x-dev",
37+
"open-code-modeling/php-code-ast": "^0.9.0 || 0.10.x-dev"
3838
},
3939
"require-dev": {
4040
"jangregor/phpstan-prophecy": "^0.8.0",
4141
"laminas/laminas-filter": "^2.9",
42+
"open-code-modeling/php-filter": "^0.1.1",
4243
"phpspec/prophecy-phpunit": "^2.0",
4344
"phpstan/phpstan": "^0.12.33",
4445
"phpstan/phpstan-strict-rules": "^0.12.4",
@@ -47,6 +48,9 @@
4748
"roave/security-advisories": "dev-master",
4849
"squizlabs/php_codesniffer": "^3.4"
4950
},
51+
"suggest": {
52+
"open-code-modeling/php-filter": "For pre-configured filters for proper class / method / property names etc."
53+
},
5054
"minimum-stability": "dev",
5155
"prefer-stable": true,
5256
"scripts": {

src/ClassGenerator.php

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
<?php
2+
3+
/**
4+
* @see https://github.com/open-code-modeling/json-schema-to-php-ast for the canonical source repository
5+
* @copyright https://github.com/open-code-modeling/json-schema-to-php-ast/blob/master/COPYRIGHT.md
6+
* @license https://github.com/open-code-modeling/json-schema-to-php-ast/blob/master/LICENSE.md MIT License
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace OpenCodeModeling\JsonSchemaToPhpAst;
12+
13+
use OpenCodeModeling\CodeAst\Builder\ClassBuilder;
14+
use OpenCodeModeling\CodeAst\Builder\ClassBuilderCollection;
15+
use OpenCodeModeling\CodeAst\Builder\ClassConstBuilder;
16+
use OpenCodeModeling\CodeAst\Builder\ClassMethodBuilder;
17+
use OpenCodeModeling\CodeAst\Builder\ClassPropertyBuilder;
18+
use OpenCodeModeling\CodeAst\Code\ClassConstGenerator;
19+
use OpenCodeModeling\CodeAst\Package\ClassInfoList;
20+
use OpenCodeModeling\JsonSchemaToPhp\Type\ArrayType;
21+
use OpenCodeModeling\JsonSchemaToPhp\Type\ObjectType;
22+
use OpenCodeModeling\JsonSchemaToPhp\Type\ReferenceType;
23+
use OpenCodeModeling\JsonSchemaToPhp\Type\ScalarType;
24+
use OpenCodeModeling\JsonSchemaToPhp\Type\TypeDefinition;
25+
use OpenCodeModeling\JsonSchemaToPhp\Type\TypeSet;
26+
use PhpParser\NodeTraverser;
27+
use PhpParser\Parser;
28+
use PhpParser\PrettyPrinterAbstract;
29+
30+
final class ClassGenerator
31+
{
32+
private ClassInfoList $classInfoList;
33+
private ValueObjectFactory $valueObjectFactory;
34+
35+
/**
36+
* @var callable
37+
*/
38+
private $classNameFilter;
39+
40+
/**
41+
* @var callable
42+
*/
43+
private $propertyNameFilter;
44+
45+
public function __construct(
46+
ClassInfoList $classInfoList,
47+
ValueObjectFactory $valueObjectFactory,
48+
callable $classNameFilter,
49+
callable $propertyNameFilter
50+
) {
51+
$this->classInfoList = $classInfoList;
52+
$this->valueObjectFactory = $valueObjectFactory;
53+
$this->classNameFilter = $classNameFilter;
54+
$this->propertyNameFilter = $propertyNameFilter;
55+
}
56+
57+
/**
58+
* @param ClassBuilder $classBuilder Main class
59+
* @param ClassBuilderCollection $classBuilderCollection Collection for other classes
60+
* @param TypeSet $typeSet
61+
* @param string $srcFolder Source folder for namespace imports
62+
* @param string|null $className Class name of other classes
63+
* @return void
64+
*/
65+
public function generateClasses(
66+
ClassBuilder $classBuilder,
67+
ClassBuilderCollection $classBuilderCollection,
68+
TypeSet $typeSet,
69+
string $srcFolder,
70+
string $className = null
71+
): void {
72+
$type = $typeSet->first();
73+
74+
$classInfo = $this->classInfoList->classInfoForPath($srcFolder);
75+
$classNamespace = $classInfo->getClassNamespaceFromPath($srcFolder);
76+
77+
if ($type instanceof ReferenceType
78+
&& $refType = $type->resolvedType()
79+
) {
80+
$type = $refType->first();
81+
}
82+
83+
switch (true) {
84+
case $type instanceof ObjectType:
85+
/** @var TypeSet $propertyTypeSet */
86+
foreach ($type->properties() as $propertyName => $propertyTypeSet) {
87+
$propertyClassName = ($this->classNameFilter)($propertyName);
88+
$propertyPropertyName = ($this->propertyNameFilter)($propertyName);
89+
90+
$propertyType = $propertyTypeSet->first();
91+
switch (true) {
92+
case $propertyType instanceof ArrayType:
93+
foreach ($propertyType->items() as $itemTypeSet) {
94+
$itemType = $itemTypeSet->first();
95+
96+
if (null === $itemType) {
97+
continue;
98+
}
99+
$itemClassName = ($this->classNameFilter)($itemType->name());
100+
$itemPropertyName = ($this->propertyNameFilter)($itemType->name());
101+
102+
$this->generateClasses(
103+
ClassBuilder::fromScratch($itemClassName, $classNamespace)->setFinal(true),
104+
$classBuilderCollection,
105+
$itemTypeSet,
106+
$srcFolder,
107+
$itemPropertyName
108+
);
109+
}
110+
// no break
111+
case $propertyType instanceof ObjectType:
112+
$this->generateClasses(
113+
ClassBuilder::fromScratch($propertyClassName, $classNamespace)->setFinal(true),
114+
$classBuilderCollection,
115+
$propertyTypeSet,
116+
$srcFolder,
117+
$propertyClassName
118+
);
119+
$classBuilder->addNamespaceImport($classNamespace . '\\' . $propertyClassName);
120+
$classBuilder->addProperty(ClassPropertyBuilder::fromScratch($propertyPropertyName, $propertyClassName));
121+
break;
122+
case $propertyType instanceof ReferenceType:
123+
if ($propertyRefType = $propertyType->resolvedType()) {
124+
$this->generateClasses(
125+
ClassBuilder::fromScratch($propertyClassName, $classNamespace)->setFinal(true),
126+
$classBuilderCollection,
127+
$propertyRefType,
128+
$srcFolder,
129+
$propertyType->name()
130+
);
131+
$propertyClassName = ($this->classNameFilter)($propertyType->name());
132+
$classBuilder->addNamespaceImport($classNamespace . '\\' . $propertyClassName);
133+
}
134+
$classBuilder->addProperty(
135+
ClassPropertyBuilder::fromScratch($propertyPropertyName, $propertyClassName)
136+
);
137+
break;
138+
case $propertyType instanceof ScalarType:
139+
$classBuilderCollection->add(
140+
$this->generateValueObject($propertyClassName, $classNamespace, $propertyType)
141+
);
142+
$classBuilder->addNamespaceImport($classNamespace . '\\' . $propertyClassName);
143+
$classBuilder->addProperty(
144+
ClassPropertyBuilder::fromScratch(
145+
$propertyPropertyName,
146+
$propertyClassName
147+
)
148+
);
149+
break;
150+
default:
151+
break;
152+
}
153+
}
154+
$classBuilderCollection->add($classBuilder);
155+
break;
156+
case $type instanceof ScalarType:
157+
$classBuilderCollection->add(
158+
$this->generateValueObject(($this->classNameFilter)($className), $classNamespace, $type)
159+
);
160+
break;
161+
case $type instanceof ArrayType:
162+
$arrayClassBuilder = $this->generateValueObject(($this->classNameFilter)($className), $classNamespace, $type);
163+
$this->addNamespaceImport($arrayClassBuilder, $type);
164+
$classBuilderCollection->add($arrayClassBuilder);
165+
break;
166+
default:
167+
break;
168+
}
169+
}
170+
171+
/**
172+
* Generation of getter methods for value object are skipped.
173+
*
174+
* @param ClassBuilderCollection $classBuilderCollection
175+
* @param bool $typed
176+
* @param callable $methodNameFilter Filter the property name to your desired method name e.g. with get prefix
177+
*/
178+
public function addGetterMethods(
179+
ClassBuilderCollection $classBuilderCollection,
180+
bool $typed,
181+
callable $methodNameFilter
182+
): void {
183+
foreach ($classBuilderCollection as $classBuilder) {
184+
foreach ($classBuilder->getProperties() as $classPropertyBuilder) {
185+
$methodName = ($methodNameFilter)($classPropertyBuilder->getName());
186+
187+
if ($this->isValueObject($classBuilder)
188+
|| $classBuilder->hasMethod($methodName)) {
189+
continue 2;
190+
}
191+
$classBuilder->addMethod(
192+
ClassMethodBuilder::fromScratch($methodName, $typed)
193+
->setReturnType($classPropertyBuilder->getType())
194+
->setReturnTypeDocBlockHint($classPropertyBuilder->getTypeDocBlockHint())
195+
->setBody('return $this->' . $classPropertyBuilder->getName() . ';')
196+
);
197+
}
198+
}
199+
}
200+
201+
/**
202+
* Generation of constants for value object are skipped.
203+
*
204+
* @param ClassBuilderCollection $classBuilderCollection
205+
* @param callable $constantNameFilter Converts the name to a proper class constant name
206+
* @param callable $constantValueFilter Converts the name to a proper class constant value e.g. snake_case or camelCase
207+
* @param int $visibility Visibility of the class constant
208+
*/
209+
public function addClassConstantsForProperties(
210+
ClassBuilderCollection $classBuilderCollection,
211+
callable $constantNameFilter,
212+
callable $constantValueFilter,
213+
int $visibility = ClassConstGenerator::FLAG_PUBLIC
214+
): void {
215+
foreach ($classBuilderCollection as $classBuilder) {
216+
foreach ($classBuilder->getProperties() as $classPropertyBuilder) {
217+
$constantName = ($constantNameFilter)($classPropertyBuilder->getName());
218+
219+
if ($this->isValueObject($classBuilder)
220+
|| $classBuilder->hasConstant($constantName)) {
221+
continue 2;
222+
}
223+
$classBuilder->addConstant(
224+
ClassConstBuilder::fromScratch(
225+
$constantName,
226+
($constantValueFilter)($classPropertyBuilder->getName()),
227+
$visibility
228+
)
229+
);
230+
}
231+
}
232+
}
233+
234+
public function generateValueObject(string $className, string $classNamespace, TypeDefinition $definition): ClassBuilder
235+
{
236+
$classBuilder = $this->valueObjectFactory->classBuilder($definition);
237+
$classBuilder->setName($className)
238+
->setNamespace($classNamespace)
239+
->setStrict(true)
240+
->setFinal(true);
241+
242+
return $classBuilder;
243+
}
244+
245+
/**
246+
* @param ClassBuilderCollection $classBuilderCollection
247+
* @param Parser $parser
248+
* @param PrettyPrinterAbstract $printer
249+
* @return array<string, string> List of filename => code
250+
*/
251+
public function generateFiles(
252+
ClassBuilderCollection $classBuilderCollection,
253+
Parser $parser,
254+
PrettyPrinterAbstract $printer
255+
): array {
256+
$files = [];
257+
258+
$previousNamespace = '__invalid//namespace__';
259+
260+
foreach ($classBuilderCollection as $classBuilder) {
261+
if ($previousNamespace !== $classBuilder->getNamespace()) {
262+
$previousNamespace = $classBuilder->getNamespace();
263+
$classInfo = $this->classInfoList->classInfoForNamespace($previousNamespace);
264+
$path = $classInfo->getPath($classBuilder->getNamespace() . '\\' . $classBuilder->getName());
265+
}
266+
// @phpstan-ignore-next-line
267+
$filename = $classInfo->getFilenameFromPathAndName($path, $classBuilder->getName());
268+
269+
$nodeTraverser = new NodeTraverser();
270+
$classBuilder->injectVisitors($nodeTraverser, $parser);
271+
272+
$files[$filename] = $printer->prettyPrintFile($nodeTraverser->traverse([]));
273+
}
274+
275+
return $files;
276+
}
277+
278+
private function addNamespaceImport(ClassBuilder $classBuilder, TypeDefinition $typeDefinition): void
279+
{
280+
switch (true) {
281+
case $typeDefinition instanceof ArrayType:
282+
foreach ($typeDefinition->items() as $itemTypeSet) {
283+
$itemType = $itemTypeSet->first();
284+
285+
if (null === $itemType) {
286+
continue;
287+
}
288+
289+
if ($itemType instanceof ReferenceType
290+
&& $refType = $itemType->resolvedType()
291+
) {
292+
$itemType = $refType->first();
293+
}
294+
$itemClassName = ($this->classNameFilter)($itemType->name());
295+
$classBuilder->addNamespaceImport($classBuilder->getNamespace() . '\\' . $itemClassName);
296+
}
297+
break;
298+
default:
299+
break;
300+
}
301+
}
302+
303+
private function isValueObject(ClassBuilder $classBuilder): bool
304+
{
305+
return $classBuilder->hasMethod('fromItems')
306+
|| $classBuilder->hasMethod('toString')
307+
|| $classBuilder->hasMethod('toInt')
308+
|| $classBuilder->hasMethod('toFloat')
309+
|| $classBuilder->hasMethod('toBool');
310+
}
311+
}

0 commit comments

Comments
 (0)