Completed
Push — master ( 4f5c14...14db60 )
by Bernhard
16s
created

AssertionFactory::resolveUsedAssertionStructure()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 14
rs 8.8571
cc 5
eloc 7
nc 4
nop 1
1
<?php
2
3
/**
4
 * \AppserverIo\Doppelgaenger\Entities\Assertions\AssertionFactory
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\Entities\Assertions;
22
23
use AppserverIo\Doppelgaenger\Dictionaries\ReservedKeywords;
24
use AppserverIo\Doppelgaenger\Entities\Lists\AssertionList;
25
use AppserverIo\Doppelgaenger\Interfaces\StructureDefinitionInterface;
26
use AppserverIo\Psr\MetaobjectProtocol\Dbc\Assertions\AssertionInterface;
27
use AppserverIo\Psr\MetaobjectProtocol\Dbc\Annotations\Ensures;
28
use AppserverIo\Psr\MetaobjectProtocol\Dbc\Annotations\Invariant;
29
use AppserverIo\Psr\MetaobjectProtocol\Dbc\Annotations\Requires;
30
31
/**
32
 * This class will help instantiating the right assertion class for any given assertion array
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 AssertionFactory
41
{
42
43
    /**
44
     * All simple data types which are known but are aliased without an is_... function.
45
     *
46
     * @var string[] $scalarTypeMappings
47
     */
48
    protected $scalarTypeMappings = array(
49
        'boolean' => 'bool',
50
        'void' => 'null'
51
    );
52
53
    /**
54
     * All simple data types which are supported by PHP
55
     * and have a is_... function.
56
     *
57
     * @var string[] $validScalarTypes
58
     */
59
    protected $validScalarTypes = array(
60
        'array',
61
        'bool',
62
        'callable',
63
        'double',
64
        'float',
65
        'int',
66
        'integer',
67
        'long',
68
        'null',
69
        'numeric',
70
        'object',
71
        'real',
72
        'resource',
73
        'scalar',
74
        'string',
75
        'boolean',
76
        'void'
77
    );
78
79
    /**
80
     * The definition of the structure we are currently iterating through
81
     *
82
     * @var StructureDefinitionInterface $currentDefinition
83
     */
84
    protected $currentDefinition;
85
86
    /**
87
     * Parse assertions which are a collection of others
88
     *
89
     * @param string    $connective How are they combined? E.g. "||"
90
     * @param \stdClass $annotation The annotation to create chained assertions from
91
     *
92
     * @return \AppserverIo\Doppelgaenger\Entities\Assertions\ChainedAssertion
93
     */
94
    protected function createChainedAssertion($connective, \stdClass $annotation)
95
    {
96
        // Get all the parts of the string
97
        $assertionArray = explode(' ', $annotation->values['typeHint']);
98
99
        // Check all string parts for the | character
100
        $combinedPart = '';
101
        $combinedIndex = 0;
102
        foreach ($assertionArray as $key => $assertionPart) {
103
            // Check which part contains the | but does not only consist of it
104
            if ($this->filterOrCombinator($assertionPart) && trim($assertionPart) !== $connective) {
105
                $combinedPart = trim($assertionPart);
106
                $combinedIndex = $key;
107
                break;
108
            }
109
        }
110
111
112
        // Now we have to create all the separate assertions for each part of the $combinedPart string
113
        $assertionList = new AssertionList();
114
        foreach (explode($connective, $combinedPart) as $partString) {
115
            // Rebuild the assertion string with one partial string of the combined part
116
            $tmp = $assertionArray;
117
            $tmp[$combinedIndex] = $partString;
118
            $annotation->values['typeHint'] = $partString;
119
            $assertion = $this->getInstance($annotation);
120
121
            if (is_bool($assertion)) {
122
                continue;
123
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
124
            } else {
125
                $assertionList->add($assertion);
126
            }
127
        }
128
129
        // We got everything. Create a ChainedAssertion instance
130
        return new ChainedAssertion($assertionList, '||');
131
    }
132
133
    /**
134
     * Will parse assertions from a DocBlock comment piece. If $usedAnnotation is given we will concentrate on that
135
     * type of assertion only.
136
     * We might return false on error
137
     *
138
     * @param \stdClass $annotation The annotation to create simple assertions from
139
     *
140
     * @return boolean|\AppserverIo\Psr\MetaobjectProtocol\Dbc\Assertions\AssertionInterface
141
     *
142
     * @throws \AppserverIo\Doppelgaenger\Exceptions\ParserException
143
     */
144
    protected function createSimpleAssertion(\stdClass $annotation)
