Completed
Push — master ( eccf6a...345cbf )
by Alexander
03:07
created

WeavingTransformer::adjustOriginalClass()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 21
ccs 13
cts 13
cp 1
rs 8.7624
cc 5
eloc 13
nc 5
nop 3
crap 5
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\ReflectionClass;
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
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 string See RESULT_XXX constants in the interface
78
     */
79 9
    public function transform(StreamMetaData $metadata): string
80
    {
81 9
        $totalTransformations = 0;
82 9
        $parsedSource         = new ReflectionFile($metadata->uri, $metadata->syntaxTree);
83
84
        // Check if we have some new aspects that weren't loaded yet
85 9
        $unloadedAspects = $this->aspectLoader->getUnloadedAspects();
86 9
        if (!empty($unloadedAspects)) {
87
            $this->loadAndRegisterAspects($unloadedAspects);
88
        }
89 9
        $advisors = $this->container->getByTag('advisor');
90
91 9
        $namespaces = $parsedSource->getFileNamespaces();
92
93 9
        foreach ($namespaces as $namespace) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
94
95 9
            $classes = $namespace->getClasses();
96 9
            foreach ($classes as $class) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
97
98
                // Skip interfaces and aspects
99 8
                if ($class->isInterface() || in_array(Aspect::class, $class->getInterfaceNames())) {
100 2
                    continue;
101
                }
102 6
                $wasClassProcessed = $this->processSingleClass($advisors, $metadata, $class);
103 6
                $totalTransformations += (integer) $wasClassProcessed;
104
            }
105 9
            $wasFunctionsProcessed = $this->processFunctions($advisors, $metadata, $namespace);
106 9
            $totalTransformations += (integer) $wasFunctionsProcessed;
107
        }
108
109 9
        $result = ($totalTransformations > 0) ? self::RESULT_TRANSFORMED : self::RESULT_ABSTAIN;
110
111 9
        return $result;
112
    }
113
114
    /**
115
     * Performs weaving of single class if needed
116
     *
117
     * @param array|Advisor[] $advisors
118
     * @param StreamMetaData $metadata Source stream information
119
     * @param ReflectionClass $class Instance of class to analyze
120
     *
121
     * @return bool True if was class processed, false otherwise
122
     */
123 6
    private function processSingleClass(array $advisors, StreamMetaData $metadata, ReflectionClass $class): bool
124
    {
125 6
        $advices = $this->adviceMatcher->getAdvicesForClass($class, $advisors);
126
127 6
        if (empty($advices)) {
128
            // Fast return if there aren't any advices for that class
129
            return false;
130
        }
131
132
        // Sort advices in advance to keep the correct order in cache
133 6
        foreach ($advices as &$typeAdvices) {
134 6
            foreach ($typeAdvices as &$joinpointAdvices) {
135 6
                if (is_array($joinpointAdvices)) {
136 6
                    $joinpointAdvices = AbstractJoinpoint::sortAdvices($joinpointAdvices);
137
                }
138
            }
139
        }
140
141
        // Prepare new class name
142 6
        $newClassName = $class->getShortName() . AspectContainer::AOP_PROXIED_SUFFIX;
143
144
        // Replace original class name with new
145 6
        $this->adjustOriginalClass($class, $metadata, $newClassName);
146
147
        // Prepare child Aop proxy
148 6
        $child = $class->isTrait()
149
            ? new TraitProxy($class, $advices)
150 6
            : new ClassProxy($class, $advices);
151
152
        // Set new parent name instead of original
153 6
        $child->setParentName($newClassName);
154 6
        $contentToInclude = $this->saveProxyToCache($class, $child);
155
156
        // Get last token for this class
157 6
        $lastClassToken = $class->getNode()->getAttribute('endTokenPos');
158
159 6
        $metadata->tokenStream[$lastClassToken][1] .= PHP_EOL . $contentToInclude;
160
161 6
        return true;
162
    }
163
164
    /**
165
     * Adjust definition of original class source to enable extending
166
     *
167
     * @param ReflectionClass $class Instance of class reflection
168
     * @param StreamMetaData $streamMetaData Source code metadata
169
     * @param string $newClassName New name for the class
170
     */
171 6
    private function adjustOriginalClass(ReflectionClass $class, StreamMetaData $streamMetaData, string $newClassName)
172
    {
173 6
        $classNode = $class->getNode();
174 6
        $position  = $classNode->getAttribute('startTokenPos');
175
        do {
176 6
            if (isset($streamMetaData->tokenStream[$position])) {
177 6
                $token = $streamMetaData->tokenStream[$position];
178
                // Remove final and following whitespace from the class, child will be final instead
179 6
                if ($token[0] === T_FINAL) {
180 1
                    unset($streamMetaData->tokenStream[$position], $streamMetaData->tokenStream[$position+1]);
181
                }
182
                // First string is class/trait name
183 6
                if ($token[0] === T_STRING) {
184 6
                    $streamMetaData->tokenStream[$position][1] = $newClassName;
185
                    // We have finished our job, can break this loop
186 6
                    break;
187
                }
188
            }
189 6
            ++$position;
190 6
        } while (true);
191 6
    }
