Completed
Pull Request — 2.x (#349)
by Alexander
02:20
created

WeavingTransformer   B

Complexity

Total Complexity 36

Size/Duplication

Total Lines 287
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 16

Test Coverage

Coverage 65.81%

Importance

Changes 0
Metric Value
wmc 36
lcom 1
cbo 16
dl 0
loc 287
ccs 77
cts 117
cp 0.6581
rs 7.4461
c 0
b 0
f 0

7 Methods

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

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
297
        );
298
299 5
        $body = '<?php' . PHP_EOL . $childCode;
300
301 5
        $isVirtualSystem = strpos($proxyFileName, 'vfs') === 0;
302 5
        file_put_contents($proxyFileName, $body, $isVirtualSystem ? 0 : LOCK_EX);
303
        // For cache files we don't want executable bits by default
304 5
        chmod($proxyFileName, $this->options['cacheFileMode'] & (~0111));
305
306 5
        return 'include_once AOP_CACHE_DIR . ' . var_export($cacheDirSuffix . $proxyRelativePath, true) . ';';
307
    }
308
309
    /**
310
     * Utility method to load and register unloaded aspects
311
     *
312
     * @param array $unloadedAspects List of unloaded aspects
313
     */
314
    private function loadAndRegisterAspects(array $unloadedAspects)
315
    {
316
        foreach ($unloadedAspects as $unloadedAspect) {
317
            $this->aspectLoader->loadAndRegister($unloadedAspect);
318
        }
319
    }
320
}
321