Completed
Pull Request — 2.x (#362)
by Alexander
02:49
created

WeavingTransformer::adjustOriginalClass()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 21
ccs 13
cts 13
cp 1
rs 8.7624
cc 5
eloc 13
nc 5
nop 3
crap 5
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
    {
0 ignored issues
show
Coding Style introduced by
The closing parenthesis and the opening brace of a multi-line function declaration must be on the same line
Loading history...
66 9
        parent::__construct($kernel);
67 9
        $this->adviceMatcher    = $adviceMatcher;
68 9
        $this->cachePathManager = $cachePathManager;
69 9
        $this->aspectLoader     = $loader;
70 9
    }
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 9
    public function transform(StreamMetaData $metadata)
79
    {
80 9
        $totalTransformations = 0;
81 9
        $parsedSource         = new ReflectionFile($metadata->uri, $metadata->syntaxTree);
82
83
        // Check if we have some new aspects that weren't loaded yet
84 9
        $unloadedAspects = $this->aspectLoader->getUnloadedAspects();
85 9
        if (!empty($unloadedAspects)) {
86
            $this->loadAndRegisterAspects($unloadedAspects);
87
        }
88 9
        $advisors = $this->container->getByTag('advisor');
89
90 9
        $namespaces = $parsedSource->getFileNamespaces();
91
92 9
        foreach ($namespaces as $namespace) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
93
94 9
            $classes = $namespace->getClasses();
95 9
            foreach ($classes as $class) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
96
97
                // Skip interfaces and aspects
98 8
                if ($class->isInterface() || in_array(Aspect::class, $class->getInterfaceNames())) {
99 2
                    continue;
100
                }
101 6
                $wasClassProcessed = $this->processSingleClass($advisors, $metadata, $class);
102 6
                $totalTransformations += (integer) $wasClassProcessed;
103
            }
104 9
            $wasFunctionsProcessed = $this->processFunctions($advisors, $metadata, $namespace);
105 9
            $totalTransformations += (integer) $wasFunctionsProcessed;
106
        }
107
108 9
        $result = ($totalTransformations > 0) ? self::RESULT_TRANSFORMED : self::RESULT_ABSTAIN;
109
110 9
        return $result;
111
    }
112
113
    /**
114
     * Performs weaving of single class if needed
115
     *
116
     * @param array|Advisor[] $advisors
117
     * @param StreamMetaData $metadata Source stream information
118
     * @param ReflectionClass $class Instance of class to analyze
119
     *
120
     * @return bool True if was class processed, false otherwise
121
     */
122 6
    private function processSingleClass(array $advisors, StreamMetaData $metadata, ReflectionClass $class)
123
    {
124 6
        $advices = $this->adviceMatcher->getAdvicesForClass($class, $advisors);
125
126 6
        if (empty($advices)) {
127
            // Fast return if there aren't any advices for that class
128
            return false;
129
        }
130
131
        // Sort advices in advance to keep the correct order in cache
132 6
        foreach ($advices as &$typeAdvices) {
133 6
            foreach ($typeAdvices as &$joinpointAdvices) {
134 6
                if (is_array($joinpointAdvices)) {
135 6
                    $joinpointAdvices = AbstractJoinpoint::sortAdvices($joinpointAdvices);
136
                }
137
            }
138
        }
139
140
        // Prepare new class name
141 6
        $newClassName = $class->getShortName() . AspectContainer::AOP_PROXIED_SUFFIX;
142
143
        // Replace original class name with new
144 6
        $this->adjustOriginalClass($class, $metadata, $newClassName);
145
146
        // Prepare child Aop proxy
147 6
        $child = $class->isTrait()
148
            ? new TraitProxy($class, $advices)
149 6
            : new ClassProxy($class, $advices);
150
151
        // Set new parent name instead of original
152 6
        $child->setParentName($newClassName);
153 6
        $contentToInclude = $this->saveProxyToCache($class, $child);
154
155
        // Get last token for this class
156 6
        $lastClassToken = $class->getNode()->getAttribute('endTokenPos');
157
158 6
        $metadata->tokenStream[$lastClassToken][1] .= PHP_EOL . $contentToInclude;
159
160 6
        return true;
161
    }
162
163
    /**
164
     * Adjust definition of original class source to enable extending
165
     *
166
     * @param ReflectionClass $class Instance of class reflection
167
     * @param StreamMetaData $streamMetaData Source code metadata
168
     * @param string $newClassName New name for the class
169
     */
170 6
    private function adjustOriginalClass(ReflectionClass $class, StreamMetaData $streamMetaData, $newClassName)
