Completed
Push — master ( aa9488...d33d6e )
by Steevan
01:56
created

OverloadClass::getClassNameFromTokens()   D

Complexity

Conditions 9
Paths 16

Size

Total Lines 30
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

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