Completed
Pull Request — master (#395)
by Alexander
04:36 queued 02:33
created

WeavingTransformer::processSingleClass()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 38
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 4.074

Importance

Changes 0
Metric Value
dl 0
loc 38
ccs 15
cts 18
cp 0.8333
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 18
nc 5
nop 3
crap 4.074
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
     * Advice matcher for class
39
     */
40
    protected $adviceMatcher;
41
42
    /**
43
     * Should we use parameter widening for our decorators
44
     */
45
    protected $useParameterWidening = false;
46
47
    /**
48
     * Cache manager
49
     */
50
    private $cachePathManager;
51
52
    /**
53
     * Loader for aspects
54
     */
55
    protected $aspectLoader;
56
57
    /**
58
     * Constructs a weaving transformer
59
     */
60 7
    public function __construct(
61
        AspectKernel $kernel,
62
        AdviceMatcher $adviceMatcher,
63
        CachePathManager $cachePathManager,
64
        AspectLoader $loader
65
    ) {
66 7
        parent::__construct($kernel);
67 7
        $this->adviceMatcher    = $adviceMatcher;
68 7
        $this->cachePathManager = $cachePathManager;
69 7
        $this->aspectLoader     = $loader;
70
71 7
        $this->useParameterWidening = $kernel->hasFeature(Features::PARAMETER_WIDENING);
72 7
    }
73
74
    /**
75
     * This method may transform the supplied source and return a new replacement for it
76
     *
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, returns true if the class was processed
114
     *
115
     * @param Advisor[] $advisors List of advisors
116
     */
117 5
    private function processSingleClass(array $advisors, StreamMetaData $metadata, ReflectionClass $class): bool
118
    {
119 5
        $advices = $this->adviceMatcher->getAdvicesForClass($class, $advisors);
120
121 5
        if (empty($advices)) {
122
            // Fast return if there aren't any advices for that class
123
            return false;
124
        }
125
126
        // Sort advices in advance to keep the correct order in cache, and leave only keys for the cache
127 5
        $advices = AbstractJoinpoint::flatAndSortAdvices($advices);
0 ignored issues
show
Documentation introduced by
$advices is of type array<integer,object<Go\Aop\Advice>>, but the function expects a array<integer,array<inte...bject<Go\Aop\Advice>>>>.

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...
128
129
        // Prepare new class name
130 5
        $newClassName = $class->getShortName() . AspectContainer::AOP_PROXIED_SUFFIX;
131
132
        // Replace original class name with new
133 5
        $this->adjustOriginalClass($class, $advices, $metadata, $newClassName);
134 5
        $newParentName = $class->getNamespaceName() . '\\' . $newClassName;
135
136
        // Prepare child Aop proxy
137 5
        $childProxyGenerator = $class->isTrait()
138
            ? new TraitProxyGenerator($class, $newParentName, $advices, $this->useParameterWidening)
139 5
            : new ClassProxyGenerator($class, $newParentName, $advices, $this->useParameterWidening);
140
141 5
        $refNamespace = new ReflectionFileNamespace($class->getFileName(), $class->getNamespaceName());
142 5
        foreach ($refNamespace->getNamespaceAliases() as $fqdn => $alias) {
143
            $childProxyGenerator->addUse($fqdn, $alias);
144
        }
145
146 5
        $contentToInclude = $this->saveProxyToCache($class, $childProxyGenerator->generate());
147
148
        // Get last token for this class
149 5
        $lastClassToken = $class->getNode()->getAttribute('endTokenPos');
150
151 5
        $metadata->tokenStream[$lastClassToken][1] .= PHP_EOL . $contentToInclude;
152
153 5
        return true;
154
    }
155
156
    /**
157
     * Adjust definition of original class source to enable extending
158
     *
159
     * @param array $advices List of class advices (used to check for final methods and make them non-final)
160
     */
161 5
    private function adjustOriginalClass(
162
        ReflectionClass $class,
163
        array $advices,
164
        StreamMetaData $streamMetaData,
165
        string $newClassName
166
    ): void {
167 5
        $classNode = $class->getNode();
168 5
        $position  = $classNode->getAttribute('startTokenPos');
169
        do {
170 5
            if (isset($streamMetaData->tokenStream[$position])) {
171 5
                $token = $streamMetaData->tokenStream[$position];
172
                // Remove final and following whitespace from the class, child will be final instead
173 5
                if ($token[0] === T_FINAL) {
174
                    unset($streamMetaData->tokenStream[$position], $streamMetaData->tokenStream[$position+1]);
175
                }
176
                // First string is class/trait name
177 5
                if ($token[0] === T_STRING) {
178 5
                    $streamMetaData->tokenStream[$position][1] = $newClassName;
179
                    // We have finished our job, can break this loop
180 5
                    break;
181
                }
182
            }
183 5
            ++$position;
184 5
        } while (true);
185
186 5
        foreach ($class->getMethods(ReflectionMethod::IS_FINAL) as $finalMethod) {
187
            if (!$finalMethod instanceof ReflectionMethod || $finalMethod->getDeclaringClass()->name !== $class->name) {
188
                continue;
189
            }
190
            $hasDynamicAdvice = isset($advices[AspectContainer::METHOD_PREFIX][$finalMethod->name]);
191
            $hasStaticAdvice  = isset($advices[AspectContainer::STATIC_METHOD_PREFIX][$finalMethod->name]);
192
            if (!$hasDynamicAdvice && !$hasStaticAdvice) {
193
                continue;
194
            }
195
            $methodNode = $finalMethod->getNode();
196
            $position   = $methodNode->getAttribute('startTokenPos');
197
            do {
198
                if (isset($streamMetaData->tokenStream[$position])) {
199
                    $token = $streamMetaData->tokenStream[$position];
200
                    // Remove final and following whitespace from the method, child will be final instead
201
                    if ($token[0] === T_FINAL) {
202
                        unset($streamMetaData->tokenStream[$position], $streamMetaData->tokenStream[$position+1]);
203
                        break;
204
                    }
205
                }
206
                ++$position;
207
            } while (true);
208
        }
209 5
    }
210
211
    /**
212
     * Performs weaving of functions in the current namespace, returns true if functions were processed, false otherwise
213
     *
214
     * @param Advisor[] $advisors List of advisors
215
     */
216 7
    private function processFunctions(
217
        array $advisors,
218
        StreamMetaData $metadata,
219
        ReflectionFileNamespace $namespace
220
    ): bool {
221 7
        static $cacheDirSuffix = '/_functions/';
222
223 7
        $wasProcessedFunctions = false;
224 7
        $functionAdvices = $this->adviceMatcher->getAdvicesForFunctions($namespace, $advisors);
225 7
        $cacheDir        = $this->cachePathManager->getCacheDir();
226 7
        if (!empty($functionAdvices)) {
227
            $cacheDir .= $cacheDirSuffix;
228
            $fileName = str_replace('\\', '/', $namespace->getName()) . '.php';
229
230
            $functionFileName = $cacheDir . $fileName;
231
            if (!file_exists($functionFileName) || !$this->container->isFresh(filemtime($functionFileName))) {
232
                $functionAdvices = AbstractJoinpoint::flatAndSortAdvices($functionAdvices);
0 ignored issues
show
Documentation introduced by
$functionAdvices is of type array<integer,object<Go\Aop\Advice>>, but the function expects a array<integer,array<inte...bject<Go\Aop\Advice>>>>.

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...
233
                $dirname         = dirname($functionFileName);
234
                if (!file_exists($dirname)) {
235
                    mkdir($dirname, $this->options['cacheFileMode'], true);
236
                }
237
                $generator = new FunctionProxyGenerator($namespace, $functionAdvices, $this->useParameterWidening);
238
                file_put_contents($functionFileName, $generator->generate(), LOCK_EX);
239
                // For cache files we don't want executable bits by default
240
                chmod($functionFileName, $this->options['cacheFileMode'] & (~0111));
241
            }
242
            $content = 'include_once AOP_CACHE_DIR . ' . var_export($cacheDirSuffix . $fileName, true) . ';';
243
244
            $lastTokenPosition = $namespace->getLastTokenPosition();
245
            $metadata->tokenStream[$lastTokenPosition][1] .= PHP_EOL . $content;
246
            $wasProcessedFunctions = true;
247
        }
248
249 7
        return $wasProcessedFunctions;
250
    }
251
252
    /**
253
     * Save AOP proxy to the separate file anr returns the php source code for inclusion
254
     */
255 5
    private function saveProxyToCache(ReflectionClass $class, string $childCode): string
256
    {
257 5
        static $cacheDirSuffix = '/_proxies/';
258
259 5
        $cacheDir          = $this->cachePathManager->getCacheDir() . $cacheDirSuffix;
260 5
        $relativePath      = str_replace($this->options['appDir'] . DIRECTORY_SEPARATOR, '', $class->getFileName());
261 5
        $proxyRelativePath = str_replace('\\', '/', $relativePath . '/' . $class->getName() . '.php');
262 5
        $proxyFileName     = $cacheDir . $proxyRelativePath;
263 5
        $dirname           = dirname($proxyFileName);
264 5
        if (!file_exists($dirname)) {
265 5
            mkdir($dirname, $this->options['cacheFileMode'], true);
266
        }
267
268 5
        $body = '<?php' . PHP_EOL . $childCode;
269
270 5
        $isVirtualSystem = strpos($proxyFileName, 'vfs') === 0;
271 5
        file_put_contents($proxyFileName, $body, $isVirtualSystem ? 0 : LOCK_EX);
272
        // For cache files we don't want executable bits by default
273 5
        chmod($proxyFileName, $this->options['cacheFileMode'] & (~0111));
274
275 5
        return 'include_once AOP_CACHE_DIR . ' . var_export($cacheDirSuffix . $proxyRelativePath, true) . ';';
276
    }
277
278
    /**
279
     * Utility method to load and register unloaded aspects
280
     *
281
     * @param array $unloadedAspects List of unloaded aspects
282
     */
283
    private function loadAndRegisterAspects(array $unloadedAspects): void
284
    {
285
        foreach ($unloadedAspects as $unloadedAspect) {
286
            $this->aspectLoader->loadAndRegister($unloadedAspect);
287
        }
288
    }
289
}
290