Completed
Pull Request — master (#328)
by Nikola
03:49
created

WeavingTransformer   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 266
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 64.75%

Importance

Changes 0
Metric Value
wmc 29
lcom 1
cbo 12
dl 0
loc 266
ccs 68
cts 105
cp 0.6475
rs 10
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 1
B transform() 0 38 6
B processSingleClass() 0 47 6
B processFunctions() 0 29 6
B saveProxyToCache() 0 37 5
A adjustOriginalClass() 0 15 3
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\ParserReflection\ReflectionEngine;
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 9
    public function __construct(
61
        AspectKernel $kernel,
62
        AdviceMatcher $adviceMatcher,
63
        CachePathManager $cachePathManager,
64
        AspectLoader $loader
65
    )
66
    {
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...
67 9
        parent::__construct($kernel);
68 9
        $this->adviceMatcher    = $adviceMatcher;
69 9
        $this->cachePathManager = $cachePathManager;
70 9
        $this->aspectLoader     = $loader;
71 9
    }
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 9
    public function transform(StreamMetaData $metadata)
80
    {
81 9
        $totalTransformations = 0;
82
83 9
        $fileName = $metadata->uri;
84
85 9
        $astTree      = ReflectionEngine::parseFile($fileName, $metadata->source);
86 9
        $parsedSource = new ReflectionFile($fileName, $astTree);
87
88
        // Check if we have some new aspects that weren't loaded yet
89 9
        $unloadedAspects = $this->aspectLoader->getUnloadedAspects();
90 9
        if (!empty($unloadedAspects)) {
91
            $this->loadAndRegisterAspects($unloadedAspects);
92
        }
93 9
        $advisors = $this->container->getByTag('advisor');
94
95 9
        $namespaces = $parsedSource->getFileNamespaces();
96 9
        $lineOffset = 0;
97
98 9
        foreach ($namespaces as $namespace) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
99
100 9
            $classes = $namespace->getClasses();
101 9
            foreach ($classes as $class) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
102
103
                // Skip interfaces and aspects
104 8
                if ($class->isInterface() || in_array(Aspect::class, $class->getInterfaceNames())) {
105 2
                    continue;
106
                }
107 6
                $wasClassProcessed = $this->processSingleClass($advisors, $metadata, $class, $lineOffset);
108 6
                $totalTransformations += (integer) $wasClassProcessed;
109
            }
110 9
            $wasFunctionsProcessed = $this->processFunctions($advisors, $metadata, $namespace);
111 9
            $totalTransformations += (integer) $wasFunctionsProcessed;
112
        }
113
114
        // If we return false this will indicate no more transformation for following transformers
115 9
        return $totalTransformations > 0;
116
    }
117
118
    /**
119
     * Performs weaving of single class if needed
120
     *
121
     * @param array|Advisor[] $advisors
122
     * @param StreamMetaData $metadata Source stream information
123
     * @param ReflectionClass $class Instance of class to analyze
124
     * @param integer $lineOffset Current offset, will be updated to store the last position
125
     *
126
     * @return bool True if was class processed, false otherwise
127
     */
128 6
    private function processSingleClass(array $advisors, StreamMetaData $metadata, ReflectionClass $class, &$lineOffset)
129
    {
130 6
        $advices = $this->adviceMatcher->getAdvicesForClass($class, $advisors);
131
132 6
        if (empty($advices)) {
133
            // Fast return if there aren't any advices for that class
134
            return false;
135
        }
136
137
        // Sort advices in advance to keep the correct order in cache
138 6
        foreach ($advices as &$typeAdvices) {
139 6
            foreach ($typeAdvices as &$joinpointAdvices) {
140 6
                if (is_array($joinpointAdvices)) {
141 6
                    $joinpointAdvices = AbstractJoinpoint::sortAdvices($joinpointAdvices);
142
                }
143
            }
144
        }
145
146
        // Prepare new parent name
147 6
        $newParentName = $class->getShortName() . AspectContainer::AOP_PROXIED_SUFFIX;
148
149
        // Replace original class name with new
150 6
        $metadata->source = $this->adjustOriginalClass($class, $metadata->source, $newParentName);
151
152
        // Prepare child Aop proxy
153 6
        $child = $class->isTrait()
154
            ? new TraitProxy($class, $advices)
155 6
            : new ClassProxy($class, $advices);
156
157
        // Set new parent name instead of original
158 6
        $child->setParentName($newParentName);
159 6
        $contentToInclude = $this->saveProxyToCache($class, $child);
160
161
        // Add child to source
162 6
        $lastLine  = $class->getEndLine() + $lineOffset; // returns the last line of class
163 6
        $dataArray = explode("\n", $metadata->source);
164
165 6
        $currentClassArray = array_splice($dataArray, 0, $lastLine);
166 6
        $childClassArray   = explode("\n", $contentToInclude);
167 6
        $lineOffset += count($childClassArray) + 2; // returns LoC for child class + 2 blank lines
168
169 6
        $dataArray = array_merge($currentClassArray, array(''), $childClassArray, array(''), $dataArray);
170
171 6
        $metadata->source = implode("\n", $dataArray);
172
173 6
        return true;
174
    }
