Completed
Push — 2.x ( 0da1ca...98b57b )
by Alexander
02:15
created

WeavingTransformer::saveProxyToCache()   B

Complexity

Conditions 4
Paths 8

Size

Total Lines 31
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 7.104

Importance

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