Completed
Push — 1.x ( c183a4...05b51a )
by Alexander
8s
created

WeavingTransformer   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 299
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 69.03%

Importance

Changes 13
Bugs 5 Features 0
Metric Value
wmc 32
c 13
b 5
f 0
lcom 1
cbo 10
dl 0
loc 299
ccs 78
cts 113
cp 0.6903
rs 9.6

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 1
B processSingleClass() 0 53 7
A adjustOriginalClass() 0 15 3
B transform() 0 58 8
B processFunctions() 0 29 6
B saveProxyToCache() 0 34 5
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\Proxy\ClassProxy;
23
use Go\Proxy\FunctionProxy;
24
use Go\Proxy\TraitProxy;
25
use TokenReflection\Broker;
26
use TokenReflection\Exception\FileProcessingException;
27
use TokenReflection\ReflectionClass as ParsedClass;
28
use TokenReflection\ReflectionFileNamespace as ParsedFileNamespace;
29
30
/**
31
 * Main transformer that performs weaving of aspects into the source code
32
 */
33
class WeavingTransformer extends BaseSourceTransformer
34
{
35
36
    /**
37
     * Reflection broker instance
38
     *
39
     * @var Broker
40
     */
41
    protected $broker;
42
43
    /**
44
     * @var AdviceMatcher
45
     */
46
    protected $adviceMatcher;
47
48
    /**
49
     * @var CachePathManager
50
     */
51
    private $cachePathManager;
52
53
    /**
54
     * Instance of aspect loader
55
     *
56
     * @var AspectLoader
57
     */
58
    protected $aspectLoader;
59
60
    /**
61
     * Constructs a weaving transformer
62
     *
63
     * @param AspectKernel $kernel Instance of aspect kernel
64
     * @param Broker $broker Instance of reflection broker to use
65
     * @param AdviceMatcher $adviceMatcher Advice matcher for class
66
     * @param CachePathManager $cachePathManager Cache manager
67
     * @param AspectLoader $loader Loader for aspects
68
     */
69 8
    public function __construct(
70
        AspectKernel $kernel,
71
        Broker $broker,
72
        AdviceMatcher $adviceMatcher,
73
        CachePathManager $cachePathManager,
74
        AspectLoader $loader
75
    )
76
    {
0 ignored issues
show
Coding Style introduced by
The closing parenthesis and the opening brace of a multi-line function declaration must be on the same line
Loading history...
77 8
        parent::__construct($kernel);
78 8
        $this->broker           = $broker;
79 8
        $this->adviceMatcher    = $adviceMatcher;
80 8
        $this->cachePathManager = $cachePathManager;
81 8
        $this->aspectLoader     = $loader;
82 8
    }
83
84
    /**
85
     * This method may transform the supplied source and return a new replacement for it
86
     *
87
     * @param StreamMetaData $metadata Metadata for source
88
     * @return boolean Return false if transformation should be stopped
89
     */
90 8
    public function transform(StreamMetaData $metadata)
91
    {
92 8
        $totalTransformations = 0;
93
94 8
        $fileName = $metadata->uri;
95
96
        try {
97 8
            CleanableMemory::enterProcessing();
98 8
            $parsedSource = $this->broker->processString($metadata->source, $fileName, true);
99
        } catch (FileProcessingException $e) {
0 ignored issues
show
Bug introduced by
The class TokenReflection\Exception\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...
100
            CleanableMemory::leaveProcessing();
101
102
            return false;
103
        }
104
105
        // Check if we have some new aspects that weren't loaded yet
106 8
        $unloadedAspects = $this->aspectLoader->getUnloadedAspects();
107 8
        if (!empty($unloadedAspects)) {
108
            $this->loadAndRegisterAspects($unloadedAspects);
109
        }
110 8
        $advisors = $this->container->getByTag('advisor');
111
112
        /** @var $namespaces ParsedFileNamespace[] */
113 8
        $namespaces = $parsedSource->getNamespaces();
114 8
        $lineOffset = 0;
115
116 8
        foreach ($namespaces as $namespace) {
117
118
            /** @var $classes ParsedClass[] */
119 8
            $classes = $namespace->getClasses();
120 8
            foreach ($classes as $class) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
121
122 7
                $parentClassNames = array_merge(
123 7
                    $class->getParentClassNameList(),
124 7
                    $class->getInterfaceNames(),
125 7
                    $class->getTraitNames()
126
                );
127
128 7
                foreach ($parentClassNames as $parentClassName) {
129 1
                    class_exists($parentClassName); // trigger autoloading of class/interface/trait
130
                }
131
132
                // Skip interfaces and aspects
133 7
                if ($class->isInterface() || in_array(Aspect::class, $class->getInterfaceNames())) {
134 2
                    continue;
135
                }
136 5
                $wasClassProcessed = $this->processSingleClass($advisors, $metadata, $class, $lineOffset);
137 5
                $totalTransformations += (integer) $wasClassProcessed;
138
            }
139 8
            $wasFunctionsProcessed = $this->processFunctions($advisors, $metadata, $namespace);
140 8
            $totalTransformations += (integer) $wasFunctionsProcessed;
141
        }
142
143 8
        CleanableMemory::leaveProcessing();
144
145
        // If we return false this will indicate no more transformation for following transformers
146 8
        return $totalTransformations > 0;
147
    }
