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

InvariantFilter::generateSetCode()   B

Complexity

Conditions 4
Paths 8

Size

Total Lines 87
Code Lines 34

Duplication

Lines 17
Ratio 19.54 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 17
loc 87
rs 8.4157
cc 4
eloc 34
nc 8
nop 2

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * \AppserverIo\Doppelgaenger\StreamFilters\InvariantFilter
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\Lists\AttributeDefinitionList;
24
use AppserverIo\Doppelgaenger\Entities\Lists\TypedListList;
25
use AppserverIo\Doppelgaenger\Exceptions\GeneratorException;
26
use AppserverIo\Doppelgaenger\Dictionaries\Placeholders;
27
use AppserverIo\Doppelgaenger\Dictionaries\ReservedKeywords;
28
use AppserverIo\Doppelgaenger\Interfaces\StructureDefinitionInterface;
29
30
/**
31
 * This filter will buffer the input stream and add all invariant related information at prepared locations
32
 * (see $dependencies)
33
 *
34
 * @author    Bernhard Wick <[email protected]>
35
 * @copyright 2015 TechDivision GmbH - <[email protected]>
36
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
37
 * @link      https://github.com/appserver-io/doppelgaenger
38
 * @link      http://www.appserver.io/
39
 */
40
class InvariantFilter extends AbstractFilter
41
{
42
43
    /**
44
     * @const integer FILTER_ORDER Order number if filters are used as a stack, higher means below others
45
     */
46
    const FILTER_ORDER = 3;
47
48
    /**
49
     * @var array $dependencies Other filters on which we depend
50
     */
51
    protected $dependencies = array('SkeletonFilter');
52
53
    /**
54
     * Filter a chunk of data by adding introductions to it
55
     *
56
     * @param string                       $chunk               The data chunk to be filtered
57
     * @param StructureDefinitionInterface $structureDefinition Definition of the structure the chunk belongs to
58
     *
59
     * @return string
60
     */
61
    public function filterChunk($chunk, StructureDefinitionInterface $structureDefinition)
62
    {
63
        // After iterate over the attributes and build up our array of attributes we have to include in our
64
        // checking mechanism.
65
        $obsoleteProperties = array();
66
        $propertyReplacements = array();
67
        $iterator = $structureDefinition->getAttributeDefinitions()->getIterator();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface AppserverIo\Doppelgaenge...tureDefinitionInterface as the method getAttributeDefinitions() does only exist in the following implementations of said interface: AppserverIo\Doppelgaenge...itions\AspectDefinition, AppserverIo\Doppelgaenge...nitions\ClassDefinition, AppserverIo\Doppelgaenge...nitions\TraitDefinition.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
68
        for ($i = 0; $i < $iterator->count(); $i++) {
69
            // Get the current attribute for more easy access
70
            $attribute = $iterator->current();
71
72
            // Only enter the attribute if it is used in an invariant and it is not private
73
            if ($attribute->inInvariant() && $attribute->getVisibility() !== 'private') {
74
                // Build up our regex expression to filter them out
75
                $obsoleteProperties[] = '/' . $attribute->getVisibility() . '.*?\\' . $attribute->getName() . '/';
76
                $propertyReplacements[] = 'private ' . $attribute->getName();
77
            }
78
79
            // Move the iterator
80
            $iterator->next();
81
        }
82
83
        // Get our buckets from the stream
84
        $functionHook = '';
85
        // We only have to do that once!
86
        if (empty($functionHook)) {
87
            $functionHook = Placeholders::STRUCTURE_END;
88
89
            // Get the code for our attribute storage
90
            $attributeCode = $this->generateAttributeCode($structureDefinition->getAttributeDefinitions());
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface AppserverIo\Doppelgaenge...tureDefinitionInterface as the method getAttributeDefinitions() does only exist in the following implementations of said interface: AppserverIo\Doppelgaenge...itions\AspectDefinition, AppserverIo\Doppelgaenge...nitions\ClassDefinition, AppserverIo\Doppelgaenge...nitions\TraitDefinition.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
91
92
            // Get the code for the assertions
93
            $code = $this->generateFunctionCode($structureDefinition->getInvariants());
94
95
            // Insert the code
96
            $chunk = str_replace(
97
                array(
98
                    $functionHook,
99
                    $functionHook
100
                ),
101
                array(
102
                    $functionHook . $attributeCode,
103
                    $functionHook . $code
104
                ),
105
                $chunk
106
            );
107
108
            // Determine if we need the __set method to be injected
109 View Code Duplication
            if ($structureDefinition->getFunctionDefinitions()->entryExists('__set')) {
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...
110
                // Get the code for our __set() method
111
                $setCode = $this->generateSetCode($structureDefinition->hasParents(), true);
112
                $chunk = str_replace(
113
                    Placeholders::METHOD_INJECT . '__set' . Placeholders::PLACEHOLDER_CLOSE,
114
                    $setCode,
115
                    $chunk
116
                );
117
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
118
            } else {
119
                $setCode = $this->generateSetCode($structureDefinition->hasParents());
120
                $chunk = str_replace(
121
                    $functionHook,
122
                    $functionHook . $setCode,
123
                    $chunk
124
                );
125
            }
126
127
            // Determine if we need the __get method to be injected
128 View Code Duplication
            if ($structureDefinition->getFunctionDefinitions()->entryExists('__get')) {
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...
129
                // Get the code for our __set() method
130
                $getCode = $this->generateGetCode($structureDefinition->hasParents(), true);
131
                $chunk = str_replace(
132
                    Placeholders::METHOD_INJECT . '__get' . Placeholders::PLACEHOLDER_CLOSE,
133
                    $getCode,
134
                    $chunk
135
                );
136
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
137
            } else {
138
                $getCode = $this->generateGetCode($structureDefinition->hasParents());
139
                $chunk = str_replace(
140
                    $functionHook,
141
                    $functionHook . $getCode,
142
                    $chunk
143
                );
144
            }
145
        }
146
147
        // We need the code to call the invariant
148
        $this->injectInvariantCall($chunk);
149
150
        // Remove all the properties we will take care of with our magic setter and getter
151
        $chunk = preg_replace($obsoleteProperties, $propertyReplacements, $chunk, 1);
152
153
154
        return $chunk;
155
    }
156
157
    /**
158
     * Will generate the code needed to for managing the attributes in regards to invariants related to them
159
     *
160
     * @param \AppserverIo\Doppelgaenger\Entities\Lists\AttributeDefinitionList $attributeDefinitions Defined attributes
161
     *
162
     * @return string
163
     */
164
    protected function generateAttributeCode(AttributeDefinitionList $attributeDefinitions)
165
    {
166
        // We should create attributes to store our attribute types
167
        $code = '
168
           /**
169
            * @var array
170
            */
171
            private $' . ReservedKeywords::ATTRIBUTE_STORAGE . ' = array(
172
                ';
173
174
        // After iterate over the attributes and build up our array
175
        $iterator = $attributeDefinitions->getIterator();
176
        for ($i = 0; $i < $iterator->count(); $i++) {
177
            // Get the current attribute for more easy access
178
            $attribute = $iterator->current();
179
180
            // Only enter the attribute if it is used in an invariant and it is not private
181
            if ($attribute->inInvariant() && $attribute->getVisibility() !== 'private') {
182
                $code .= '"' . substr($attribute->getName(), 1) . '"';
183
                $code .= ' => array(
184
                        "visibility" => "' . $attribute->getVisibility() . '",
185
                        "line" => "' . $attribute->getLine() . '",
186
                        ';
187
                // Now check if we need any keywords for the variable identity
188
                if ($attribute->isStatic()) {
189
                    $code .= '"static" => true';
190
                } else {
191
                    $code .= '"static" => false';
192
                }
193
                $code .= '
194
                    ),
195
                    ';
196
            }
197
198
            // Move the iterator
199
            $iterator->next();
200
        }
201
        $code .= ');
202
        ';
203
204
        return $code;
205
    }
206
207
    /**
208
     * Will generate the code of the magic __set() method needed to check invariants related to member variables
209
     *
210
     * @param boolean $hasParents Does this structure have parents
211
     * @param boolean $injected   Will the created method be injected or is it a stand alone method?
212
     *
213
     * @return string
214
     */
215
    protected function generateSetCode($hasParents, $injected = false)
216
    {
217
218
        // We only need the method header if we don't inject
219
        if ($injected === false) {
220
            $code = '/**
221
             * Magic function to forward writing property access calls if within visibility boundaries.
222
             *
223
             * @throws \Exception
224
             */
225
            public function __set($name, $value)
226
            {';
227
        } else {
228
            $code = '';
229
        }
230
231
        $code .= ReservedKeywords::CONTRACT_CONTEXT . ' = \AppserverIo\Doppelgaenger\ContractContext::open();
232
            ' . ReservedKeywords::FAILURE_VARIABLE . ' = array();
233
            ' . ReservedKeywords::UNWRAPPED_FAILURE_VARIABLE . ' = array();
234
            // Does this property even exist? If not, throw an exception
235
            if (!isset($this->' . ReservedKeywords::ATTRIBUTE_STORAGE . '[$name])) {';
236
237 View Code Duplication
        if ($hasParents) {
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...
238
            $code .= 'return parent::__set($name, $value);';
239
        } else {
240
            $code .= 'if (property_exists($this, $name)) {' .
241
242
                ReservedKeywords::FAILURE_VARIABLE . '[] = "accessing $name in an invalid way";' .
243
                Placeholders::ENFORCEMENT . 'InvalidArgumentException' . Placeholders::PLACEHOLDER_CLOSE .
244
                '\AppserverIo\Doppelgaenger\ContractContext::close();
245
                return false;
246
                } else {' .
247
248
                ReservedKeywords::FAILURE_VARIABLE . '[] = "accessing $name as it does not exist";' .
249
                Placeholders::ENFORCEMENT . 'MissingPropertyException' . Placeholders::PLACEHOLDER_CLOSE .
250
                '\AppserverIo\Doppelgaenger\ContractContext::close();
251
                return false;
252
                }';
253
        }
254
255
        $code .= '}
256
        // Check if the invariant holds
257
            ' . Placeholders::INVARIANT_CALL .
258
            '// Now check what kind of visibility we would have
259
            $attribute = $this->' . ReservedKeywords::ATTRIBUTE_STORAGE . '[$name];
260
            switch ($attribute["visibility"]) {
261
262
                case "protected" :
263
264
                    if (is_subclass_of(get_called_class(), __CLASS__)) {
265
266
                        $this->$name = $value;
267
268
                    } else {' .
269
270
            ReservedKeywords::FAILURE_VARIABLE . '[] = "accessing $name in an invalid way";' .
271
            Placeholders::ENFORCEMENT . 'InvalidArgumentException' . Placeholders::PLACEHOLDER_CLOSE .
272
            '\AppserverIo\Doppelgaenger\ContractContext::close();
273
            return false;
274
            }
275
                    break;
276
277
                case "public" :
278
279
                    $this->$name = $value;
280
                    break;
281
282
                default :' .
283
284
            ReservedKeywords::FAILURE_VARIABLE . '[] = "accessing $name in an invalid way";' .
285
            Placeholders::ENFORCEMENT . 'InvalidArgumentException' . Placeholders::PLACEHOLDER_CLOSE .
286
            '\AppserverIo\Doppelgaenger\ContractContext::close();
287
            return false;
288
            break;
289
            }
290
291
            // Check if the invariant holds
292
            ' . Placeholders::INVARIANT_CALL .
293
            '\AppserverIo\Doppelgaenger\ContractContext::close();';
294
295
        // We do not need the method encasing brackets if we inject
296
        if ($injected === false) {
297
            $code .= '}';
298
        }
299
300
        return $code;
301
    }
302
303
    /**
304
     * Will generate the code of the magic __get() method needed to access member variables which are hidden
305
     * in order to force the usage of __set()
306
     *
307
     * @param boolean $hasParents Does this structure have parents
308
     * @param boolean $injected   Will the created method be injected or is it a stand alone method?
309
     *
310
     * @return string
311
     */
312
    protected function generateGetCode($hasParents, $injected = false)
313
    {
314
315
        // We only need the method header if we don't inject
316
        if ($injected === false) {
317
            $code = '/**
318
         * Magic function to forward reading property access calls if within visibility boundaries.
319
         *
320
         * @throws \Exception
321
         */
322
        public function __get($name)
323
        {';
324
        } else {
325
            $code = '';
326
        }
327
        $code .= ReservedKeywords::FAILURE_VARIABLE . ' = array();
328
            ' . ReservedKeywords::UNWRAPPED_FAILURE_VARIABLE . ' = array();
329
            // Does this property even exist? If not, throw an exception
330
            if (!isset($this->' . ReservedKeywords::ATTRIBUTE_STORAGE . '[$name])) {';
331
332 View Code Duplication
        if ($hasParents) {
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...
333
            $code .= 'return parent::__get($name);';
334
        } else {
335
            $code .= 'if (property_exists($this, $name)) {' .
336
337
                ReservedKeywords::FAILURE_VARIABLE . '[] = "accessing $name in an invalid way";' .
338
                Placeholders::ENFORCEMENT . 'InvalidArgumentException' . Placeholders::PLACEHOLDER_CLOSE .
339
                '\AppserverIo\Doppelgaenger\ContractContext::close();
340
                return false;
341
                } else {' .
342
343
                ReservedKeywords::FAILURE_VARIABLE . '[] = "accessing $name as it does not exist";' .
344
                Placeholders::ENFORCEMENT . 'MissingPropertyException' . Placeholders::PLACEHOLDER_CLOSE .
345
                '\AppserverIo\Doppelgaenger\ContractContext::close();
346
                return false;
347
                }';
348
        }
349
350
        $code .= '}
351
352
        // Now check what kind of visibility we would have
353
        $attribute = $this->' . ReservedKeywords::ATTRIBUTE_STORAGE . '[$name];
354
        switch ($attribute["visibility"]) {
355
356
            case "protected" :
357
358
                if (is_subclass_of(get_called_class(), __CLASS__)) {
359
360
                    return $this->$name;
361
362
                } else {' .
363
364
            ReservedKeywords::FAILURE_VARIABLE . '[] = "accessing $name in an invalid way";' .
365
            Placeholders::ENFORCEMENT . 'InvalidArgumentException' . Placeholders::PLACEHOLDER_CLOSE .
366
            '\AppserverIo\Doppelgaenger\ContractContext::close();
367
            return false;}
368
                break;
369
370
            case "public" :
371
372
                return $this->$name;
373
                break;
374
375
            default :' .
376
377
            ReservedKeywords::FAILURE_VARIABLE . '[] = "accessing $name in an invalid way";' .
378
            Placeholders::ENFORCEMENT . 'InvalidArgumentException' . Placeholders::PLACEHOLDER_CLOSE .
379
            '\AppserverIo\Doppelgaenger\ContractContext::close();
380
            return false;
381
            break;
382
        }';
383
384
        // We do not need the method encasing brackets if we inject
385
        if ($injected === false) {
386
            $code .= '}';
387
        }
388
389
        return $code;
390
    }
391
392
    /**
393
     * Will inject the call to the invariant checking method at encountered placeholder strings within the passed
394
     * bucket data
395
     *
396
     * @param string $bucketData Payload of the currently filtered bucket
397
     *
398
     * @return boolean
399
     */
400
    protected function injectInvariantCall(& $bucketData)
401
    {
402
        $tmpMapping = array(
403
            Placeholders::INVARIANT_CALL => '\'unknown\'',
404
            Placeholders::INVARIANT_CALL_START => ReservedKeywords::START_LINE_VARIABLE,
405
            Placeholders::INVARIANT_CALL_END => ReservedKeywords::END_LINE_VARIABLE
406
        );
407
408
        foreach ($tmpMapping as $placeholder => $lineIndicator) {
409
            $code = 'if (' . ReservedKeywords::CONTRACT_CONTEXT . ' === true) {
410
                $this->' . ReservedKeywords::CLASS_INVARIANT . '(__METHOD__, ' . $lineIndicator . ');
411
            }';
412
413
            // inject the clone statement to preserve an instance of the object prior to our call.
414
            $bucketData = str_replace(
415
                $placeholder,
416
                $code,
417
                $bucketData
418
            );
419
        }
420
421
        // Still here? We encountered no error then.
422
        return true;
423
    }
424
425
    /**
426
     * Will generate the code needed to enforce made invariant assertions
427
     *
428
     * @param \AppserverIo\Doppelgaenger\Entities\Lists\TypedListList $assertionLists List of assertion lists
429
     *
430
     * @return string
431
     */
432
    protected function generateFunctionCode(TypedListList $assertionLists)
433
    {
434
        $code = 'protected function ' . ReservedKeywords::CLASS_INVARIANT . '(' . ReservedKeywords::INVARIANT_CALLER_VARIABLE . ', ' . ReservedKeywords::ERROR_LINE_VARIABLE . ') {
435
            ' . ReservedKeywords::CONTRACT_CONTEXT . ' = \AppserverIo\Doppelgaenger\ContractContext::open();
436
            if (' . ReservedKeywords::CONTRACT_CONTEXT . ') {
437
                ' . ReservedKeywords::FAILURE_VARIABLE . ' = array();
438
                ' . ReservedKeywords::UNWRAPPED_FAILURE_VARIABLE . ' = array();';
439
440
        $conditionCounter = 0;
441
        $invariantIterator = $assertionLists->getIterator();
442 View Code Duplication
        for ($i = 0; $i < $invariantIterator->count(); $i++) {
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...
443
            // Create the inner loop for the different assertions
444
            if ($invariantIterator->current()->count() !== 0) {
445
                $assertionIterator = $invariantIterator->current()->getIterator();
446
447
                // collect all assertion code for assertions of this instance
448
                for ($j = 0; $j < $assertionIterator->count(); $j++) {
449
                    // Code to catch failed assertions
450
                    $code .= $assertionIterator->current()->toCode();
451
                    $assertionIterator->next();
452
                    $conditionCounter++;
453
                }
454
455
                // generate the check for assertions results
456
                if ($conditionCounter > 0) {
457
                    $code .= 'if (!empty(' . ReservedKeywords::FAILURE_VARIABLE . ') || !empty(' . ReservedKeywords::UNWRAPPED_FAILURE_VARIABLE . ')) {
458
                        ' . Placeholders::ENFORCEMENT . 'invariant' . Placeholders::PLACEHOLDER_CLOSE . '
459
                    }';
460
                }
461
            }
462
463
            // increment the outer loop
464
            $invariantIterator->next();
465
        }
466
467
        $code .= '}
468
            \AppserverIo\Doppelgaenger\ContractContext::close();
469
        }';
470
471
        return $code;
472
    }
473
}
474