192
193
    /**
194
     * Performs weaving of functions in the current namespace
195
     *
196
     * @param array|Advisor[] $advisors List of advisors
197
     * @param StreamMetaData $metadata Source stream information
198
     * @param ReflectionFileNamespace $namespace Current namespace for file
199
     *
200
     * @return boolean True if functions were processed, false otherwise
201
     */
202 9
    private function processFunctions(
203
        array $advisors,
204
        StreamMetaData $metadata,
205
        ReflectionFileNamespace $namespace
206
    ): bool {
207 9
        static $cacheDirSuffix = '/_functions/';
208
209 9
        $wasProcessedFunctions = false;
210 9
        $functionAdvices = $this->adviceMatcher->getAdvicesForFunctions($namespace, $advisors);
211 9
        $cacheDir        = $this->cachePathManager->getCacheDir();
212 9
        if (!empty($functionAdvices)) {
213
            $cacheDir .= $cacheDirSuffix;
214
            $fileName = str_replace('\\', '/', $namespace->getName()) . '.php';
215
216
            $functionFileName = $cacheDir . $fileName;
217
            if (!file_exists($functionFileName) || !$this->container->isFresh(filemtime($functionFileName))) {
218
                $dirname = dirname($functionFileName);
219
                if (!file_exists($dirname)) {
220
                    mkdir($dirname, $this->options['cacheFileMode'], true);
221
                }
222
                $source = new FunctionProxy($namespace, $functionAdvices);
223
                file_put_contents($functionFileName, $source, LOCK_EX);
224
                // For cache files we don't want executable bits by default
225
                chmod($functionFileName, $this->options['cacheFileMode'] & (~0111));
226
            }
227
            $content = 'include_once AOP_CACHE_DIR . ' . var_export($cacheDirSuffix . $fileName, true) . ';';
228
229
            $lastTokenPosition = $namespace->getLastTokenPosition();
230
            $metadata->tokenStream[$lastTokenPosition][1] .= PHP_EOL . $content;
231
            $wasProcessedFunctions = true;
232
        }
233
234 9
        return $wasProcessedFunctions;
235
    }
236
237
    /**
238
     * Save AOP proxy to the separate file anr returns the php source code for inclusion
239
     *
240
     * @param ReflectionClass $class Original class reflection
241
     * @param string|ClassProxy $child
242
     *
243
     * @return string
244
     */
245 6
    private function saveProxyToCache(ReflectionClass $class, $child): string
246
    {
247 6
        static $cacheDirSuffix = '/_proxies/';
248
249 6
        $cacheDir = $this->cachePathManager->getCacheDir();
250
251
        // Without cache we should rewrite original file
252 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...
253 6
            return (string) $child;
254
        }
255
        $cacheDir .= $cacheDirSuffix;
256
        $fileName = str_replace($this->options['appDir'] . DIRECTORY_SEPARATOR, '', $class->getFileName());
257
258
        $proxyFileName = $cacheDir . $fileName;
259
        $dirname       = dirname($proxyFileName);
260
        if (!file_exists($dirname)) {
261
            mkdir($dirname, $this->options['cacheFileMode'], true);
262
        }
263
264
        $body      = '<?php' . PHP_EOL;
265
        $namespace = $class->getNamespaceName();
266
        if ($namespace) {
267
            $body .= "namespace {$namespace};" . PHP_EOL . PHP_EOL;
268
        }
269
270
        $refNamespace = new ReflectionFileNamespace($class->getFileName(), $namespace);
271
        foreach ($refNamespace->getNamespaceAliases() as $fqdn => $alias) {
272
            $body .= "use {$fqdn} as {$alias};" . PHP_EOL;
273
        }
274
275
        $body .= $child;
276
        file_put_contents($proxyFileName, $body, LOCK_EX);
277
        // For cache files we don't want executable bits by default
278
        chmod($proxyFileName, $this->options['cacheFileMode'] & (~0111));
279
280
        return 'include_once AOP_CACHE_DIR . ' . var_export($cacheDirSuffix . $fileName, true) . ';';
281
    }
282
283
    /**
284
     * Utility method to load and register unloaded aspects
285
     *
286
     * @param array $unloadedAspects List of unloaded aspects
287
     */
288
    private function loadAndRegisterAspects(array $unloadedAspects)
289
    {
290
        foreach ($unloadedAspects as $unloadedAspect) {
291
            $this->aspectLoader->loadAndRegister($unloadedAspect);
292
        }
293
    }
294
}
295