Completed
Pull Request — master (#240)
by Alexander
03:13
created

WeavingTransformer::saveProxyToCache()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 32
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 1 Features 0
Metric Value
dl 0
loc 32
rs 8.5806
c 6
b 1
f 0
cc 4
eloc 18
nc 5
nop 2
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\Features;
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\Instrument\CleanableMemory;
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
use ReflectionClass;
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
    public function __construct(
61
        AspectKernel $kernel,
62
        AdviceMatcher $adviceMatcher,
63
        CachePathManager $cachePathManager,
64
        AspectLoader $loader
65
    )
66
    {
67
        parent::__construct($kernel);
68
        $this->adviceMatcher    = $adviceMatcher;
69
        $this->cachePathManager = $cachePathManager;
70
        $this->aspectLoader     = $loader;
71
    }
72
73
    /**
74
     * This method may transform the supplied source and return a new replacement for it
75
     *
76
     * @param StreamMetaData $metadata Metadata for source
77
     * @return void|bool Return false if transformation should be stopped
78
     */
79
    public function transform(StreamMetaData $metadata)
80
    {
81
        $totalTransformations = 0;
82
83
        $fileName = $metadata->uri;
84
85
        try {
86
            $parsedSource = new ReflectionFile($fileName);
87
        } catch (FileProcessingException $e) {
0 ignored issues
show
Bug introduced by
The class Go\Instrument\Transformer\FileProcessingException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
88
89
            return false;
90
        }
91
92
        // Check if we have some new aspects that weren't loaded yet
93
        $unloadedAspects = $this->aspectLoader->getUnloadedAspects();
94
        if ($unloadedAspects) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $unloadedAspects of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
95
            $this->loadAndRegisterAspects($unloadedAspects);
96
        }
97
        $advisors = $this->container->getByTag('advisor');
98
99
        /** @var $namespaces ReflectionFileNamespace[] */
100
        $namespaces = $parsedSource->getFileNamespaces();
101
        $lineOffset = 0;
102
103
        foreach ($namespaces as $namespace) {
104
105
            /** @var $classes ReflectionClass[] */
106
            $classes = $namespace->getClasses();
107
            foreach ($classes as $class) {
108
109
                // Skip interfaces and aspects
110
                if ($class->isInterface() || in_array('Go\Aop\Aspect', $class->getInterfaceNames())) {
111
                    continue;
112
                }
113
                $wasClassProcessed    = $this->processSingleClass($advisors, $metadata, $class, $lineOffset);
114
                $totalTransformations += (integer) $wasClassProcessed;
115
            }
116
            $wasFunctionsProcessed = $this->processFunctions($advisors, $metadata, $namespace);
117
            $totalTransformations  += (integer) $wasFunctionsProcessed;
118
        }
119
120
        // If we return false this will indicate no more transformation for following transformers
121
        return $totalTransformations > 0;
122
    }
123
124
    /**
125
     * Performs weaving of single class if needed
126
     *
127
     * @param array|Advisor[] $advisors
128
     * @param StreamMetaData $metadata Source stream information
129
     * @param ReflectionClass $class Instance of class to analyze
130
     * @param integer $lineOffset Current offset, will be updated to store the last position
131
     *
132
     * @return bool True if was class processed, false otherwise
133
     */
134
    private function processSingleClass(array $advisors, StreamMetaData $metadata, ReflectionClass $class, &$lineOffset)
0 ignored issues
show
Unused Code introduced by
The parameter $lineOffset is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
135
    {
136
        $advices = $this->adviceMatcher->getAdvicesForClass($class, $advisors);
137
138
        if (!$advices) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $advices of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
139
            // Fast return if there aren't any advices for that class
140
            return false;
141
        }
142
143
        // Sort advices in advance to keep the correct order in cache
144
        foreach ($advices as &$typeAdvices) {
145
            foreach ($typeAdvices as &$joinpointAdvices) {
146
                if (is_array($joinpointAdvices)) {
147
                    $joinpointAdvices = AbstractJoinpoint::sortAdvices($joinpointAdvices);
148
                }
149
            }
150
        }
151
152
        // Prepare new parent name
153
        $newParentName = $class->getShortName() . AspectContainer::AOP_PROXIED_SUFFIX;
154
155
        // Replace original class name with new
156
        $metadata->source = $this->adjustOriginalClass($class, $metadata->source, $newParentName);
157
158
        // Prepare child Aop proxy
159
        $useStatic = $this->kernel->hasFeature(Features::USE_STATIC_FOR_LSB);
160
        $child     = ($this->kernel->hasFeature(Features::USE_TRAIT) && $class->isTrait())
161
            ? new TraitProxy($class, $advices, $useStatic)
162
            : new ClassProxy($class, $advices, $useStatic);
163
164
        // Set new parent name instead of original
165
        $child->setParentName($newParentName);
166
        $contentToInclude = $this->saveProxyToCache($class, $child);
167
168
        $metadata->source .= $contentToInclude . PHP_EOL;
