Passed
Push — master ( 0ab53d...d8b364 )
by Gerrit
15:09
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 (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
The variable $entityPHP does not seem to be defined for all execution paths leading up to this point.
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
            self::relativePathFromTo($targetFilePath, $filePath)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $targetFilePath seems to be never defined.
Loading history...
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 relativePathFromTo(string $originPath, string $targetPath): string
135
    {
136
        /** @var array<int, string> $originPathSteps */
137
        $originPathSteps = explode('/', $originPath);
138
139
        /** @var mixed $targetPathSteps */
140
        $targetPathSteps = explode('/', $targetPath);
141
142
        while ($originPathSteps[0] === $targetPathSteps[0]) {
143
            array_shift($originPathSteps);
144
            array_shift($targetPathSteps);
145
        }
146
147
        $relativePathSteps = array_merge(
148
            array_pad([], count($originPathSteps) - 1, '..'),
149
            $targetPathSteps
150
        );
151
152
        return implode('/', $relativePathSteps);
153
    }
154
155
    private static function findClassStartPosition(
156
        string $fullClassName, 
157
        string $entityPHP
158
    ): int {
159
        
160
        /** @var int|null|false $classStartPosition */
161
        $classStartPosition = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $classStartPosition is dead and can be removed.
Loading history...
162
        
163
        /** @var int $position */
164
        $position = 0;
165
        
166
        /** @var array<int, string> $namespacePath */
167
        $namespacePath = explode("\\", $fullClassName);
168
        
169
        /** @var string $className */
170
        $className = array_pop($namespacePath);
171
        
172
        /** @var string $pattern */
173
        $pattern = sprintf(
174
            '/^(interface|trait|class)\s+%s\W*/is',
175
            $className
176
        );
177
        
178
        /** @var int $codeLength */
179
        $codeLength = strlen($entityPHP);
180
        
181
        do {
182
            if (preg_match($pattern, substr($entityPHP, $position, 128))) {
183
                $classStartPosition = strpos($entityPHP, '{', $position);
184
                
185
                if (is_int($classStartPosition)) {
186
                    return $classStartPosition + 1;
187
188
                } else {
189
                    $classStartPosition = null;
190
                }
191
            }
192
            
193
            self::skipIrrelevantCode($entityPHP, $position);
194
            
195
            $position++;
196
        } while ($position < $codeLength);
197
        
198
        throw new ErrorException(sprintf(
199
            'Could not find start position of class "%s" if file "%s"!',
200
            $fullClassName,
201
            $entityPHP
202
        ));
203
    }
204
    
205
    private static function skipIrrelevantCode(string $phpCode, int &$position): void
206
    {
207
        /** @var int $codeLength */
208
        $codeLength = strlen($phpCode);
209
        
210
        if (substr($phpCode, $position, 2) === '/*') {
211
            do {
212
                $position++;
213
            } while (substr($phpCode, $position, 2) !== '*/' && $position < $codeLength);
214
            
215
        } elseif (substr($phpCode, $position, 2) === '//') {
216
            do {
217
                $position++;
218
            } while ($phpCode[$position] !== "\n" && $position < $codeLength);
219
            
220
        } elseif (substr($phpCode, $position, 3) === '<<<') {
221
            $position += 3;
222
            $eofFlag = "";
223
            do {
224
                $eofFlag .= $phpCode[$position];
225
                $position++;
226
            } while ($phpCode[$position] !== "\n");
227
            
228
            do {
229
                $position++;
230
                $charsAtPosition = substr($phpCode, $position, strlen($eofFlag) + 1);
231
            } while ($charsAtPosition !== $eofFlag . ';' && $position < $codeLength);
232
            
233
        } elseif ($phpCode[$position] === '"') {
234
            do {
235
                $position++;
236
            } while ($phpCode[$position] !== '"' && $phpCode[$position - 1] !== '\\' && $position < $codeLength);
237
238
        } elseif ($phpCode[$position] === "'") {
239
            do {
240
                $position++;
241
            } while ($phpCode[$position] !== "'" && $phpCode[$position - 1] !== '\\' && $position < $codeLength);
242
        }
243
    }
244
    
245
}
246