Passed
Push — master ( 2256b9...0d7121 )
by Nate
01:51
created

TypeToken::createFromVariable()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 2
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
     * An array of cached types
93
     *
94
     * @var TypeToken[]
95
     */
96
    private static $types = [];
97
98
    /**
99
     * Constructor
100
     *
101
     * @param string $type
102
     */
103 38
    public function __construct(string $type)
104
    {
105 38
        $type = \trim($type);
106 38
        $this->fullTypeString = $type;
107 38
        $this->parseType($type);
108 37
    }
109
110
    /**
111
     * Singleton factory for creating types
112
     *
113
     * @param string $type
114
     * @return TypeToken
115
     */
116 16
    public static function create(string $type): TypeToken
117
    {
118 16
        if (!isset(self::$types[$type])) {
119 10
            self::$types[$type] = new static($type);
120
        }
121
122 16
        return self::$types[$type];
123
    }
124
125
    /**
126
     * Create a new instance from a variable
127
     *
128
     * @param mixed $variable
129
     * @return TypeToken
130
     */
131 9
    public static function createFromVariable($variable): TypeToken
132
    {
133 9
        $type = \is_object($variable) ? \get_class($variable) : \gettype($variable);
134
135 9
        return self::create($type);
136
    }
137
138
    /**
139
     * Returns the class if an object, or the type as a string
140
     *
141
     * @return string
142
     */
143 8
    public function getRawType(): string
144
    {
145 8
        return $this->rawType;
146
    }
147
148
    /**
149
     * Returns the core php type
150
     *
151
     * @return string
152
     */
153 2
    public function getPhpType(): string
154
    {
155 2
        return $this->phpType;
156
    }
157
158
    /**
159
     * Returns an array of generic types
160
     *
161
     * @return TypeToken[]
162
     */
163 30
    public function getGenerics(): array
164
    {
165 30
        return $this->genericTypes;
166
    }
167
168
    /**
169
     * Returns true if the type matches the class, parent, full type, or one of the interfaces
170
     *
171
     * @param string $type
172
     * @return bool
173
     */
174 4
    public function isA(string $type): bool
175
    {
176 4
        if ($this->rawType === $type) {
177 4
            return true;
178
        }
179
180 2
        if ($this->fullTypeString === $type) {
181 1
            return true;
182
        }
183
184 1
        if (\in_array($type, $this->parents, true)) {
185 1
            return true;
186
        }
187
188 1
        return false;
189
    }
190
191
    /**
192
     * Returns true if this is a string
193
     *
194
     * @return bool
195
     */
196 1
    public function isString(): bool
197
    {
198 1
        return $this->phpType === self::STRING;
199
    }
200
201
    /**
202
     * Returns true if this is an integer
203
     *
204
     * @return bool
205
     */
206 2
    public function isInteger(): bool
207
    {
208 2
        return $this->phpType === self::INTEGER;
209
    }
210
211
    /**
212
     * Returns true if this is a float
213
     *
214
     * @return bool
215
     */
216 2
    public function isFloat(): bool
217
    {
218 2
        return $this->phpType === self::FLOAT;
219
    }
220
221
    /**
222
     * Returns true if this is a boolean
223
     *
224
     * @return bool
225
     */
226 2
    public function isBoolean(): bool
227
    {
228 2
        return $this->phpType === self::BOOLEAN;
229
    }
230
231
    /**
232
     * Returns true if this is an array
233
     *
234
     * @return bool
235
     */
236 4
    public function isArray(): bool
237
    {
238 4
        return $this->phpType === self::HASH;
239
    }
240
241
    /**
242
     * Returns true if this is an object
243
     *
244
     * @return bool
245
     */
246 37
    public function isObject(): bool
247
    {
248 37
        return $this->phpType === self::OBJECT;
249
    }
250
251
    /**
252
     * Returns true if this is null
253
     *
254
     * @return bool
255
     */
256 2
    public function isNull(): bool
257
    {
258 2
        return $this->phpType === self::NULL;
259
    }
260
261
    /**
262
     * Returns true if this is a resource
263
     *
264
     * @return bool
265
     */
266 1
    public function isResource(): bool
267
    {
268 1
        return $this->phpType === self::RESOURCE;
269
    }
270
271
    /**
272
     * Returns true if the type could be anything
273
     *
274
     * @return bool
275
     */
