1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Part of ci-phpunit-test |
4
|
|
|
* |
5
|
|
|
* @author Kenji Suzuki <https://github.com/kenjis> |
6
|
|
|
* @license MIT License |
7
|
|
|
* @copyright 2015 Kenji Suzuki |
8
|
|
|
* @link https://github.com/kenjis/ci-phpunit-test |
9
|
|
|
*/ |
10
|
|
|
|
11
|
|
|
namespace Kenjis\MonkeyPatch; |
12
|
|
|
|
13
|
|
|
use LogicException; |
14
|
|
|
use RuntimeException; |
15
|
|
|
use PhpParser\ParserFactory; |
16
|
|
|
use Kenjis\MonkeyPatch\Patcher\FunctionPatcher; |
17
|
|
|
|
18
|
|
View Code Duplication |
class MonkeyPatchManager |
|
|
|
|
19
|
|
|
{ |
20
|
|
|
public static $debug = false; |
21
|
|
|
|
22
|
|
|
private static $php_parser = ParserFactory::PREFER_PHP5; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* The path to the log file if `$debug` is true. |
26
|
|
|
* Will be set in {@link MonkeyPatchManager::setDebug}. |
27
|
|
|
* @var string|null */ |
28
|
|
|
public static $log_file = null; |
29
|
|
|
private static $load_patchers = false; |
30
|
|
|
private static $exit_exception_classname = |
31
|
|
|
'Kenjis\MonkeyPatch\Exception\ExitException'; |
32
|
|
|
/** |
33
|
|
|
* @var array list of patcher classname |
34
|
|
|
*/ |
35
|
|
|
private static $patcher_list = [ |
36
|
|
|
'ExitPatcher', |
37
|
|
|
'FunctionPatcher', |
38
|
|
|
'MethodPatcher', |
39
|
|
|
'ConstantPatcher', |
40
|
|
|
]; |
41
|
|
|
|
42
|
|
|
public static function log($message) |
43
|
|
|
{ |
44
|
|
|
if (! self::$debug) |
45
|
|
|
{ |
46
|
|
|
return; |
47
|
|
|
} |
48
|
|
|
|
49
|
|
|
$time = date('Y-m-d H:i:s'); |
50
|
|
|
list($usec, $sec) = explode(' ', microtime()); |
|
|
|
|
51
|
|
|
$usec = substr($usec, 1); |
52
|
|
|
$log = "[$time $usec] $message\n"; |
53
|
|
|
file_put_contents(self::$log_file, $log, FILE_APPEND); |
54
|
|
|
} |
55
|
|
|
|
56
|
|
|
public static function setExitExceptionClassname($name) |
57
|
|
|
{ |
58
|
|
|
self::$exit_exception_classname = $name; |
59
|
|
|
} |
60
|
|
|
|
61
|
|
|
public static function getExitExceptionClassname() |
62
|
|
|
{ |
63
|
|
|
return self::$exit_exception_classname; |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
public static function getPhpParser() |
67
|
|
|
{ |
68
|
|
|
return self::$php_parser; |
69
|
|
|
} |
70
|
|
|
|
71
|
|
|
protected static function setDebug(array $config) |
72
|
|
|
{ |
73
|
|
|
if (isset($config['debug'])) |
74
|
|
|
{ |
75
|
|
|
self::$debug = $config['debug']; |
76
|
|
|
} |
77
|
|
|
if (isset($config['log_file'])) |
78
|
|
|
{ |
79
|
|
|
self::$debug = $config['log_file']; |
80
|
|
|
} |
81
|
|
|
if (is_null(self::$log_file)) |
82
|
|
|
{ |
83
|
|
|
self::$log_file = __DIR__ . '/debug.log'; |
84
|
|
|
} |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
protected static function setDir(array $config) |
88
|
|
|
{ |
89
|
|
|
if (isset($config['root_dir'])) |
90
|
|
|
{ |
91
|
|
|
Cache::setProjectRootDir($config['root_dir']); |
92
|
|
|
} |
93
|
|
|
else |
94
|
|
|
{ |
95
|
|
|
// APPPATH is constant in CodeIgniter |
96
|
|
|
Cache::setProjectRootDir(APPPATH . '../'); |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
if (! isset($config['cache_dir'])) |
100
|
|
|
{ |
101
|
|
|
throw new LogicException('You have to set "cache_dir"'); |
102
|
|
|
} |
103
|
|
|
self::setCacheDir($config['cache_dir']); |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
protected static function setPaths(array $config) |
107
|
|
|
{ |
108
|
|
|
if (! isset($config['include_paths'])) |
109
|
|
|
{ |
110
|
|
|
throw new LogicException('You have to set "include_paths"'); |
111
|
|
|
} |
112
|
|
|
self::setIncludePaths($config['include_paths']); |
113
|
|
|
|
114
|
|
|
if (isset($config['exclude_paths'])) |
115
|
|
|
{ |
116
|
|
|
self::setExcludePaths($config['exclude_paths']); |
117
|
|
|
} |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
public static function init(array $config) |
121
|
|
|
{ |
122
|
|
|
self::setDebug($config); |
123
|
|
|
|
124
|
|
|
if (isset($config['php_parser'])) |
125
|
|
|
{ |
126
|
|
|
self::$php_parser = constant('PhpParser\ParserFactory::'.$config['php_parser']); |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
self::setDir($config); |
130
|
|
|
self::setPaths($config); |
131
|
|
|
|
132
|
|
|
Cache::createTmpListDir(); |
133
|
|
|
|
134
|
|
|
if (isset($config['patcher_list'])) |
135
|
|
|
{ |
136
|
|
|
self::setPatcherList($config['patcher_list']); |
137
|
|
|
} |
138
|
|
|
self::checkPatcherListUpdate(); |
139
|
|
|
self::checkPathsUpdate(); |
140
|
|
|
|
141
|
|
|
self::loadPatchers(); |
142
|
|
|
|
143
|
|
|
self::addTmpFunctionBlacklist(); |
144
|
|
|
|
145
|
|
|
if (isset($config['functions_to_patch'])) |
146
|
|
|
{ |
147
|
|
|
FunctionPatcher::addWhitelists($config['functions_to_patch']); |
148
|
|
|
} |
149
|
|
|
self::checkFunctionWhitelistUpdate(); |
150
|
|
|
FunctionPatcher::lockFunctionList(); |
151
|
|
|
|
152
|
|
|
if (isset($config['exit_exception_classname'])) |
153
|
|
|
{ |
154
|
|
|
self::setExitExceptionClassname($config['exit_exception_classname']); |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
// Register include stream wrapper for monkey patching |
158
|
|
|
self::wrap(); |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
protected static function checkPathsUpdate() |
162
|
|
|
{ |
163
|
|
|
$cached = Cache::getTmpIncludePaths(); |
164
|
|
|
$current = PathChecker::getIncludePaths(); |
165
|
|
|
|
166
|
|
|
// Updated? |
167
|
|
|
if ($cached !== $current) |
168
|
|
|
{ |
169
|
|
|
MonkeyPatchManager::log('clear_src_cache: from ' . __METHOD__); |
170
|
|
|
Cache::clearSrcCache(); |
171
|
|
|
Cache::writeTmpIncludePaths($current); |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
$cached = Cache::getTmpExcludePaths(); |
175
|
|
|
$current = PathChecker::getExcludePaths(); |
176
|
|
|
|
177
|
|
|
// Updated? |
178
|
|
|
if ($cached !== $current) |
179
|
|
|
{ |
180
|
|
|
MonkeyPatchManager::log('clear_src_cache: from ' . __METHOD__); |
181
|
|
|
Cache::clearSrcCache(); |
182
|
|
|
Cache::writeTmpExcludePaths($current); |
183
|
|
|
} |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
protected static function checkPatcherListUpdate() |
187
|
|
|
{ |
188
|
|
|
$cached = Cache::getTmpPatcherList(); |
189
|
|
|
|
190
|
|
|
// Updated? |
191
|
|
|
if ($cached !== self::$patcher_list) |
192
|
|
|
{ |
193
|
|
|
MonkeyPatchManager::log('clear_src_cache: from ' . __METHOD__); |
194
|
|
|
Cache::clearSrcCache(); |
195
|
|
|
Cache::writeTmpPatcherList(self::$patcher_list); |
196
|
|
|
} |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
protected static function checkFunctionWhitelistUpdate() |
200
|
|
|
{ |
201
|
|
|
$cached = Cache::getTmpFunctionWhitelist(); |
202
|
|
|
$current = FunctionPatcher::getFunctionWhitelist(); |
203
|
|
|
|
204
|
|
|
// Updated? |
205
|
|
|
if ($cached !== $current) |
206
|
|
|
{ |
207
|
|
|
MonkeyPatchManager::log('clear_src_cache: from ' . __METHOD__); |
208
|
|
|
Cache::clearSrcCache(); |
209
|
|
|
Cache::writeTmpFunctionWhitelist($current); |
210
|
|
|
} |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
protected static function addTmpFunctionBlacklist() |
214
|
|
|
{ |
215
|
|
|
$list = file(Cache::getTmpFunctionBlacklistFile()); |
216
|
|
|
foreach ($list as $function) |
217
|
|
|
{ |
218
|
|
|
FunctionPatcher::addBlacklist(trim($function)); |
219
|
|
|
} |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
public static function isEnabled($patcher) |
223
|
|
|
{ |
224
|
|
|
return in_array($patcher, self::$patcher_list); |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
public static function setPatcherList(array $list) |
228
|
|
|
{ |
229
|
|
|
if (self::$load_patchers) |
230
|
|
|
{ |
231
|
|
|
throw new LogicException("Can't change patcher list after initialisation"); |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
self::$patcher_list = $list; |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
public static function setCacheDir($dir) |
238
|
|
|
{ |
239
|
|
|
Cache::setCacheDir($dir); |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
public static function setIncludePaths(array $dir_list) |
243
|
|
|
{ |
244
|
|
|
PathChecker::setIncludePaths($dir_list); |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
public static function setExcludePaths(array $dir_list) |
248
|
|
|
{ |
249
|
|
|
PathChecker::setExcludePaths($dir_list); |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
public static function wrap() |
253
|
|
|
{ |
254
|
|
|
IncludeStream::wrap(); |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
public static function unwrap() |
258
|
|
|
{ |
259
|
|
|
IncludeStream::unwrap(); |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
/** |
263
|
|
|
* @param string $path original source file path |
264
|
|
|
* @return resource |
265
|
|
|
* @throws LogicException |
266
|
|
|
*/ |
267
|
|
|
public static function patch($path) |
268
|
|
|
{ |
269
|
|
|
if (! is_readable($path)) |
270
|
|
|
{ |
271
|
|
|
throw new LogicException("Can't read '$path'"); |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
// Check cache file |
275
|
|
|
if ($cache_file = Cache::getValidSrcCachePath($path)) |
276
|
|
|
{ |
277
|
|
|
self::log('cache_hit: ' . $path); |
278
|
|
|
return fopen($cache_file, 'r'); |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
self::log('cache_miss: ' . $path); |
282
|
|
|
$source = file_get_contents($path); |
283
|
|
|
|
284
|
|
|
list($new_source, $patched) = self::execPatchers($source); |
|
|
|
|
285
|
|
|
|
286
|
|
|
// Write to cache file |
287
|
|
|
self::log('write_cache: ' . $path); |
288
|
|
|
Cache::writeSrcCacheFile($path, $new_source); |
289
|
|
|
|
290
|
|
|
$resource = fopen('php://memory', 'rb+'); |
291
|
|
|
fwrite($resource, $new_source); |
292
|
|
|
rewind($resource); |
293
|
|
|
return $resource; |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
protected static function loadPatchers() |
297
|
|
|
{ |
298
|
|
|
if (self::$load_patchers) |
299
|
|
|
{ |
300
|
|
|
return; |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
require __DIR__ . '/Patcher/AbstractPatcher.php'; |
304
|
|
|
require __DIR__ . '/Patcher/Backtrace.php'; |
305
|
|
|
|
306
|
|
|
foreach (self::$patcher_list as $classname) |
307
|
|
|
{ |
308
|
|
|
require __DIR__ . '/Patcher/' . $classname . '.php'; |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
self::$load_patchers = true; |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
protected static function execPatchers($source) |
315
|
|
|
{ |
316
|
|
|
$patched = false; |
317
|
|
|
foreach (self::$patcher_list as $classname) |
318
|
|
|
{ |
319
|
|
|
$classname = 'Kenjis\MonkeyPatch\Patcher\\' . $classname; |
320
|
|
|
$patcher = new $classname; |
321
|
|
|
list($source, $patched_this) = $patcher->patch($source); |
322
|
|
|
$patched = $patched || $patched_this; |
323
|
|
|
} |
324
|
|
|
|
325
|
|
|
return [ |
326
|
|
|
$source, |
327
|
|
|
$patched, |
328
|
|
|
]; |
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.