Completed
Pull Request — master (#42)
by
unknown
02:49
created

AssertionFactory::getAssertionClassPath()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 23
rs 8.5906
cc 5
eloc 13
nc 5
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
     * @var StructureDefinitionInterface
81
     */
82
    protected $currentDefinition;
83
84
    /**
85
     * Parse assertions which are a collection of others
86
     *
87
     * @param string    $connective How are they combined? E.g. "||"
88
     * @param \stdClass $annotation The annotation to create chained assertions from
89
     *
90
     * @return \AppserverIo\Doppelgaenger\Entities\Assertions\ChainedAssertion
91
     */
92
    protected function createChainedAssertion($connective, \stdClass $annotation)
93
    {
94
        // Get all the parts of the string
95
        $assertionArray = explode(' ', $annotation->values['typeHint']);
96
97
        // Check all string parts for the | character
98
        $combinedPart = '';
99
        $combinedIndex = 0;
100
        foreach ($assertionArray as $key => $assertionPart) {
101
            // Check which part contains the | but does not only consist of it
102
            if ($this->filterOrCombinator($assertionPart) && trim($assertionPart) !== $connective) {
103
                $combinedPart = trim($assertionPart);
104
                $combinedIndex = $key;
105
                break;
106
            }
107
        }
108
109
110
        // Now we have to create all the separate assertions for each part of the $combinedPart string
111
        $assertionList = new AssertionList();
112
        foreach (explode($connective, $combinedPart) as $partString) {
113
            // Rebuild the assertion string with one partial string of the combined part
114
            $tmp = $assertionArray;
115
            $tmp[$combinedIndex] = $partString;
116
            $annotation->values['typeHint'] = $partString;
117
            $assertion = $this->getInstance($annotation);
118
119
            if (is_bool($assertion)) {
120
                continue;
121
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
122
            } else {
123
                $assertionList->add($assertion);
124
            }
125
        }
126
127
        // We got everything. Create a ChainedAssertion instance
128
        return new ChainedAssertion($assertionList, '||');
129
    }
130
131
    /**
132
     * Will parse assertions from a DocBlock comment piece. If $usedAnnotation is given we will concentrate on that
133
     * type of assertion only.
134
     * We might return false on error
135
     *
136
     * @param \stdClass $annotation The annotation to create simple assertions from
137
     *
138
     * @return boolean|\AppserverIo\Psr\MetaobjectProtocol\Dbc\Assertions\AssertionInterface
139
     *
140
     * @throws \AppserverIo\Doppelgaenger\Exceptions\ParserException
141
     */
142
    protected function createSimpleAssertion(\stdClass $annotation)
143
    {
144
        // Do we have an or connective aka "|"?
145
        if ($this->filterOrCombinator($annotation->values['typeHint'])) {
146
            // If we got invalid arguments then we will fail
147
            try {
148
                return $this->createChainedAssertion('|', $annotation);
149
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
150
            } catch (\InvalidArgumentException $e) {
151
                return false;
152
            }
153
        }
154
155
        // check what we have got
156
        $variable = $annotation->values['operand'];
157
        $type = $this->filterScalarType($annotation->values['typeHint']);
158
        $class = $this->filterType($annotation->values['typeHint']);
159
        $collectionType = $this->filterTypedCollection($annotation->values['typeHint']);
160
161
        // "mixed" is something we cannot work with, special case is a mixed typed collection which basically is an array
162
        if ($type === 'mixed' || $class === 'mixed') {
163
            return false;
164
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
165
        } elseif ($collectionType === 'mixed') {
166
            // we have a collection with mixed content, so basically an array
167
            $type = 'array';
168
            $collectionType = false;
169
        }
170
171
        if ($annotation->name === 'return') {
172
            $variable = ReservedKeywords::RESULT;
173
        }
174
175
        // Now we have to check what we got
176
        // First of all handle if we got a simple type
177
        if ($type !== false && !empty($type)) {
178
            return new TypeAssertion($variable, $type);
179
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
180
        } elseif ($class !== false && !empty($class)) {
181
            // seems we have an instance assertion here
182
183
            return new InstanceAssertion($variable, $class);
184
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
185
        } elseif ($collectionType !== false && !empty($collectionType)) {
186
            // seems we have a typed collection here
187
188
            return new TypedCollectionAssertion($variable, $collectionType);
189
        } else {
190
            return false;
191
        }
192
    }
193
194
    /**
195
     * Will filter for any referenced structure as a indicated type hinting of complex types
196
     *
197
     * @param string $string The string potentially containing a structure name
198
     *
199
     * @return boolean
200
     */
201
    protected function filterType($string)
202
    {
203
        // if the string is neither a scalar type, a typed collection and contains a namespace separator we assume a class
204
205
        // check if we know the simple type
206
        $validScalarTypes = array_flip($this->validScalarTypes);
207
        if (isset($validScalarTypes[strtolower($string)])) {
208
            return false;
209
        }
210
211
        // check if it might be a typed collection
212
        foreach (array('<', '>', '[', ']') as $needle) {
213
            if (strpos($string, $needle) !== false) {
214
                return false;
215
            }
216
        }
217
218
        // check if we have a namespace separator
219
        if (strpos($string, '\\') !== false) {
220
            return $string;
221
        }
222
223
        // check if we start with upper case and do only contain valid characters
224
        if (ctype_upper($string[0]) && preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $string)) {
225
            return $string;
226
        }
227
228
        // We found nothing; tell them.
229
        return false;
230
    }