171
    {
172 6
        $classNode = $class->getNode();
173 6
        $position  = $classNode->getAttribute('startTokenPos');
174
        do {
175 6
            if (isset($streamMetaData->tokenStream[$position])) {
176 6
                $token = $streamMetaData->tokenStream[$position];
177
                // Remove final and following whitespace from the class, child will be final instead
178 6
                if ($token[0] === T_FINAL) {
179 1
                    unset($streamMetaData->tokenStream[$position], $streamMetaData->tokenStream[$position+1]);
180
                }
181
                // First string is class/trait name
182 6
                if ($token[0] === T_STRING) {
183 6
                    $streamMetaData->tokenStream[$position][1] = $newClassName;
184
                    // We have finished our job, can break this loop
185 6
                    break;
186
                }
187
            }
188 6
            ++$position;
189 6
        } while (true);
190 6
    }
191
192
    /**
193
     * Performs weaving of functions in the current namespace
194
     *
195
     * @param array|Advisor[] $advisors List of advisors
196
     * @param StreamMetaData $metadata Source stream information
197
     * @param ReflectionFileNamespace $namespace Current namespace for file
198
     *
199
     * @return boolean True if functions were processed, false otherwise
200
     */
201 9
    private function processFunctions(array $advisors, StreamMetaData $metadata, ReflectionFileNamespace $namespace)
202
    {
203 9
        static $cacheDirSuffix = '/_functions/';
204
205 9
        $wasProcessedFunctions = false;
206 9
        $functionAdvices = $this->adviceMatcher->getAdvicesForFunctions($namespace, $advisors);
207 9
        $cacheDir        = $this->cachePathManager->getCacheDir();
208 9
        if (!empty($functionAdvices)) {
209
            $cacheDir .= $cacheDirSuffix;
210
            $fileName = str_replace('\\', '/', $namespace->getName()) . '.php';
211
212
            $functionFileName = $cacheDir . $fileName;
213
            if (!file_exists($functionFileName) || !$this->container->isFresh(filemtime($functionFileName))) {
214
                $dirname = dirname($functionFileName);
215
                if (!file_exists($dirname)) {
216
                    mkdir($dirname, $this->options['cacheFileMode'], true);
217
                }
218
                $source = new FunctionProxy($namespace, $functionAdvices);
219
                file_put_contents($functionFileName, $source, LOCK_EX);
220
                // For cache files we don't want executable bits by default
221
                chmod($functionFileName, $this->options['cacheFileMode'] & (~0111));
222
            }
223
            $content = 'include_once AOP_CACHE_DIR . ' . var_export($cacheDirSuffix . $fileName, true) . ';';
224
225
            $lastTokenPosition = $namespace->getLastTokenPosition();
226
            $metadata->tokenStream[$lastTokenPosition][1] .= PHP_EOL . $content;
227
            $wasProcessedFunctions = true;
228
        }
229
230 9
        return $wasProcessedFunctions;
231
    }
232
233
    /**
234
     * Save AOP proxy to the separate file anr returns the php source code for inclusion
235
     *
236
     * @param ReflectionClass $class Original class reflection
237
     * @param string|ClassProxy $child
238
     *
239
     * @return string
240
     */
241 6
    private function saveProxyToCache($class, $child)
242
    {
243 6
        static $cacheDirSuffix = '/_proxies/';
244
245 6
        $cacheDir = $this->cachePathManager->getCacheDir();
246
247
        // Without cache we should rewrite original file
248 6
        if (!$cacheDir) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $cacheDir of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
249 6
            return $child;
250
        }
251
        $cacheDir .= $cacheDirSuffix;
252
        $fileName = str_replace($this->options['appDir'] . DIRECTORY_SEPARATOR, '', $class->getFileName());
253
254
        $proxyFileName = $cacheDir . $fileName;
255
        $dirname       = dirname($proxyFileName);
256
        if (!file_exists($dirname)) {
257
            mkdir($dirname, $this->options['cacheFileMode'], true);
258
        }
259
260
        $body      = '<?php' . PHP_EOL;
261
        $namespace = $class->getNamespaceName();
262
        if ($namespace) {
263
            $body .= "namespace {$namespace};" . PHP_EOL . PHP_EOL;
264
        }
265
266
        $refNamespace = new ReflectionFileNamespace($class->getFileName(), $namespace);
267
        foreach ($refNamespace->getNamespaceAliases() as $fqdn => $alias) {
268
            $body .= "use {$fqdn} as {$alias};" . PHP_EOL;
269
        }
270
271
        $body .= $child;
272
        file_put_contents($proxyFileName, $body, LOCK_EX);
273
        // For cache files we don't want executable bits by default
274
        chmod($proxyFileName, $this->options['cacheFileMode'] & (~0111));
275
276
        return 'include_once AOP_CACHE_DIR . ' . var_export($cacheDirSuffix . $fileName, true) . ';';
277
    }
278
279
    /**
280
     * Utility method to load and register unloaded aspects
281
     *
282
     * @param array $unloadedAspects List of unloaded aspects
283
     */
284
    private function loadAndRegisterAspects(array $unloadedAspects)
285
    {
286
        foreach ($unloadedAspects as $unloadedAspect) {
287
            $this->aspectLoader->loadAndRegister($unloadedAspect);
288
        }
289
    }
290
}
291