Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.45% covered (success)
95.45%
126 / 132
70.00% covered (warning)
70.00%
7 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
CodeWriter
95.45% covered (success)
95.45%
126 / 132
70.00% covered (warning)
70.00%
7 / 10
43
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 startWriting
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 writeIdentifier
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
3
 writeResource
96.10% covered (success)
96.10%
74 / 77
0.00% covered (danger)
0.00%
0 / 1
22
 addOrGetNamespace
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 addOrGetUse
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
2.50
 addOrGetClass
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
3.21
 addOrGetImplement
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 addOrGetMethod
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 addOrGetProperty
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2namespace Apie\Maker\BoundedContext\Services;
3
4use Apie\Core\Entities\EntityInterface;
5use Apie\Core\Identifiers\IdentifierInterface;
6use Apie\Core\Other\FileReaderInterface;
7use Apie\Core\Other\FileWriterInterface;
8use Apie\Core\ValueObjects\Utils;
9use Apie\Maker\BoundedContext\Dtos\PropertyDefinition;
10use Apie\Maker\BoundedContext\Interfaces\CodeWriterConfigurationInterface;
11use Apie\Maker\BoundedContext\Resources\BoundedContextDefinition;
12use Apie\Maker\BoundedContext\Resources\ResourceDefinition;
13use Apie\Maker\Enums\NullableOption;
14use Apie\Maker\Enums\OverwriteStrategy;
15use Apie\Maker\Enums\PrimitiveType;
16use Nette\InvalidArgumentException;
17use Nette\InvalidStateException;
18use Nette\PhpGenerator\ClassType;
19use Nette\PhpGenerator\Method;
20use Nette\PhpGenerator\Parameter;
21use Nette\PhpGenerator\PhpFile;
22use Nette\PhpGenerator\PhpNamespace;
23use Nette\PhpGenerator\PromotedParameter;
24use Nette\PhpGenerator\Property;
25use Nette\PhpGenerator\PsrPrinter;
26use ReflectionClass;
27
28final class CodeWriter
29{
30    public function __construct(private readonly FileWriterInterface&FileReaderInterface $fileWriter)
31    {
32    }
33    public function startWriting(CodeWriterConfigurationInterface $log): void
34    {
35        if ($log->getOverwriteStrategy() === OverwriteStrategy::Reset) {
36            $this->fileWriter->clearPath($log->getTargetPath());
37        }
38    }
39
40    public function writeIdentifier(
41        CodeWriterConfigurationInterface $log,
42        ResourceDefinition $resourceDefinition,
43        BoundedContextDefinition $boundedContextDefinition
44    ): void {
45        $boundedNs = $boundedContextDefinition->name->toPascalCaseSlug()->toNative();
46        $targetNamespace = $log->getTargetNamespace(
47            $boundedNs . '\\Identifiers'
48        );
49        $targetFile = $log->getTargetPath() . '/' . $boundedNs . '/Identifiers/' . $resourceDefinition->getName()->toNative() . 'Identifier.php';
50        if ($log->getOverwriteStrategy() === OverwriteStrategy::Merge && $this->fileWriter->fileExists($targetFile)) {
51            $file = PhpFile::fromCode($this->fileWriter->readContents($targetFile));
52        } else {
53            $file = new PhpFile();
54        }
55
56        $namespace = $this->addOrGetNamespace($file, $targetNamespace);
57        $targetIdentifier = $log->getTargetNamespace(
58            $boundedNs . '\\Resources\\' . $resourceDefinition->getName()->toNative()
59        );
60        $this->addOrGetUse($namespace, ReflectionClass::class);
61        $this->addOrGetUse($namespace, IdentifierInterface::class);
62        $this->addOrGetUse($namespace, $targetIdentifier);
63        $this->addOrGetUse($namespace, $resourceDefinition->idType->value);
64        $class = $this->addOrGetClass($namespace, $resourceDefinition->getName()->toNative() . 'Identifier');
65        $class->setExtends($resourceDefinition->idType->value);
66        $this->addOrGetImplement($class, IdentifierInterface::class);
67
68        $method = $this->addOrGetMethod($class, 'getReferenceFor');
69        $method->setStatic(true);
70        $method->setBody('return new ReflectionClass(' . $resourceDefinition->getName()->toNative() . '::class);');
71        $method->setReturnType(ReflectionClass::class);
72
73        $printer = new PsrPrinter();
74        $this->fileWriter->writeFile($targetFile, $printer->printFile($file));
75    }
76
77    public function writeResource(
78        CodeWriterConfigurationInterface $log,
79        ResourceDefinition $resourceDefinition,
80        BoundedContextDefinition $boundedContextDefinition
81    ): void {
82        $boundedNs = $boundedContextDefinition->name->toPascalCaseSlug()->toNative();
83        $targetNamespace = $log->getTargetNamespace(
84            $boundedNs . '\\Resources'
85        );
86        $targetFile = $log->getTargetPath() . '/' . $boundedNs . '/Resources/' . $resourceDefinition->getName()->toNative() . '.php';
87        if ($log->getOverwriteStrategy() === OverwriteStrategy::Merge && $this->fileWriter->fileExists($targetFile)) {
88            $file = PhpFile::fromCode($this->fileWriter->readContents($targetFile));
89        } else {
90            $file = new PhpFile();
91        }
92        $namespace = $this->addOrGetNamespace($file, $targetNamespace);
93        $class = $this->addOrGetClass($namespace, $resourceDefinition->getName()->toNative());
94        $this->addOrGetImplement($class, EntityInterface::class);
95        $this->addOrGetUse($namespace, EntityInterface::class);
96
97        $constructorArguments = [];
98
99        
100        $idClass = $resourceDefinition->getName()->toNative() . 'Identifier';
101        $targetIdentifier = $log->getTargetNamespace($boundedNs . '\\Identifiers\\' . $idClass);
102        $constructorArgument = $resourceDefinition->idType->toConstructorArgument($targetIdentifier);
103        if (!($constructorArgument instanceof PromotedParameter)) {
104            $this->addOrGetProperty($class, 'id', $targetIdentifier);
105        }
106        if ($constructorArgument instanceof Parameter) {
107            $constructorArguments[] = $constructorArgument;
108        }
109        $this->addOrgetUse($namespace, $targetIdentifier);
110
111        $constructor = $this->addOrGetMethod($class, '__construct');
112        $constructor->setParameters($constructorArguments);
113        $constructor->setBody($resourceDefinition->idType->toConstructorBody($idClass));
114        $constructor->setStatic(false);
115
116        $getId = $this->addOrGetMethod($class, 'getId');
117        $getId->setBody('return $this->id;');
118        $getId->setReturnType($targetIdentifier);
119
120        foreach ($resourceDefinition->getProperties() as $propertyDefinition) {
121            /** @var PropertyDefinition $propertyDefinition  */
122            if (!($propertyDefinition->type instanceof PrimitiveType)) {
123                $this->addOrGetUse($namespace, $propertyDefinition->type->toNative());
124            }
125            $propertyName = $propertyDefinition->name->toNative();
126            $propertyType = Utils::toString($propertyDefinition->type);
127            $propertyNullable = ($propertyDefinition->nullable === NullableOption::NeverNullable || $propertyType === 'null')
128                ? ''
129                : '?';
130            
131            if ($propertyDefinition->requiredOnConstruction) {
132                if ($propertyDefinition->nullable !== NullableOption::NeverNullable || $propertyType === 'null') {
133                    $property = $constructor->addPromotedParameter($propertyName, null);
134                } else {
135                    $property = $constructor->addPromotedParameter($propertyName);
136                }
137                $property->setType($propertyNullable . $propertyType);
138                $property->setPrivate();
139
140                if ($class->hasProperty($propertyName)) {
141                    $class->removeProperty($propertyName);
142                }
143            } else {
144                $property = $this->addOrGetProperty($class, $propertyName, $propertyNullable . $propertyType, null);
145                $property->setInitialized($propertyDefinition->nullable !== NullableOption::NeverNullable || $propertyType === 'null');
146            }
147            
148            if ($propertyDefinition->writable) {
149                $setter = $this->addOrGetMethod($class, 'set' . ucfirst($propertyName));
150                $parameter = new Parameter($propertyName);
151                if ($propertyDefinition->nullable === NullableOption::AlwaysNull) {
152                    $parameter->setType($propertyNullable . $propertyType);
153                } else {
154                    $parameter->setType($propertyType);
155                }
156                $parameters = array_keys($setter->getParameters());
157                if (empty($parameters)) {
158                    $parameters[] = $parameter;
159                } else {
160                    $parameters[count($parameters) - 1] = $parameter;
161                }
162                $setter->setParameters($parameters);
163                $setter->setBody('$this->' . $propertyName . ' = $' . $propertyName . ';');
164            }
165
166            if ($propertyDefinition->readable) {
167                $getter = $this->addOrGetMethod($class, 'get' . ucfirst($propertyName));
168                $getter->setReturnType($propertyNullable . $propertyType);
169                $body = '';
170                if ($propertyNullable === '' && !$propertyDefinition->requiredOnConstruction) {
171                    $body .= 'if (!isset($this->' . $propertyName . ')) {' . PHP_EOL;
172                    $body .= '    throw new \LogicException("Property \"' . $propertyName . '\" is not set yet!");' . PHP_EOL;
173                    $body .= '}' . PHP_EOL;
174                }
175                $getter->setBody($body . 'return $this->' . $propertyName . ';');
176            }
177        }
178        // TODO sort constructor arguments in without default value and with default value
179        $constructorArguments = $constructor->getParameters();
180        $withDefaultValues = [];
181        $withoutDefaultValues = [];
182        foreach ($constructorArguments as $constructorArgument) {
183            if ($constructorArgument->hasDefaultValue()) {
184                $withDefaultValues[] = $constructorArgument;
185            } else {
186                $withoutDefaultValues[] = $constructorArgument;
187            }
188        }
189        $constructor->setParameters([...$withoutDefaultValues, ...$withDefaultValues]);
190
191        $printer = new PsrPrinter();
192        $this->fileWriter->writeFile($targetFile, $printer->printFile($file));
193    }
194
195    private function addOrGetNamespace(PhpFile $file, string $namespaceName): PhpNamespace
196    {
197        $namespaces = $file->getNamespaces();
198        $namespaceNameCmp = strtolower($namespaceName);
199        foreach ($namespaces as $namespace) {
200            if (strtolower($namespace->getName()) === $namespaceNameCmp) {
201                return $namespace;
202            }
203        }
204        return $file->addNamespace($namespaceName);
205    }
206
207    private function addOrGetUse(PhpNamespace $namespace, string $use): void
208    {
209        try {
210            $namespace->addUse($use);
211        } catch (InvalidStateException) {
212        }
213    }
214
215    private function addOrGetClass(PhpNamespace $namespace, string $className): ClassType
216    {
217        try {
218            $classType = $namespace->getClass($className);
219            if ($classType instanceof ClassType) {
220                return $classType;
221            }
222            $namespace->removeClass($className);
223            return $namespace->addClass($className);
224        } catch (InvalidArgumentException) {
225            return $namespace->addClass($className);
226        }
227    }
228
229    private function addOrGetImplement(ClassType $class, string $interfaceName): void
230    {
231        if (!in_array($interfaceName, $class->getImplements())) {
232            $class->addImplement($interfaceName);
233        }
234    }
235
236    private function addOrGetMethod(ClassType $class, string $methodName): Method
237    {
238        $methodNameCmp = strtolower($methodName);
239        foreach ($class->getMethods() as $method) {
240            if (strtolower($method->getName()) === $methodNameCmp) {
241                return $method;
242            }
243        }
244        return $class->addMethod($methodName);
245    }
246
247    private function addOrGetProperty(
248        ClassType $class,
249        string $propertyName,
250        string $propertyType = 'mixed',
251        mixed $defaultValue = null
252    ): Property {
253        $type = $class->addProperty($propertyName, value: $defaultValue, overwrite: true);
254        $type->setInitialized($defaultValue !== null || count(func_get_args()) > 3);
255        $type->setType($propertyType);
256        $type->setPrivate();
257        return $type;
258    }
259}