231
232
    /**
233
     * Will filter any combinator defining a logical or-relation
234
     *
235
     * @param string $docString The DocBlock piece to search in
236
     *
237
     * @return boolean
238
     */
239
    protected function filterOrCombinator($docString)
240
    {
241
        if (strpos($docString, '|')) {
242
            return true;
243
        }
244
245
        // We found nothing; tell them.
246
        return false;
247
    }
248
249
    /**
250
     * Will filter for any simple type that may be used to indicate type hinting
251
     *
252
     * @param string $string The string potentially containing a scalar type hint
253
     *
254
     * @return boolean|string
255
     */
256
    protected function filterScalarType($string)
257
    {
258
        // check if we know the simple type
259
        $validScalarTypes = array_flip($this->validScalarTypes);
260
        if (!isset($validScalarTypes[$string])) {
261
            return false;
262
        }
263
264
        // if we have a mapping we have to return the mapped value instead
265
        if (isset($this->scalarTypeMappings[$string])) {
266
            $string = $this->scalarTypeMappings[$string];
267
        }
268
269
        // is it a scalar type we can check for?
270
        if (function_exists('is_' . $string)) {
271
            return $string;
272
        }
273
274
        // We found nothing; tell them.
275
        return false;
276
    }
277
278
    /**
279
     * Will filter for type safe collections of the form array<Type> or Type[]
280
     *
281
     * @param string $string The string potentially containing a type hint for a typed collection
282
     *
283
     * @return boolean|string
284
     */
285
    protected function filterTypedCollection($string)
286
    {
287
        $tmp = strpos($string, 'array<');
288
        if ($tmp !== false) {
289
            // we have a Java Generics like syntax
290
291
            if (strpos($string, '>') > $tmp) {
292
                $stringPiece = explode('array<', $string);
293
                $stringPiece = $stringPiece[1];
294
295
                return strstr($stringPiece, '>', true);
296
            }
297
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
298
        } elseif (strpos($string, '[]')) {
299
            // we have a common <TYPE>[] syntax
300
301
            return strstr($string, '[]', true);
302
        }
303
304
        // We found nothing; tell them.
305
        return false;
306
    }
307
308
    /**
309
     * Will return an instance of an assertion fitting the passed annotation object
310
     *
311
     * @param \stdClass $annotation Annotation object to generate assertion from
312
     *
313
     * @return \AppserverIo\Psr\MetaobjectProtocol\Dbc\Assertions\AssertionInterface
314
     * @throws \Exception
315
     */
316
    public function getInstance(\stdClass $annotation)
