Completed
Pull Request — 2.x (#349)
by Alexander
02:20
created

AopComposerManipulator::loadClass()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 0
cts 7
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 1
crap 6
1
<?php
2
/*
3
 * Go! AOP framework
4
 *
5
 * @copyright Copyright 2013, Lisachenko Alexander <[email protected]>
6
 *
7
 * This source file is subject to the license that is bundled
8
 * with this source code in the file LICENSE.
9
 */
10
11
namespace Go\Instrument\ClassLoading;
12
13
use Go\Aop\AspectException;
14
use Go\Core\AspectContainer;
15
use Go\Instrument\FileSystem\Enumerator;
16
use Go\Instrument\PathResolver;
17
use Go\Instrument\Transformer\FilterInjectorTransformer;
18
use Composer\Autoload\ClassLoader;
19
use Doctrine\Common\Annotations\AnnotationRegistry;
20
21
/**
22
 * AopComposerManipulator class adjusts composer to work with AOP-weaving mechanism
23
 */
24
class AopComposerManipulator
25
{
26
    /**
27
     * List of packages to exclude from analysis
28
     *
29
     * @var array
30
     */
31
    public static $excludedPackages = [
32
        'Dissect'                         => true,
33
        'Doctrine\\Common\Lexer\\'        => true,
34
        'Doctrine\\Common\\Annotations\\' => true,
35
        'Go\\'                            => true,
36
        'Go\\ParserReflection\\'          => true,
37
        'PhpParser\\'                     => true
38
    ];
39
40
    /**
41
     * Instance of original autoloader
42
     *
43
     * @var ClassLoader
44
     */
45
    protected $original = null;
46
47
    /**
48
     * AOP kernel options
49
     *
50
     * @var array
51
     */
52
    protected $options = [];
53
54
    /**
55
     * File enumerator
56
     *
57
     * @var Enumerator
58
     */
59
    protected $fileEnumerator;
60
61
    /**
62
     * Cache state
63
     *
64
     * @var array
65
     */
66
    private $cacheState;
67
68
    /**
69
     * Constructs an wrapper for the composer loader
70
     *
71
     * @param ClassLoader $original Instance of current loader
72
     * @param AspectContainer $container Instance of the container
73
     * @param array $options Configuration options
74
     */
75
    public function __construct(ClassLoader $original, AspectContainer $container, array $options = [])
76
    {
77
        $this->options    = $options;
78
        $this->original   = $original;
79
        $this->cacheState = &$container->get('aspect.cache.path.manager')->queryClassMap();
80
81
        $this->adjustClassLoader();
82
    }
83
84
    /**
85
     * Initialize aspect autoloader
86
     *
87
     * Replaces original composer autoloader with wrapper
88
     *
89
     * @param array $options Aspect kernel options
90
     * @param AspectContainer $container
91
     */
92
    public static function init(array $options = [], AspectContainer $container)
0 ignored issues
show
Coding Style introduced by
Parameters which have default values should be placed at the end.

If you place a parameter with a default value before a parameter with a default value, the default value of the first parameter will never be used as it will always need to be passed anyway:

// $a must always be passed; it's default value is never used.
function someFunction($a = 5, $b) { }
Loading history...
93
    {
94
        $wasInitialized = false;
95
        $loaders = spl_autoload_functions();
96
97
        foreach ($loaders as &$loader) {
98
            $loaderToUnregister = $loader;
99
            if (is_array($loader) && ($loader[0] instanceof ClassLoader)) {
0 ignored issues
show
Bug introduced by
The class Composer\Autoload\ClassLoader does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
100
                $originalLoader = $loader[0];
101
                // Configure library loader for doctrine annotation loader
102
                AnnotationRegistry::registerLoader(function($class) use ($originalLoader) {
0 ignored issues
show
Deprecated Code introduced by
The method Doctrine\Common\Annotati...istry::registerLoader() has been deprecated with message: this method is deprecated and will be removed in doctrine/annotations 2.0 autoloading should be deferred to the globally registered autoloader by then. For now, use @example AnnotationRegistry::registerLoader('class_exists')

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
Coding Style introduced by
Expected 1 space after FUNCTION keyword; 0 found
Loading history...
103
                    $originalLoader->loadClass($class);
104
105
                    return class_exists($class, false);
106
                });
107
                $loader[0]      = new AopComposerManipulator($loader[0], $container, $options);
108
                $wasInitialized = true;
109
            }
110
            spl_autoload_unregister($loaderToUnregister);
111
        }
112
        unset($loader);
113
114
        foreach ($loaders as $loader) {
115
            spl_autoload_register($loader);
116
        }
117
118
        if (!$wasInitialized) {
119
            throw new AspectException("Initialization of aspect loader failed, check your composer initialization");
120
        };
121
    }
122
123
    /**
124
     * Autoload a class by it's name
125
     */
126
    public function loadClass($class)
127
    {
128
        $file = $this->findFile($class);
129
130
        if ($file) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $file of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
131
            include $file;
132
        }
133
    }
134
135
    /**
136
     * Finds either the path to the file where the class is defined,
137
     * or gets the appropriate php://filter stream for the given class.
138
     *
139
     * @param string $class
140
     * @return string|false The path/resource if found, false otherwise.
141
     */
142
    public function findFile($class)
143
    {
144
        $file = $this->original->findFile($class);
145
146
        // Our special marker for intercepted files
147
        if (strpos($file, 'file://') === 0) {
148
            // cut first symbols 'file://' from the path and rewrite it
149
            $file = substr($file, 7);
150
            // last check to be sure, that there aren't any internal dirs or files in exclude list
151
            if (!$this->underPath($file, $this->options['excludePaths'])) {
152
                $file = FilterInjectorTransformer::rewrite($file);
153
            }
154
        }
155
156
        return $file;
157
    }
158
159
    /**
160
     * Adjusts original composer loader to work together with AOP engine
161
     */
162
    private function adjustClassLoader()
163
    {
164
        // PSR-0 prefixes analysis
165
        $prefixes = $this->original->getPrefixes();
166
        foreach ($prefixes as $prefix => $prefixPaths) {
167
            // Ignore core packages
168
            if (isset(static::$excludedPackages[$prefix])) {
169
                continue;
170
            }
171
            $adjustedPrefixes = $this->analysePrefixPaths($prefixPaths);
172
            $this->original->set($prefix, $adjustedPrefixes);
173
        }
174
        // PSR-4 prefixes analysis
175
        $prefixesPsr4 = $this->original->getPrefixesPsr4();
176
        foreach ($prefixesPsr4 as $prefix => $prefixPaths) {
177
            // Ignore core packages
178
            if (isset(static::$excludedPackages[$prefix])) {
179
                continue;
180
            }
181
            $adjustedPrefixes = $this->analysePrefixPaths($prefixPaths);
182
            $this->original->setPsr4($prefix, $adjustedPrefixes);
183
        }
184
        $this->original->addClassMap($this->cacheState);
185
    }
186
187
    /**
188
     * Checks if the path belongs to the specific directory
189
     *
190
     * @param string $absolutePath Absolute path to check (should be normalized)
191
     * @param array  $listOfPaths  List of absolute paths to check against
192
     *
193
     * @return bool True, if given path belongs to the list of directories
194
     */
195
    private function underPath($absolutePath, array $listOfPaths)
196
    {
197
        foreach ($listOfPaths as $singlePath) {
198
            if (strpos($absolutePath, $singlePath) === 0) {
199
                return true;
200
            }
201
        }
202
203
        return false;
204
    }
205
206
    /**
207
     * Perform analysis of prefix paths
208
     *
209
     * @param array  $prefixPaths List of prefix paths
210
     *
211
     * @return array List of normalized/transformed paths
212
     */
213
    private function analysePrefixPaths(array $prefixPaths)
214
    {
215
        $adjustedPrefixes = [];
216
        foreach ($prefixPaths as $prefixPath) {
217
            $normalizedPath = PathResolver::realpath($prefixPath);
218
            $isUnderRoot    = $this->underPath($normalizedPath, (array)$this->options['appDir']);
219
            $hasIncluded    = !empty($this->options['includePaths']);
220
            $isIncluded     = $hasIncluded && $this->underPath($normalizedPath, $this->options['includePaths']);
221
            $isExcluded     = $this->underPath($normalizedPath, $this->options['excludePaths']);
222
            $canProcess     = $isUnderRoot && ($hasIncluded ? $isIncluded : true) && !$isExcluded;
223
            if ($canProcess) {
224
                // Trick to distinguish between intercepted files without affecting file checking logic
225
                $normalizedPath = 'file://' . $normalizedPath;
226
            }
227
228
            $adjustedPrefixes[] = $normalizedPath;
229
        }
230
231
        return $adjustedPrefixes;
232
    }
233
}
234