276 1
    public function isWildcard(): bool
277
    {
278 1
        return $this->phpType === self::WILDCARD;
279
    }
280
281
    /**
282
     * Return the initial type including generics
283
     *
284
     * @return string
285
     */
286 15
    public function __toString(): string
287
    {
288 15
        return $this->fullTypeString;
289
    }
290
291
    /**
292
     * Recursively parse type.  If generics are found, this will create
293
     * new PhpTypes.
294
     *
295
     * @param string $type
296
     * @return void
297
     * @throws \Tebru\PhpType\Exception\MalformedTypeException If the type cannot be processed
298
     */
299 38
    private function parseType(string $type): void
300
    {
301 38
        $start = \strpos($type, '<');
302 38
        if ($start === false) {
303 34
            $this->setTypes($type);
304 34
            return;
305
        }
306
307
        // get start and end positions of generic
308 8
        $end = \strrpos($type, '>');
309 8
        if ($end === false) {
310 1
            throw new MalformedTypeException('Could not find ending ">" for generic type');
311
        }
312
313 7
        $originalType = $type;
314
315
        // get generic types
316 7
        $generics = \substr($type, $start + 1, $end - $start - 1);
317
318
        // iterate over subtype to determine if format is <type> or <key, type>
319 7
        $depth = 0;
320 7
        $type = '';
321 7
        foreach (\str_split($generics) as $char) {
322
            // stepping into another generic type
323 7
            if ($char === '<') {
324 2
                $depth++;
325
            }
326
327
            // stepping out of generic type
328 7
            if ($char === '>') {
329 2
                $depth--;
330
            }
331
332
            // if the character is not a comma, or we're not on the first level
333
            // write the character to the buffer and continue loop
334 7
            if ($char !== ',' || $depth !== 0) {
335 7
                $type .= $char;
336 7
                continue;
337
            }
338
339
            // add new type to list
340 3
            $this->genericTypes[] = static::create($type);
341
342
            // reset type buffer
343 3
            $type = '';
344
        }
345
346
        // add final type
347 7
        $this->genericTypes[] = static::create($type);
348
349
        // set the main type
350 7
        $this->setTypes(\substr($originalType, 0, $start));
351 7
    }
352
353
    /**
354
     * Create a type enum and set the class if necessary
355
     *
356
     * @param string $rawType
357
     * @return void
358
     */
359 37
    private function setTypes(string $rawType): void
360
    {
361 37
        $this->phpType = $this->getNormalizedType($rawType);
362
363
        // if we're not working with an object, set the raw type to
364
        // the core php type so we can make sure it's normalized
365 37
        if (!$this->isObject()) {
366 29
            $this->rawType = $this->phpType;
367
368
            // if there aren't any generics, overwrite full type as well
369 29
            if ($this->getGenerics() === []) {
370 26
                $this->fullTypeString = $this->rawType;
371
            }
372 29
            return;
373
        }
374
375
        // use \stdClass as the class name for generic objects
376 9
        $this->rawType = self::OBJECT === $rawType ? stdClass::class : $rawType;
377
378
        // if we're dealing with a real class, get parents and interfaces so
379
        // it's easy to check if the type is an instance of another
380 9
        if (\class_exists($rawType)) {
381 8
            $this->parents = \array_merge(\class_parents($this->rawType), \class_implements($this->rawType));
382
        }
383 9
    }
384
385
    /**
386
     * Get a normalized core php type
387
     *
388
     * @param string $type
389
     * @return string
390
     */
391 37
    private function getNormalizedType(string $type): string
392
    {
393
        switch ($type) {
394 37
            case 'string':
395 4
                return self::STRING;
396 34
            case 'int':
397 31
            case 'integer':
398 7
                return self::INTEGER;
399 29
            case 'double':
400 27
            case 'float':
401 3
                return self::FLOAT;
402 26
            case 'bool':
403 25
            case 'boolean':
404 4
                return self::BOOLEAN;
405 23
            case 'array':
406 10
                return self::HASH;
407 14
            case 'null':
408 13
            case 'NULL':
409 3
                return self::NULL;
410 11
            case 'resource':
411 1
                return self::RESOURCE;
412 10
            case '?':
413 1
                return self::WILDCARD;
414
            default:
415 9
                return self::OBJECT;
416
        }
417
    }
418
}
419