Completed
Pull Request — master (#463)
by Alexander
30:17 queued 05:15
created

WeavingTransformer   A

Complexity

Total Complexity 37

Size/Duplication

Total Lines 281
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 17

Test Coverage

Coverage 72.95%

Importance

Changes 0
Metric Value
wmc 37
lcom 1
cbo 17
dl 0
loc 281
ccs 89
cts 122
cp 0.7295
rs 9.44
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types = 1);
4
/*
5
 * Go! AOP framework
6
 *
7
 * @copyright Copyright 2011, Lisachenko Alexander <[email protected]>
8
 *
9
 * This source file is subject to the license that is bundled
10
 * with this source code in the file LICENSE.
11
 */
12
13
namespace Go\Instrument\Transformer;
14
15
use Go\Aop\Advisor;
16
use Go\Aop\Aspect;
17
use Go\Aop\Features;
18
use Go\Aop\Framework\AbstractJoinpoint;
19
use Go\Core\AdviceMatcher;
20
use Go\Core\AdviceMatcherInterface;
21
use Go\Core\AspectContainer;
22
use Go\Core\AspectKernel;
23
use Go\Core\AspectLoader;
24
use Go\Instrument\ClassLoading\CachePathManager;
25
use Go\ParserReflection\ReflectionClass;
26
use Go\ParserReflection\ReflectionFile;
27
use Go\ParserReflection\ReflectionFileNamespace;
28
use Go\ParserReflection\ReflectionMethod;
29
use Go\Proxy\ClassProxyGenerator;
30
use Go\Proxy\FunctionProxyGenerator;
31
use Go\Proxy\TraitProxyGenerator;
32
33
/**
34
 * Main transformer that performs weaving of aspects into the source code
35
 */
