Completed
Push — 2.x ( 325133...00cf1d )
by Alexander
03:57
created

WeavingTransformer::transform()   B

Complexity

Conditions 7
Paths 16

Size

Total Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 7.0071

Importance

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