Completed
Push — master ( e90e59...bc944e )
by Steevan
02:32
created

OverloadClass::defineAutoloadFiles()   D

Complexity

Conditions 9
Paths 18

Size

Total Lines 42
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 42
rs 4.909
cc 9
eloc 27
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) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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