Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
40.00% covered (danger)
40.00%
2 / 5
CRAP
94.12% covered (success)
94.12%
112 / 119
DecoratingGenerator
0.00% covered (danger)
0.00%
0 / 1
40.00% covered (danger)
40.00%
2 / 5
44.39
94.12% covered (success)
94.12%
112 / 119
 writeReferenceClass
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
16 / 16
 detectDtoInterface
0.00% covered (danger)
0.00%
0 / 1
6.40
77.78% covered (warning)
77.78%
7 / 9
 generateDataGetterMethods
0.00% covered (danger)
0.00%
0 / 1
24
95.71% covered (success)
95.71%
67 / 70
 getLoadDataCall
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
3 / 3
 generateHydratorMethod
0.00% covered (danger)
0.00%
0 / 1
7.04
90.48% covered (success)
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();
    }
}