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 (isset($walkedFiles[$filePath])) { |
||
74 | break; # Circular reference detected |
||
75 | } |
||
76 | $walkedFiles[$filePath] = true; |
||
77 | continue; |
||
78 | } |
||
79 | break; |
||
80 | } while (true); |
||
81 | |||
82 | /** @var int $classStartPosition */ |
||
83 | $classStartPosition = self::findClassStartPosition($fullClassName, $entityPHP); |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
Loading history...
|
|||
84 | |||
85 | /** @var array<string, Column> $writtenFieldNames */ |
||
86 | $writtenFieldNames = array(); |
||
87 | |||
88 | foreach ($columns as $column) { |
||
89 | |||
90 | /** @var string $fieldName */ |
||
91 | $fieldName = $this->dataLoader->columnToFieldName($column); |
||
92 | |||
93 | /** @var string $fieldPHP */ |
||
94 | $fieldPHP = sprintf( |
||
95 | "\n%spublic $%s;\n", |
||
96 | $this->indenting, |
||
97 | $fieldName |
||
98 | ); |
||
99 | |||
100 | if (isset($writtenFieldNames[$fieldName]) || str_contains($entityPHP, $fieldPHP)) { |
||
101 | continue; |
||
102 | } |
||
103 | |||
104 | $writtenFieldNames[$fieldName] = $column; |
||
105 | |||
106 | $entityPHP = sprintf( |
||
107 | '%s%s%s', |
||
108 | substr($entityPHP, 0, $classStartPosition), |
||
109 | $fieldPHP, |
||
110 | substr($entityPHP, $classStartPosition) |
||
111 | ); |
||
112 | } |
||
113 | |||
114 | $entityPHP .= sprintf( |
||
115 | "\n\n/** @addiks-original-file %s */\n", |
||
116 | $filePath |
||
117 | ); |
||
118 | |||
119 | $targetFilePath = sprintf( |
||
120 | '%s/%s.php', |
||
121 | $this->targetDirectory, |
||
122 | str_replace('\\', '_', $fullClassName) |
||
123 | ); |
||
124 | |||
125 | if (!is_dir($this->targetDirectory)) { |
||
126 | mkdir($this->targetDirectory, 0777, true); |
||
127 | } |
||
128 | |||
129 | file_put_contents($targetFilePath, $entityPHP); |
||
130 | |||
131 | return $targetFilePath; |
||
132 | } |
||
133 | |||
134 | private static function findClassStartPosition( |
||
135 | string $fullClassName, |
||
136 | string $entityPHP |
||
137 | ): int { |
||
138 | |||
139 | /** @var int|null|false $classStartPosition */ |
||
140 | $classStartPosition = null; |
||
0 ignored issues
–
show
|
|||
141 | |||
142 | /** @var int $position */ |
||
143 | $position = 0; |
||
144 | |||
145 | /** @var array<int, string> $namespacePath */ |
||
146 | $namespacePath = explode("\\", $fullClassName); |
||
147 | |||
148 | /** @var string $className */ |
||
149 | $className = array_pop($namespacePath); |
||
150 | |||
151 | /** @var string $pattern */ |
||
152 | $pattern = sprintf( |
||
153 | '/^(interface|trait|class)\s+%s\W*/is', |
||
154 | $className |
||
155 | ); |
||
156 | |||
157 | /** @var int $codeLength */ |
||
158 | $codeLength = strlen($entityPHP); |
||
159 | |||
160 | do { |
||
161 | if (preg_match($pattern, substr($entityPHP, $position, 128))) { |
||
162 | $classStartPosition = strpos($entityPHP, '{', $position); |
||
163 | |||
164 | if (is_int($classStartPosition)) { |
||
165 | return $classStartPosition + 1; |
||
166 | |||
167 | } else { |
||
168 | $classStartPosition = null; |
||
169 | } |
||
170 | } |
||
171 | |||
172 | self::skipIrrelevantCode($entityPHP, $position); |
||
173 | |||
174 | $position++; |
||
175 | } while ($position < $codeLength); |
||
176 | |||
177 | throw new ErrorException(sprintf( |
||
178 | 'Could not find start position of class "%s" if file "%s"!', |
||
179 | $fullClassName, |
||
180 | $entityPHP |
||
181 | )); |
||
182 | } |
||
183 | |||
184 | private static function skipIrrelevantCode(string $phpCode, int &$position): void |
||
185 | { |
||
186 | /** @var int $codeLength */ |
||
187 | $codeLength = strlen($phpCode); |
||
188 | |||
189 | if (substr($phpCode, $position, 2) === '/*') { |
||
190 | do { |
||
191 | $position++; |
||
192 | } while (substr($phpCode, $position, 2) !== '*/' && $position < $codeLength); |
||
193 | |||
194 | } elseif (substr($phpCode, $position, 2) === '//') { |
||
195 | do { |
||
196 | $position++; |
||
197 | } while ($phpCode[$position] !== "\n" && $position < $codeLength); |
||
198 | |||
199 | } elseif (substr($phpCode, $position, 3) === '<<<') { |
||
200 | $position += 3; |
||
201 | $eofFlag = ""; |
||
202 | do { |
||
203 | $eofFlag .= $phpCode[$position]; |
||
204 | $position++; |
||
205 | } while ($phpCode[$position] !== "\n"); |
||
206 | |||
207 | do { |
||
208 | $position++; |
||
209 | $charsAtPosition = substr($phpCode, $position, strlen($eofFlag) + 1); |
||
210 | } while ($charsAtPosition !== $eofFlag . ';' && $position < $codeLength); |
||
211 | |||
212 | } elseif ($phpCode[$position] === '"') { |
||
213 | do { |
||
214 | $position++; |
||
215 | } while ($phpCode[$position] !== '"' && $phpCode[$position - 1] !== '\\' && $position < $codeLength); |
||
216 | |||
217 | } elseif ($phpCode[$position] === "'") { |
||
218 | do { |
||
219 | $position++; |
||
220 | } while ($phpCode[$position] !== "'" && $phpCode[$position - 1] !== '\\' && $position < $codeLength); |
||
221 | } |
||
222 | } |
||
223 | |||
224 | } |
||
225 |