|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
namespace steevanb\ComposerOverloadClass; |
|
4
|
|
|
|
|
5
|
|
|
use Composer\Script\Event; |
|
6
|
|
|
use Composer\IO\IOInterface; |
|
7
|
|
|
|
|
8
|
|
|
class OverloadClass |
|
9
|
|
|
{ |
|
10
|
|
|
const EXTRA_OVERLOAD_CACHE_DIR = 'composer-overload-cache-dir'; |
|
11
|
|
|
const EXTRA_OVERLOAD_CACHE_DIR_DEV = 'composer-overload-cache-dir-dev'; |
|
12
|
|
|
const EXTRA_OVERLOAD_CLASS = 'composer-overload-class'; |
|
13
|
|
|
const EXTRA_OVERLOAD_CLASS_DEV = 'composer-overload-class-dev'; |
|
14
|
|
|
const EXTRA_OVERLOAD_DUPLICATE_ORIGINAL_FILE = 'duplicate-original-file'; |
|
15
|
|
|
const NAMESPACE_PREFIX = 'ComposerOverloadClass'; |
|
16
|
|
|
|
|
17
|
|
|
public static function overload(Event $event) |
|
18
|
|
|
{ |
|
19
|
|
|
static::defineAutoloadExcludeFromClassmap($event); |
|
20
|
|
|
static::defineAutoloadFiles($event); |
|
21
|
|
|
} |
|
22
|
|
|
|
|
23
|
|
|
protected static function defineAutoloadExcludeFromClassmap(Event $event) |
|
24
|
|
|
{ |
|
25
|
|
|
$originalFiles = ($event->isDevMode()) |
|
26
|
|
|
? [static::EXTRA_OVERLOAD_CLASS, static::EXTRA_OVERLOAD_CLASS_DEV] |
|
27
|
|
|
: [static::EXTRA_OVERLOAD_CLASS]; |
|
28
|
|
|
$overloadFiles = ($event->isDevMode()) ? [] : [static::EXTRA_OVERLOAD_CLASS_DEV]; |
|
29
|
|
|
$autoload = static::getAutoload($event); |
|
30
|
|
|
$extra = $event->getComposer()->getPackage()->getExtra(); |
|
31
|
|
|
|
|
32
|
|
View Code Duplication |
foreach ($originalFiles as $env) { |
|
33
|
|
|
if (array_key_exists($env, $extra)) { |
|
34
|
|
|
foreach ($extra[$env] as $className => $infos) { |
|
35
|
|
|
$autoload['exclude-from-classmap'][] = $infos['original-file']; |
|
36
|
|
|
} |
|
37
|
|
|
} |
|
38
|
|
|
} |
|
39
|
|
|
|
|
40
|
|
View Code Duplication |
foreach ($overloadFiles as $env) { |
|
41
|
|
|
if (array_key_exists($env, $extra)) { |
|
42
|
|
|
foreach ($extra[$env] as $className => $infos) { |
|
43
|
|
|
$autoload['exclude-from-classmap'][] = $infos['overload-file']; |
|
44
|
|
|
} |
|
45
|
|
|
} |
|
46
|
|
|
} |
|
47
|
|
|
|
|
48
|
|
|
$event->getComposer()->getPackage()->setAutoload($autoload); |
|
49
|
|
|
} |
|
50
|
|
|
|
|
51
|
|
|
protected static function defineAutoloadFiles(Event $event) |
|
52
|
|
|
{ |
|
53
|
|
|
$extra = $event->getComposer()->getPackage()->getExtra(); |
|
54
|
|
|
|
|
55
|
|
|
if ($event->isDevMode()) { |
|
56
|
|
|
$envs = [static::EXTRA_OVERLOAD_CLASS, static::EXTRA_OVERLOAD_CLASS_DEV]; |
|
57
|
|
|
$cacheDirKey = static::EXTRA_OVERLOAD_CACHE_DIR_DEV; |
|
58
|
|
|
if (array_key_exists($cacheDirKey, $extra) === false) { |
|
59
|
|
|
$cacheDirKey = static::EXTRA_OVERLOAD_CACHE_DIR; |
|
60
|
|
|
} |
|
61
|
|
|
} else { |
|
62
|
|
|
$envs = [static::EXTRA_OVERLOAD_CLASS]; |
|
63
|
|
|
$cacheDirKey = static::EXTRA_OVERLOAD_CACHE_DIR; |
|
64
|
|
|
} |
|
65
|
|
|
if (array_key_exists($cacheDirKey, $extra) === false) { |
|
66
|
|
|
throw new \Exception('You must specify extra/' . $cacheDirKey . ' in composer.json'); |
|
67
|
|
|
} |
|
68
|
|
|
$cacheDir = $extra[$cacheDirKey]; |
|
69
|
|
|
|
|
70
|
|
|
$autoload = static::getAutoload($event); |
|
71
|
|
|
|
|
72
|
|
|
foreach ($envs as $extraKey) { |
|
73
|
|
|
if (array_key_exists($extraKey, $extra)) { |
|
74
|
|
|
foreach ($extra[$extraKey] as $className => $infos) { |
|
75
|
|
|
if ( |
|
76
|
|
|
array_key_exists(static::EXTRA_OVERLOAD_DUPLICATE_ORIGINAL_FILE, $infos) === false |
|
77
|
|
|
|| $infos[static::EXTRA_OVERLOAD_DUPLICATE_ORIGINAL_FILE] === false |
|
78
|
|
|
) { |
|
79
|
|
|
static::generateProxy( |
|
80
|
|
|
$cacheDir, |
|
81
|
|
|
$className, |
|
82
|
|
|
$infos['original-file'], |
|
83
|
|
|
$event->getIO() |
|
84
|
|
|
); |
|
85
|
|
|
} |
|
86
|
|
|
$autoload['files'][$className] = $infos['overload-file']; |
|
87
|
|
|
|
|
88
|
|
|
$message = '<info>' . $infos['original-file'] . '</info>'; |
|
89
|
|
|
$message .= ' is overloaded by <comment>' . $infos['overload-file'] . '</comment>'; |
|
90
|
|
|
$event->getIO()->write($message, true, IOInterface::VERBOSE); |
|
91
|
|
|
} |
|
92
|
|
|
} |
|
93
|
|
|
} |
|
94
|
|
|
|
|
95
|
|
|
$event->getComposer()->getPackage()->setAutoload($autoload); |
|
96
|
|
|
} |
|
97
|
|
|
|
|
98
|
|
|
/** @return array */ |
|
99
|
|
|
protected static function getAutoload(Event $event) |
|
100
|
|
|
{ |
|
101
|
|
|
$return = $event->getComposer()->getPackage()->getAutoload(); |
|
102
|
|
|
if (array_key_exists('files', $return) === false) { |
|
103
|
|
|
$return['files'] = array(); |
|
104
|
|
|
} |
|
105
|
|
|
if (array_key_exists('exclude-from-classmap', $return) === false) { |
|
106
|
|
|
$return['exclude-from-classmap'] = array(); |
|
107
|
|
|
} |
|
108
|
|
|
|
|
109
|
|
|
return $return; |
|
110
|
|
|
} |
|
111
|
|
|
|
|
112
|
|
|
/** @param string $path */ |
|
113
|
|
|
protected static function createDirectories($path, IOInterface $io) |
|
114
|
|
|
{ |
|
115
|
|
|
if (is_dir($path) === false) { |
|
116
|
|
|
$io->write('Creating directory <info>' . $path . '</info>.', true, IOInterface::VERBOSE); |
|
117
|
|
|
|
|
118
|
|
|
$createdPath = null; |
|
119
|
|
|
foreach (explode('/', $path) as $directory) { |
|
120
|
|
|
if (is_dir($createdPath . $directory) === false) { |
|
121
|
|
|
mkdir($createdPath . $directory); |
|
122
|
|
|
} |
|
123
|
|
|
$createdPath .= $directory . '/'; |
|
124
|
|
|
} |
|
125
|
|
|
} |
|
126
|
|
|
} |
|
127
|
|
|
|
|
128
|
|
|
/** |
|
129
|
|
|
* @param string $cacheDir |
|
130
|
|
|
* @param string $fullyQualifiedClassName |
|
131
|
|
|
* @param string $filePath |
|
132
|
|
|
* @return string |
|
133
|
|
|
*/ |
|
134
|
|
|
protected static function generateProxy($cacheDir, $fullyQualifiedClassName, $filePath, IOInterface $io) |
|
135
|
|
|
{ |
|
136
|
|
|
$php = static::getPhpForDuplicatedFile($filePath, $fullyQualifiedClassName); |
|
137
|
|
|
$classNameParts = array_merge(array(static::NAMESPACE_PREFIX), explode('\\', $fullyQualifiedClassName)); |
|
138
|
|
|
array_pop($classNameParts); |
|
139
|
|
|
$finalCacheDir = $cacheDir . '/' . implode('/', $classNameParts); |
|
140
|
|
|
static::createDirectories($finalCacheDir, $io); |
|
141
|
|
|
|
|
142
|
|
|
$overloadedFilePath = $finalCacheDir . '/' . basename($filePath); |
|
143
|
|
|
file_put_contents($overloadedFilePath, $php); |
|
144
|
|
|
|
|
145
|
|
|
$io->write( |
|
146
|
|
|
'Generate proxy for <info>' |
|
147
|
|
|
. $fullyQualifiedClassName |
|
148
|
|
|
. '</info> in <comment>' |
|
149
|
|
|
. $overloadedFilePath |
|
150
|
|
|
. '</comment>', |
|
151
|
|
|
true, |
|
152
|
|
|
IOInterface::VERBOSE |
|
153
|
|
|
); |
|
154
|
|
|
} |
|
155
|
|
|
|
|
156
|
|
|
/** |
|
157
|
|
|
* @param string $filePath |
|
158
|
|
|
* @param string $fullyQualifiedClassName |
|
159
|
|
|
* @return string |
|
160
|
|
|
*/ |
|
161
|
|
|
protected static function getPhpForDuplicatedFile($filePath, $fullyQualifiedClassName) |
|
162
|
|
|
{ |
|
163
|
|
|
if (is_readable($filePath) === false) { |
|
164
|
|
|
throw new \Exception('File "' . $filePath . '" does not exists, or is not readable.'); |
|
165
|
|
|
} |
|
166
|
|
|
|
|
167
|
|
|
$phpLines = file($filePath); |
|
168
|
|
|
$namespace = substr($fullyQualifiedClassName, 0, strrpos($fullyQualifiedClassName, '\\')); |
|
169
|
|
|
$nextIsNamespace = false; |
|
170
|
|
|
$namespaceFound = null; |
|
171
|
|
|
$classesFound = []; |
|
172
|
|
|
$phpCodeForNamespace = null; |
|
173
|
|
|
$namespaceLine = null; |
|
174
|
|
|
$uses = []; |
|
175
|
|
|
$addUses = []; |
|
176
|
|
|
$isGlobalUse = true; |
|
177
|
|
|
$lastUseLine = null; |
|
178
|
|
|
$tokens = token_get_all(implode(null, $phpLines)); |
|
179
|
|
|
foreach ($tokens as $index => $token) { |
|
180
|
|
|
if (is_array($token)) { |
|
181
|
|
|
if ($token[0] === T_NAMESPACE) { |
|
182
|
|
|
$nextIsNamespace = true; |
|
183
|
|
|
$namespaceLine = $token[2]; |
|
184
|
|
View Code Duplication |
} elseif ($isGlobalUse && $token[0] === T_CLASS) { |
|
185
|
|
|
$classesFound[] = static::getClassNameFromTokens($tokens, $index + 1); |
|
186
|
|
|
$isGlobalUse = false; |
|
187
|
|
|
} elseif ($token[0] === T_EXTENDS) { |
|
188
|
|
|
static::addUse(static::getClassNameFromTokens($tokens, $index + 1), $namespaceFound, $uses, $addUses); |
|
189
|
|
|
} elseif ($isGlobalUse && $token[0] === T_USE) { |
|
190
|
|
|
// What is the next token which is NOT a whitespace.. |
|
191
|
|
|
$walkForwardSteps = 1; |
|
192
|
|
|
$continue = true; |
|
193
|
|
|
do { |
|
194
|
|
|
$nextNonWhitespaceToken = $tokens[$index + $walkForwardSteps]; |
|
195
|
|
|
if($nextNonWhitespaceToken[0] !== T_WHITESPACE){ |
|
196
|
|
|
$continue = false; |
|
197
|
|
|
} |
|
198
|
|
|
$walkForwardSteps++; |
|
199
|
|
|
} while($continue); |
|
200
|
|
View Code Duplication |
if($nextNonWhitespaceToken[0] === T_STRING ) { |
|
|
|
|
|
|
201
|
|
|
$uses[] = static::getClassNameFromTokens($tokens, $index + 1); |
|
202
|
|
|
$lastUseLine = $token[2]; |
|
203
|
|
|
} |
|
204
|
|
|
} |
|
205
|
|
|
|
|
206
|
|
|
if ($nextIsNamespace) { |
|
207
|
|
|
$phpCodeForNamespace .= $token[1]; |
|
208
|
|
|
if ($token[0] === T_NS_SEPARATOR || $token[0] === T_STRING) { |
|
209
|
|
|
$namespaceFound .= $token[1]; |
|
210
|
|
|
} |
|
211
|
|
|
} |
|
212
|
|
|
} elseif ($nextIsNamespace && $token === ';') { |
|
213
|
|
|
$phpCodeForNamespace .= $token; |
|
214
|
|
|
if ($namespaceFound !== $namespace) { |
|
215
|
|
|
$message = 'Expected namespace "' . $namespace . '", found "' . $namespaceFound . '" '; |
|
216
|
|
|
$message .= 'in "' . $filePath . '".'; |
|
217
|
|
|
throw new \Exception($message); |
|
218
|
|
|
} |
|
219
|
|
|
$nextIsNamespace = false; |
|
220
|
|
|
} |
|
221
|
|
|
} |
|
222
|
|
|
|
|
223
|
|
|
static::assertOnlyRightClassFound($classesFound, $fullyQualifiedClassName, $filePath); |
|
224
|
|
|
static::replaceNamespace($namespaceFound, $phpCodeForNamespace, $phpLines, $namespaceLine); |
|
225
|
|
|
static::addUsesInPhpLines($addUses, $phpLines, ($lastUseLine === null ? $namespaceLine : $lastUseLine)); |
|
226
|
|
|
|
|
227
|
|
|
return implode(null, $phpLines); |
|
228
|
|
|
} |
|
229
|
|
|
|
|
230
|
|
|
/** |
|
231
|
|
|
* @param string $fullyQualifiedClassName |
|
232
|
|
|
* @param string $filePath |
|
233
|
|
|
*/ |
|
234
|
|
|
protected static function assertOnlyRightClassFound(array $classFound, $fullyQualifiedClassName, $filePath) |
|
235
|
|
|
{ |
|
236
|
|
|
$className = substr($fullyQualifiedClassName, strrpos($fullyQualifiedClassName, '\\') + 1); |
|
237
|
|
|
if (count($classFound) !== 1) { |
|
238
|
|
|
throw new \Exception('Expected 1 class, found "' . implode(', ', $classFound) . '" in "' . $filePath . '".'); |
|
239
|
|
|
} elseif ($classFound[0] !== $className) { |
|
240
|
|
|
$message = 'Expected "' . $className . '" class, found "' . $classFound[0] . '" '; |
|
241
|
|
|
$message .= 'in "' . $filePath . '".'; |
|
242
|
|
|
throw new \Exception($message); |
|
243
|
|
|
} |
|
244
|
|
|
} |
|
245
|
|
|
|
|
246
|
|
|
/** |
|
247
|
|
|
* @param string $namespace |
|
248
|
|
|
* @param string $phpCodeForNamespace |
|
249
|
|
|
* @param array $phpLines |
|
250
|
|
|
* @param int $namespaceLine |
|
251
|
|
|
*/ |
|
252
|
|
|
protected static function replaceNamespace($namespace, $phpCodeForNamespace, &$phpLines, $namespaceLine) |
|
253
|
|
|
{ |
|
254
|
|
|
$phpLines[$namespaceLine - 1] = str_replace( |
|
255
|
|
|
$phpCodeForNamespace, |
|
256
|
|
|
'namespace ' . static::NAMESPACE_PREFIX . '\\' . $namespace . ';', |
|
257
|
|
|
$phpLines[$namespaceLine - 1] |
|
258
|
|
|
); |
|
259
|
|
|
} |
|
260
|
|
|
|
|
261
|
|
|
/** |
|
262
|
|
|
* @param int $index |
|
263
|
|
|
* @return string |
|
264
|
|
|
*/ |
|
265
|
|
|
protected static function getClassNameFromTokens(array &$tokens, $index) |
|
266
|
|
|
{ |
|
267
|
|
|
$return = null; |
|
268
|
|
|
do { |
|
269
|
|
|
if ( |
|
270
|
|
|
is_array($tokens[$index]) |
|
271
|
|
|
&& ( |
|
272
|
|
|
$tokens[$index][0] === T_STRING |
|
273
|
|
|
|| $tokens[$index][0] === T_NS_SEPARATOR |
|
274
|
|
|
) |
|
275
|
|
|
) { |
|
276
|
|
|
$return .= $tokens[$index][1]; |
|
277
|
|
|
} |
|
278
|
|
|
|
|
279
|
|
|
$index++; |
|
280
|
|
|
$continue = |
|
281
|
|
|
is_array($tokens[$index]) |
|
282
|
|
|
&& ( |
|
283
|
|
|
$tokens[$index][0] === T_STRING |
|
284
|
|
|
|| $tokens[$index][0] === T_NS_SEPARATOR |
|
285
|
|
|
|| $tokens[$index][0] === T_WHITESPACE |
|
286
|
|
|
); |
|
287
|
|
|
} while ($continue); |
|
288
|
|
|
|
|
289
|
|
|
if ($return === null) { |
|
290
|
|
|
throw new \Exception('Class not found in tokens.'); |
|
291
|
|
|
} |
|
292
|
|
|
|
|
293
|
|
|
return $return; |
|
294
|
|
|
} |
|
295
|
|
|
|
|
296
|
|
|
/** |
|
297
|
|
|
* @param string $className |
|
298
|
|
|
* @param string $namespace |
|
299
|
|
|
*/ |
|
300
|
|
|
protected static function addUse($className, $namespace, array $uses, array &$addUses) |
|
301
|
|
|
{ |
|
302
|
|
|
if (substr($className, 0, 1) !== '\\') { |
|
303
|
|
|
$alreadyInUses = false; |
|
304
|
|
|
foreach ($uses as $use) { |
|
305
|
|
|
if (substr($use, strrpos($use, '\\') + 1) === $className) { |
|
306
|
|
|
$alreadyInUses = true; |
|
307
|
|
|
} |
|
308
|
|
|
} |
|
309
|
|
|
|
|
310
|
|
|
if ($alreadyInUses === false) { |
|
311
|
|
|
$addUses[] = $namespace . '\\' . $className; |
|
312
|
|
|
} |
|
313
|
|
|
} |
|
314
|
|
|
} |
|
315
|
|
|
|
|
316
|
|
|
/** @param int $line */ |
|
317
|
|
|
protected static function addUsesInPhpLines(array $addUses, array &$phpLines, $line) |
|
318
|
|
|
{ |
|
319
|
|
|
$linesBefore = ($line > 0) ? array_slice($phpLines, 0, $line) : []; |
|
320
|
|
|
$linesAfter = array_slice($phpLines, $line); |
|
321
|
|
|
|
|
322
|
|
|
array_walk($addUses, function(&$addUse) { |
|
323
|
|
|
$addUse = 'use ' . $addUse . ';' . "\n"; |
|
324
|
|
|
}); |
|
325
|
|
|
|
|
326
|
|
|
$phpLines = array_merge($linesBefore, $addUses, $linesAfter); |
|
327
|
|
|
} |
|
328
|
|
|
} |
|
329
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.