Completed
Push — master ( 9e3bdf...aed36c )
by Alexander
07:28 queued 04:20
created

WeavingTransformer::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2.0078

Importance

Changes 0
Metric Value
dl 0
loc 14
c 0
b 0
f 0
ccs 7
cts 8
cp 0.875
rs 9.4285
cc 2
eloc 11
nc 2
nop 4
crap 2.0078
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\ClassProxy;
28
use Go\Proxy\FunctionProxy;
29
use Go\Proxy\TraitProxy;
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
142 5
        foreach ($advices as &$typeAdvices) {
143 5
            foreach ($typeAdvices as &$joinpointAdvices) {
144 5
                if (is_array($joinpointAdvices)) {
145 5
                    $joinpointAdvices = AbstractJoinpoint::sortAdvices($joinpointAdvices);
146
                }
147
            }
148
        }
149
150
        // Prepare new class name
151 5
        $newClassName = $class->getShortName() . AspectContainer::AOP_PROXIED_SUFFIX;
152
153
        // Replace original class name with new
154 5
        $this->adjustOriginalClass($class, $advices, $metadata, $newClassName);
155
156
        // Prepare child Aop proxy
157 5
        $child = $class->isTrait()
158
            ? new TraitProxy($class, $advices, $this->useParameterWidening)
159 5
            : new ClassProxy($class, $advices, $this->useParameterWidening);
160
161
        // Set new parent name instead of original
162 5
        $child->setParentName($newClassName);
163 5
        $contentToInclude = $this->saveProxyToCache($class, $child);
164
165
        // Get last token for this class
166 5
        $lastClassToken = $class->getNode()->getAttribute('endTokenPos');
167
168 5
        $metadata->tokenStream[$lastClassToken][1] .= PHP_EOL . $contentToInclude;
169
170 5
        return true;
171
    }
172
173
    /**
174
     * Adjust definition of original class source to enable extending
175
     *
176
     * @param ReflectionClass $class Instance of class reflection
177
     * @param array $advices List of class advices (used to check for final methods and make them non-final)
178
     * @param StreamMetaData $streamMetaData Source code metadata
179
     * @param string $newClassName New name for the class
180
     */
181 5
    private function adjustOriginalClass(
182
        ReflectionClass $class,
183
        array $advices,
184
        StreamMetaData $streamMetaData,
185
        string $newClassName
186
    ) {
187 5
        $classNode = $class->getNode();
188 5
        $position  = $classNode->getAttribute('startTokenPos');
189
        do {
190 5
            if (isset($streamMetaData->tokenStream[$position])) {
191 5
                $token = $streamMetaData->tokenStream[$position];
192
                // Remove final and following whitespace from the class, child will be final instead
193 5
                if ($token[0] === T_FINAL) {
194
                    unset($streamMetaData->tokenStream[$position], $streamMetaData->tokenStream[$position+1]);
195
                }
196
                // First string is class/trait name
197 5
                if ($token[0] === T_STRING) {
198 5
                    $streamMetaData->tokenStream[$position][1] = $newClassName;
199
                    // We have finished our job, can break this loop
200 5
                    break;
201
                }
202
            }
203 5
            ++$position;
204 5
        } while (true);
205
206 5
        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 5
    }
230
231
    /**
232
     * Performs weaving of functions in the current namespace
233
     *
234
     * @param array|Advisor[] $advisors List of advisors
235
     * @param StreamMetaData $metadata Source stream information
236
     * @param ReflectionFileNamespace $namespace Current namespace for file
237
     *
238
     * @return boolean True if functions were processed, false otherwise
239
     */
