Completed
Pull Request — master (#240)
by Alexander
05:40
created

WeavingTransformer   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 273
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 64.44%

Importance

Changes 13
Bugs 4 Features 0
Metric Value
c 13
b 4
f 0
dl 0
loc 273
wmc 29
lcom 1
cbo 11
ccs 58
cts 90
cp 0.6444
rs 10

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 1
C transform() 0 44 7
B processSingleClass() 0 55 6
A adjustOriginalClass() 0 15 3
B processFunctions() 0 27 6
B saveProxyToCache() 0 32 4
A loadAndRegisterAspects() 0 6 2
1
<?php
2
/*
3
 * Go! AOP framework
4
 *
5
 * @copyright Copyright 2011, 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\Transformer;
12
13
use Go\Aop\Advisor;
14
use Go\Aop\Aspect;
15
use Go\Aop\Framework\AbstractJoinpoint;
16
use Go\Core\AdviceMatcher;
17
use Go\Core\AspectContainer;
18
use Go\Core\AspectKernel;
19
use Go\Core\AspectLoader;
20
use Go\Instrument\ClassLoading\CachePathManager;
21
use Go\Instrument\CleanableMemory;
22
use Go\ParserReflection\ReflectionFile;
23
use Go\ParserReflection\ReflectionFileNamespace;
24
use Go\Proxy\ClassProxy;
25
use Go\Proxy\FunctionProxy;
26
use Go\Proxy\TraitProxy;
27
use ReflectionClass;
28
29
/**
30
 * Main transformer that performs weaving of aspects into the source code
31
 */
32
class WeavingTransformer extends BaseSourceTransformer
33
{
34
35
    /**
36
     * @var AdviceMatcher
37
     */
38
    protected $adviceMatcher;
39
40
    /**
41
     * @var CachePathManager
42
     */
43
    private $cachePathManager;
44
45
    /**
46
     * Instance of aspect loader
47
     *
48
     * @var AspectLoader
49
     */
50
    protected $aspectLoader;
51
52
    /**
53
     * Constructs a weaving transformer
54
     *
55
     * @param AspectKernel $kernel Instance of aspect kernel
56
     * @param AdviceMatcher $adviceMatcher Advice matcher for class
57
     * @param CachePathManager $cachePathManager Cache manager
58
     * @param AspectLoader $loader Loader for aspects
59
     */
60 8
    public function __construct(
61
        AspectKernel $kernel,
62
        AdviceMatcher $adviceMatcher,
63
        CachePathManager $cachePathManager,
64
        AspectLoader $loader
65
    )
0 ignored issues
show
Coding Style introduced by
There must be a single space between the closing parenthesis and the opening brace of a multi-line function declaration; found newline
Loading history...
66
    {
67 8
        parent::__construct($kernel);
68 8
        $this->adviceMatcher    = $adviceMatcher;
69 8
        $this->cachePathManager = $cachePathManager;
70 8
        $this->aspectLoader     = $loader;
71 8
    }
72
73
    /**
74
     * This method may transform the supplied source and return a new replacement for it
75
     *
76
     * @param StreamMetaData $metadata Metadata for source
77
     * @return boolean Return false if transformation should be stopped
78
     */
79 8
    public function transform(StreamMetaData $metadata)
80
    {
81 8
        $totalTransformations = 0;
82
83 8
        $fileName = $metadata->uri;
84
85
        try {
86 8
            $parsedSource = new ReflectionFile($fileName);
87
        } catch (FileProcessingException $e) {
0 ignored issues
show
Bug introduced by
The class Go\Instrument\Transformer\FileProcessingException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
88
89
            return false;
90
        }
91
92
        // Check if we have some new aspects that weren't loaded yet
93 8
        $unloadedAspects = $this->aspectLoader->getUnloadedAspects();
94 8
        if (!empty($unloadedAspects)) {
95
            $this->loadAndRegisterAspects($unloadedAspects);
96
        }
97 8
        $advisors = $this->container->getByTag('advisor');
98
99
        /** @var $namespaces ReflectionFileNamespace[] */
100 8
        $namespaces = $parsedSource->getFileNamespaces();
101 8
        $lineOffset = 0;
102
103 8
        foreach ($namespaces as $namespace) {
104
105
            /** @var $classes ReflectionClass[] */
106 8
            $classes = $namespace->getClasses();
107 8
            foreach ($classes as $class) {
108
109
                // Skip interfaces and aspects
110 8
                if ($class->isInterface() || in_array(Aspect::class, $class->getInterfaceNames())) {
111
                    continue;
112
                }
113 8
                $wasClassProcessed = $this->processSingleClass($advisors, $metadata, $class, $lineOffset);
114 8
                $totalTransformations += (integer) $wasClassProcessed;
115
            }
116 8
            $wasFunctionsProcessed = $this->processFunctions($advisors, $metadata, $namespace);
117 8
            $totalTransformations += (integer) $wasFunctionsProcessed;
118
        }
119
120
        // If we return false this will indicate no more transformation for following transformers
121 8
        return $totalTransformations > 0;
122
    }
123
124
    /**
125
     * Performs weaving of single class if needed
126
     *
127
     * @param array|Advisor[] $advisors
128
     * @param StreamMetaData $metadata Source stream information
129
     * @param ReflectionClass $class Instance of class to analyze
130
     * @param integer $lineOffset Current offset, will be updated to store the last position
131
     *
132
     * @return bool True if was class processed, false otherwise
133
     */
134 8
    private function processSingleClass(array $advisors, StreamMetaData $metadata, ReflectionClass $class, &$lineOffset)
0 ignored issues
show
Unused Code introduced by
The parameter $lineOffset is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
135
    {
136 8
        $advices = $this->adviceMatcher->getAdvicesForClass($class, $advisors);
137
138 8
        if (empty($advices)) {
139
            // Fast return if there aren't any advices for that class
140
            return false;
141
        }
142
143
        // Sort advices in advance to keep the correct order in cache
144 8
        foreach ($advices as &$typeAdvices) {
145 8
            foreach ($typeAdvices as &$joinpointAdvices) {
146 8
                if (is_array($joinpointAdvices)) {
147 8
                    $joinpointAdvices = AbstractJoinpoint::sortAdvices($joinpointAdvices);
148
                }
149
            }
150
        }
151
152
        // Prepare new parent name
153 8
        $newParentName = $class->getShortName() . AspectContainer::AOP_PROXIED_SUFFIX;
154
155
        // Replace original class name with new
156 8
        $metadata->source = $this->adjustOriginalClass($class, $metadata->source, $newParentName);
157
158
        // Prepare child Aop proxy
159 8
        $child = $class->isTrait()
160
            ? new TraitProxy($class, $advices)
161 8
            : new ClassProxy($class, $advices);
162
163
        // Set new parent name instead of original
164 8
        $child->setParentName($newParentName);
165 8
        $contentToInclude = $this->saveProxyToCache($class, $child);
166
167 8
        $metadata->source .= $contentToInclude . PHP_EOL;
168
169
/*        // Add child to source
170
        $tokenCount = $class->getBroker()->getFileTokens($class->getFileName())->count();
171
        if ($tokenCount - $class->getEndPosition() < 3) {
172
            // If it's the last class in a file, just add child source
173
            $metadata->source .= $contentToInclude . PHP_EOL;
174
        } else {
175
            $lastLine  = $class->getEndLine() + $lineOffset; // returns the last line of class
176
            $dataArray = explode("\n", $metadata->source);
177
178
            $currentClassArray = array_splice($dataArray, 0, $lastLine);
179
            $childClassArray   = explode("\n", $contentToInclude);
180
            $lineOffset += count($childClassArray) + 2; // returns LoC for child class + 2 blank lines
181
182
            $dataArray = array_merge($currentClassArray, array(''), $childClassArray, array(''), $dataArray);
183
184
            $metadata->source = implode("\n", $dataArray);
185
        }*/
186
187 8
        return true;
188
    }
189
190
    /**
191
     * Adjust definition of original class source to enable extending
192
     *
193
     * @param ReflectionClass $class Instance of class reflection
194
     * @param string $source Source code
195
     * @param string $newParentName New name for the parent class
196
     *
197
     * @return string Replaced code for class
198
     */
199 8
    private function adjustOriginalClass($class, $source, $newParentName)
200
    {
201 8
        $type = $class->isTrait() ? 'trait' : 'class';
202 8
        $source = preg_replace(
203 8
            "/{$type}\s+(" . $class->getShortName() . ')(\b)/iS',
204 8
            "{$type} {$newParentName}$2",
205
            $source
206
        );
207 8
        if ($class->isFinal()) {
208
            // Remove final from class, child will be final instead
209
            $source = str_replace("final {$type}", $type, $source);
210
        }
211
212 8
        return $source;
213
    }
214
215
    /**
216
     * Performs weaving of functions in the current namespace
217
     *
218
     * @param array|Advisor[] $advisors List of advisors
219
     * @param StreamMetaData $metadata Source stream information
220
     * @param ReflectionFileNamespace $namespace Current namespace for file
221
     *
222
     * @return boolean True if functions were processed, false otherwise
223
     */
224 8
    private function processFunctions(array $advisors, StreamMetaData $metadata, $namespace)
225
    {
226 8
        static $cacheDirSuffix = '/_functions/';
227
228 8
        $wasProcessedFunctions = false;
229 8
        $functionAdvices = $this->adviceMatcher->getAdvicesForFunctions($namespace, $advisors);
230 8
        $cacheDir        = $this->cachePathManager->getCacheDir();
231 8
        if (!empty($functionAdvices) && $cacheDir) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $cacheDir of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null 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...
232
            $cacheDir = $cacheDir . $cacheDirSuffix;
233
            $fileName = str_replace('\\', '/', $namespace->getName()) . '.php';
234
235
            $functionFileName = $cacheDir . $fileName;
236
            if (!file_exists($functionFileName) || !$this->container->isFresh(filemtime($functionFileName))) {
237
                $dirname = dirname($functionFileName);
238
                if (!file_exists($dirname)) {
239
                    mkdir($dirname, 0770, true);
240
                }
241
                $source = new FunctionProxy($namespace, $functionAdvices);
242
                file_put_contents($functionFileName, $source);
243
            }
244
            $content = 'include_once AOP_CACHE_DIR . ' . var_export($cacheDirSuffix . $fileName, true) . ';' . PHP_EOL;
245
            $metadata->source .= $content;
246
            $wasProcessedFunctions = true;
247
        }
248
249 8
        return $wasProcessedFunctions;
250
    }
251
252
    /**
253
     * Save AOP proxy to the separate file anr returns the php source code for inclusion
254
     *
255
     * @param ReflectionClass $class Original class reflection
256
     * @param string|ClassProxy $child
257
     *
258
     * @return string
259
     */
260 8
    private function saveProxyToCache($class, $child)
261
    {
262 8
        static $cacheDirSuffix = '/_proxies/';
263
264 8
        $cacheDir = $this->cachePathManager->getCacheDir();
265
266
        // Without cache we should rewrite original file
267 8
        if (!$cacheDir) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $cacheDir of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null 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...
268 8
            return $child;
269
        }
270
        $cacheDir = $cacheDir . $cacheDirSuffix;
271
        $fileName = str_replace($this->options['appDir'] . DIRECTORY_SEPARATOR, '', $class->getFileName());
272
273
        $proxyFileName = $cacheDir . $fileName;
274
        $dirname       = dirname($proxyFileName);
275
        if (!file_exists($dirname)) {
276
            mkdir($dirname, 0770, true);
277
        }
278
279
        $body      = '<?php' . PHP_EOL;
280
        $namespace = $class->getNamespaceName();
281
        if ($namespace) {
282
            $body .= "namespace {$namespace};" . PHP_EOL . PHP_EOL;
283
        }
284
//        foreach ($class->getNamespaceAliases() as $alias => $fqdn) {
285
//            $body .= "use {$fqdn} as {$alias};" . PHP_EOL;
286
//        }
287
        $body .= $child;
288
        file_put_contents($proxyFileName, $body);
289
290
        return 'include_once AOP_CACHE_DIR . ' . var_export($cacheDirSuffix . $fileName, true) . ';' . PHP_EOL;
291
    }
292
293
    /**
294
     * Utility method to load and register unloaded aspects
295
     *
296
     * @param array $unloadedAspects List of unloaded aspects
297
     */
298
    private function loadAndRegisterAspects(array $unloadedAspects)
299
    {
300
        foreach ($unloadedAspects as $unloadedAspect) {
301
            $this->aspectLoader->loadAndRegister($unloadedAspect);
302
        }
303
    }
304
}
305