Completed
Push — master ( 00527f...28ac1a )
by Nate
02:23
created

TypeToken::convertArrayToGeneric()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 15
ccs 9
cts 9
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 9
nc 2
nop 1
crap 2
1
<?php
2
/*
3
 * Copyright (c) Nate Brunette.
4
 * Distributed under the MIT License (http://opensource.org/licenses/MIT)
5
 */
6
7
declare(strict_types=1);
8
9
namespace Tebru\PhpType;
10
11
use stdClass;
12
use Tebru\PhpType\Exception\MalformedTypeException;
13
14
/**
15
 * Class TypeToken
16
 *
17
 * Wrapper around core php types and custom types.  It can be used as simply as
18
 *
19
 *     new TypeToken('string');
20
 *
21
 * To create a string type.
22
 *
23
 * This class also allows us to fake generic types.  The syntax to
24
 * represent generics uses angle brackets <>.
25
 *
26
 * For example:
27
 *
28
 *     array<int>
29
 *
30
 * Would represent an array of ints.
31
 *
32
 *     array<string, int>
33
 *
34
 * Would represent an array using string keys and int values.
35
 *
36
 * They can be combined, like so
37
 *
38
 *     array<string, array<int>>
39
 *
40
 * To represent a array with string keys and an array of ints as values.
41
 *
42
 * @author Nate Brunette <[email protected]>
43
 */
