Completed
Push — master ( bc944e...9b474c )
by Steevan
01:27
created

OverloadClass::defineAutoloadFiles()   C

Complexity

Conditions 9
Paths 18

Size

Total Lines 46
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 46
rs 5.0942
cc 9
eloc 31
nc 18
nop 1
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
                    } else {
90
                        $message = '<info>' . $infos['original-file'] . '</info>';
91
                        $message .= ' is overloaded by <comment>' . $infos['overload-file'] . '</comment>';
92
                        $event->getIO()->write($message, true, IOInterface::VERBOSE);
93
                    }
94
                    $autoload['files'][$className] = $infos['overload-file'];
95
                }
96
            }
97
        }
98
99
        $event->getComposer()->getPackage()->setAutoload($autoload);
100
    }
101
102
    /**
103
     * @param Event $event
104
     * @return array
105
     */
106
    protected static function getAutoload(Event $event)
107
    {
108
        $return = $event->getComposer()->getPackage()->getAutoload();
109
        if (array_key_exists('files', $return) === false) {
110
            $return['files'] = array();
111
        }
112
        if (array_key_exists('exclude-from-classmap', $return) === false) {
113
            $return['exclude-from-classmap'] = array();
114
        }
115
116
        return $return;
117
    }
118
119
    /**
120
     * @param string $path
121
     * @param IOInterface $io
122
     */
123
    protected static function createDirectories($path, IOInterface $io)
124
    {
125
        if (is_dir($path) === false) {
126
            $io->write('Creating directory <info>' . $path . '</info>.', true, IOInterface::VERBOSE);
127
128
            $createdPath = null;
129
            foreach (explode(DIRECTORY_SEPARATOR, $path) as $directory) {
130
                if (is_dir($createdPath . $directory) === false) {
131
                    mkdir($createdPath . $directory);
132
                }
133
                $createdPath .= $directory . DIRECTORY_SEPARATOR;
134
            }
135
        }
136
    }
137
138
    /**
139
     * @param string $cacheDir
140
     * @param string $fullyQualifiedClassName
141
     * @param string $filePath
142
     * @param IOInterface $io
143
     * @return string
144
     */
145
    protected static function generateProxy($cacheDir, $fullyQualifiedClassName, $filePath, IOInterface $io)
146
    {
147
        $php = static::getPhpForDuplicatedFile($filePath, $fullyQualifiedClassName);
148
        $classNameParts = array_merge(array(static::NAMESPACE_PREFIX), explode('\\', $fullyQualifiedClassName));
149
        array_pop($classNameParts);
150
        $finalCacheDir = $cacheDir . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $classNameParts);
151
        static::createDirectories($finalCacheDir, $io);
152
153
        $overloadedFilePath = $finalCacheDir . DIRECTORY_SEPARATOR . basename($filePath);
154
        file_put_contents($overloadedFilePath, $php);
155
156
        $io->write(
157
            '<info>' . $filePath . '</info> is overloaded by <comment>' . $overloadedFilePath . '</comment>',
158
            true,
159
            IOInterface::VERBOSE
160
        );
161
    }
162
163
    /**
164
     * @param string $filePath
165
     * @param string $fullyQualifiedClassName
166
     * @return string
167
     * @throws \Exception
168
     */
169
    protected static function getPhpForDuplicatedFile($filePath, $fullyQualifiedClassName)