317
    {
318
        switch ($annotation->name) {
319
            case Ensures::ANNOTATION:
320
            case Invariant::ANNOTATION:
321
            case Requires::ANNOTATION:
322
                // complex annotations leave us with two possibilities: raw or custom assertions
323
                if (isset($annotation->values['type'], $annotation->values['constraint'])) {
324
                    // we need a custom assertion here
325
                    return $this->createAssertion($annotation->values['type'], $annotation->values['constraint']);
326
                }
327
                // RawAssertion is sufficient
328
                return new RawAssertion(array_pop($annotation->values));
329
            case 'param':
330
            case 'return':
331
                // simple assertions leave with a wide range of type assertions
332
                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 332 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...
333
            default:
334
                break;
335
        }
336
    }
337
338
    /**
339
     * Tries to create assertion of $assertionType
340
     * @param string $assertionType the assertion type
341
     * @param string $constraint    the constraint to validate
342
     * @return null|AssertionInterface
343
     * @throws \Exception
344
     */
345
    public function createAssertion($assertionType, $constraint)
346
    {
347
        if (null === ($assertionClassPath = $this->getAssertionClassPath($assertionType))) {
348
            throw new \Exception(
349
                sprintf(
350
                    'Cannot create complex assertion of type %s',
351
                    $assertionType
352
                )
353
            );
354
        }
355
356
        $potentialAssertion = new $assertionClassPath($constraint);
357
        if (!$potentialAssertion instanceof AssertionInterface) {
358
            throw new \Exception(
359
                sprintf(
360
                    'Specified assertion of type %s does not implement %s',
361
                    $potentialAssertion,
362
                    AssertionInterface::class
363
                )
364
            );
365
        }
366
367
        return $potentialAssertion;
368
    }
369
370
    /**
371
     * Resolves and returns the fully qualified namespace of $assertionType
372
     * or null if $assertionType cannot be resolved to an accessible class
373
     * @param string $assertionType the assertion type
374
     * @return null|string
375
     */
376
    private function getAssertionClassPath($assertionType)
377
    {
378
        if (class_exists($assertionType)) {
379
            return $assertionType;
380
        }
381
382
        $potentialAssertion = $this->resolveUsedAssertionStructure($assertionType);
383
        if (class_exists($potentialAssertion)) {
384
            return $potentialAssertion;
385
        }
386
387
        $potentialAssertion = '\AppserverIo\Doppelgaenger\Entities\Assertions\\' . $assertionType;
388
        if (class_exists($potentialAssertion)) {
389
            return $potentialAssertion;
390
        }
391
392
        $potentialAssertion .= 'Assertion';
393
        if (class_exists($potentialAssertion)) {
394
            return $potentialAssertion;
395
        }
396
397
        return null;
398
    }
399
400
    /**
401
     * Iterates through the 'use' operators of the current structure and
402
     * returns the fully qualified namespace to the Assertion or null if none is found
403
     * @param string $assertionType the assertion type
404
     * @return null|string
405
     */
406
    private function resolveUsedAssertionStructure($assertionType)
407
    {
408
        if (!$this->getCurrentDefinition()) {
409
            return null;
410
        }
411
412
        foreach ($this->getCurrentDefinition()->getUsedStructures() as $structure) {
413
            if (preg_match('/\w+$/', $structure, $matches) && $matches[0] === $assertionType) {
414
                return $structure;
415
            }
416
        }
417
418
        return null;
419
    }
420
421
    /**
422
     * Getter for all valid scalar types we can create assertions for
423
     *
424
     * @return string[]
425
     */
426
    public function getValidScalarTypes()
427
    {
428
        return $this->validScalarTypes;
429
    }
430
431
    /**
432
     * @param StructureDefinitionInterface $currentDefinition the definition of the current structure
433
     * @return void
434
     */
435
    public function setCurrentDefinition(StructureDefinitionInterface $currentDefinition)
436
    {
437
        $this->currentDefinition = $currentDefinition;
438
    }
439
440
    /**
441
     * @return null|StructureDefinitionInterface
442
     */
443
    public function getCurrentDefinition()
444
    {
445
        return $this->currentDefinition;
446
    }
447
}
448