44
final class TypeToken
45
{
46
    public const STRING = 'string';
47
    public const INTEGER = 'integer';
48
    public const FLOAT = 'float';
49
    public const BOOLEAN = 'boolean';
50
    public const HASH = 'array';
51
    public const OBJECT = 'object';
52
    public const NULL = 'null';
53
    public const RESOURCE = 'resource';
54
    public const WILDCARD = '?';
55
56
    /**
57
     * The full initial type
58
     *
59
     * @var string
60
     */
61
    private $fullTypeString;
62
63
    /**
64
     * The core php type (string, int, etc) or class if object
65
     *
66
     * @var string
67
     */
68
    private $rawType;
69
70
    /**
71
     * The core php type (string, int, object, etc)
72
     *
73
     * @var string
74
     */
75
    private $phpType;
76
77
    /**
78
     * An array of parent classes and interfaces that a class implements
79
     *
80
     * @var array
81
     */
82
    private $parents = [];
83
84
    /**
85
     * Generic types, if they exist
86
     *
87
     * @var array
88
     */
89
    private $genericTypes = [];
90
91
    /**
92
     * Constructor
93
     *
94
     * @param string $type
95
     */
96 42
    public function __construct(string $type)
97
    {
98 42
        $type = trim($type);
99 42
        $this->fullTypeString = $type;
100 42
        $this->parseType($type);
101 41
    }
102
103
    /**
104
     * Create a new instance from a variable
105
     *
106
     * @param mixed $variable
107
     * @return TypeToken
108
     */
109 8
    public static function createFromVariable($variable): TypeToken
110
    {
111 8
        return \is_object($variable) ? new self(\get_class($variable)) : new self(\gettype($variable));
112
    }
113
114
    /**
115
     * Returns the class if an object, or the type as a string
116
     *
117
     * @return string
118
     */
119 8
    public function getRawType(): string
120
    {
121 8
        return $this->rawType;
122
    }
123
124
    /**
125
     * Returns the core php type
126
     *
127
     * @return string
128
     */
129 2
    public function getPhpType(): string
130
    {
131 2
        return $this->phpType;
132
    }
133
134
    /**
135
     * Returns an array of generic types
136
     *
137
     * @return TypeToken[]
138
     */
139 34
    public function getGenerics(): array
140
    {
141 34
        return $this->genericTypes;
142
    }
143
144
    /**
145
     * Returns true if the type matches the class, parent, full type, or one of the interfaces
146
     *
147
     * @param string $type
148
     * @return bool
149
     */
150 4
    public function isA(string $type): bool
151
    {
152 4
        if ($this->rawType === $type) {
153 4
            return true;
154
        }
155
156 2
        if ($this->fullTypeString === $type) {
157 1
            return true;
158
        }
159
160 1
        if (\in_array($type, $this->parents, true)) {
161 1
            return true;
162
        }
163
164 1
        return false;
165
    }
166
167
    /**
168
     * Returns true if this is a string
169
     *
170
     * @return bool
171
     */
172 2
    public function isString(): bool
173
    {
174 2
        return $this->phpType === self::STRING;
175
    }
176
177
    /**
178
     * Returns true if this is an integer
179
     *
180
     * @return bool
181
     */
182 3
    public function isInteger(): bool
183
    {
184 3
        return $this->phpType === self::INTEGER;
185
    }
186
187
    /**
188
     * Returns true if this is a float
189
     *
190
     * @return bool
191
     */
192 2
    public function isFloat(): bool
193
    {
194 2
        return $this->phpType === self::FLOAT;
195
    }
196
197
    /**
198
     * Returns true if this is a boolean
199
     *
200
     * @return bool
201
     */
202 2
    public function isBoolean(): bool
203
    {
204 2
        return $this->phpType === self::BOOLEAN;
205
    }
206
207
    /**
208
     * Returns true if this is an array
209
     *
210
     * @return bool
211
     */
212 6
    public function isArray(): bool
213
    {
214 6
        return $this->phpType === self::HASH;
215
    }
216
217
    /**
218
     * Returns true if this is an object
219
     *
220
     * @return bool
221
     */
222 41
    public function isObject(): bool
223
    {
224 41
        return $this->phpType === self::OBJECT;
225
    }
226
227
    /**
228
     * Returns true if this is null
229
     *
230
     * @return bool
231
     */
232 2
    public function isNull(): bool
233
    {
234 2
        return $this->phpType === self::NULL;
235
    }
236
237
    /**
238
     * Returns true if this is a resource
239
     *
240
     * @return bool
241
     */
242 1
    public function isResource(): bool
243
    {
244 1
        return $this->phpType === self::RESOURCE;
245
    }
246
247
    /**
248
     * Returns true if the type could be anything
249
     *
250
     * @return bool
251
     */
252 1
    public function isWildcard(): bool
253
    {
254 1
        return $this->phpType === self::WILDCARD;
255
    }
256
257
    /**
258
     * Return the initial type including generics
259
     *
260
     * @return string
261
     */
262 17
    public function __toString(): string
263
    {
264 17
        return $this->fullTypeString;
265
    }
266
267
    /**
268
     * Recursively parse type.  If generics are found, this will create
269
     * new PhpTypes.
270
     *
271
     * @param string $type
272
     * @return void
273
     * @throws \Tebru\PhpType\Exception\MalformedTypeException If the type cannot be processed
274
     */
275 42
    private function parseType(string $type): void
276
    {
277 42
        $genericStart = strpos($type, '<');
278 42
        $isArrayGeneric = strpos($type, '[]');
279
280 42
        if ($genericStart === false && $isArrayGeneric === false) {
281 41
            $this->setTypes($type);
282 41
            return;
283
        }
284
285 10
        if ($isArrayGeneric) {
286 2
            $type = $this->convertArrayToGeneric($type);
287 2
            $this->fullTypeString = $type;
288 2
            $genericStart = strpos($type, '<');
289
        }
290
291
        // get start and end positions of generic
292 10
        $end = strrpos($type, '>');
293 10
        if ($end === false) {
294 1
            throw new MalformedTypeException('Could not find ending ">" for generic type');
295
        }
296
297 9
        $originalType = $type;
298
299
        // get generic types
300 9
        $generics = substr($type, $genericStart + 1, $end - $genericStart - 1);
301
302
        // iterate over subtype to determine if format is <type> or <key, type>
303 9
        $depth = 0;
304 9
        $type = '';
305 9
        foreach (str_split($generics) as $char) {
306
            // stepping into another generic type
307 9
            if ($char === '<') {
308 3
                $depth++;
309
            }
310
311
            // stepping out of generic type
312 9
            if ($char === '>') {
313 3
                $depth--;
314
            }
315
316
            // if the character is not a comma, or we're not on the first level
317
            // write the character to the buffer and continue loop
318 9
            if ($char !== ',' || $depth !== 0) {
319 9
                $type .= $char;
320 9
                continue;
321
            }
322
323
            // add new type to list
324 4
            $this->genericTypes[] = new TypeToken($type);
325
326
            // reset type buffer
327 4
            $type = '';
328
        }
329
330
        // add final type
331 9
        $this->genericTypes[] = new TypeToken($type);
332
333
        // set the main type
334 9
        $this->setTypes(substr($originalType, 0, $genericStart));
335 9
    }
336
337
    /**
338
     * Create a type enum and set the class if necessary
339
     *
340
     * @param string $rawType
341
     * @return void
342
     */
343 41
    private function setTypes(string $rawType): void
344
    {
345 41
        $this->phpType = $this->getNormalizedType($rawType);
346
347
        // if we're not working with an object, set the raw type to
348
        // the core php type so we can make sure it's normalized
349 41
        if (!$this->isObject()) {
350 34
            $this->rawType = $this->phpType;
351
352
            // if there aren't any generics, overwrite full type as well
353 34
            if ($this->getGenerics() === []) {
354 34
                $this->fullTypeString = $this->rawType;
355
            }
356 34
            return;
357
        }
358
359
        // use \stdClass as the class name for generic objects
360 10
        $this->rawType = self::OBJECT === $rawType ? stdClass::class : $rawType;
361
362
        // if we're dealing with a real class, get parents and interfaces so
363
        // it's easy to check if the type is an instance of another
364 10
        if (class_exists($rawType)) {
365 9
            $this->parents = array_merge(class_parents($this->rawType), class_implements($this->rawType));
366
        }
367 10
    }
368
369
    /**
370
     * Get a normalized core php type
371
     *
372
     * @param string $type
373
     * @return string
374
     */
375 41
    private function getNormalizedType(string $type): string
376
    {
377
        switch ($type) {
378 41
            case 'string':
379 9
                return self::STRING;
380 37
            case 'int':
381 34
            case 'integer':
382 11
                return self::INTEGER;
383 32
            case 'double':
384 30
            case 'float':
385 3
                return self::FLOAT;
386 29
            case 'bool':
387 28
            case 'boolean':
388 6
                return self::BOOLEAN;
389 25
            case 'array':
390 12
                return self::HASH;
391 15
            case 'null':
392 14
            case 'NULL':
393 3
                return self::NULL;
394 12
            case 'resource':
395 1
                return self::RESOURCE;
396 11
            case '?':
397 1
                return self::WILDCARD;
398
            default:
399 10
                return self::OBJECT;
400
        }
401
    }
402
403
    /**
404
     * Take an array documented like string[] and convert to array<string>
405
     *
406
     * @param $type
407
     * @return string
408
     */
409 2
    private function convertArrayToGeneric($type): string
410
    {
411 2
        $parts = explode('[]', $type);
412 2
        $arrayType = array_shift($parts);
413
414 2
        $type = '';
415 2
        $arrayEnd = '';
416
417 2
        foreach ($parts as $part) {
418 2
            $type .= 'array<';
419 2
            $arrayEnd .= '>';
420
        }
421
422 2
        return $type.$arrayType.$arrayEnd;
423
    }
424
}
425