148
149
    /**
150
     * Performs weaving of single class if needed
151
     *
152
     * @param array|Advisor[] $advisors
153
     * @param StreamMetaData $metadata Source stream information
154
     * @param ParsedClass $class Instance of class to analyze
155
     * @param integer $lineOffset Current offset, will be updated to store the last position
156
     *
157
     * @return bool True if was class processed, false otherwise
158
     */
159 5
    private function processSingleClass(array $advisors, StreamMetaData $metadata, ParsedClass $class, &$lineOffset)
160
    {
161 5
        $advices = $this->adviceMatcher->getAdvicesForClass($class, $advisors);
162
163 5
        if (empty($advices)) {
164
            // Fast return if there aren't any advices for that class
165
            return false;
166
        }
167
168
        // Sort advices in advance to keep the correct order in cache
169 5
        foreach ($advices as &$typeAdvices) {
170 5
            foreach ($typeAdvices as &$joinpointAdvices) {
171 5
                if (is_array($joinpointAdvices)) {
172 5
                    $joinpointAdvices = AbstractJoinpoint::sortAdvices($joinpointAdvices);
173
                }
174
            }
175
        }
176
177
        // Prepare new parent name
178 5
        $newParentName = $class->getShortName() . AspectContainer::AOP_PROXIED_SUFFIX;
179
180
        // Replace original class name with new
181 5
        $metadata->source = $this->adjustOriginalClass($class, $metadata->source, $newParentName);
182
183
        // Prepare child Aop proxy
184 5
        $child = $class->isTrait()
185
            ? new TraitProxy($class, $advices)
186 5
            : new ClassProxy($class, $advices);
187
188
        // Set new parent name instead of original
189 5
        $child->setParentName($newParentName);
190 5
        $contentToInclude = $this->saveProxyToCache($class, $child);
191
192
        // Add child to source
193 5
        $tokenCount = $class->getBroker()->getFileTokens($class->getFileName())->count();
194 5
        if ($tokenCount - $class->getEndPosition() < 3) {
195
            // If it's the last class in a file, just add child source
196 3
            $metadata->source .= $contentToInclude . PHP_EOL;
197
        } else {
198 2
            $lastLine  = $class->getEndLine() + $lineOffset; // returns the last line of class
199 2
            $dataArray = explode("\n", $metadata->source);
200
201 2
            $currentClassArray = array_splice($dataArray, 0, $lastLine);
202 2
            $childClassArray   = explode("\n", $contentToInclude);
203 2
            $lineOffset += count($childClassArray) + 2; // returns LoC for child class + 2 blank lines
204
205 2
            $dataArray = array_merge($currentClassArray, array(''), $childClassArray, array(''), $dataArray);
206
207 2
            $metadata->source = implode("\n", $dataArray);
208
        }
209
210 5
        return true;
211
    }
212
213
    /**
214
     * Adjust definition of original class source to enable extending
215
     *
216
     * @param ParsedClass $class Instance of class reflection
217
     * @param string $source Source code
218
     * @param string $newParentName New name for the parent class
219
     *
220
     * @return string Replaced code for class
221
     */
222 5
    private function adjustOriginalClass($class, $source, $newParentName)
223
    {
224 5
        $type = $class->isTrait() ? 'trait' : 'class';
225 5
        $source = preg_replace(
226 5
            "/{$type}\s+(" . $class->getShortName() . ')(\b)/iS',
227 5
            "{$type} {$newParentName}$2",
228
            $source
229
        );
230 5
        if ($class->isFinal()) {
231
            // Remove final from class, child will be final instead
232 1
            $source = str_replace("final {$type}", $type, $source);
233
        }
234
235 5
        return $source;
236
    }
