Completed
Pull Request — master (#40)
by Bernhard
04:17
created

SkeletonFilter::filterChunk()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 17
rs 9.4285
cc 1
eloc 7
nc 1
nop 2
1
<?php
2
3
/**
4
 * \AppserverIo\Doppelgaenger\StreamFilters\SkeletonFilter
5
 *
6
 * NOTICE OF LICENSE
7
 *
8
 * This source file is subject to the Open Software License (OSL 3.0)
9
 * that is available through the world-wide-web at this URL:
10
 * http://opensource.org/licenses/osl-3.0.php
11
 *
12
 * PHP version 5
13
 *
14
 * @author    Bernhard Wick <[email protected]>
15
 * @copyright 2015 TechDivision GmbH - <[email protected]>
16
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
17
 * @link      https://github.com/appserver-io/doppelgaenger
18
 * @link      http://www.appserver.io/
19
 */
20
21
namespace AppserverIo\Doppelgaenger\StreamFilters;
22
23
use AppserverIo\Doppelgaenger\Entities\Definitions\FunctionDefinition;
24
use AppserverIo\Doppelgaenger\Exceptions\GeneratorException;
25
use AppserverIo\Doppelgaenger\Dictionaries\Placeholders;
26
use AppserverIo\Doppelgaenger\Dictionaries\ReservedKeywords;
27
use AppserverIo\Doppelgaenger\Interfaces\StructureDefinitionInterface;
28
use AppserverIo\Doppelgaenger\Utils\Parser;
29
use AppserverIo\Doppelgaenger\Entities\Definitions\ClassDefinition;
30
use AppserverIo\Doppelgaenger\Entities\Definitions\InterfaceDefinition;
31
use AppserverIo\Doppelgaenger\Entities\Definitions\TraitDefinition;
32
33
/**
34
 * This filter is the most important one!
35
 * It will analyze the need to act upon the content we get and prepare placeholder for coming filters so they
36
 * do not have to do the analyzing part again.
37
 * This placeholder system also makes them highly optional, configur- and interchangeable.
38
 *
39
 * @author    Bernhard Wick <[email protected]>
40
 * @copyright 2015 TechDivision GmbH - <[email protected]>
41
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
42
 * @link      https://github.com/appserver-io/doppelgaenger
43
 * @link      http://www.appserver.io/
44
 */
45
class SkeletonFilter extends AbstractFilter
46
{
47
48
    /**
49
     * Order number if filters are used as a stack, higher means below others
50
     *
51
     * @const integer FILTER_ORDER
52
     */
53
    const FILTER_ORDER = 0;
54
55
    /**
56
     * Will filter portions of incoming stream content.
57
     * Will always contain false to enforce buffering of all buckets.
58
     *
59
     * @param string $content The content to be filtered
60
     *
61
     * @return boolean
62
     */
63
    public function filterContent($content)
64
    {
65
        return false;
66
    }
67
68
    /**
69
     * Preparation hook which is intended to be called at the start of the first filter() iteration.
70
     * We will inject the original path hint here
71
     *
72
     * @param string $bucketData Payload of the first filtered bucket
73
     *
74
     * @return void
75
     */
76
    public function firstBucket(&$bucketData)
77
    {
78
        $this->injectOriginalPathHint($bucketData, $this->structureDefinition->getPath());
0 ignored issues
show
Bug introduced by
The property structureDefinition does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
79
    }
80
81
    /**
82
     * Will find the index of the last character within a structure
83
     *
84
     * @param string $code          The structure code to search in
85
     * @param string $structureName Name of the structure to get the last index for
86
     * @param string $structureType Type of the structure in question
87
     *
88
     * @return integer
89
     */
90
    protected function findLastStructureIndex($code, $structureName, $structureType)
91
    {
92
        // determine which keyword we should search for
93
        switch ($structureType) {
94
            case InterfaceDefinition::TYPE:
95
                $structureKeyword = 'interface';
96
                break;
97
98
            case TraitDefinition::TYPE:
99
                $structureKeyword = 'trait';
100
                break;
101
102
            default:
103
                $structureKeyword = 'class';
104
                break;
105
        }
106
107
        // cut everything in front of the first bracket so we have a better start
108
        $matches = array();
109
        preg_match('/.*' . $structureKeyword . '\s+' . $structureName . '.+?{/s', $code, $matches);
110
        if (count($matches) != 1) {
111
            throw new GeneratorException(sprintf('Could not find last index for stucture %s. Cannot generate proxy skeleton.', $structureName));
112
        }
113
114
        $offset = (strlen(reset($matches)) - 1);
115
        // get a parser util and get the bracket span
116
        $parserUtil = new Parser();
117
        $structureSpan = $parserUtil->getBracketSpan($code, '{', $offset);
118
119
        return (($structureSpan + $offset) - 1);
120
    }
121
122
    /**
123
     * Filter a chunk of data by adding a doppelgaenger skeleton to it
124
     *
125
     * @param string                       $chunk               The data chunk to be filtered
126
     * @param StructureDefinitionInterface $structureDefinition Definition of the structure the chunk belongs to
127
     *
128
     * @return string
129
     */
130
    public function filterChunk($chunk, StructureDefinitionInterface $structureDefinition)
131
    {
132
        // we have to substitute magic __DIR__ and __FILE__ constants
133
        $this->substituteLocationConstants($chunk, $structureDefinition->getPath());
134
135
        // substitute the original function declarations for the renamed ones
136
        $this->substituteFunctionHeaders($chunk, $structureDefinition);
137
138
        // mark the end of the structure as this is an important hook for other things to be woven
139
        $lastIndex = $this->findLastStructureIndex($chunk, $structureDefinition->getName(), $structureDefinition->getType());
140
        $chunk = substr_replace($chunk, Placeholders::STRUCTURE_END, $lastIndex, 0);
141
142
        // inject the code for the function skeletons
143
        $this->injectFunctionSkeletons($chunk, $structureDefinition, true);
144
145
        return $chunk;
146
    }
147
148
    /**
149
     * Will inject condition checking code in front and behind the functions body.
150
     *
151
     * @param string                                                             $bucketData          Payload of the currently filtered bucket
152
     * @param \AppserverIo\Doppelgaenger\Interfaces\StructureDefinitionInterface $structureDefinition The original path we have to place as our constants
153
     * @param boolean                                                            $beautify            Whether or not the injected code should be beautified first
154
     *
155
     * @return boolean
156
     */
157
    protected function injectFunctionSkeletons(& $bucketData, StructureDefinitionInterface $structureDefinition, $beautify = false)
0 ignored issues
show
Unused Code introduced by
The parameter $beautify 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...
158
    {
159
160
        // generate the skeleton code for all known functions
161
        $functionSkeletonsCode = '';
162
        foreach ($structureDefinition->getFunctionDefinitions() as $functionDefinition) {
0 ignored issues
show
Bug introduced by
The expression $structureDefinition->getFunctionDefinitions() of type null|object<AppserverIo\...FunctionDefinitionList> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
163
            // we do not have to act on abstract methods
164
            if ($functionDefinition->isAbstract()) {
165
                continue;
166
            }
167
168
            // __get and __set need some special steps so we can inject our own logic into them
169
            $injectNeeded = false;
170
            if ($functionDefinition->getName() === '__get' || $functionDefinition->getName() === '__set') {
171
                $injectNeeded = true;
172
            }
173
174
            // get the code used before the original body
175
            $functionSkeletonsCode .= $this->generateSkeletonCode($injectNeeded, $functionDefinition);
176
        }
177
178
        // inject the new code at the end of the original structure body
179
        $bucketData = str_replace(Placeholders::STRUCTURE_END, Placeholders::STRUCTURE_END . $functionSkeletonsCode, $bucketData);
180
181
        // if we are still here we seem to have succeeded
182
        return true;
183
    }
184
185
    /**
186
     * Will generate the skeleton code for the passed function definition.
187
     * Will result in a string resembling the following example:
188
     *
189
     *      <FUNCTION_DOCBLOCK>
190
     *      <FUNCTION_MODIFIERS> function <FUNCTION_NAME>(<FUNCTION_PARAMS>)
191
     *      {
192
     *          $dgStartLine = <FUNCTION_START_LINE>;
193
     *          $dgEndLine = <FUNCTION_END_LINE>;
194
     *          / DOPPELGAENGER_FUNCTION_BEGIN_PLACEHOLDER <FUNCTION_NAME> /
195
     *          / DOPPELGAENGER_BEFORE_JOINPOINT <FUNCTION_NAME> /
196
     *          $dgOngoingContract = \AppserverIo\Doppelgaenger\ContractContext::open();
197
     *          / DOPPELGAENGER_INVARIANT_PLACEHOLDER /
198
     *          / DOPPELGAENGER_PRECONDITION_PLACEHOLDER <FUNCTION_NAME> /
199
     *          / DOPPELGAENGER_OLD_SETUP_PLACEHOLDER <FUNCTION_NAME> /
200
     *          $dgResult = null;
201
     *          try {
202
     *              / DOPPELGAENGER_AROUND_JOINPOINT <FUNCTION_NAME> /
203
     *
204
     *          } catch (\Exception $dgThrownExceptionObject) {
205
     *              / DOPPELGAENGER_AFTERTHROWING_JOINPOINT <FUNCTION_NAME> /
206
     *
207
     *              // rethrow the exception
208
     *              throw $dgThrownExceptionObject;
209
     *
210
     *          } finally {
211
     *              / DOPPELGAENGER_AFTER_JOINPOINT <FUNCTION_NAME> /
212
     *
213
     *          }
214
     *          / DOPPELGAENGER_POSTCONDITION_PLACEHOLDER <FUNCTION_NAME> /
215
     *          / DOPPELGAENGER_INVARIANT_PLACEHOLDER /
216
     *          if ($dgOngoingContract) {
217
     *              \AppserverIo\Doppelgaenger\ContractContext::close();
218
     *          } / DOPPELGAENGER_AFTERRETURNING_JOINPOINT <FUNCTION_NAME> /
219
     *
220
     *          return $dgResult;
221
     *      }
222
     *
223
     * @param boolean            $injectNeeded       Determine if we have to use a try...catch block
224
     * @param FunctionDefinition $functionDefinition The function definition object
225
     *
226
     * @return string
227
     */
228
    protected function generateSkeletonCode($injectNeeded, FunctionDefinition $functionDefinition)
229
    {
230
231
        // first of all: the docblock
232
        $code = '
233
        ' . $functionDefinition->getDocBlock() . '
234
        ' . $functionDefinition->getHeader('definition') . '
235
        {
236
            ' . ReservedKeywords::START_LINE_VARIABLE . ' = ' . (integer) $functionDefinition->getStartLine() . ';
237
            ' . ReservedKeywords::END_LINE_VARIABLE . ' = ' . (integer) $functionDefinition->getEndLine() . ';
238
            ' . Placeholders::FUNCTION_BEGIN . $functionDefinition->getName() . Placeholders::PLACEHOLDER_CLOSE . '
239
            ';
240
241
        // right after: the "before" join-point
242
        $code .= Placeholders::BEFORE_JOINPOINT . $functionDefinition->getName() . Placeholders::PLACEHOLDER_CLOSE . '
243
            ';
244
245
        // open the contract context so we are able to avoid endless recursion
246
        $code .= ReservedKeywords::CONTRACT_CONTEXT . ' = \AppserverIo\Doppelgaenger\ContractContext::open();
247
            ';
248
249
        // Invariant is not needed in private or static functions.
250
        // Also make sure that there is none in front of the constructor check
251 View Code Duplication
        if ($functionDefinition->getVisibility() !== 'private' &&
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
252
            !$functionDefinition->isStatic() && $functionDefinition->getName() !== '__construct'
253
        ) {
254
            $code .= Placeholders::INVARIANT_CALL_START . '
255
            ';
256
        }
257
258
        $code .= Placeholders::PRECONDITION . $functionDefinition->getName() . Placeholders::PLACEHOLDER_CLOSE . '
259
            ' . Placeholders::OLD_SETUP . $functionDefinition->getName() . Placeholders::PLACEHOLDER_CLOSE. '
260
            ';
261
262
        // we will wrap code execution in order to provide a "finally" and "after throwing" placeholder hook.
263
        // we will also predefine the result as NULL to avoid warnings
264
        $code .= ReservedKeywords::RESULT . ' = null;
265
            try {
266
                ' .  Placeholders::AROUND_JOINPOINT . $functionDefinition->getName() . Placeholders::PLACEHOLDER_CLOSE . '
267
        ';
268
269
        // add the second part of the try/catch/finally block
270
        $code .= '
271
            } catch (\Exception ' . ReservedKeywords::THROWN_EXCEPTION_OBJECT . ') {
272
                ' . Placeholders::AFTERTHROWING_JOINPOINT . $functionDefinition->getName() . Placeholders::PLACEHOLDER_CLOSE . '
273
                // rethrow the exception
274
                throw ' . ReservedKeywords::THROWN_EXCEPTION_OBJECT . ';
275
            } finally {
276
        ';
277
278
        // if we have to inject additional code, we might do so here
279
        if ($injectNeeded === true) {
280
            $code .= Placeholders::METHOD_INJECT . $functionDefinition->getName() . Placeholders::PLACEHOLDER_CLOSE;
281
        }
282
283
        // finish of the block
284
        $code .= '        ' . Placeholders::AFTER_JOINPOINT . $functionDefinition->getName() . Placeholders::PLACEHOLDER_CLOSE . '
285
            }
286
        ';
287
288
        // now just place all the other placeholder for other filters to come
289
        $code .= '    ' . Placeholders::POSTCONDITION . $functionDefinition->getName() . Placeholders::PLACEHOLDER_CLOSE;
290
291
        // Invariant is not needed in private or static functions
292 View Code Duplication
        if ($functionDefinition->getVisibility() !== 'private' && !$functionDefinition->isStatic()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
293
            $code .= '
294
            ' . Placeholders::INVARIANT_CALL_END . '
295
            ';
296
        }
297
298
        // close of the contract context
299
        $code .= 'if (' . ReservedKeywords::CONTRACT_CONTEXT . ') {
300
                \AppserverIo\Doppelgaenger\ContractContext::close();
301
            }
302
        ';
303
304
        // last of all: the "after returning" join-point and the final return from the proxy
305
        $code .= '    ' . Placeholders::AFTERRETURNING_JOINPOINT . $functionDefinition->getName() . Placeholders::PLACEHOLDER_CLOSE . '
306
            return ' . ReservedKeywords::RESULT . ';
307
        }
308
        ';
309
310
        return $code;
311
    }
312
313
    /**
314
     * Will substitute all function headers (we know about) with function headers indicating an original implementation by appending
315
     * a specific suffix
316
     *
317
     * @param string                                                             $bucketData          Payload of the currently filtered bucket
318
     * @param \AppserverIo\Doppelgaenger\Interfaces\StructureDefinitionInterface $structureDefinition The original path we have to place as our constants
319
     *
320
     * @return boolean
321
     */
322
    protected function substituteFunctionHeaders(& $bucketData, StructureDefinitionInterface $structureDefinition)
323
    {
324
        // is there event anything to substitute?
325
        if ($structureDefinition->getFunctionDefinitions()->count() <= 0) {
326
            return true;
327
        }
328
329
        // first of all we have to collect all functions we have to substitute
330
        $functionSubstitutes = array();
331
        $functionPatterns = array();
332
        foreach ($structureDefinition->getFunctionDefinitions() as $functionDefinition) {
0 ignored issues
show
Bug introduced by
The expression $structureDefinition->getFunctionDefinitions() of type null|object<AppserverIo\...FunctionDefinitionList> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
333
            // we do not have to act on abstract methods
334
            if ($functionDefinition->isAbstract()) {
335
                continue;
336
            }
337
338
            $functionPatterns[] = '/function\s' . $functionDefinition->getName() . '\s*\(/';
339
            $functionSubstitutes[] = 'function ' . $functionDefinition->getName() . ReservedKeywords::ORIGINAL_FUNCTION_SUFFIX . '(';
340
        }
341
342
        // do the actual replacing and propagate the result in success
343
        $result = preg_replace($functionPatterns, $functionSubstitutes, $bucketData);
344
        if (!is_null($result)) {
345
            $bucketData = $result;
346
            return true;
347
        }
348
349
        // still here? That seems to be wrong
350
        return false;
351
    }
352
353
    /**
354
     * Will substitute all magic __DIR__ and __FILE__ constants with our prepared substitutes to
355
     * emulate original original filesystem context when in cache folder.
356
     *
357
     * @param string $bucketData Payload of the currently filtered bucket
358
     * @param string $file       The original path we have to place as our constants
359
     *
360
     * @return boolean
361
     */
362
    protected function substituteLocationConstants(& $bucketData, $file)
363
    {
364
        $dir = dirname($file);
365
        // Inject the code
366
        $bucketData = str_replace(
367
            array('__DIR__', '__FILE__'),
368
            array('\'' . $dir . '\'', '\'' . $file . '\''),
369
            $bucketData
370
        );
371
372
        // Still here? Success then
373
        return true;
374
    }
375
376
    /**
377
     * Will inject a placeholder that is used to store metadata about the original file
378
     *
379
     * @param string $bucketData Payload of the currently filtered bucket
380
     * @param string $file       The original file path we have to inject
381
     *
382
     * @return boolean
383
     */
384
    protected function injectOriginalPathHint(& $bucketData, $file)
385
    {
386
        // Do need to do this?
387
        if (strpos($bucketData, '<?php') === false) {
388
            return false;
389
        }
390
391
        // Build up the needed code for our hint
392
        $code = ' ' . Placeholders::PLACEHOLDER_OPEN . Placeholders::ORIGINAL_PATH_HINT . $file . '#' .
393
            filemtime(
394
                $file
395
            ) . Placeholders::ORIGINAL_PATH_HINT . Placeholders::PLACEHOLDER_CLOSE;
396
397
        // Inject the code
398
        $index = strpos($bucketData, '<?php');
399
        $bucketData = substr_replace($bucketData, $code, $index + 5, 0);
400
401
        // Still here? Success then.
402
        return true;
403
    }
404
}
405