36
class WeavingTransformer extends BaseSourceTransformer
37
{
38
    /**
39
     * Advice matcher for class
40
     */
41
    protected AdviceMatcherInterface $adviceMatcher;
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected T_STRING, expecting T_FUNCTION or T_CONST
Loading history...
42
43
    /**
44
     * Should we use parameter widening for our decorators
45
     */
46
    protected bool $useParameterWidening = false;
47
48
    /**
49
     * Cache manager
50
     */
51
    private CachePathManager $cachePathManager;
52
53
    /**
54
     * Loader for aspects
55
     */
56
    protected AspectLoader $aspectLoader;
57
58
    /**
59
     * Constructs a weaving transformer
60 8
     */
61
    public function __construct(
62
        AspectKernel $kernel,
63
        AdviceMatcherInterface $adviceMatcher,
64
        CachePathManager $cachePathManager,
65
        AspectLoader $loader
66 8
    ) {
67 8
        parent::__construct($kernel);
68 8
        $this->adviceMatcher    = $adviceMatcher;
69 8
        $this->cachePathManager = $cachePathManager;
70
        $this->aspectLoader     = $loader;
71 8
72 8
        $this->useParameterWidening = $kernel->hasFeature(Features::PARAMETER_WIDENING);
73
    }
74
75
    /**
76
     * This method may transform the supplied source and return a new replacement for it
77
     *
78
     * @param StreamMetaData $metadata
79
     * @return string See RESULT_XXX constants in the interface
80 8
     */
81
    public function transform(StreamMetaData $metadata): string
82 8
    {
83 8
        $totalTransformations = 0;
84
        $parsedSource         = new ReflectionFile($metadata->uri, $metadata->syntaxTree);
85
86 8
        // Check if we have some new aspects that weren't loaded yet
87 8
        $unloadedAspects = $this->aspectLoader->getUnloadedAspects();
88 1
        if (!empty($unloadedAspects)) {
89
            $this->loadAndRegisterAspects($unloadedAspects);
90 8
        }
91
        $advisors = $this->container->getByTag('advisor');
92 8
93
        $namespaces = $parsedSource->getFileNamespaces();
94 8
95 8
        foreach ($namespaces as $namespace) {
96 8
            $classes = $namespace->getClasses();
97
            foreach ($classes as $class) {
98 7
                // Skip interfaces and aspects
99 2
                if ($class->isInterface() || in_array(Aspect::class, $class->getInterfaceNames(), true)) {
100
                    continue;
101 6
                }
102 6
                $wasClassProcessed = $this->processSingleClass(
103 6
                    $advisors,
104 6
                    $metadata,
105 6
                    $class,
106
                    $parsedSource->isStrictMode()
107 6
                );
108
                $totalTransformations += (integer) $wasClassProcessed;
109 8
            }
110 8
            $wasFunctionsProcessed = $this->processFunctions($advisors, $metadata, $namespace);
111
            $totalTransformations += (integer) $wasFunctionsProcessed;
112
        }
113 8
114
        $result = ($totalTransformations > 0) ? self::RESULT_TRANSFORMED : self::RESULT_ABSTAIN;
115 8
116
        return $result;
117
    }
118
119
    /**
120
     * Performs weaving of single class if needed, returns true if the class was processed
121
     *
122
     * @param Advisor[]       $advisors List of advisors
123
     * @param StreamMetaData  $metadata
124
     * @param ReflectionClass $class
125
     * @param bool            $useStrictMode If the source file used strict mode, the proxy should too
126
     * @return bool
127 6
     */
128
    private function processSingleClass(
129
        array $advisors,
130
        StreamMetaData $metadata,
131
        ReflectionClass $class,
132
        bool $useStrictMode
133 6
    ): bool {
134
        $advices = $this->adviceMatcher->getAdvicesForClass($class, $advisors);
135 6
136
        if (empty($advices)) {
137 1
            // Fast return if there aren't any advices for that class
138
            return false;
139
        }
140
141 6
        // Sort advices in advance to keep the correct order in cache, and leave only keys for the cache
142
        $advices = AbstractJoinpoint::flatAndSortAdvices($advices);
143
144 6
        // Prepare new class name
145
        $newClassName = $class->getShortName() . AspectContainer::AOP_PROXIED_SUFFIX;
146
147 6
        // Replace original class name with new
148 6
        $this->adjustOriginalClass($class, $advices, $metadata, $newClassName);
149
        $newParentName = $class->getNamespaceName() . '\\' . $newClassName;
150
151 6
        // Prepare child Aop proxy
152
        $childProxyGenerator = $class->isTrait()
153 6
            ? new TraitProxyGenerator($class, $newParentName, $advices, $this->useParameterWidening)
154
            : new ClassProxyGenerator($class, $newParentName, $advices, $this->useParameterWidening);
155 6
156 6
        $refNamespace = new ReflectionFileNamespace($class->getFileName(), $class->getNamespaceName());
157
        foreach ($refNamespace->getNamespaceAliases() as $fqdn => $alias) {
158 1
            // Either we have a string or Identifier node
159 1
            if ($alias !== null) {
160
                $childProxyGenerator->addUse($fqdn, (string) $alias);
161
            } else {
162
                $childProxyGenerator->addUse($fqdn);
163
            }
164
        }
165 6
166
        $childCode = $childProxyGenerator->generate();
167 6
168 5
        if ($useStrictMode) {
169
            $childCode = 'declare(strict_types=1);' . PHP_EOL . $childCode;
170
        }
171 6
172
        $contentToInclude = $this->saveProxyToCache($class, $childCode);
173
174 6
        // Get last token for this class
175
        $lastClassToken = $class->getNode()->getAttribute('endTokenPos');
176 6
177
        $metadata->tokenStream[$lastClassToken][1] .= PHP_EOL . $contentToInclude;
178 6
179
        return true;
180
    }
181
182
    /**
183
     * Adjust definition of original class source to enable extending
184
     *
185
     * @param array $advices List of class advices (used to check for final methods and make them non-final)
186 6
     */
187
    private function adjustOriginalClass(
188
        ReflectionClass $class,
189
        array $advices,
190
        StreamMetaData $streamMetaData,
191
        string $newClassName
192 6
    ): void {
193 6
        $classNode = $class->getNode();
194
        $position  = $classNode->getAttribute('startTokenPos');
195 6
        do {
196 6
            if (isset($streamMetaData->tokenStream[$position])) {
197
                $token = $streamMetaData->tokenStream[$position];
198 6
                // Remove final and following whitespace from the class, child will be final instead
199
                if ($token[0] === T_FINAL) {
200
                    unset($streamMetaData->tokenStream[$position], $streamMetaData->tokenStream[$position+1]);
201
                }
202 6
                // First string is class/trait name
203 6
                if ($token[0] === T_STRING) {
204
                    $streamMetaData->tokenStream[$position][1] = $newClassName;
205 6
                    // We have finished our job, can break this loop
206
                    break;
207
                }
208 6
            }
209 6
            ++$position;
210
        } while (true);
211 6
212
        foreach ($class->getMethods(ReflectionMethod::IS_FINAL) as $finalMethod) {
213
            if (!$finalMethod instanceof ReflectionMethod || $finalMethod->getDeclaringClass()->name !== $class->name) {
214
                continue;
215
            }
216
            $hasDynamicAdvice = isset($advices[AspectContainer::METHOD_PREFIX][$finalMethod->name]);
217
            $hasStaticAdvice  = isset($advices[AspectContainer::STATIC_METHOD_PREFIX][$finalMethod->name]);
218
            if (!$hasDynamicAdvice && !$hasStaticAdvice) {
219
                continue;
220
            }
221
            $methodNode = $finalMethod->getNode();
222
            $position   = $methodNode->getAttribute('startTokenPos');
223
            do {
224
                if (isset($streamMetaData->tokenStream[$position])) {
225
                    $token = $streamMetaData->tokenStream[$position];
226
                    // Remove final and following whitespace from the method, child will be final instead
227
                    if ($token[0] === T_FINAL) {
228
                        unset($streamMetaData->tokenStream[$position], $streamMetaData->tokenStream[$position+1]);
229
                        break;
230
                    }
231
                }
232
                ++$position;
233
            } while (true);
234 6
        }
235
    }
236
237
    /**
238
     * Performs weaving of functions in the current namespace, returns true if functions were processed, false otherwise
239
     *
240
     * @param Advisor[] $advisors List of advisors
241 8
     */
242
    private function processFunctions(
243
        array $advisors,
244
        StreamMetaData $metadata,
245
        ReflectionFileNamespace $namespace
246 8
    ): bool {
247
        static $cacheDirSuffix = '/_functions/';
248 8
249 8
        $wasProcessedFunctions = false;
250 8
        $functionAdvices = $this->adviceMatcher->getAdvicesForFunctions($namespace, $advisors);
251 8
        $cacheDir        = $this->cachePathManager->getCacheDir();
252
        if (!empty($functionAdvices)) {
253
            $cacheDir .= $cacheDirSuffix;
254
            $fileName = str_replace('\\', '/', $namespace->getName()) . '.php';
255
256
            $functionFileName = $cacheDir . $fileName;
257
            if (!file_exists($functionFileName) || !$this->container->isFresh(filemtime($functionFileName))) {
258
                $functionAdvices = AbstractJoinpoint::flatAndSortAdvices($functionAdvices);
259
                $dirname         = dirname($functionFileName);
260
                if (!file_exists($dirname)) {
261
                    mkdir($dirname, $this->options['cacheFileMode'], true);
262
                }
263
                $generator = new FunctionProxyGenerator($namespace, $functionAdvices, $this->useParameterWidening);
264
                file_put_contents($functionFileName, $generator->generate(), LOCK_EX);
265
                // For cache files we don't want executable bits by default
266
                chmod($functionFileName, $this->options['cacheFileMode'] & (~0111));
267
            }
268
            $content = 'include_once AOP_CACHE_DIR . ' . var_export($cacheDirSuffix . $fileName, true) . ';';
269
270
            $lastTokenPosition = $namespace->getLastTokenPosition();
271
            $metadata->tokenStream[$lastTokenPosition][1] .= PHP_EOL . $content;
272
            $wasProcessedFunctions = true;
273
        }
274 8
275
        return $wasProcessedFunctions;
276
    }
277
278
    /**
279
     * Save AOP proxy to the separate file anr returns the php source code for inclusion
280 6
     */
281
    private function saveProxyToCache(ReflectionClass $class, string $childCode): string
282 6
    {
283
        static $cacheDirSuffix = '/_proxies/';
284 6
285 6
        $cacheDir          = $this->cachePathManager->getCacheDir() . $cacheDirSuffix;
286 6
        $relativePath      = str_replace($this->options['appDir'] . DIRECTORY_SEPARATOR, '', $class->getFileName());
287 6
        $proxyRelativePath = str_replace('\\', '/', $relativePath . '/' . $class->getName() . '.php');
288 6
        $proxyFileName     = $cacheDir . $proxyRelativePath;
289 6
        $dirname           = dirname($proxyFileName);
290 6
        if (!file_exists($dirname)) {
291
            mkdir($dirname, $this->options['cacheFileMode'], true);
292
        }
293 6
294
        $body = '<?php' . PHP_EOL . $childCode;
295 6
296 6
        $isVirtualSystem = strpos($proxyFileName, 'vfs') === 0;
297
        file_put_contents($proxyFileName, $body, $isVirtualSystem ? 0 : LOCK_EX);
298 6
        // For cache files we don't want executable bits by default
299
        chmod($proxyFileName, $this->options['cacheFileMode'] & (~0111));
300 6
301
        return 'include_once AOP_CACHE_DIR . ' . var_export($cacheDirSuffix . $proxyRelativePath, true) . ';';
302
    }
303
304
    /**
305
     * Utility method to load and register unloaded aspects
306
     *
307
     * @param array $unloadedAspects List of unloaded aspects
308 1
     */
309
    private function loadAndRegisterAspects(array $unloadedAspects): void
310 1
    {
311 1
        foreach ($unloadedAspects as $unloadedAspect) {
312
            $this->aspectLoader->loadAndRegister($unloadedAspect);
313 1
        }
314
    }
315
}
316