Completed
Push — master ( 9cbee3...aa0cd3 )
by Steevan
02:20
created

OverloadClass::overload()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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