145
    {
146
        // Do we have an or connective aka "|"?
147
        if ($this->filterOrCombinator($annotation->values['typeHint'])) {
148
            // If we got invalid arguments then we will fail
149
            try {
150
                return $this->createChainedAssertion('|', $annotation);
151
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
152
            } catch (\InvalidArgumentException $e) {
153
                return false;
154
            }
155
        }
156
157
        // check what we have got
158
        $variable = $annotation->values['operand'];
159
        $type = $this->filterScalarType($annotation->values['typeHint']);
160
        $class = $this->filterType($annotation->values['typeHint']);
161
        $collectionType = $this->filterTypedCollection($annotation->values['typeHint']);
162
163
        // "mixed" is something we cannot work with, special case is a mixed typed collection which basically is an array
164
        if ($type === 'mixed' || $class === 'mixed') {
165
            return false;
166
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
167
        } elseif ($collectionType === 'mixed') {
168
            // we have a collection with mixed content, so basically an array
169
            $type = 'array';
170
            $collectionType = false;
171
        }
172
173
        if ($annotation->name === 'return') {
174
            $variable = ReservedKeywords::RESULT;
175
        }
176
177
        // Now we have to check what we got
178
        // First of all handle if we got a simple type
179
        if ($type !== false && !empty($type)) {
180
            return new TypeAssertion($variable, $type);
181
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
182
        } elseif ($class !== false && !empty($class)) {
183
            // seems we have an instance assertion here
184
185
            return new InstanceAssertion($variable, $class);
186
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
187
        } elseif ($collectionType !== false && !empty($collectionType)) {
188
            // seems we have a typed collection here
189
190
            return new TypedCollectionAssertion($variable, $collectionType);
191
        } else {
192
            return false;
193
        }
194
    }
195
196
    /**
197
     * Will filter for any referenced structure as a indicated type hinting of complex types
198
     *
199
     * @param string $string The string potentially containing a structure name
200
     *
201
     * @return boolean
202
     */
203
    protected function filterType($string)
204
    {
205
        // if the string is neither a scalar type, a typed collection and contains a namespace separator we assume a class
206
207
        // check if we know the simple type
208
        $validScalarTypes = array_flip($this->validScalarTypes);
209
        if (isset($validScalarTypes[strtolower($string)])) {
210
            return false;
211
        }
212
213
        // check if it might be a typed collection
214
        foreach (array('<', '>', '[', ']') as $needle) {
215
            if (strpos($string, $needle) !== false) {
216
                return false;
217
            }
218
        }
219
220
        // check if we have a namespace separator
221
        if (strpos($string, '\\') !== false) {
222
            return $string;
223
        }
224
225
        // check if we start with upper case and do only contain valid characters
226
        if (ctype_upper($string[0]) && preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $string)) {
227
            return $string;
228
        }
229
230
        // We found nothing; tell them.
231
        return false;
232
    }
233
234
    /**
235
     * Will filter any combinator defining a logical or-relation
236
     *
237
     * @param string $docString The DocBlock piece to search in
238
     *
239
     * @return boolean
240
     */
241
    protected function filterOrCombinator($docString)
242
    {
243
        if (strpos($docString, '|')) {
244
            return true;
245
        }
246
247
        // We found nothing; tell them.
248
        return false;
249
    }
250
251
    /**
252
     * Will filter for any simple type that may be used to indicate type hinting
253
     *
254
     * @param string $string The string potentially containing a scalar type hint
255
     *
256
     * @return boolean|string
257
     */
258
    protected function filterScalarType($string)
259
    {
260
        // check if we know the simple type
261
        $validScalarTypes = array_flip($this->validScalarTypes);
262
        if (!isset($validScalarTypes[$string])) {
263
            return false;
264
        }
265
266
        // if we have a mapping we have to return the mapped value instead
267
        if (isset($this->scalarTypeMappings[$string])) {
268
            $string = $this->scalarTypeMappings[$string];
269
        }
270
271
        // is it a scalar type we can check for?
272
        if (function_exists('is_' . $string)) {
273
            return $string;
274
        }
275
276
        // We found nothing; tell them.
277
        return false;
278
    }
279
280
    /**
281
     * Will filter for type safe collections of the form array<Type> or Type[]
282
     *
283
     * @param string $string The string potentially containing a type hint for a typed collection
284
     *
285
     * @return boolean|string
286
     */
287
    protected function filterTypedCollection($string)
288
    {
289
        $tmp = strpos($string, 'array<');
290
        if ($tmp !== false) {
291
            // we have a Java Generics like syntax
292
293
            if (strpos($string, '>') > $tmp) {
294
                $stringPiece = explode('array<', $string);
295
                $stringPiece = $stringPiece[1];
296
297
                return strstr($stringPiece, '>', true);
298
            }
299
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
300
        } elseif (strpos($string, '[]')) {
301
            // we have a common <TYPE>[] syntax
302
303
            return strstr($string, '[]', true);
304
        }
305
306
        // We found nothing; tell them.
307
        return false;
308
    }
309
310
    /**
311
     * Will return an instance of an assertion fitting the passed annotation object
312
     *
313
     * @param \stdClass $annotation Annotation object to generate assertion from
314
     *
315
     * @return \AppserverIo\Psr\MetaobjectProtocol\Dbc\Assertions\AssertionInterface
316
     * @throws \Exception
317
     */
