Completed
Push — master ( 2118f9...230580 )
by Alexander
08:12 queued 11s
created

WeavingTransformer::processSingleClass()   B

Complexity

Conditions 6
Paths 13

Size

Total Lines 53

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 6.0237

Importance

Changes 0
Metric Value
dl 0
loc 53
ccs 21
cts 23
cp 0.913
rs 8.4032
c 0
b 0
f 0
cc 6
nc 13
nop 4
crap 6.0237

How to fix   Long Method   

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
            // Either we have a string or Identifier node
158 1
            if ($alias !== null) {
159 1
                $childProxyGenerator->addUse($fqdn, (string) $alias);
160
            } else {
161
                $childProxyGenerator->addUse($fqdn);
162
            }
163
        }
164
165 6
        $childCode = $childProxyGenerator->generate();
166
167 6
        if ($useStrictMode) {
168 5
            $childCode = 'declare(strict_types=1);' . PHP_EOL . $childCode;
169
        }
170
171 6
        $contentToInclude = $this->saveProxyToCache($class, $childCode);
172
173
        // Get last token for this class
174 6
        $lastClassToken = $class->getNode()->getAttribute('endTokenPos');
175
176 6
        $metadata->tokenStream[$lastClassToken][1] .= PHP_EOL . $contentToInclude;
177
178 6
        return true;
179
    }
180
181
    /**
182
     * Adjust definition of original class source to enable extending
183
     *
184
     * @param array $advices List of class advices (used to check for final methods and make them non-final)
185
     */
186 6
    private function adjustOriginalClass(
187
        ReflectionClass $class,
188
        array $advices,
189
        StreamMetaData $streamMetaData,
190
        string $newClassName
191
    ): void {
192 6
        $classNode = $class->getNode();
193 6
        $position  = $classNode->getAttribute('startTokenPos');
194
        do {
195 6
            if (isset($streamMetaData->tokenStream[$position])) {
196 6
                $token = $streamMetaData->tokenStream[$position];
197
                // Remove final and following whitespace from the class, child will be final instead
198 6
                if ($token[0] === T_FINAL) {
199
                    unset($streamMetaData->tokenStream[$position], $streamMetaData->tokenStream[$position+1]);
200
                }
201
                // First string is class/trait name
202 6
                if ($token[0] === T_STRING) {
203 6
                    $streamMetaData->tokenStream[$position][1] = $newClassName;
204
                    // We have finished our job, can break this loop
205 6
                    break;
206
                }
207
            }
208 6
            ++$position;
209 6
        } while (true);
210
211 6
        foreach ($class->getMethods(ReflectionMethod::IS_FINAL) as $finalMethod) {
212
            if (!$finalMethod instanceof ReflectionMethod || $finalMethod->getDeclaringClass()->name !== $class->name) {
213
                continue;
214
            }
215
            $hasDynamicAdvice = isset($advices[AspectContainer::METHOD_PREFIX][$finalMethod->name]);
216
            $hasStaticAdvice  = isset($advices[AspectContainer::STATIC_METHOD_PREFIX][$finalMethod->name]);
217
            if (!$hasDynamicAdvice && !$hasStaticAdvice) {
218
                continue;
219
            }
220
            $methodNode = $finalMethod->getNode();
221
            $position   = $methodNode->getAttribute('startTokenPos');
222
            do {
223
                if (isset($streamMetaData->tokenStream[$position])) {
224
                    $token = $streamMetaData->tokenStream[$position];
225
                    // Remove final and following whitespace from the method, child will be final instead
226
                    if ($token[0] === T_FINAL) {
227
                        unset($streamMetaData->tokenStream[$position], $streamMetaData->tokenStream[$position+1]);
228
                        break;
229
                    }
230
                }
231
                ++$position;
232
            } while (true);
233
        }
234 6
    }
