Completed
Push — master ( 75665e...742229 )
by Bernhard
36:40 queued 32:07
created

src/StreamFilters/SkeletonFilter.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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());
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
     * Preparation hook which is intended to be called at the start of the first filter() iteration.
124
     * We will inject the original path hint here
125
     *
126
     * @return void
127
     */
128
    public function finish()
129
    {
130
        // we have to substitute magic __DIR__ and __FILE__ constants
131
        $this->substituteLocationConstants($this->bucketBuffer, $this->structureDefinition->getPath());
132
133
        // substitute the original function declarations for the renamed ones
134
        $this->substituteFunctionHeaders($this->bucketBuffer, $this->structureDefinition);
135
136
        // mark the end of the structure as this is an important hook for other things to be woven
137
        $lastIndex = $this->findLastStructureIndex($this->bucketBuffer, $this->structureDefinition->getName(), $this->structureDefinition->getType());
138
        $this->bucketBuffer = substr_replace($this->bucketBuffer, Placeholders::STRUCTURE_END, $lastIndex, 0);
139
140
        // inject the code for the function skeletons
141
        $this->injectFunctionSkeletons($this->bucketBuffer, $this->structureDefinition, true);
142
    }
143
144
    /**
145
     * The main filter method.
146
     * Implemented according to \php_user_filter class. Will loop over all stream buckets, buffer them and perform
147
     * the needed actions.
148
     *
149
     * @param resource $in       Incoming bucket brigade we need to filter
150
     * @param resource $out      Outgoing bucket brigade with already filtered content
151
     * @param integer  $consumed The count of altered characters as buckets pass the filter
152
     * @param boolean  $closing  Is the stream about to close?
153
     *
154
     * @throws \AppserverIo\Doppelgaenger\Exceptions\GeneratorException
155
     *
156
     * @return integer
157
     *
158
     * @link http://www.php.net/manual/en/php-user-filter.filter.php
159
     */
160
    public function filter($in, $out, &$consumed, $closing)
161
    {
162
        // make the params more prominent
163
        $this->structureDefinition = $this->params;
164
165
        // use the parent filter method to allow for proper hook usage
166
        return parent::filter($in, $out, $consumed, $closing);
167
    }
168
169
    /**
170
     * Will inject condition checking code in front and behind the functions body.
171
     *
172
     * @param string                                                             $bucketData          Payload of the currently filtered bucket
173
     * @param \AppserverIo\Doppelgaenger\Interfaces\StructureDefinitionInterface $structureDefinition The original path we have to place as our constants
174
     * @param boolean                                                            $beautify            Whether or not the injected code should be beautified first
175
     *
176
     * @return boolean
177
     */
178
    protected function injectFunctionSkeletons(& $bucketData, StructureDefinitionInterface $structureDefinition, $beautify = false)
179
    {
180
181
        // generate the skeleton code for all known functions
182
        $functionSkeletonsCode = '';
183
        foreach ($structureDefinition->getFunctionDefinitions() as $functionDefinition) {
184
            // we do not have to act on abstract methods
185
            if ($functionDefinition->isAbstract()) {
186
                continue;
187
            }
188
189
            // __get and __set need some special steps so we can inject our own logic into them
190
            $injectNeeded = false;
191
            if ($functionDefinition->getName() === '__get' || $functionDefinition->getName() === '__set') {
192
                $injectNeeded = true;
193
            }
194
195
            // get the code used before the original body
196
            $functionSkeletonsCode .= $this->generateSkeletonCode($injectNeeded, $functionDefinition);
197
        }
198
199
        // inject the new code at the end of the original structure body
200
        $bucketData = str_replace(Placeholders::STRUCTURE_END, Placeholders::STRUCTURE_END . $functionSkeletonsCode, $bucketData);
201
202
        // if we are still here we seem to have succeeded
203
        return true;
204
    }