318
    public function getInstance(\stdClass $annotation)
319
    {
320
        switch ($annotation->name) {
321
            case Ensures::ANNOTATION:
322
            case Invariant::ANNOTATION:
323
            case Requires::ANNOTATION:
324
                // complex annotations leave us with two possibilities: raw or custom assertions
325
                if (isset($annotation->values['type'], $annotation->values['constraint'])) {
326
                    // we need a custom assertion here
327
                    return $this->createAssertion($annotation->values['type'], $annotation->values['constraint']);
328
                }
329
                // RawAssertion is sufficient
330
                return new RawAssertion(array_pop($annotation->values));
331
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
332
            case 'param':
333
            case 'return':
334
                // simple assertions leave with a wide range of type assertions
335
                return $this->createSimpleAssertion($annotation);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression $this->createSimpleAssertion($annotation); of type AppserverIo\Doppelgaenge...ypedCollectionAssertion adds false to the return on line 335 which is incompatible with the return type documented by AppserverIo\Doppelgaenge...ionFactory::getInstance of type AppserverIo\Psr\Metaobje...AssertionInterface|null. It seems like you forgot to handle an error condition.
Loading history...
336
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
337
            default:
338
                break;
339
        }
340
    }
341
342
    /**
343
     * Tries to create assertion of $assertionType
344
     *
345
     * @param string $assertionType the assertion type
346
     * @param string $constraint    the constraint to validate
347
     *
348
     * @return null|AssertionInterface
349
     * @throws \Exception
350
     */
351
    protected function createAssertion($assertionType, $constraint)
352
    {
353
        $assertionClassPath = $this->getAssertionClassPath($assertionType);
354
        if (null === $assertionClassPath) {
355
            throw new \Exception(
356
                sprintf(
357
                    'Cannot create complex assertion of type %s',
358
                    $assertionType
359
                )
360
            );
361
        }
362
363
        $potentialAssertion = new $assertionClassPath($constraint);
364
        if (!$potentialAssertion instanceof AssertionInterface) {
365
            throw new \Exception(
366
                sprintf(
367
                    'Specified assertion of type %s does not implement %s',
368
                    $potentialAssertion,
369
                    AssertionInterface::class
370
                )
371
            );
372
        }
373
374
        return $potentialAssertion;
375
    }
376
377
    /**
378
     * Resolves and returns the fully qualified namespace of $assertionType
379
     * or null if $assertionType cannot be resolved to an accessible class
380
     *
381
     * @param string $assertionType the assertion type
382
     *
383
     * @return null|string
384
     */
385
    protected function getAssertionClassPath($assertionType)
386
    {
387
        if (class_exists($assertionType)) {
388
            return $assertionType;
389
        }
390
391
        $potentialAssertion = $this->resolveUsedAssertionStructure($assertionType);
392
        if (class_exists($potentialAssertion)) {
393
            return $potentialAssertion;
394
        }
395
396
        $potentialAssertion = '\AppserverIo\Doppelgaenger\Entities\Assertions\\' . $assertionType;
397
        if (class_exists($potentialAssertion)) {
398
            return $potentialAssertion;
399
        }
400
401
        $potentialAssertion .= 'Assertion';
402
        if (class_exists($potentialAssertion)) {
403
            return $potentialAssertion;
404
        }
405
406
        return null;
407
    }
408
409
    /**
410
     * Iterates through the 'use' operators of the current structure and
411
     * returns the fully qualified namespace to the Assertion or null if none is found
412
     *
413
     * @param string $assertionType the assertion type
414
     *
415
     * @return null|string
416
     */
417
    protected function resolveUsedAssertionStructure($assertionType)
418
    {
419
        if (!$this->getCurrentDefinition()) {
420
            return null;
421
        }
422
423
        foreach ($this->getCurrentDefinition()->getUsedStructures() as $structure) {
424
            if (preg_match('/\w+$/', $structure, $matches) && $matches[0] === $assertionType) {
425
                return $structure;
426
            }
427
        }
428
429
        return null;
430
    }
431
432
    /**
433
     * Getter for all valid scalar types we can create assertions for
434
     *
435
     * @return string[]
436
     */
437
    public function getValidScalarTypes()
438
    {
439
        return $this->validScalarTypes;
440
    }
441
442
    /**
443
     * Sets the instance of the current definition
444
     *
445
     * @param StructureDefinitionInterface $currentDefinition the definition of the current structure
446
     *
447
     * @return void
448
     */
449
    public function setCurrentDefinition(StructureDefinitionInterface $currentDefinition)
450
    {
451
        $this->currentDefinition = $currentDefinition;
452
    }
453
454
    /**
455
     * Returns the instance of the current definition
456
     *
457
     * @return null|StructureDefinitionInterface
458
     */
459
    public function getCurrentDefinition()
460
    {
461
        return $this->currentDefinition;
462
    }
463
}
464