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