205
206
    /**
207
     * Will generate the skeleton code for the passed function definition.
208
     * Will result in a string resembling the following example:
209
     *
210
     *      <FUNCTION_DOCBLOCK>
211
     *      <FUNCTION_MODIFIERS> function <FUNCTION_NAME>(<FUNCTION_PARAMS>)
212
     *      {
213
     *          $dgStartLine = <FUNCTION_START_LINE>;
214
     *          $dgEndLine = <FUNCTION_END_LINE>;
215
     *          / DOPPELGAENGER_FUNCTION_BEGIN_PLACEHOLDER <FUNCTION_NAME> /
216
     *          / DOPPELGAENGER_BEFORE_JOINPOINT <FUNCTION_NAME> /
217
     *          $dgOngoingContract = \AppserverIo\Doppelgaenger\ContractContext::open();
218
     *          / DOPPELGAENGER_INVARIANT_PLACEHOLDER /
219
     *          / DOPPELGAENGER_PRECONDITION_PLACEHOLDER <FUNCTION_NAME> /
220
     *          / DOPPELGAENGER_OLD_SETUP_PLACEHOLDER <FUNCTION_NAME> /
221
     *          $dgResult = null;
222
     *          try {
223
     *              / DOPPELGAENGER_AROUND_JOINPOINT <FUNCTION_NAME> /
224
     *
225
     *          } catch (\Exception $dgThrownExceptionObject) {
226
     *              / DOPPELGAENGER_AFTERTHROWING_JOINPOINT <FUNCTION_NAME> /
227
     *
228
     *              // rethrow the exception
229
     *              throw $dgThrownExceptionObject;
230
     *
231
     *          } finally {
232
     *              / DOPPELGAENGER_AFTER_JOINPOINT <FUNCTION_NAME> /
233
     *
234
     *          }
235
     *          / DOPPELGAENGER_POSTCONDITION_PLACEHOLDER <FUNCTION_NAME> /
236
     *          / DOPPELGAENGER_INVARIANT_PLACEHOLDER /
237
     *          if ($dgOngoingContract) {
238
     *              \AppserverIo\Doppelgaenger\ContractContext::close();
239
     *          } / DOPPELGAENGER_AFTERRETURNING_JOINPOINT <FUNCTION_NAME> /
240
     *
241
     *          return $dgResult;
242
     *      }
243
     *
244
     * @param boolean            $injectNeeded       Determine if we have to use a try...catch block
245
     * @param FunctionDefinition $functionDefinition The function definition object
246
     *
247
     * @return string
248
     */
249
    protected function generateSkeletonCode($injectNeeded, FunctionDefinition $functionDefinition)
250
    {
251
252
        // first of all: the docblock
253
        $code = '
254
        ' . $functionDefinition->getDocBlock() . '
255
        ' . $functionDefinition->getHeader('definition') . '
256
        {
257
            ' . ReservedKeywords::START_LINE_VARIABLE . ' = ' . (integer) $functionDefinition->getStartLine() . ';
258
            ' . ReservedKeywords::END_LINE_VARIABLE . ' = ' . (integer) $functionDefinition->getEndLine() . ';
259
            ' . Placeholders::FUNCTION_BEGIN . $functionDefinition->getName() . Placeholders::PLACEHOLDER_CLOSE . '
260
            ';
261
262
        // right after: the "before" join-point
263
        $code .= Placeholders::BEFORE_JOINPOINT . $functionDefinition->getName() . Placeholders::PLACEHOLDER_CLOSE . '
264
            ';
265
266
        // open the contract context so we are able to avoid endless recursion
267
        $code .= ReservedKeywords::CONTRACT_CONTEXT . ' = \AppserverIo\Doppelgaenger\ContractContext::open();
268
            ';
269
270
        // Invariant is not needed in private or static functions.
271
        // Also make sure that there is none in front of the constructor check
272 View Code Duplication
        if ($functionDefinition->getVisibility() !== 'private' &&
273
            !$functionDefinition->isStatic() && $functionDefinition->getName() !== '__construct'
274
        ) {
275
            $code .= Placeholders::INVARIANT_CALL_START . '
276
            ';
277
        }
278
279
        $code .= Placeholders::PRECONDITION . $functionDefinition->getName() . Placeholders::PLACEHOLDER_CLOSE . '
280
            ' . Placeholders::OLD_SETUP . $functionDefinition->getName() . Placeholders::PLACEHOLDER_CLOSE. '
281
            ';
282
283
        // we will wrap code execution in order to provide a "finally" and "after throwing" placeholder hook.
284
        // we will also predefine the result as NULL to avoid warnings
285
        $code .= ReservedKeywords::RESULT . ' = null;
286
            try {
287
                ' .  Placeholders::AROUND_JOINPOINT . $functionDefinition->getName() . Placeholders::PLACEHOLDER_CLOSE . '
288
        ';
289
290
        // add the second part of the try/catch/finally block
291
        $code .= '
292
            } catch (\Exception ' . ReservedKeywords::THROWN_EXCEPTION_OBJECT . ') {
293
                ' . Placeholders::AFTERTHROWING_JOINPOINT . $functionDefinition->getName() . Placeholders::PLACEHOLDER_CLOSE . '
294
                // rethrow the exception
295
                throw ' . ReservedKeywords::THROWN_EXCEPTION_OBJECT . ';
296
            } finally {
297
        ';
298
299
        // if we have to inject additional code, we might do so here
300
        if ($injectNeeded === true) {
301
            $code .= Placeholders::METHOD_INJECT . $functionDefinition->getName() . Placeholders::PLACEHOLDER_CLOSE;
302
        }
303
304
        // finish of the block
305
        $code .= '        ' . Placeholders::AFTER_JOINPOINT . $functionDefinition->getName() . Placeholders::PLACEHOLDER_CLOSE . '
306
            }
307
        ';
308
309
        // now just place all the other placeholder for other filters to come
310
        $code .= '    ' . Placeholders::POSTCONDITION . $functionDefinition->getName() . Placeholders::PLACEHOLDER_CLOSE;
311
312
        // Invariant is not needed in private or static functions
313 View Code Duplication
        if ($functionDefinition->getVisibility() !== 'private' && !$functionDefinition->isStatic()) {
314
            $code .= '
315
            ' . Placeholders::INVARIANT_CALL_END . '
316
            ';
317
        }
318
319
        // close of the contract context
320
        $code .= 'if (' . ReservedKeywords::CONTRACT_CONTEXT . ') {
321
                \AppserverIo\Doppelgaenger\ContractContext::close();
322
            }
323
        ';
324
325
        // last of all: the "after returning" join-point and the final return from the proxy
326
        $code .= '    ' . Placeholders::AFTERRETURNING_JOINPOINT . $functionDefinition->getName() . Placeholders::PLACEHOLDER_CLOSE . '
327
            return ' . ReservedKeywords::RESULT . ';
328
        }
