Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
69.23% |
9 / 13 |
CRAP | |
95.13% |
215 / 226 |
DtoGenerator | |
0.00% |
0 / 1 |
|
69.23% |
9 / 13 |
68 | |
95.13% |
215 / 226 |
__construct | |
100.00% |
1 / 1 |
2 | |
100.00% |
3 / 3 |
|||
calculateTargetClassNames | |
100.00% |
1 / 1 |
3 | |
100.00% |
9 / 9 |
|||
generateDtoClasses | |
100.00% |
1 / 1 |
2 | |
100.00% |
8 / 8 |
|||
inferImmutableClass | |
100.00% |
1 / 1 |
2 | |
100.00% |
6 / 6 |
|||
inferMutableClass | |
100.00% |
1 / 1 |
2 | |
100.00% |
6 / 6 |
|||
inferImmutableInterface | |
100.00% |
1 / 1 |
7 | |
100.00% |
14 / 14 |
|||
inferFormDataMapper | |
100.00% |
1 / 1 |
2 | |
100.00% |
21 / 21 |
|||
hasPublicMutatorAnnotation | |
100.00% |
1 / 1 |
3 | |
100.00% |
5 / 5 |
|||
writeClass | |
100.00% |
1 / 1 |
3 | |
100.00% |
15 / 15 |
|||
generateGetters | |
0.00% |
0 / 1 |
6 | |
95.00% |
19 / 20 |
|||
generateSetters | |
0.00% |
0 / 1 |
8 | |
95.00% |
19 / 20 |
|||
generateWithers | |
0.00% |
0 / 1 |
6 | |
95.83% |
23 / 24 |
|||
generateConstructors | |
0.00% |
0 / 1 |
22.59 | |
89.33% |
67 / 75 |
<?php declare(strict_types = 1); | |
/* | |
* Copyright (c) 2019-2020, Josef Kufner <josef@kufner.cz> | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
* | |
*/ | |
namespace Smalldb\CodeCooker\Generator; | |
use DateTimeImmutable; | |
use InvalidArgumentException; | |
use ReflectionClass; | |
use ReflectionMethod; | |
use ReflectionNamedType; | |
use Smalldb\CodeCooker\Annotation\GeneratedClass; | |
use Smalldb\CodeCooker\Annotation\PublicMutator; | |
use Smalldb\StateMachine\Utils\AnnotationReader\AnnotationReader; | |
use Smalldb\StateMachine\Utils\AnnotationReader\AnnotationReaderInterface; | |
use Smalldb\ClassLocator\ClassLocator; | |
use Smalldb\PhpFileWriter\PhpFileWriter; | |
use Symfony\Component\Form\DataMapperInterface; | |
use Symfony\Component\Form\Exception\UnexpectedTypeException; | |
use Symfony\Component\OptionsResolver\OptionsResolver; | |
class DtoGenerator | |
{ | |
use GeneratorHelpers; | |
private ClassLocator $classLocator; | |
private AnnotationReaderInterface $annotationReader; | |
public function __construct(ClassLocator $classLocator, ?AnnotationReaderInterface $annotationReader = null) | |
{ | |
$this->classLocator = $classLocator; | |
$this->annotationReader = $annotationReader ?? (new AnnotationReader()); | |
} | |
public static function calculateTargetClassNames(ReflectionClass $sourceClass, ?string $targetName = null): array | |
{ | |
$suffixes = [ | |
'', | |
'Immutable', | |
'Mutable', | |
'FormDataMapper', | |
]; | |
$sourceShortName = $sourceClass->getShortName(); | |
$targetName ??= $sourceShortName; | |
$targetNamespace = $sourceClass->getNamespaceName() . '\\' . $targetName; | |
$targetClassNames = []; | |
foreach ($suffixes as $suffix) { | |
$targetShortName = $targetName . $suffix; | |
$targetClassNames[] = $targetNamespace . '\\' . $targetShortName; | |
} | |
return $targetClassNames; | |
} | |
public function generateDtoClasses(ReflectionClass $sourceClass, ?string $targetName = null): array | |
{ | |
$immutableInterface = $this->inferImmutableInterface($sourceClass, $targetName, ''); | |
$immutableClass = $this->inferImmutableClass($sourceClass, $targetName, 'Immutable', $immutableInterface); | |
$mutableClass = $this->inferMutableClass($sourceClass, $targetName, 'Mutable', $immutableInterface); | |
$formDataMapper = $this->inferFormDataMapper($sourceClass, $targetName, 'FormDataMapper', $immutableInterface, $immutableClass); | |
return [ | |
$immutableInterface, | |
$immutableClass, | |
$mutableClass, | |
$formDataMapper, | |
]; | |
} | |
private function inferImmutableClass(ReflectionClass $sourceClass, ?string $targetName, string $suffix, string $immutableInterfaceName): string | |
{ | |
return $this->writeClass($sourceClass, $targetName, $suffix, function (PhpFileWriter $w, $targetNamespace, $targetShortName) | |
use ($sourceClass, $immutableInterfaceName) | |
{ | |
$w->beginClass($targetShortName, $w->useClass($sourceClass->getName(), 'Source_' . $sourceClass->getShortName()), [$w->useClass($immutableInterfaceName)]); | |
$this->generateConstructors($w, $sourceClass, $immutableInterfaceName); | |
$this->generateGetters($w, $sourceClass, $immutableInterfaceName); | |
$this->generateWithers($w, $sourceClass); | |
$w->endClass(); | |
}); | |
} | |
private function inferMutableClass(ReflectionClass $sourceClass, ?string $targetName, string $suffix, string $immutableInterfaceName): string | |
{ | |
return $this->writeClass($sourceClass, $targetName, $suffix, function (PhpFileWriter $w, $targetNamespace, $targetShortName) | |
use ($sourceClass, $immutableInterfaceName) | |
{ | |
$w->beginClass($targetShortName, $w->useClass($sourceClass->getName(), 'Source_' . $sourceClass->getShortName()), [$w->useClass($immutableInterfaceName)]); | |
$this->generateConstructors($w, $sourceClass, $immutableInterfaceName); | |
$this->generateGetters($w, $sourceClass, $immutableInterfaceName); | |
$this->generateSetters($w, $sourceClass); | |
$w->endClass(); | |
}); | |
} | |
private function inferImmutableInterface(ReflectionClass $sourceClass, ?string $targetName, string $suffix): string | |
{ | |
return $this->writeClass($sourceClass, $targetName, $suffix, function (PhpFileWriter $w, $targetNamespace, $targetShortName) | |
use ($sourceClass) | |
{ | |
$w->beginInterface($targetShortName); | |
foreach ($sourceClass->getProperties() as $propertyReflection) { | |
$propertyName = $propertyReflection->getName(); | |
$getterName = 'get' . ucfirst($propertyName); | |
$typehint = $w->getTypeAsCode($propertyReflection->getType()); | |
$w->writeInterfaceMethod($getterName, [], $typehint); | |
} | |
foreach ($sourceClass->getMethods() as $methodReflection) { | |
$methodName = $methodReflection->getName(); | |
if ($methodName !== '__construct' && $methodReflection->isPublic() && !$w->hasMethod($methodName)) { | |
[$argMethod, $argCall] = $w->getMethodParametersCode($methodReflection); | |
$w->writeInterfaceMethod($methodName, $argMethod, | |
$w->getTypeAsCode($methodReflection->getReturnType())); | |
} | |
} | |
$w->endInterface(); | |
}); | |
} | |
protected function inferFormDataMapper(ReflectionClass $sourceClass, ?string $targetName, string $suffix, string $immutableInterfaceName, string $immutableClassName) | |
{ | |
return $this->writeClass($sourceClass, $targetName, $suffix, function (PhpFileWriter $w, $targetNamespace, $targetShortName) | |
use ($sourceClass, $immutableInterfaceName, $immutableClassName) | |
{ | |
$w->beginClass($targetShortName, null, [$w->useClass(DataMapperInterface::class)]); | |
$w->beginMethod('mapDataToForms', ['$viewData', 'iterable $forms']); | |
{ | |
$w->beginBlock('if ($viewData === null)'); | |
{ | |
$w->writeln('return;'); | |
} | |
$w->midBlock('else if ($viewData instanceof ' . $w->useClass($immutableInterfaceName) . ')'); | |
{ | |
$w->beginBlock('foreach ($forms as $prop => $field)'); | |
{ | |
$w->writeln('$field->setData(' . $w->useClass($immutableClassName) . '::get($viewData, $prop));'); | |
} | |
$w->endBlock(); | |
} | |
$w->midBlock('else'); | |
{ | |
$w->writeln('throw new ' . $w->useClass(UnexpectedTypeException::class) . '($viewData, ' . $w->useClass($immutableClassName) . '::class);'); | |
} | |
$w->endBlock(); | |
} | |
$w->endMethod(); | |
$w->beginMethod('mapFormsToData', ['iterable $forms', '& $viewData']); | |
{ | |
$w->writeln('$viewData = ' . $w->useClass($immutableClassName) . '::fromIterable($viewData, (function() use ($forms) { foreach($forms as $k => $field) yield $k => $field->getData(); })());'); | |
} | |
$w->endMethod(); | |
$w->beginMethod('configureOptions', [$w->useClass(OptionsResolver::class) . ' $optionsResolver']); | |
{ | |
$w->writeln('$optionsResolver->setDefault("empty_data", null);'); | |
$w->writeln('$optionsResolver->setDefault("data_class", ' . $w->useClass($immutableInterfaceName) . '::class);'); | |
} | |
$w->endMethod(); | |
$w->endClass(); | |
}); | |
} | |
private function hasPublicMutatorAnnotation(ReflectionMethod $methodReflection) | |
{ | |
$methodAnnotations = $this->annotationReader->getMethodAnnotations($methodReflection); | |
foreach ($methodAnnotations as $annotation) { | |
if ($annotation instanceof PublicMutator) { | |
return true; | |
} | |
} | |
return false; | |
} | |
protected function writeClass(ReflectionClass $sourceClass, ?string $targetName, string $suffix, callable $writeCallback): string | |
{ | |
$sourceShortName = $sourceClass->getShortName(); | |
$targetName ??= $sourceShortName; | |
$targetShortName = $targetName . $suffix; | |
$targetNamespace = $sourceClass->getNamespaceName() . '\\' . $targetName; | |
$targetClassName = $targetNamespace . '\\' . $targetShortName; | |
$targetFilename = $this->classLocator->mapClassNameToFileName($targetClassName); | |
$targetDirectory = dirname($targetFilename); | |
$w = $this->createFileWriter($targetNamespace); | |
$w->docComment("@" . $w->useClass(GeneratedClass::class) . "\n" | |
. "@see \\" . $sourceClass->getName()); | |
$writeCallback($w, $targetNamespace, $targetShortName); | |
if (!is_dir($targetDirectory)) { | |
mkdir($targetDirectory); | |
} | |
$w->write($targetFilename); | |
return $targetClassName; | |
} | |
private function generateGetters(PhpFileWriter $w, ReflectionClass $sourceClass, string $immutableInterface): void | |
{ | |
foreach ($sourceClass->getProperties() as $propertyReflection) { | |
$propertyName = $propertyReflection->getName(); | |
$typehint = $w->getTypeAsCode($propertyReflection->getType()); | |
$getterName = 'get' . ucfirst($propertyName); | |
$getter = $sourceClass->hasMethod($getterName) ? $sourceClass->getMethod($getterName) : null; | |
$w->beginMethod($getterName, [], $typehint); | |
{ | |
if ($getter && !$getter->isAbstract()) { | |
// Do not reimplement the existing getter -- call it. | |
$w->writeln("return parent::$getterName();"); | |
} else { | |
// Get the property | |
$w->writeln("return \$this->$propertyName;"); | |
} | |
} | |
$w->endMethod(); | |
} | |
$w->beginStaticMethod('get', [$w->useClass($immutableInterface) . ' $source', 'string $propertyName']); | |
{ | |
$w->beginBlock('switch ($propertyName)'); | |
{ | |
foreach ($sourceClass->getProperties() as $propertyReflection) { | |
$propertyName = $propertyReflection->getName(); | |
$getterName = 'get' . ucfirst($propertyName); | |
$w->writeln("case %s: return \$source->$getterName();", $propertyName); | |
} | |
$w->writeln('default: throw new \InvalidArgumentException("Unknown property: " . $propertyName);'); | |
} | |
$w->endBlock(); | |
} | |
$w->endMethod(); | |
} | |
private function generateSetters(PhpFileWriter $w, ReflectionClass $sourceClass): void | |
{ | |
foreach ($sourceClass->getProperties() as $propertyReflection) { | |
$propertyName = $propertyReflection->getName(); | |
$param = $w->getParamCode($propertyReflection->getType(), $propertyReflection->getName()); | |
$setterName = 'set' . ucfirst($propertyName); | |
$sourceSetter = $sourceClass->hasMethod($setterName) ? $sourceClass->getMethod($setterName) : null; | |
$w->beginMethod($setterName, [$param], 'void'); | |
{ | |
if ($sourceSetter && $sourceSetter->isAbstract()) { | |
// Do not reimplement the existing setter -- call it. | |
$w->writeln("parent::$setterName(\$$propertyName);"); | |
} else { | |
// Set the property | |
$w->writeln("\$this->$propertyName = \$$propertyName;"); | |
} | |
} | |
$w->endMethod(); | |
} | |
// Export annotated mutators | |
foreach ($sourceClass->getMethods() as $methodReflection) { | |
if ($this->hasPublicMutatorAnnotation($methodReflection)) { | |
$methodName = $methodReflection->getName(); | |
[$argMethod, $argCall] = $w->getMethodParametersCode($methodReflection); | |
$returnTypehint = $w->getTypeAsCode($methodReflection->getReturnType()); | |
$w->beginMethod($methodName, $argMethod, $returnTypehint); | |
{ | |
$parentCall = "parent::$methodName(" . join(', ', $argCall) . ");"; | |
$w->writeln($returnTypehint !== 'void' ? "return " . $parentCall : $parentCall); | |
} | |
$w->endMethod(); | |
} | |
} | |
} | |
private function generateWithers(PhpFileWriter $w, ReflectionClass $sourceClass): void | |
{ | |
foreach ($sourceClass->getProperties() as $propertyReflection) { | |
$propertyName = $propertyReflection->getName(); | |
$param = $w->getParamCode($propertyReflection->getType(), $propertyReflection->getName()); | |
$setterName = 'set' . ucfirst($propertyName); | |
$witherName = 'with' . ucfirst($propertyName); | |
$hasSourceSetter = $sourceClass->hasMethod($setterName); | |
$w->beginMethod($witherName, [$param], 'self'); | |
{ | |
$w->writeln("\$t = clone \$this;"); | |
if ($hasSourceSetter) { | |
// Do not reimplement the existing setter -- call it. | |
$w->writeln("\$t->$setterName(\$$propertyName);"); | |
} else { | |
// Set the property | |
$w->writeln("\$t->$propertyName = \$$propertyName;"); | |
} | |
$w->writeln("return \$t;"); | |
} | |
$w->endMethod(); | |
} | |
// Export annotated mutators | |
foreach ($sourceClass->getMethods() as $methodReflection) { | |
if ($this->hasPublicMutatorAnnotation($methodReflection)) { | |
$methodName = $methodReflection->getName(); | |
$witherName = strncmp($methodName, 'set', 3) === 0 ? 'with' . substr($methodName, 3) : 'with' . ucfirst($methodName); | |
[$argMethod, $argCall] = $w->getMethodParametersCode($methodReflection); | |
$w->beginMethod($witherName, $argMethod, 'self'); | |
{ | |
$w->writeln("\$t = clone \$this;"); | |
$w->writeln("\$t->$methodName(" . join(', ', $argCall) . ");"); | |
$w->writeln("return \$t;"); | |
} | |
$w->endMethod(); | |
} | |
} | |
} | |
private function generateConstructors(PhpFileWriter $w, ReflectionClass $sourceClass, string $copyInterfaceName): void | |
{ | |
$sourceConstructor = $sourceClass->hasMethod('__construct') ? $sourceClass->getMethod('__construct') : null; | |
$w->beginMethod('__construct', ['?' . $w->useClass($copyInterfaceName) . ' $source = null']); | |
{ | |
// Call parent constructor if present | |
if ($sourceConstructor) { | |
$constructorParameters = $sourceConstructor->getParameters(); | |
$firstParamType = $constructorParameters[0]->getType(); | |
$firstParamTypeName = $firstParamType instanceof ReflectionNamedType ? $firstParamType->getName() : null; | |
if ($firstParamTypeName === $copyInterfaceName) { | |
$w->writeln("parent::__construct(\$source);"); | |
} else { | |
$w->writeln("parent::__construct();"); | |
} | |
} | |
$w->beginBlock('if ($source !== null)'); | |
{ | |
$w->beginBlock('if ($source instanceof ' . $w->useClass($sourceClass->getName()) . ')'); | |
{ | |
foreach ($sourceClass->getProperties() as $property) { | |
$propertyName = $property->getName(); | |
$w->writeln("\$this->$propertyName = \$source->$propertyName;"); | |
} | |
} | |
$w->midBlock('else'); | |
{ | |
foreach ($sourceClass->getProperties() as $property) { | |
$propertyName = $property->getName(); | |
$getterName = 'get' . ucfirst($propertyName); | |
$w->writeln("\$this->$propertyName = \$source->$getterName();"); | |
} | |
} | |
$w->endBlock(); | |
} | |
$w->endBlock(); | |
} | |
$w->endMethod(); | |
$w->beginStaticMethod('fromArray', ['?array $source', '?' . $w->useClass($copyInterfaceName) . ' $sourceObj = null'], '?self'); | |
{ | |
$w->beginBlock("if (\$source === null)"); | |
{ | |
$w->writeln("return null;"); | |
} | |
$w->endBlock(); | |
$w->writeln("\$t = \$sourceObj instanceof self ? clone \$sourceObj : new self(\$sourceObj);"); | |
foreach ($sourceClass->getProperties() as $property) { | |
$propertyName = $property->getName(); | |
$type = $property->getType(); | |
if ($type instanceof ReflectionNamedType) { | |
$typehint = $type->getName(); | |
} else { | |
$typehint = null; | |
} | |
//$w->writeln("\$t->$propertyName = \$source['$propertyName'];"); | |
// Convert value to fit the typehint | |
switch ($typehint) { | |
case 'int': | |
case 'float': | |
case 'bool': | |
case 'string': | |
if ($type->allowsNull()) { | |
$w->writeln("\$t->$propertyName = isset(\$source[%s]) ? ($typehint) \$source[%s] : null;", $propertyName, $propertyName); | |
} else { | |
$w->writeln("\$t->$propertyName = ($typehint) \$source[%s];", $propertyName, $propertyName); | |
} | |
break; | |
case 'array': | |
// TODO: Implement proper SQL to Object mapping. | |
if ($type->allowsNull()) { | |
$w->writeln("\$t->$propertyName = isset(\$source[%s])" | |
. " ? (is_string(\$source[%s]) ? json_decode(\$source[%s], TRUE) : \$source[%s])" | |
. " : null;", $propertyName, $propertyName, $propertyName, $propertyName); | |
} else { | |
$w->writeln("\$t->$propertyName = is_string(\$source[%s]) ? json_decode(\$source[%s], TRUE) : (array) \$source[%s];", | |
$propertyName, $propertyName, $propertyName); | |
} | |
break; | |
default: | |
if ($typehint && class_exists($typehint)) { | |
$c = $w->useClass($typehint); | |
if ($typehint === DateTimeImmutable::class) { | |
$w->writeln("\$t->$propertyName = (\$v = \$source[%s] ?? null) instanceof \\DateTimeImmutable || \$v === null ? \$v " | |
. ": (\$v instanceof \\DateTime ? \\DateTimeImmutable::createFromMutable(\$v) : new \\DateTimeImmutable(\$v));", $propertyName); | |
} else { | |
$w->writeln("\$t->$propertyName = (\$v = \$source[%s] ?? null) instanceof $c || \$v === null ? \$v : new $c(\$v);", $propertyName); | |
} | |
} else { | |
$w->writeln("\$t->$propertyName = \$source[%s] ?? null;", $propertyName); | |
} | |
break; | |
} | |
} | |
$w->writeln("return \$t;"); | |
} | |
$w->endMethod(); | |
$w->beginStaticMethod('fromIterable', ['?' . $w->useClass($copyInterfaceName) . ' $sourceObj', 'iterable $source'], 'self'); | |
{ | |
$w->writeln("\$t = \$sourceObj instanceof self ? clone \$sourceObj : new self(\$sourceObj);"); | |
$w->beginBlock("foreach (\$source as \$prop => \$value)"); | |
{ | |
$w->beginBlock("switch (\$prop)"); | |
{ | |
foreach ($sourceClass->getProperties() as $property) { | |
$propertyName = $property->getName(); | |
$propertyType = $property->getType(); | |
if ($propertyType instanceof ReflectionNamedType && $propertyType->getName() === DateTimeImmutable::class) { | |
$w->writeln("case '$propertyName': \$t->$propertyName = \$value instanceof \\DateTime ? \\DateTimeImmutable::createFromMutable(\$value) : \$value; break;"); | |
} else { | |
$w->writeln("case '$propertyName': \$t->$propertyName = \$value; break;"); | |
} | |
} | |
$w->writeln("default: throw new " . $w->useClass(InvalidArgumentException::class) . "('Unknown property: \"' . \$prop . '\" not in ' . __CLASS__);"); | |
} | |
$w->endBlock(); | |
} | |
$w->endBlock(); | |
$w->writeln("return \$t;"); | |
} | |
$w->endMethod(); | |
} | |
} | |