|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
namespace steevanb\ComposerOverloadClass; |
|
4
|
|
|
|
|
5
|
|
|
use Composer\Script\Event; |
|
6
|
|
|
|
|
7
|
|
|
class OverloadClass |
|
8
|
|
|
{ |
|
9
|
|
|
const EXTRA_OVERLOAD_CACHE_DIR = 'composer-overload-cache-dir'; |
|
10
|
|
|
const EXTRA_OVERLOAD_CACHE_DIR_DEV = 'composer-overload-cache-dir-dev'; |
|
11
|
|
|
const EXTRA_OVERLOAD_CLASS = 'composer-overload-class'; |
|
12
|
|
|
const EXTRA_OVERLOAD_CLASS_DEV = 'composer-overload-class-dev'; |
|
13
|
|
|
const NAMESPACE_PREFIX = 'ComposerOverloadClass'; |
|
14
|
|
|
|
|
15
|
|
|
/** |
|
16
|
|
|
* @param Event $event |
|
17
|
|
|
*/ |
|
18
|
|
|
public static function overload(Event $event) |
|
19
|
|
|
{ |
|
20
|
|
|
$extra = $event->getComposer()->getPackage()->getExtra(); |
|
21
|
|
|
|
|
22
|
|
|
if ($event->isDevMode()) { |
|
23
|
|
|
$envs = [static::EXTRA_OVERLOAD_CLASS, static::EXTRA_OVERLOAD_CLASS_DEV]; |
|
24
|
|
|
$cacheDir = static::EXTRA_OVERLOAD_CACHE_DIR_DEV; |
|
25
|
|
|
if (array_key_exists($cacheDir, $extra) === false) { |
|
26
|
|
|
$cacheDir = static::EXTRA_OVERLOAD_CACHE_DIR; |
|
27
|
|
|
} |
|
28
|
|
|
} else { |
|
29
|
|
|
$envs = [static::EXTRA_OVERLOAD_CLASS]; |
|
30
|
|
|
$cacheDir = static::EXTRA_OVERLOAD_CACHE_DIR; |
|
31
|
|
|
} |
|
32
|
|
|
|
|
33
|
|
|
foreach ($envs as $extraKey) { |
|
34
|
|
|
if (array_key_exists($extraKey, $extra)) { |
|
35
|
|
|
$autoload = $event->getComposer()->getPackage()->getAutoload(); |
|
36
|
|
|
if (array_key_exists('classmap', $autoload) === false) { |
|
37
|
|
|
$autoload['classmap'] = array(); |
|
38
|
|
|
} |
|
39
|
|
|
|
|
40
|
|
|
foreach ($extra[$extraKey] as $className => $infos) { |
|
41
|
|
|
static::generateProxy( |
|
42
|
|
|
$extra[$cacheDir], |
|
43
|
|
|
$className, |
|
44
|
|
|
$infos['original-file'] |
|
45
|
|
|
); |
|
46
|
|
|
$autoload['classmap'][$className] = $infos['overload-file']; |
|
47
|
|
|
} |
|
48
|
|
|
|
|
49
|
|
|
$event->getComposer()->getPackage()->setAutoload($autoload); |
|
50
|
|
|
} |
|
51
|
|
|
} |
|
52
|
|
|
} |
|
53
|
|
|
|
|
54
|
|
|
/** |
|
55
|
|
|
* @param string $cacheDir |
|
56
|
|
|
* @param string $fullyQualifiedClassName |
|
57
|
|
|
* @param string $filePath |
|
58
|
|
|
* @return string |
|
59
|
|
|
*/ |
|
60
|
|
|
protected static function generateProxy($cacheDir, $fullyQualifiedClassName, $filePath) |
|
61
|
|
|
{ |
|
62
|
|
|
$php = static::getPhpForDuplicatedFile($filePath, $fullyQualifiedClassName); |
|
63
|
|
|
$classNameParts = array_merge(array(static::NAMESPACE_PREFIX), explode('\\', $fullyQualifiedClassName)); |
|
64
|
|
|
array_pop($classNameParts); |
|
65
|
|
|
foreach ($classNameParts as $part) { |
|
66
|
|
|
$cacheDir .= DIRECTORY_SEPARATOR . $part; |
|
67
|
|
|
if (is_dir($cacheDir) === false) { |
|
68
|
|
|
mkdir($cacheDir); |
|
69
|
|
|
} |
|
70
|
|
|
} |
|
71
|
|
|
|
|
72
|
|
|
$overloadedFilePath = $cacheDir . DIRECTORY_SEPARATOR . basename($filePath); |
|
73
|
|
|
file_put_contents($overloadedFilePath, $php); |
|
74
|
|
|
} |
|
75
|
|
|
|
|
76
|
|
|
/** |
|
77
|
|
|
* @param string $filePath |
|
78
|
|
|
* @param string $fullyQualifiedClassName |
|
79
|
|
|
* @return string |
|
80
|
|
|
* @throws \Exception |
|
81
|
|
|
*/ |
|
82
|
|
|
protected static function getPhpForDuplicatedFile($filePath, $fullyQualifiedClassName) |
|
83
|
|
|
{ |
|
84
|
|
|
if (is_readable($filePath) === false) { |
|
85
|
|
|
throw new \Exception('File "' . $filePath . '" does not exists, or is not readable.'); |
|
86
|
|
|
} |
|
87
|
|
|
|
|
88
|
|
|
$phpLines = file($filePath); |
|
89
|
|
|
$namespace = substr($fullyQualifiedClassName, 0, strrpos($fullyQualifiedClassName, '\\')); |
|
90
|
|
|
$nextIsNamespace = false; |
|
91
|
|
|
$namespaceFound = null; |
|
92
|
|
|
$classesFound = []; |
|
93
|
|
|
$phpCodeForNamespace = null; |
|
94
|
|
|
$namespaceLine = null; |
|
95
|
|
|
$uses = []; |
|
96
|
|
|
$addUses = []; |
|
97
|
|
|
$isGlobalUse = true; |
|
98
|
|
|
$lastUseLine = null; |
|
99
|
|
|
$tokens = token_get_all(implode(null, $phpLines)); |
|
100
|
|
|
foreach ($tokens as $index => $token) { |
|
101
|
|
|
if (is_array($token)) { |
|
102
|
|
|
if ($token[0] === T_NAMESPACE) { |
|
103
|
|
|
$nextIsNamespace = true; |
|
104
|
|
|
$namespaceLine = $token[2]; |
|
105
|
|
View Code Duplication |
} elseif ($isGlobalUse && $token[0] === T_CLASS) { |
|
|
|
|
|
|
106
|
|
|
$classesFound[] = static::getClassNameFromTokens($tokens, $index + 1); |
|
107
|
|
|
$isGlobalUse = false; |
|
108
|
|
|
} elseif ($token[0] === T_EXTENDS) { |
|
109
|
|
|
static::addUse(static::getClassNameFromTokens($tokens, $index + 1), $namespaceFound, $uses, $addUses); |
|
110
|
|
View Code Duplication |
} elseif ($isGlobalUse && $token[0] === T_USE) { |
|
|
|
|
|
|
111
|
|
|
$uses[] = static::getClassNameFromTokens($tokens, $index + 1); |
|
112
|
|
|
$lastUseLine = $token[2]; |
|
113
|
|
|
} |
|
114
|
|
|
|
|
115
|
|
|
if ($nextIsNamespace) { |
|
116
|
|
|
$phpCodeForNamespace .= $token[1]; |
|
117
|
|
|
if ($token[0] === T_NS_SEPARATOR || $token[0] === T_STRING) { |
|
118
|
|
|
$namespaceFound .= $token[1]; |
|
119
|
|
|
} |
|
120
|
|
|
} |
|
121
|
|
|
} elseif ($nextIsNamespace && $token === ';') { |
|
122
|
|
|
$phpCodeForNamespace .= $token; |
|
123
|
|
|
if ($namespaceFound !== $namespace) { |
|
124
|
|
|
$message = 'Expected namespace "' . $namespace . '", found "' . $namespaceFound . '" '; |
|
125
|
|
|
$message .= 'in "' . $filePath . '".'; |
|
126
|
|
|
throw new \Exception($message); |
|
127
|
|
|
} |
|
128
|
|
|
$nextIsNamespace = false; |
|
129
|
|
|
} |
|
130
|
|
|
} |
|
131
|
|
|
|
|
132
|
|
|
static::assertOnlyRightClassFound($classesFound, $fullyQualifiedClassName, $filePath); |
|
133
|
|
|
static::replaceNamespace($namespaceFound, $phpCodeForNamespace, $phpLines, $namespaceLine); |
|
134
|
|
|
static::addUsesInPhpLines($addUses, $phpLines, ($lastUseLine === null ? $namespaceLine : $lastUseLine)); |
|
135
|
|
|
|
|
136
|
|
|
return implode(null, $phpLines); |
|
137
|
|
|
} |
|
138
|
|
|
|
|
139
|
|
|
/** |
|
140
|
|
|
* @param array $classFound |
|
141
|
|
|
* @param string $fullyQualifiedClassName |
|
142
|
|
|
* @param string $filePath |
|
143
|
|
|
* @throws \Exception |
|
144
|
|
|
*/ |
|
145
|
|
|
protected static function assertOnlyRightClassFound(array $classFound, $fullyQualifiedClassName, $filePath) |
|
146
|
|
|
{ |
|
147
|
|
|
$className = substr($fullyQualifiedClassName, strrpos($fullyQualifiedClassName, '\\') + 1); |
|
148
|
|
|
if (count($classFound) !== 1) { |
|
149
|
|
|
throw new \Exception('Expected 1 class, found "' . implode(', ', $classFound) . '" in "' . $filePath . '".'); |
|
150
|
|
|
} elseif ($classFound[0] !== $className) { |
|
151
|
|
|
$message = 'Expected "' . $className . '" class, found "' . $classFound[0] . '" '; |
|
152
|
|
|
$message .= 'in "' . $filePath . '".'; |
|
153
|
|
|
throw new \Exception($message); |
|
154
|
|
|
} |
|
155
|
|
|
} |
|
156
|
|
|
|
|
157
|
|
|
/** |
|
158
|
|
|
* @param string $namespace |
|
159
|
|
|
* @param string $phpCodeForNamespace |
|
160
|
|
|
* @param array $phpLines |
|
161
|
|
|
* @param int $namespaceLine |
|
162
|
|
|
*/ |
|
163
|
|
|
protected static function replaceNamespace($namespace, $phpCodeForNamespace, &$phpLines, $namespaceLine) |
|
164
|
|
|
{ |
|
165
|
|
|
$phpLines[$namespaceLine - 1] = str_replace( |
|
166
|
|
|
$phpCodeForNamespace, |
|
167
|
|
|
'namespace ' . static::NAMESPACE_PREFIX . '\\' . $namespace . ';', |
|
168
|
|
|
$phpLines[$namespaceLine - 1] |
|
169
|
|
|
); |
|
170
|
|
|
} |
|
171
|
|
|
|
|
172
|
|
|
/** |
|
173
|
|
|
* @param array $tokens |
|
174
|
|
|
* @param int $index |
|
175
|
|
|
* @return string |
|
176
|
|
|
* @throws \Exception |
|
177
|
|
|
*/ |
|
178
|
|
|
protected static function getClassNameFromTokens(array &$tokens, $index) |
|
179
|
|
|
{ |
|
180
|
|
|
$return = null; |
|
181
|
|
|
do { |
|
182
|
|
|
if ( |
|
183
|
|
|
is_array($tokens[$index]) |
|
184
|
|
|
&& ( |
|
185
|
|
|
$tokens[$index][0] === T_STRING |
|
186
|
|
|
|| $tokens[$index][0] === T_NS_SEPARATOR |
|
187
|
|
|
) |
|
188
|
|
|
) { |
|
189
|
|
|
$return .= $tokens[$index][1]; |
|
190
|
|
|
} |
|
191
|
|
|
|
|
192
|
|
|
$index++; |
|
193
|
|
|
$continue = |
|
194
|
|
|
is_array($tokens[$index]) |
|
195
|
|
|
&& ( |
|
196
|
|
|
$tokens[$index][0] === T_STRING |
|
197
|
|
|
|| $tokens[$index][0] === T_NS_SEPARATOR |
|
198
|
|
|
|| $tokens[$index][0] === T_WHITESPACE |
|
199
|
|
|
); |
|
200
|
|
|
} while ($continue); |
|
201
|
|
|
|
|
202
|
|
|
if ($return === null) { |
|
203
|
|
|
throw new \Exception('Class not found in tokens.'); |
|
204
|
|
|
} |
|
205
|
|
|
|
|
206
|
|
|
return $return; |
|
207
|
|
|
} |
|
208
|
|
|
|
|
209
|
|
|
/** |
|
210
|
|
|
* @param string $className |
|
211
|
|
|
* @param string $namespace |
|
212
|
|
|
* @param array $uses |
|
213
|
|
|
* @param array $addUses |
|
214
|
|
|
*/ |
|
215
|
|
|
protected static function addUse($className, $namespace, array $uses, array &$addUses) |
|
216
|
|
|
{ |
|
217
|
|
|
if (substr($className, 0, 1) !== '\\') { |
|
218
|
|
|
$alreadyInUses = false; |
|
219
|
|
|
foreach ($uses as $use) { |
|
220
|
|
|
if (substr($use, strrpos($use, '\\') + 1) === $className) { |
|
221
|
|
|
$alreadyInUses = true; |
|
222
|
|
|
} |
|
223
|
|
|
} |
|
224
|
|
|
|
|
225
|
|
|
if ($alreadyInUses === false) { |
|
226
|
|
|
$addUses[] = $namespace . '\\' . $className; |
|
227
|
|
|
} |
|
228
|
|
|
} |
|
229
|
|
|
} |
|
230
|
|
|
|
|
231
|
|
|
/** |
|
232
|
|
|
* @param array $addUses |
|
233
|
|
|
* @param array $phpLines |
|
234
|
|
|
* @param int $line |
|
235
|
|
|
*/ |
|
236
|
|
|
protected static function addUsesInPhpLines(array $addUses, array &$phpLines, $line) |
|
237
|
|
|
{ |
|
238
|
|
|
$linesBefore = ($line > 0) ? array_slice($phpLines, 0, $line) : []; |
|
239
|
|
|
$linesAfter = array_slice($phpLines, $line); |
|
240
|
|
|
|
|
241
|
|
|
array_walk($addUses, function(&$addUse) { |
|
242
|
|
|
$addUse = 'use ' . $addUse . ';' . "\n"; |
|
243
|
|
|
}); |
|
244
|
|
|
|
|
245
|
|
|
$phpLines = array_merge($linesBefore, $addUses, $linesAfter); |
|
246
|
|
|
} |
|
247
|
|
|
} |
|
248
|
|
|
|
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.