329
        ';
330
331
        return $code;
332
    }
333
334
    /**
335
     * Will substitute all function headers (we know about) with function headers indicating an original implementation by appending
336
     * a specific suffix
337
     *
338
     * @param string                                                             $bucketData          Payload of the currently filtered bucket
339
     * @param \AppserverIo\Doppelgaenger\Interfaces\StructureDefinitionInterface $structureDefinition The original path we have to place as our constants
340
     *
341
     * @return boolean
342
     */
343
    protected function substituteFunctionHeaders(& $bucketData, StructureDefinitionInterface $structureDefinition)
344
    {
345
        // is there event anything to substitute?
346
        if ($structureDefinition->getFunctionDefinitions()->count() <= 0) {
347
            return true;
348
        }
349
350
        // first of all we have to collect all functions we have to substitute
351
        $functionSubstitutes = array();
352
        $functionPatterns = array();
353
        foreach ($structureDefinition->getFunctionDefinitions() as $functionDefinition) {
0 ignored issues
show
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...
354
            // we do not have to act on abstract methods
355
            if ($functionDefinition->isAbstract()) {
356
                continue;
357
            }
358
359
            $functionPatterns[] = '/function\s' . $functionDefinition->getName() . '\s*\(/';
360
            $functionSubstitutes[] = 'function ' . $functionDefinition->getName() . ReservedKeywords::ORIGINAL_FUNCTION_SUFFIX . '(';
361
        }
362
363
        // do the actual replacing and propagate the result in success
364
        $result = preg_replace($functionPatterns, $functionSubstitutes, $bucketData);
365
        if (!is_null($result)) {
366
            $bucketData = $result;
367
            return true;
368
        }
369
370
        // still here? That seems to be wrong
371
        return false;
372
    }
373
374
    /**
375
     * Will substitute all magic __DIR__ and __FILE__ constants with our prepared substitutes to
376
     * emulate original original filesystem context when in cache folder.
377
     *
378
     * @param string $bucketData Payload of the currently filtered bucket
379
     * @param string $file       The original path we have to place as our constants
380
     *
381
     * @return boolean
382
     */
383
    protected function substituteLocationConstants(& $bucketData, $file)
384
    {
385
        $dir = dirname($file);
386
        // Inject the code
387
        $bucketData = str_replace(
388
            array('__DIR__', '__FILE__'),
389
            array('\'' . $dir . '\'', '\'' . $file . '\''),
390
            $bucketData
391
        );
392
393
        // Still here? Success then
394
        return true;
395
    }
396
397
    /**
398
     * Will inject a placeholder that is used to store metadata about the original file
399
     *
400
     * @param string $bucketData Payload of the currently filtered bucket
401
     * @param string $file       The original file path we have to inject
402
     *
403
     * @return boolean
404
     */
405
    protected function injectOriginalPathHint(& $bucketData, $file)
406
    {
407
        // Do need to do this?
408
        if (strpos($bucketData, '<?php') === false) {
409
            return false;
410
        }
411
412
        // Build up the needed code for our hint
413
        $code = ' ' . Placeholders::PLACEHOLDER_OPEN . Placeholders::ORIGINAL_PATH_HINT . $file . '#' .
414
            filemtime(
415
                $file
416
            ) . Placeholders::ORIGINAL_PATH_HINT . Placeholders::PLACEHOLDER_CLOSE;
417
418
        // Inject the code
419
        $index = strpos($bucketData, '<?php');
420
        $bucketData = substr_replace($bucketData, $code, $index + 5, 0);
421
422
        // Still here? Success then.
423
        return true;
424
    }
425
}
426