Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
78.02% covered (warning)
78.02%
71 / 91
33.33% covered (danger)
33.33%
3 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
OrmBuilder
78.02% covered (warning)
78.02%
71 / 91
33.33% covered (danger)
33.33%
3 / 9
51.53
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
 validate
15.38% covered (danger)
15.38%
2 / 13
0.00% covered (danger)
0.00%
0 / 1
4.42
 wrapNamespace
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
7.01
 createCurrentPathCode
84.62% covered (warning)
84.62%
11 / 13
0.00% covered (danger)
0.00%
0 / 1
6.13
 createAliasCode
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 createReferencedCode
86.67% covered (warning)
86.67%
13 / 15
0.00% covered (danger)
0.00%
0 / 1
7.12
 createOrm
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 getLastGeneratedCode
62.50% covered (warning)
62.50%
5 / 8
0.00% covered (danger)
0.00%
0 / 1
4.84
 putFile
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
1<?php
2namespace Apie\DoctrineEntityConverter;
3
4use Apie\Core\BoundedContext\BoundedContextHashmap;
5use Apie\Core\Entities\EntityInterface;
6use Apie\DoctrineEntityConverter\Factories\PersistenceLayerFactory;
7use Apie\StorageMetadataBuilder\Lists\GeneratedCodeHashmap;
8use Apie\StorageMetadataBuilder\Resources\GeneratedCodeTimestamp;
9use Doctrine\Common\Collections\Collection;
10use Nette\PhpGenerator\ClassType;
11use Nette\PhpGenerator\PhpNamespace;
12use PhpParser\Error;
13use PhpParser\ParserFactory;
14use PhpParser\PhpVersion;
15use RuntimeException;
16use Symfony\Component\Finder\Finder;
17
18final class OrmBuilder
19{
20    private GeneratedCodeTimestamp $lastGeneratedCode;
21
22    public function __construct(
23        private readonly PersistenceLayerFactory $persistenceLayerFactory,
24        private readonly BoundedContextHashmap $boundedContextHashmap,
25        private readonly bool $validatePhpCode = true,
26        private readonly string $namespace = 'Generated\\ApieEntities'
27    ) {
28    }
29
30    private function validate(string $phpCode, string $tableName): void
31    {
32        $parser = (new ParserFactory)->createForVersion(PhpVersion::fromString('8.3'));
33        try {
34            $parser->parse($phpCode);
35        } catch (Error $error) {
36            throw new RuntimeException(
37                sprintf(
38                    'I rendered an invalid PHP file for table %s. The error message is "%s". The generated code is ' . PHP_EOL . '%s',
39                    $tableName(),
40                    $error->getMessage(),
41                    $phpCode
42                ),
43                0,
44                $error
45            );
46        }
47    }
48
49    private function wrapNamespace(ClassType $classType): string
50    {
51        $namespace = 'Generated\\ApieEntities' . $this->lastGeneratedCode->getId();
52        $type = new PhpNamespace($namespace);
53        $type->addUse('Apie\\StorageMetadata\\Attributes', 'Attr');
54        $type->addUse('Apie\\StorageMetadata\\Interfaces', 'StorageMetadata');
55        $type->addUse(EntityInterface::class);
56        $type->addUse('Doctrine\\ORM\\Mapping', 'DoctrineMapping');
57        $type->addUse(Collection::class);
58        foreach ($classType->getProperties() as $property) {
59            if ($property->getType() && str_starts_with($property->getType(), 'apie_')) {
60                $property->setType($namespace . '\\' . $property->getType());
61            }
62        }
63        foreach ($classType->getMethods() as $method) {
64            if ($method->getReturnType() && str_starts_with($method->getReturnType(), 'apie_')) {
65                $method->setReturnType($namespace . '\\' . $method->getReturnType());
66            }
67        }
68        $type->add($classType);
69        return '<?php' . PHP_EOL . '// @codingStandardsIgnoreStart' . PHP_EOL . $type;
70    }
71
72    private function createCurrentPathCode(string $path, GeneratedCodeHashmap $generatedCode): bool
73    {
74        $modified = false;
75        foreach (Finder::create()->files()->in($path)->name('*.php') as $file) {
76            $filePath = basename($file->getRelativePathname(), '.php');
77            if (!isset($generatedCode[$filePath])) {
78                @unlink($file->getRealPath());
79                $modified = true;
80            }
81        }
82        foreach ($generatedCode as $filePath => $code) {
83            $fileName = $path . DIRECTORY_SEPARATOR . $filePath . '.php';
84            $phpCode = $this->wrapNamespace($code);
85            if ($this->validatePhpCode) {
86                $this->validate($phpCode, $filePath);
87            }
88            $modified = $this->putFile($fileName, $phpCode) || $modified;
89        }
90
91        return $modified;
92    }
93
94    private function createAliasCode(string $className): string
95    {
96        $generatedNamespace = 'Generated\\ApieEntities' . $this->lastGeneratedCode->getId();
97        $buildPath = '/../build' . $this->lastGeneratedCode->getId() . '/' . $className . '.php';
98        return '<?php
99
100        // This file has been auto-generated by Apie for internal use.
101        
102        if (!\class_exists(\\' . $generatedNamespace . '\\' . $className . '::class, false)) {
103            include_once(__DIR__. ' . var_export($buildPath, true) . ');
104        }
105        
106        
107        if (!\class_exists(\\' . $this->namespace . '\\' . $className . '::class, false)) {
108            \class_alias(\\' . $generatedNamespace . '\\' . $className . '::class, \\' . $this->namespace . '\\' . $className . '::class, false);
109        }';
110    }
111
112    private function createReferencedCode(string $currentPath, GeneratedCodeHashmap $hashmap): bool
113    {
114        $generatedCode = [];
115        foreach ($hashmap as $filePath => $code) {
116            $fullPath = $currentPath . '/' . $filePath . '.php';
117            $generatedCode[$fullPath] = $this->createAliasCode($filePath);
118        }
119
120        $modified = false;
121        foreach (Finder::create()->files()->in($currentPath)->name('*.php') as $file) {
122            $filePath = (string) $file;
123            if (!isset($generatedCode[$filePath])) {
124                @unlink($file->getRealPath());
125                $modified = true;
126            }
127        }
128        foreach ($generatedCode as $filePath => $phpCode) {
129            if ($this->validatePhpCode) {
130                $this->validate($phpCode, $filePath);
131            }
132            $modified = $this->putFile($filePath, $phpCode) || $modified;
133        }
134
135        return $modified;
136    }
137
138    public function createOrm(string $path): bool
139    {
140        $tableList = $this->persistenceLayerFactory->create($this->boundedContextHashmap);
141        $this->lastGeneratedCode = new GeneratedCodeTimestamp($tableList->generatedCodeHashmap);
142        $currentPath = $path . '/current';
143        $buildPath = $path . '/build' . $this->lastGeneratedCode->getId();
144        if (!is_dir($currentPath)) {
145            @mkdir($currentPath, recursive: true);
146        }
147        if (!is_dir($buildPath)) {
148            @mkdir($buildPath, recursive: true);
149        }
150        $modified = $this->createCurrentPathCode($buildPath, $tableList->generatedCodeHashmap);
151        $modified = $this->createReferencedCode($currentPath, $tableList->generatedCodeHashmap) || $modified;
152        if ($modified) {
153            file_put_contents($path . '/apie.meta', serialize($this->lastGeneratedCode));
154        }
155        return $modified;
156    }
157
158    public function getLastGeneratedCode(string $path): GeneratedCodeTimestamp
159    {
160        if (!isset($this->lastGeneratedCode)) {
161            $this->lastGeneratedCode = new GeneratedCodeTimestamp(new GeneratedCodeHashmap());
162            $lastContents = @file_get_contents($path . '/apie.meta');
163            if ($lastContents) {
164                $lastGeneratedCode = @unserialize($lastContents);
165                if ($lastGeneratedCode instanceof GeneratedCodeTimestamp) {
166                    $this->lastGeneratedCode = $lastGeneratedCode;
167                }
168            }
169        }
170        return $this->lastGeneratedCode;
171    }
172
173    private function putFile(string $fileName, string $phpCode): bool
174    {
175        if (is_readable($fileName) && file_get_contents($fileName) === $phpCode) {
176            // this keeps the current modification date active
177            return false;
178        }
179        if (false === @file_put_contents($fileName, $phpCode)) {
180            throw new RuntimeException(sprintf('Could not write file "%s"', $fileName));
181        }
182
183        return true;
184    }
185}