240 7
    private function processFunctions(
241
        array $advisors,
242
        StreamMetaData $metadata,
243
        ReflectionFileNamespace $namespace
244
    ): bool {
245 7
        static $cacheDirSuffix = '/_functions/';
246
247 7
        $wasProcessedFunctions = false;
248 7
        $functionAdvices = $this->adviceMatcher->getAdvicesForFunctions($namespace, $advisors);
249 7
        $cacheDir        = $this->cachePathManager->getCacheDir();
250 7
        if (!empty($functionAdvices)) {
251
            $cacheDir .= $cacheDirSuffix;
252
            $fileName = str_replace('\\', '/', $namespace->getName()) . '.php';
253
254
            $functionFileName = $cacheDir . $fileName;
255
            if (!file_exists($functionFileName) || !$this->container->isFresh(filemtime($functionFileName))) {
256
                $dirname = dirname($functionFileName);
257
                if (!file_exists($dirname)) {
258
                    mkdir($dirname, $this->options['cacheFileMode'], true);
259
                }
260
                $source = new FunctionProxy($namespace, $functionAdvices);
261
                file_put_contents($functionFileName, $source, LOCK_EX);
262
                // For cache files we don't want executable bits by default
263
                chmod($functionFileName, $this->options['cacheFileMode'] & (~0111));
264
            }
265
            $content = 'include_once AOP_CACHE_DIR . ' . var_export($cacheDirSuffix . $fileName, true) . ';';
266
267
            $lastTokenPosition = $namespace->getLastTokenPosition();
268
            $metadata->tokenStream[$lastTokenPosition][1] .= PHP_EOL . $content;
269
            $wasProcessedFunctions = true;
270
        }
271
272 7
        return $wasProcessedFunctions;
273
    }
274
275
    /**
276
     * Save AOP proxy to the separate file anr returns the php source code for inclusion
277
     *
278
     * @param ReflectionClass $class Original class reflection
279
     * @param string|ClassProxy $child
280
     *
281
     * @return string
282
     */
283 5
    private function saveProxyToCache(ReflectionClass $class, $child): string
284
    {
285 5
        static $cacheDirSuffix = '/_proxies/';
286
287 5
        $cacheDir          = $this->cachePathManager->getCacheDir() . $cacheDirSuffix;
288 5
        $relativePath      = str_replace($this->options['appDir'] . DIRECTORY_SEPARATOR, '', $class->getFileName());
289 5
        $proxyRelativePath = str_replace('\\', '/', $relativePath . '/' . $class->getName() . '.php');
290 5
        $proxyFileName     = $cacheDir . $proxyRelativePath;
291 5
        $dirname           = dirname($proxyFileName);
292 5
        if (!file_exists($dirname)) {
293 5
            mkdir($dirname, $this->options['cacheFileMode'], true);
294
        }
295
296 5
        $body       = '<?php' . PHP_EOL;
297 5
        $namespace  = $class->getNamespaceName();
298 5
        if (!empty($namespace)) {
299 4
            $body .= "namespace {$namespace};" . PHP_EOL . PHP_EOL;
300
        }
301
302 5
        $refNamespace = new ReflectionFileNamespace($class->getFileName(), $namespace);
303 5
        foreach ($refNamespace->getNamespaceAliases() as $fqdn => $alias) {
304
            $body .= "use {$fqdn} as {$alias};" . PHP_EOL;
305
        }
306
307 5
        $body .= $child;
308 5
        $isVirtualSystem = strpos($proxyFileName, 'vfs') === 0;
309 5
        file_put_contents($proxyFileName, $body, $isVirtualSystem ? 0 : LOCK_EX);
310
        // For cache files we don't want executable bits by default
311 5
        chmod($proxyFileName, $this->options['cacheFileMode'] & (~0111));
312
313 5
        return 'include_once AOP_CACHE_DIR . ' . var_export($cacheDirSuffix . $proxyRelativePath, true) . ';';
314
    }
315
316
    /**
317
     * Utility method to load and register unloaded aspects
318
     *
319
     * @param array $unloadedAspects List of unloaded aspects
320
     */
321
    private function loadAndRegisterAspects(array $unloadedAspects)
322
    {
323
        foreach ($unloadedAspects as $unloadedAspect) {
324
            $this->aspectLoader->loadAndRegister($unloadedAspect);
325
        }
326
    }
327
}
328