Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 93
InheritingGenerator
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 3
756
0.00% covered (danger)
0.00%
0 / 93
 writeReferenceClass
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 9
 generateDataGetterMethods
0.00% covered (danger)
0.00%
0 / 1
210
0.00% covered (danger)
0.00%
0 / 55
 generateHydratorMethod
0.00% covered (danger)
0.00%
0 / 1
156
0.00% covered (danger)
0.00%
0 / 29
<?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\InvalidArgumentException;
use Smalldb\StateMachine\ReferenceDataSource\NotExistsException;
use Smalldb\StateMachine\ReferenceInterface;
use Smalldb\StateMachine\RuntimeException;
use Smalldb\PhpFileWriter\PhpFileWriter;
class InheritingGenerator extends AbstractGenerator
{
    /**
     * Generate a new class implementing the missing methods in $sourceReferenceClassName.
     */
    public function writeReferenceClass(PhpFileWriter $w, ReflectionClass $sourceClassReflection, StateMachineDefinition $definition): string
    {
        // Begin the Reference class
        $targetReferenceClassName = $this->beginReferenceClass($w, $sourceClassReflection);
        // Create methods
        $this->generateReferenceMethods($w, $definition);
        $this->generateIdMethods($w);
        $this->generateTransitionMethods($w, $definition, $sourceClassReflection);
        $this->generateDataGetterMethods($w, $definition, $sourceClassReflection);
        $this->generateGetMethod($w, $definition, $sourceClassReflection);
        $this->generateHydratorMethod($w, $definition, $sourceClassReflection);
        $w->endClass();
        return $targetReferenceClassName;
    }
    private function generateDataGetterMethods(PhpFileWriter $w, StateMachineDefinition $definition, ReflectionClass $sourceClassReflection)
    {
        $w->writeln("/** @var bool */");
        $w->writeln('private $dataLoaded = false;');
        $w->beginMethod('invalidateCache', [], 'void');
        {
            $w->writeln('$this->dataLoaded = false;');
            $w->writeln('$this->dataSource->invalidateCache($this->getMachineId());');
        }
        $w->endMethod();
        $w->beginProtectedMethod('loadData', [], 'bool');
        {
            $w->writeln("\$data = \$this->dataSource->loadData(\$this->getMachineId());");
            $w->beginBlock("if (\$data !== null)");
            {
                $w->writeln("static::hydrateFromArray(\$this, \$data);");
                $w->writeln("\$this->dataLoaded = true;");
                $w->writeln("return true;");
            }
            $w->midBlock("else");
            {
                $w->writeln("return false;");
            }
            $w->endBlock();
        }
        $w->endMethod();
        $referenceInterfaceReflection = new ReflectionClass(ReferenceInterface::class);
        foreach ($sourceClassReflection->getMethods() as $method) {
            $methodName = $method->getName();
            if (strncmp('get', $methodName, 3) === 0 && $method->isPublic() && !$w->hasMethod($methodName) && !$referenceInterfaceReflection->hasMethod($methodName)) {
                $w->beginMethodOverride($method, $argCall);
                $w->beginBlock("if (\$this->dataLoaded || \$this->loadData())");
                {
                    $w->writeln("return parent::$methodName(" . join(', ', $argCall) . ");");
                }
                $w->midBlock("else");
                {
                    $w->writeln("throw new " . $w->useClass(NotExistsException::class) . "(\"Cannot load data in the Not Exists state.\");");
                }
                $w->endBlock();
                $w->endMethod();
            }
        }
        // Implement state method
        if (!$w->hasMethod('getState') && ($stateMethod = $sourceClassReflection->getMethod('getState')) && $stateMethod->isAbstract()) {
            $w->beginMethod('getState', [], 'string');
            {
                $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->dataLoaded || \$this->loadData() ? (\$this->state ?? %s) : $notExists;", $theOtherState);
                        break;
                    default:
                        $w->beginBlock("if (\$this->dataLoaded || \$this->loadData())");
                        {
                            $w->beginBlock("if (\$this->state === null)");
                            {
                                $w->writeln("throw new " . $w->useClass(RuntimeException::class) . "('Failed to load state machine state.');");
                            }
                            $w->midBlock("else");
                            {
                                $w->writeln("return \$this->state;");
                            }
                        }
                        $w->midBlock("else");
                        {
                            $w->writeln("return $notExists;");
                        }
                        $w->endBlock();
                        break;
                }
            }
            $w->endMethod();
        }
    }
    private function generateHydratorMethod(PhpFileWriter $w, StateMachineDefinition $definition, ReflectionClass $sourceClassReflection): void
    {
        if ($sourceClassReflection->hasMethod('hydrateFromArray')) {
            throw new InvalidArgumentException('Method hydrateFromArray already defined in class ' . $sourceClassReflection->getName() . '.');
        }
        $w->beginStaticMethod('hydrateFromArray', ['self $target', 'array $row'], 'void');
        {
            foreach ($definition->getProperties() as $property) {
                $name = $property->getName();
                $typehint = $property->getType();
                // Fallback: Get a typehint from getter return type
                if ($typehint === null) {
                    $getterName = 'get' . ucfirst($name);
                    $returnType = $sourceClassReflection->hasMethod($getterName)
                        ? $sourceClassReflection->getMethod($getterName)->getReturnType()
                        : null;
                    $typehint = $returnType ? $returnType->getName() : null;
                }
                // TODO: Support mapping of multiple SQL columns into a single machine property.
                // TODO: Support mapping of a single SQL column (e.g., JSON object) into multiple machine properties.
                // Convert value to fit the typehint
                switch ($typehint) {
                    case 'int':
                    case 'float':
                    case 'bool':
                    case 'string':
                        $w->writeln("\$target->$name = isset(\$row[%s]) ? ($typehint) \$row[%s] : null;", $name, $name);
                        break;
                    default:
                        if ($typehint && class_exists($typehint)) {
                            $c = $w->useClass($typehint);
                            $w->writeln("\$target->$name = (\$v = \$row[%s] ?? null) instanceof $c || \$v === null ? \$v : new $c(\$v);", $name);
                        } else {
                            $w->writeln("\$target->$name = \$row[%s] ?? null;", $name);
                        }
                        break;
                }
            }
            $w->writeln();
            $w->writeln("\$target->state = isset(\$row['state']) ? (string) \$row['state'] : null;");
            $w->writeln("\$target->dataLoaded = true;");
        }
        $w->endMethod();
    }
}