Completed
Push — master ( 354384...2118f9 )
by Alexander
02:25
created

WeavingTransformer::adjustOriginalClass()   C

Complexity

Conditions 13
Paths 30

Size

Total Lines 49

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 41.3789

Importance

Changes 0
Metric Value
dl 0
loc 49
ccs 13
cts 29
cp 0.4483
rs 6.6166
c 0
b 0
f 0
cc 13
nc 30
nop 4
crap 41.3789

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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