Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
| Total | |
0.00% |
0 / 1 |
|
40.00% |
2 / 5 |
CRAP | |
94.12% |
112 / 119 |
| DecoratingGenerator | |
0.00% |
0 / 1 |
|
40.00% |
2 / 5 |
44.39 | |
94.12% |
112 / 119 |
| writeReferenceClass | |
100.00% |
1 / 1 |
3 | |
100.00% |
16 / 16 |
|||
| detectDtoInterface | |
0.00% |
0 / 1 |
6.40 | |
77.78% |
7 / 9 |
|||
| generateDataGetterMethods | |
0.00% |
0 / 1 |
24 | |
95.71% |
67 / 70 |
|||
| getLoadDataCall | |
100.00% |
1 / 1 |
4 | |
100.00% |
3 / 3 |
|||
| generateHydratorMethod | |
0.00% |
0 / 1 |
7.04 | |
90.48% |
19 / 21 |
|||
| <?php declare(strict_types = 1); | |
| /* | |
| * Copyright (c) 2019, 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\StateMachine\ClassGenerator\ReferenceClassGenerator; | |
| use ReflectionClass; | |
| use Smalldb\StateMachine\Definition\StateMachineDefinition; | |
| use Smalldb\StateMachine\DtoExtension\Definition\DtoExtension; | |
| use Smalldb\StateMachine\InvalidArgumentException; | |
| use Smalldb\StateMachine\MachineIdentifierInterface; | |
| use Smalldb\StateMachine\ReferenceDataSource\NotExistsException; | |
| use Smalldb\StateMachine\ReferenceDataSource\StatefulEntity; | |
| use Smalldb\StateMachine\ReferenceInterface; | |
| use Smalldb\PhpFileWriter\PhpFileWriter; | |
| use Smalldb\StateMachine\SqlExtension\Definition\SqlPropertyExtension; | |
| class DecoratingGenerator extends AbstractGenerator | |
| { | |
| /** | |
| * Generate a new class implementing the missing methods in $sourceReferenceClassName. | |
| */ | |
| public function writeReferenceClass(PhpFileWriter $w, ReflectionClass $sourceClassReflection, StateMachineDefinition $definition): string | |
| { | |
| $dtoClassName = null; | |
| if ($definition->hasExtension(DtoExtension::class)) { | |
| /** @var DtoExtension $dtoExt */ | |
| $dtoExt = $definition->getExtension(DtoExtension::class); | |
| $dtoClassName = $dtoExt->getDtoClassName(); | |
| } | |
| // Use the DTO class name provided by the definition or try to autodetect the name | |
| $dtoClass = $dtoClassName ? new ReflectionClass($dtoClassName) | |
| : $this->detectDtoInterface($sourceClassReflection); | |
| // Begin the Reference class | |
| $targetReferenceClassName = $this->beginReferenceClass($w, $sourceClassReflection); | |
| $loadDataCall = $this->getLoadDataCall($w, $dtoClass); | |
| // Create methods | |
| $this->generateIdMethods($w); | |
| $this->generateReferenceMethods($w, $definition); | |
| $this->generateTransitionMethods($w, $definition, $sourceClassReflection); | |
| $this->generateDataGetterMethods($w, $definition, $sourceClassReflection, $dtoClass, $loadDataCall); | |
| $this->generateGetMethod($w, $definition, $sourceClassReflection); | |
| $this->generateHydratorMethod($w, $definition, $sourceClassReflection, $dtoClass); | |
| $w->endClass(); | |
| return $targetReferenceClassName; | |
| } | |
| private function detectDtoInterface(ReflectionClass $sourceClassReflection): ReflectionClass | |
| { | |
| $dtoInterface = null; | |
| foreach ($sourceClassReflection->getInterfaces() as $interface) { | |
| if (!$interface->implementsInterface(ReferenceInterface::class) && $interface->getName() !== MachineIdentifierInterface::class) { | |
| if ($dtoInterface) { | |
| // TODO: This may not be that bad. We may support multiple DTOs. | |
| throw new InvalidArgumentException("Multiple DTO interfaces found in " . $sourceClassReflection->getName()); | |
| } else { | |
| $dtoInterface = $interface; | |
| } | |
| } | |
| } | |
| if ($dtoInterface === null) { | |
| throw new InvalidArgumentException("No DTO interface found in " . $sourceClassReflection->getName()); | |
| } | |
| return $dtoInterface; | |
| } | |
| private function generateDataGetterMethods(PhpFileWriter $w, StateMachineDefinition $definition, ReflectionClass $sourceClassReflection, | |
| ReflectionClass $dtoInterface, string $loadDataCall) | |
| { | |
| $dtoInterfaceAlias = $w->useClass($dtoInterface->getName()); | |
| $referenceInterface = new ReflectionClass(ReferenceInterface::class); | |
| $w->writeln("private ?$dtoInterfaceAlias \$data = null;"); | |
| $w->beginMethod('invalidateCache', [], 'void'); | |
| { | |
| $w->writeln('$this->data = null;'); | |
| $w->writeln('$this->dataSource->invalidateCache($this->getMachineId());'); | |
| } | |
| $w->endMethod(); | |
| // Implement missing methods from $dtoInterface | |
| foreach ($dtoInterface->getMethods() as $dtoMethod) { | |
| $dtoMethodName = $dtoMethod->getName(); | |
| if ($dtoMethodName === '__construct') { | |
| // Skip constructors. | |
| continue; | |
| } | |
| if (!$sourceClassReflection->hasMethod($dtoMethodName)) { | |
| // Skip methods that do not exist in the reference class. | |
| continue; | |
| } | |
| if ($referenceInterface->hasMethod($dtoMethodName)) { | |
| // Skip helper methods of ReferenceInterface | |
| continue; | |
| } | |
| $sourceMethod = $sourceClassReflection->getMethod($dtoMethodName); | |
| if ($sourceMethod->isStatic()) { | |
| // Skip static methods. | |
| continue; | |
| } | |
| if (!$w->hasMethod($dtoMethodName) && $dtoMethod->isPublic() && (!$sourceMethod || $sourceMethod->isAbstract())) { | |
| $w->beginMethodOverride($dtoMethod, $parentCallArgs); | |
| { | |
| $w->beginBlock("if (\$this->data === null && (\$this->data = $loadDataCall) === null)"); | |
| { | |
| $w->writeln("throw new " . $w->useClass(NotExistsException::class) . "(\"Cannot load data in the Not Exists state.\");"); | |
| } | |
| $w->midBlock("else"); | |
| { | |
| $w->writeln("return \$this->data->$dtoMethodName(" . join(", ", $parentCallArgs) . ");"); | |
| } | |
| $w->endBlock(); | |
| } | |
| $w->endMethod(); | |
| } | |
| } | |
| // Implement state method | |
| if (!$w->hasMethod('getState') && ($stateMethod = $sourceClassReflection->getMethod('getState')) && $stateMethod->isAbstract()) { | |
| $w->beginMethod('getState', [], 'string'); | |
| { | |
| $w->beginBlock("if (\$this->data === null)"); | |
| { | |
| $w->writeln("\$this->data = $loadDataCall;"); | |
| } | |
| $w->endBlock(); | |
| $notExists = $w->useClass(ReferenceInterface::class) . '::NOT_EXISTS'; | |
| $states = $definition->getStates(); | |
| switch (count($states)) { | |
| case 0: | |
| case 1: | |
| $w->writeln("return $notExists;"); | |
| break; | |
| case 2: | |
| // There are two states: NOT_EXISTS and EXISTS. If there are any data, it EXISTS. | |
| $theOtherState = null; | |
| foreach ($states as $state) { | |
| if ($state->getName() !== ReferenceInterface::NOT_EXISTS) { | |
| $theOtherState = $state->getName(); | |
| break; | |
| } | |
| } | |
| $w->writeln("return \$this->data === null ? $notExists : %s;", $theOtherState); | |
| break; | |
| default: | |
| $w->beginBlock("switch (true)"); | |
| { | |
| $w->writeln("case \$this->data instanceof " . $w->useClass(StatefulEntity::class) . ":" | |
| . " return \$this->data->getState() ?: $notExists;"); | |
| $w->writeln("case is_array(\$this->data): return \$this->data['state'] ?: $notExists;"); | |
| $w->writeln("case isset(\$this->data->state): return \$this->data->state ?: $notExists;"); | |
| $w->writeln("default: return $notExists;"); | |
| } | |
| $w->endBlock(); | |
| break; | |
| } | |
| } | |
| $w->endMethod(); | |
| } | |
| // Implement the remaining abstract methods | |
| foreach ($sourceClassReflection->getMethods() as $method) { | |
| $methodName = $method->getName(); | |
| if ($method->isAbstract() && !$w->hasMethod($methodName)) { | |
| $returnType = $method->getReturnType(); | |
| // Implement data loader methods | |
| if ($returnType instanceof \ReflectionNamedType | |
| && $returnType->getName() === $dtoInterface->getName() | |
| && empty($method->getParameters())) { | |
| $w->beginMethodOverride($method, $parentCallArgs); | |
| { | |
| $w->beginBlock("if (\$this->data === null && (\$this->data = $loadDataCall) === null)"); | |
| { | |
| $w->writeln("return null;"); | |
| } | |
| $w->midBlock("else"); | |
| { | |
| // FIXME: To clone or not to clone? | |
| $w->writeln("return clone \$this->data;"); | |
| } | |
| $w->endBlock(); | |
| } | |
| $w->endBlock(); | |
| } | |
| } | |
| } | |
| } | |
| private function getLoadDataCall(PhpFileWriter $w, ?ReflectionClass $dtoClass): string | |
| { | |
| if ($dtoClass->hasMethod('fromArray') && $dtoClass->getMethod('fromArray')->isStatic()) { | |
| return $w->useClass($dtoClass->getName()) . '::fromArray($this->dataSource->loadData($this->getMachineId()))'; | |
| } else { | |
| return '$this->dataSource->loadData($this->getMachineId())'; | |
| } | |
| } | |
| private function generateHydratorMethod(PhpFileWriter $w, StateMachineDefinition $definition, ReflectionClass $sourceClassReflection, ReflectionClass $dtoClass): void | |
| { | |
| if ($sourceClassReflection->hasMethod('hydrateFromArray')) { | |
| throw new InvalidArgumentException('Method hydrateFromArray already defined in class ' . $sourceClassReflection->getName() . '.'); | |
| } | |
| $w->beginStaticMethod('hydrateFromArray', ['self $target', 'array $row'], 'void'); | |
| { | |
| $w->writeln("\$target->data = " . $w->useClass($dtoClass->getName()) . '::fromArray($row);'); | |
| // TODO: Retrieve ID properly. | |
| $idProps = []; | |
| foreach ($definition->getProperties() as $p) { | |
| if ($p->hasExtension(SqlPropertyExtension::class)) { | |
| $sqlExt = $p->getExtension(SqlPropertyExtension::class); | |
| if ($sqlExt->isId()) { | |
| $idProps[] = $p; | |
| } | |
| } | |
| } | |
| $getters = array_map(fn($p) => "\$target->data->get" . ucfirst($p->getName() . "()"), $idProps); | |
| if (count($idProps) === 1) { | |
| $w->writeln("\$target->machineId = " . $getters[0] . ";"); | |
| } else { | |
| $w->writeln("\$target->machineId = [" . join(', ', $getters) . "];"); | |
| } | |
| } | |
| $w->endMethod(); | |
| if ($sourceClassReflection->hasMethod('hydrateWithDTO')) { | |
| throw new InvalidArgumentException('Method hydrateWithDTO already defined in class ' . $sourceClassReflection->getName() . '.'); | |
| } | |
| $w->beginStaticMethod('hydrateWithDTO', ['self $target', $w->useClass($dtoClass->getName()) . ' $dto'], 'void'); | |
| { | |
| $w->writeln("\$target->data = \$dto;"); | |
| } | |
| $w->endMethod(); | |
| } | |
| } |