Completed
Push — master ( 06da57...18ce8b )
by Alexander
02:22
created

WeavingTransformer::processFunctions()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 34
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 12.4085

Importance

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