169
170
/*        // Add child to source
171
        $tokenCount = $class->getBroker()->getFileTokens($class->getFileName())->count();
172
        if ($tokenCount - $class->getEndPosition() < 3) {
173
            // If it's the last class in a file, just add child source
174
            $metadata->source .= $contentToInclude . PHP_EOL;
175
        } else {
176
            $lastLine  = $class->getEndLine() + $lineOffset; // returns the last line of class
177
            $dataArray = explode("\n", $metadata->source);
178
179
            $currentClassArray = array_splice($dataArray, 0, $lastLine);
180
            $childClassArray   = explode("\n", $contentToInclude);
181
            $lineOffset += count($childClassArray) + 2; // returns LoC for child class + 2 blank lines
182
183
            $dataArray = array_merge($currentClassArray, array(''), $childClassArray, array(''), $dataArray);
184
185
            $metadata->source = implode("\n", $dataArray);
186
        }*/
187
188
        return true;
189
    }
190
191
    /**
192
     * Adjust definition of original class source to enable extending
193
     *
194
     * @param ReflectionClass $class Instance of class reflection
195
     * @param string $source Source code
196
     * @param string $newParentName New name for the parent class
197
     *
198
     * @return string Replaced code for class
199
     */
200
    private function adjustOriginalClass($class, $source, $newParentName)
201
    {
202
        $type = ($this->kernel->hasFeature(Features::USE_TRAIT) && $class->isTrait()) ? 'trait' : 'class';
203
        $source = preg_replace(
204
            "/{$type}\s+(" . $class->getShortName() . ')(\b)/iS',
205
            "{$type} {$newParentName}$2",
206
            $source
207
        );
208
        if ($class->isFinal()) {
209
            // Remove final from class, child will be final instead
210
            $source = str_replace("final {$type}", $type, $source);
211
        }
212
213
        return $source;
214
    }
215
216
    /**
217
     * Performs weaving of functions in the current namespace
218
     *
219
     * @param array|Advisor[] $advisors List of advisors
220
     * @param StreamMetaData $metadata Source stream information
221
     * @param ReflectionFileNamespace $namespace Current namespace for file
222
     *
223
     * @return boolean True if functions were processed, false otherwise
224
     */
225
    private function processFunctions(array $advisors, StreamMetaData $metadata, $namespace)
226
    {
227
        static $cacheDirSuffix = '/_functions/';
228
229
        $wasProcessedFunctions = false;
230
        $functionAdvices = $this->adviceMatcher->getAdvicesForFunctions($namespace, $advisors);
231
        $cacheDir        = $this->cachePathManager->getCacheDir();
232
        if ($functionAdvices && $cacheDir) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $functionAdvices of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Bug Best Practice introduced by
The expression $cacheDir of type string|null is loosely compared to true; 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...
233
            $cacheDir = $cacheDir . $cacheDirSuffix;
234
            $fileName = str_replace('\\', '/', $namespace->getName()) . '.php';
235
236
            $functionFileName = $cacheDir . $fileName;
237
            if (!file_exists($functionFileName) || !$this->container->isFresh(filemtime($functionFileName))) {
238
                $dirname = dirname($functionFileName);
239
                if (!file_exists($dirname)) {
240
                    mkdir($dirname, 0770, true);
241
                }
242
                $source = new FunctionProxy($namespace, $functionAdvices);
243
                file_put_contents($functionFileName, $source);
244
            }
245
            $content = 'include_once AOP_CACHE_DIR . ' . var_export($cacheDirSuffix . $fileName, true) . ';' . PHP_EOL;
246
            $metadata->source .= $content;
247
            $wasProcessedFunctions = true;
248
        }
249
250
        return $wasProcessedFunctions;
251
    }
252
253
    /**
254
     * Save AOP proxy to the separate file anr returns the php source code for inclusion
255
     *
256
     * @param ReflectionClass $class Original class reflection
257
     * @param string|ClassProxy $child
258
     *
259
     * @return string
260
     */
261
    private function saveProxyToCache($class, $child)
262
    {
263
        static $cacheDirSuffix = '/_proxies/';
264
265
        $cacheDir = $this->cachePathManager->getCacheDir();
266
267
        // Without cache we should rewrite original file
268
        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...
269
            return $child;
270
        }
271
        $cacheDir = $cacheDir . $cacheDirSuffix;
272
        $fileName = str_replace($this->options['appDir'] . DIRECTORY_SEPARATOR, '', $class->getFileName());
273
274
        $proxyFileName = $cacheDir . $fileName;
275
        $dirname       = dirname($proxyFileName);
276
        if (!file_exists($dirname)) {
277
            mkdir($dirname, 0770, true);
278
        }
279
280
        $body      = '<?php' . PHP_EOL;
281
        $namespace = $class->getNamespaceName();
282
        if ($namespace) {
283
            $body .= "namespace {$namespace};" . PHP_EOL . PHP_EOL;
284
        }
285
//        foreach ($class->getNamespaceAliases() as $alias => $fqdn) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
45% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
286
//            $body .= "use {$fqdn} as {$alias};" . PHP_EOL;
287
//        }
288
        $body .= $child;
289
        file_put_contents($proxyFileName, $body);
290
291
        return 'include_once AOP_CACHE_DIR . ' . var_export($cacheDirSuffix . $fileName, true) . ';' . PHP_EOL;
292
    }
293
294
    /**
295
     * Utility method to load and register unloaded aspects
296
     *
297
     * @param array $unloadedAspects List of unloaded aspects
298
     */
299
    private function loadAndRegisterAspects(array $unloadedAspects)
300
    {
301
        foreach ($unloadedAspects as $unloadedAspect) {
302
            $this->aspectLoader->loadAndRegister($unloadedAspect);
303
        }
304
    }
305
}
306