235
236
    /**
237
     * Performs weaving of functions in the current namespace, returns true if functions were processed, false otherwise
238
     *
239
     * @param Advisor[] $advisors List of advisors
240
     */
241 8
    private function processFunctions(
242
        array $advisors,
243
        StreamMetaData $metadata,
244
        ReflectionFileNamespace $namespace
245
    ): bool {
246 8
        static $cacheDirSuffix = '/_functions/';
247
248 8
        $wasProcessedFunctions = false;
249 8
        $functionAdvices = $this->adviceMatcher->getAdvicesForFunctions($namespace, $advisors);
250 8
        $cacheDir        = $this->cachePathManager->getCacheDir();
251 8
        if (!empty($functionAdvices)) {
252
            $cacheDir .= $cacheDirSuffix;
253
            $fileName = str_replace('\\', '/', $namespace->getName()) . '.php';
254
255
            $functionFileName = $cacheDir . $fileName;
256
            if (!file_exists($functionFileName) || !$this->container->isFresh(filemtime($functionFileName))) {
257
                $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...
258
                $dirname         = dirname($functionFileName);
259
                if (!file_exists($dirname)) {
260
                    mkdir($dirname, $this->options['cacheFileMode'], true);
261
                }
262
                $generator = new FunctionProxyGenerator($namespace, $functionAdvices, $this->useParameterWidening);
263
                file_put_contents($functionFileName, $generator->generate(), LOCK_EX);
264
                // For cache files we don't want executable bits by default
265
                chmod($functionFileName, $this->options['cacheFileMode'] & (~0111));
266
            }
267
            $content = 'include_once AOP_CACHE_DIR . ' . var_export($cacheDirSuffix . $fileName, true) . ';';
268
269
            $lastTokenPosition = $namespace->getLastTokenPosition();
270
            $metadata->tokenStream[$lastTokenPosition][1] .= PHP_EOL . $content;
271
            $wasProcessedFunctions = true;
272
        }
273
274 8
        return $wasProcessedFunctions;
275
    }
276
277
    /**
278
     * Save AOP proxy to the separate file anr returns the php source code for inclusion
279
     */
280 6
    private function saveProxyToCache(ReflectionClass $class, string $childCode): string
281
    {
282 6
        static $cacheDirSuffix = '/_proxies/';
283
284 6
        $cacheDir          = $this->cachePathManager->getCacheDir() . $cacheDirSuffix;
285 6
        $relativePath      = str_replace($this->options['appDir'] . DIRECTORY_SEPARATOR, '', $class->getFileName());
286 6
        $proxyRelativePath = str_replace('\\', '/', $relativePath . '/' . $class->getName() . '.php');
287 6
        $proxyFileName     = $cacheDir . $proxyRelativePath;
288 6
        $dirname           = dirname($proxyFileName);
289 6
        if (!file_exists($dirname)) {
290 6
            mkdir($dirname, $this->options['cacheFileMode'], true);
291
        }
292
293 6
        $body = '<?php' . PHP_EOL . $childCode;
294
295 6
        $isVirtualSystem = strpos($proxyFileName, 'vfs') === 0;
296 6
        file_put_contents($proxyFileName, $body, $isVirtualSystem ? 0 : LOCK_EX);
297
        // For cache files we don't want executable bits by default
298 6
        chmod($proxyFileName, $this->options['cacheFileMode'] & (~0111));
299
300 6
        return 'include_once AOP_CACHE_DIR . ' . var_export($cacheDirSuffix . $proxyRelativePath, true) . ';';
301
    }
302
303
    /**
304
     * Utility method to load and register unloaded aspects
305
     *
306
     * @param array $unloadedAspects List of unloaded aspects
307
     */
308 1
    private function loadAndRegisterAspects(array $unloadedAspects): void
309
    {
310 1
        foreach ($unloadedAspects as $unloadedAspect) {
311 1
            $this->aspectLoader->loadAndRegister($unloadedAspect);
312
        }
313 1
    }
314
}
315