Passed
Push — master ( c067d3...1dee6e )
by Gerrit
03:50
created

relativePathFromTo()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 2
eloc 9
c 1
b 1
f 0
nc 2
nop 2
dl 0
loc 19
rs 9.9666
1
<?php
2
/**
3
 * Copyright (C) 2019 Gerrit Addiks.
4
 * This package (including this file) was released under the terms of the GPL-3.0.
5
 * You should have received a copy of the GNU General Public License along with this program.
6
 * If not, see <http://www.gnu.org/licenses/> or send me a mail so i can send you a copy.
7
 *
8
 * @license GPL-3.0
9
 *
10
 * @author Gerrit Addiks <[email protected]>
11
 */
12
13
namespace Addiks\RDMBundle\DataLoader\BlackMagic;
14
15
use Addiks\RDMBundle\Mapping\EntityMappingInterface;
16
use Composer\Autoload\ClassLoader;
17
use Webmozart\Assert\Assert;
18
use Addiks\RDMBundle\Mapping\MappingInterface;
19
use ErrorException;
20
use Doctrine\DBAL\Schema\Column;
21
use Addiks\RDMBundle\DataLoader\BlackMagic\BlackMagicDataLoader;
22
23
final class BlackMagicEntityCodeGenerator
24
{
25
26
    public function __construct(
27
        public readonly string $targetDirectory,
28
        private BlackMagicDataLoader $dataLoader,
29
        public readonly string $indenting = "    "
30
    ) {
31
    }
32
    
33
    public function processMapping(
34
        EntityMappingInterface $mapping,
35
        ClassLoader $loader
36
    ): string|null {
37
        /** @var array<array-key, Column> $columns */
38
        $columns = $mapping->collectDBALColumns();
39
        
40
        if (empty($columns)) {
41
            return null;
42
        }
43
        
44
        /** @var string $fullClassName */
45
        $fullClassName = $mapping->getEntityClassName();
46
        
47
        /** @var string|false $filePath */
48
        $filePath = $loader->findFile($fullClassName);
49
        
50
        if ($filePath === false) {
51
            return null;
52
        }
53
54
        /** @var array<string, bool> $walkedFiles */
55
        $walkedFiles = [$filePath => true];
56
        
57
        /** @var string $safeFilePath */
58
        $safeFilePath = $filePath;
59
60
        do {
61
            if (!file_exists($filePath)) {
62
                $filePath = $safeFilePath;
63
                break;
64
            }
65
            
66
            /** @var string $entityPHP */
67
            $entityPHP = file_get_contents($filePath);
68
69
            if (1 === preg_match("#\/\*\* \@addiks-original-file ([^\*]*) \*\/#is", $entityPHP, $matches)) {
70
                $safeFilePath = $filePath;
71
                $filePath = trim($matches[1]);
72
73
                if (!file_exists($filePath)) {
74
                    /** @var string $relativePath */
75
                    $relativePath = dirname($safeFilePath) . '/' . $filePath;
76
77
                    if (file_exists($relativePath)) {
78
                        $filePath = realpath($relativePath);
79
                    }
80
                }
81
82
                if (isset($walkedFiles[$filePath])) {
83
                    break; # Circular reference detected
84
                }
85
                $walkedFiles[$filePath] = true;
86
                continue;
87
            }
88
            break;
89
        } while (true);
90
        
91
        /** @var int $classStartPosition */
92
        $classStartPosition = self::findClassStartPosition($fullClassName, $entityPHP);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $entityPHP does not seem to be defined for all execution paths leading up to this point.
Loading history...
93
        
94
        /** @var array<string, Column> $writtenFieldNames */
95
        $writtenFieldNames = array();
96
        
97
        foreach ($columns as $column) {
98
            
99
            /** @var string $fieldName */
100
            $fieldName = $this->dataLoader->columnToFieldName($column);
101
            
102
            /** @var string $fieldPHP */
103
            $fieldPHP = sprintf(
104
                "\n%spublic $%s;\n",
105
                $this->indenting,
106
                $fieldName
107
            );
108
            
109
            if (isset($writtenFieldNames[$fieldName]) || str_contains($entityPHP, $fieldPHP)) {
110
                continue;
111
            }
112
113
            $writtenFieldNames[$fieldName] = $column;
114
115
            $entityPHP = sprintf(
116
                '%s%s%s',
117
                substr($entityPHP, 0, $classStartPosition),
118
                $fieldPHP,
119
                substr($entityPHP, $classStartPosition)
120
            );
121
        }
122
        
123
        $targetFilePath = sprintf(
124
            '%s/%s.php',
125
            $this->targetDirectory,
126
            str_replace('\\', '_', $fullClassName)
127
        );
128
        
129
        $entityPHP .= sprintf(
130
            "\n\n/** @addiks-original-file %s */\n",
131
            self::relativePathFromTo($targetFilePath, realpath($filePath))
132
        );
133
134
        if (!is_dir($this->targetDirectory)) {
135
            mkdir($this->targetDirectory, 0777, true);
136
        }
137
        
138
        file_put_contents($targetFilePath, $entityPHP);
139
        
140
        return $targetFilePath;
141
    }
142
    
143
    private static function relativePathFromTo(string $originPath, string $targetPath): string
144
    {
145
        /** @var array<int, string> $originPathSteps */
146
        $originPathSteps = explode('/', $originPath);
147
148
        /** @var mixed $targetPathSteps */
149
        $targetPathSteps = explode('/', $targetPath);
150
151
        while ($originPathSteps[0] === $targetPathSteps[0]) {
152
            array_shift($originPathSteps);
153
            array_shift($targetPathSteps);
154
        }
155
156
        $relativePathSteps = array_merge(
157
            array_pad([], count($originPathSteps) - 1, '..'),
158
            $targetPathSteps
159
        );
160
161
        return implode('/', $relativePathSteps);
162
    }
163
164
    private static function findClassStartPosition(
165
        string $fullClassName, 
166
        string $entityPHP
167
    ): int {
168
        
169
        /** @var int|null|false $classStartPosition */
170
        $classStartPosition = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $classStartPosition is dead and can be removed.
Loading history...
171
        
172
        /** @var int $position */
173
        $position = 0;
174
        
175
        /** @var array<int, string> $namespacePath */
176
        $namespacePath = explode("\\", $fullClassName);
177
        
178
        /** @var string $className */
179
        $className = array_pop($namespacePath);
180
        
181
        /** @var string $pattern */
182
        $pattern = sprintf(
183
            '/^(interface|trait|class)\s+%s\W*/is',
184
            $className
185
        );
186
        
187
        /** @var int $codeLength */
188
        $codeLength = strlen($entityPHP);
189
        
190
        do {
191
            if (preg_match($pattern, substr($entityPHP, $position, 128))) {
192
                $classStartPosition = strpos($entityPHP, '{', $position);
193
                
194
                if (is_int($classStartPosition)) {
195
                    return $classStartPosition + 1;
196
197
                } else {
198
                    $classStartPosition = null;
199
                }
200
            }
201
            
202
            self::skipIrrelevantCode($entityPHP, $position);
203
            
204
            $position++;
205
        } while ($position < $codeLength);
206
        
207
        throw new ErrorException(sprintf(
208
            'Could not find start position of class "%s" if file "%s"!',
209
            $fullClassName,
210
            $entityPHP
211
        ));
212
    }
213
    
214
    private static function skipIrrelevantCode(string $phpCode, int &$position): void
215
    {
216
        /** @var int $codeLength */
217
        $codeLength = strlen($phpCode);
218
        
219
        if (substr($phpCode, $position, 2) === '/*') {
220
            do {
221
                $position++;
222
            } while (substr($phpCode, $position, 2) !== '*/' && $position < $codeLength);
223
            
224
        } elseif (substr($phpCode, $position, 2) === '//') {
225
            do {
226
                $position++;
227
            } while ($phpCode[$position] !== "\n" && $position < $codeLength);
228
            
229
        } elseif (substr($phpCode, $position, 3) === '<<<') {
230
            $position += 3;
231
            $eofFlag = "";
232
            do {
233
                $eofFlag .= $phpCode[$position];
234
                $position++;
235
            } while ($phpCode[$position] !== "\n");
236
            
237
            do {
238
                $position++;
239
                $charsAtPosition = substr($phpCode, $position, strlen($eofFlag) + 1);
240
            } while ($charsAtPosition !== $eofFlag . ';' && $position < $codeLength);
241
            
242
        } elseif ($phpCode[$position] === '"') {
243
            do {
244
                $position++;
245
            } while ($phpCode[$position] !== '"' && $phpCode[$position - 1] !== '\\' && $position < $codeLength);
246
247
        } elseif ($phpCode[$position] === "'") {
248
            do {
249
                $position++;
250
            } while ($phpCode[$position] !== "'" && $phpCode[$position - 1] !== '\\' && $position < $codeLength);
251
        }
252
    }
253
    
254
}
255