170
    {
171
        if (is_readable($filePath) === false) {
172
            throw new \Exception('File "' . $filePath . '" does not exists, or is not readable.');
173
        }
174
175
        $phpLines = file($filePath);
176
        $namespace = substr($fullyQualifiedClassName, 0, strrpos($fullyQualifiedClassName, '\\'));
177
        $nextIsNamespace = false;
178
        $namespaceFound = null;
179
        $classesFound = [];
180
        $phpCodeForNamespace = null;
181
        $namespaceLine = null;
182
        $uses = [];
183
        $addUses = [];
184
        $isGlobalUse = true;
185
        $lastUseLine = null;
186
        $tokens = token_get_all(implode(null, $phpLines));
187
        foreach ($tokens as $index => $token) {
188
            if (is_array($token)) {
189
                if ($token[0] === T_NAMESPACE) {
190
                    $nextIsNamespace = true;
191
                    $namespaceLine = $token[2];
192 View Code Duplication
                } elseif ($isGlobalUse && $token[0] === T_CLASS) {
193
                    $classesFound[] = static::getClassNameFromTokens($tokens, $index + 1);
194
                    $isGlobalUse = false;
195
                } elseif ($token[0] === T_EXTENDS) {
196
                    static::addUse(static::getClassNameFromTokens($tokens, $index + 1), $namespaceFound, $uses, $addUses);
197 View Code Duplication
                } elseif ($isGlobalUse && $token[0] === T_USE) {
198
                    $uses[] = static::getClassNameFromTokens($tokens, $index + 1);
199
                    $lastUseLine = $token[2];
200
                }
201
202
                if ($nextIsNamespace) {
203
                    $phpCodeForNamespace .= $token[1];
204
                    if ($token[0] === T_NS_SEPARATOR || $token[0] === T_STRING) {
205
                        $namespaceFound .= $token[1];
206
                    }
207
                }
208
            } elseif ($nextIsNamespace && $token === ';') {
209
                $phpCodeForNamespace .= $token;
210
                if ($namespaceFound !== $namespace) {
211
                    $message = 'Expected namespace "' . $namespace . '", found "' . $namespaceFound . '" ';
212
                    $message .= 'in "' . $filePath . '".';
213
                    throw new \Exception($message);
214
                }
215
                $nextIsNamespace = false;
216
            }
217
        }
218
219
        static::assertOnlyRightClassFound($classesFound, $fullyQualifiedClassName, $filePath);
220
        static::replaceNamespace($namespaceFound, $phpCodeForNamespace, $phpLines, $namespaceLine);
221
        static::addUsesInPhpLines($addUses, $phpLines, ($lastUseLine === null ? $namespaceLine : $lastUseLine));
222
223
        return implode(null, $phpLines);
224
    }
225
226
    /**
227
     * @param array $classFound
228
     * @param string $fullyQualifiedClassName
229
     * @param string $filePath
230
     * @throws \Exception
231
     */
232
    protected static function assertOnlyRightClassFound(array $classFound, $fullyQualifiedClassName, $filePath)
233
    {
234
        $className = substr($fullyQualifiedClassName, strrpos($fullyQualifiedClassName, '\\') + 1);
235
        if (count($classFound) !== 1) {
236
            throw new \Exception('Expected 1 class, found "' . implode(', ', $classFound) . '" in "' . $filePath . '".');
237
        } elseif ($classFound[0] !== $className) {
238
            $message = 'Expected "' . $className . '" class, found "' . $classFound[0] . '" ';
239
            $message .= 'in "' . $filePath . '".';
240
            throw new \Exception($message);
241
        }
242
    }
243
244
    /**
245
     * @param string $namespace
246
     * @param string $phpCodeForNamespace
247
     * @param array $phpLines
248
     * @param int $namespaceLine
249
     */
250
    protected static function replaceNamespace($namespace, $phpCodeForNamespace, &$phpLines, $namespaceLine)
251
    {
252
        $phpLines[$namespaceLine - 1] = str_replace(
253
            $phpCodeForNamespace,
254
            'namespace ' . static::NAMESPACE_PREFIX . '\\' . $namespace . ';',
255
            $phpLines[$namespaceLine - 1]
256
        );
257
    }
258
259
    /**
260
     * @param array $tokens
261
     * @param int $index
262
     * @return string
263
     * @throws \Exception
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
     * @param array $uses
300
     * @param array $addUses
301
     */
302
    protected static function addUse($className, $namespace, array $uses, array &$addUses)
303
    {
304
        if (substr($className, 0, 1) !== '\\') {
305
            $alreadyInUses = false;
306
            foreach ($uses as $use) {
307
                if (substr($use, strrpos($use, '\\') + 1) === $className) {
308
                    $alreadyInUses = true;
309
                }
310
            }
311
312
            if ($alreadyInUses === false) {
313
                $addUses[] = $namespace . '\\' . $className;
314
            }
315
        }
316
    }
317
318
    /**
319
     * @param array $addUses
320
     * @param array $phpLines
321
     * @param int $line
322
     */
323
    protected static function addUsesInPhpLines(array $addUses, array &$phpLines, $line)
324
    {
325
        $linesBefore = ($line > 0) ? array_slice($phpLines, 0, $line) : [];
326
        $linesAfter = array_slice($phpLines, $line);
327
328
        array_walk($addUses, function(&$addUse) {
329
            $addUse = 'use ' . $addUse . ';' . "\n";
330
        });
331
332
        $phpLines = array_merge($linesBefore, $addUses, $linesAfter);
333
    }
334
}
335