Issues (258)

BlackMagic/BlackMagicEntityCodeGenerator.php (2 issues)

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
            $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
The assignment to $classStartPosition is dead and can be removed.
Loading history...
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