Completed
Push — master ( bfa80e...3adc88 )
by Alexander
02:11
created

WeavingTransformer::transform()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 38
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 6.0038

Importance

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