237
238
    /**
239
     * Performs weaving of functions in the current namespace
240
     *
241
     * @param array|Advisor[] $advisors List of advisors
242
     * @param StreamMetaData $metadata Source stream information
243
     * @param ParsedFileNamespace $namespace Current namespace for file
244
     *
245
     * @return boolean True if functions were processed, false otherwise
246
     */
247 8
    private function processFunctions(array $advisors, StreamMetaData $metadata, $namespace)
248
    {
249 8
        static $cacheDirSuffix = '/_functions/';
250
251 8
        $wasProcessedFunctions = false;
252 8
        $functionAdvices = $this->adviceMatcher->getAdvicesForFunctions($namespace, $advisors);
253 8
        $cacheDir        = $this->cachePathManager->getCacheDir();
254 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...
255
            $cacheDir = $cacheDir . $cacheDirSuffix;
256
            $fileName = str_replace('\\', '/', $namespace->getName()) . '.php';
257
258
            $functionFileName = $cacheDir . $fileName;
259
            if (!file_exists($functionFileName) || !$this->container->isFresh(filemtime($functionFileName))) {
260
                $dirname = dirname($functionFileName);
261
                if (!file_exists($dirname)) {
262
                    mkdir($dirname, $this->options['cacheFileMode'], true);
263
                }
264
                $source = new FunctionProxy($namespace, $functionAdvices);
265
                file_put_contents($functionFileName, $source, LOCK_EX);
266
                // For cache files we don't want executable bits by default
267
                chmod($functionFileName, $this->options['cacheFileMode'] & (~0111));
268
            }
269
            $content = 'include_once AOP_CACHE_DIR . ' . var_export($cacheDirSuffix . $fileName, true) . ';' . PHP_EOL;
270
            $metadata->source .= $content;
271
            $wasProcessedFunctions = true;
272
        }
273
274 8
        return $wasProcessedFunctions;
275
    }
276
277
    /**
278
     * Save AOP proxy to the separate file anr returns the php source code for inclusion
279
     *
280
     * @param ParsedClass $class Original class reflection
281
     * @param ClassProxy $child
282
     *
283
     * @return string
284
     */
285 5
    private function saveProxyToCache($class, $child)
286
    {
287 5
        static $cacheDirSuffix = '/_proxies/';
288
289 5
        $cacheDir = $this->cachePathManager->getCacheDir();
290
291
        // Without cache we should rewrite original file
292 5
        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...
293 5
            return $child;
294
        }
295
        $cacheDir = $cacheDir . $cacheDirSuffix;
296
        $fileName = str_replace($this->options['appDir'] . DIRECTORY_SEPARATOR, '', $class->getFileName());
297
298
        $proxyFileName = $cacheDir . $fileName;
299
        $dirname       = dirname($proxyFileName);
300
        if (!file_exists($dirname)) {
301
            mkdir($dirname, $this->options['cacheFileMode'], true);
302
        }
303
304
        $body      = '<?php' . PHP_EOL;
305
        $namespace = $class->getNamespaceName();
306
        if ($namespace) {
307
            $body .= "namespace {$namespace};" . PHP_EOL . PHP_EOL;
308
        }
309
        foreach ($class->getNamespaceAliases() as $alias => $fqdn) {
310
            $body .= "use {$fqdn} as {$alias};" . PHP_EOL;
311
        }
312
        $body .= $child;
313
        file_put_contents($proxyFileName, $body, LOCK_EX);
314
        // For cache files we don't want executable bits by default
315
        chmod($proxyFileName, $this->options['cacheFileMode'] & (~0111));
316
317
        return 'include_once AOP_CACHE_DIR . ' . var_export($cacheDirSuffix . $fileName, true) . ';' . PHP_EOL;
318
    }
319
320
    /**
321
     * Utility method to load and register unloaded aspects
322
     *
323
     * @param array $unloadedAspects List of unloaded aspects
324
     */
325
    private function loadAndRegisterAspects(array $unloadedAspects)
326
    {
327
        foreach ($unloadedAspects as $unloadedAspect) {
328
            $this->aspectLoader->loadAndRegister($unloadedAspect);
329
        }
330
    }
331
}
332