175
176
    /**
177
     * Adjust definition of original class source to enable extending
178
     *
179
     * @param ReflectionClass $class Instance of class reflection
180
     * @param string $source Source code
181
     * @param string $newParentName New name for the parent class
182
     *
183
     * @return string Replaced code for class
184
     */
185 6
    private function adjustOriginalClass($class, $source, $newParentName)
186
    {
187 6
        $type = $class->isTrait() ? 'trait' : 'class';
188 6
        $source = preg_replace(
189 6
            "/{$type}\s+(" . $class->getShortName() . ')(\b)/iS',
190 6
            "{$type} {$newParentName}$2",
191 6
            $source
192
        );
193 6
        if ($class->isFinal()) {
194
            // Remove final from class, child will be final instead
195 1
            $source = str_replace("final {$type}", $type, $source);
196
        }
197
198 6
        return $source;
199
    }
200
201
    /**
202
     * Performs weaving of functions in the current namespace
203
     *
204
     * @param array|Advisor[] $advisors List of advisors
205
     * @param StreamMetaData $metadata Source stream information
206
     * @param ReflectionFileNamespace $namespace Current namespace for file
207
     *
208
     * @return boolean True if functions were processed, false otherwise
209
     */
210 9
    private function processFunctions(array $advisors, StreamMetaData $metadata, $namespace)
211
    {
212 9
        static $cacheDirSuffix = '/_functions/';
213
214 9
        $wasProcessedFunctions = false;
215 9
        $functionAdvices = $this->adviceMatcher->getAdvicesForFunctions($namespace, $advisors);
216 9
        $cacheDir        = $this->cachePathManager->getCacheDir();
217 9
        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...
218
            $cacheDir = $cacheDir . $cacheDirSuffix;
219
            $fileName = str_replace('\\', '/', $namespace->getName()) . '.php';
220
221
            $functionFileName = $cacheDir . $fileName;
222
            if (!file_exists($functionFileName) || !$this->container->isFresh(filemtime($functionFileName))) {
223
                $dirname = dirname($functionFileName);
224
                if (!file_exists($dirname)) {
225
                    mkdir($dirname, $this->options['cacheFileMode'], true);
226
                }
227
                $source = new FunctionProxy($namespace, $functionAdvices);
228
                file_put_contents($functionFileName, $source, LOCK_EX);
229
                // For cache files we don't want executable bits by default
230
                chmod($functionFileName, $this->options['cacheFileMode'] & (~0111));
231
            }
232
            $content = 'include_once AOP_CACHE_DIR . ' . var_export($cacheDirSuffix . $fileName, true) . ';' . PHP_EOL;
233
            $metadata->source .= $content;
234
            $wasProcessedFunctions = true;
235
        }
236
237 9
        return $wasProcessedFunctions;
238
    }
239
240
    /**
241
     * Save AOP proxy to the separate file anr returns the php source code for inclusion
242
     *
243
     * @param ReflectionClass $class Original class reflection
244
     * @param string|ClassProxy $child
245
     *
246
     * @return string
247
     */
248 6
    private function saveProxyToCache($class, $child)
249
    {
250 6
        static $cacheDirSuffix = '/_proxies/';
251
252 6
        $cacheDir = $this->cachePathManager->getCacheDir();
253
254
        // Without cache we should rewrite original file
255 6
        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...
256 6
            return $child;
257
        }
258
        $cacheDir = $cacheDir . $cacheDirSuffix;
259
        $fileName = str_replace($this->options['appDir'] . DIRECTORY_SEPARATOR, '', $class->getFileName());
260
261
        $proxyFileName = $cacheDir . $fileName;
262
        $dirname       = dirname($proxyFileName);
263
        if (!file_exists($dirname)) {
264
            mkdir($dirname, $this->options['cacheFileMode'], true);
265
        }
266
267
        $body      = '<?php' . PHP_EOL;
268
        $namespace = $class->getNamespaceName();
269
        if ($namespace) {
270
            $body .= "namespace {$namespace};" . PHP_EOL . PHP_EOL;
271
        }
272
273
        $refNamespace = new ReflectionFileNamespace($class->getFileName(), $namespace);
274
        foreach ($refNamespace->getNamespaceAliases() as $fqdn => $alias) {
275
            $body .= "use {$fqdn} as {$alias};" . PHP_EOL;
276
        }
277
278
        $body .= $child;
279
        file_put_contents($proxyFileName, $body, LOCK_EX);
280
        // For cache files we don't want executable bits by default
281
        chmod($proxyFileName, $this->options['cacheFileMode'] & (~0111));
282
283
        return 'include_once AOP_CACHE_DIR . ' . var_export($cacheDirSuffix . $fileName, true) . ';' . PHP_EOL;
284
    }
285
286
    /**
287
     * Utility method to load and register unloaded aspects
288
     *
289
     * @param array $unloadedAspects List of unloaded aspects
290
     */
291
    private function loadAndRegisterAspects(array $unloadedAspects)
292
    {
293
        foreach ($unloadedAspects as $unloadedAspect) {
294
            $this->aspectLoader->loadAndRegister($unloadedAspect);
295
        }
296
    }
297
}
298