Type   F
last analyzed

Complexity

Total Complexity 62

Size/Duplication

Total Lines 483
Duplicated Lines 0 %

Test Coverage

Coverage 87.5%

Importance

Changes 14
Bugs 1 Features 1
Metric Value
eloc 107
c 14
b 1
f 1
dl 0
loc 483
ccs 105
cts 120
cp 0.875
rs 3.44
wmc 62

22 Methods

Rating   Name   Duplication   Size   Complexity  
A getMethods() 0 7 2
A hasMethod() 0 7 2
A getName() 0 3 1
A getShortName() 0 3 1
A getNamespace() 0 3 1
A getInterfaces() 0 11 3
B getTraits() 0 27 7
A getVars() 0 3 1
A getProperties() 0 8 2
A isNotNull() 0 3 1
A is() 0 9 3
A isReferenceType() 0 3 1
A isNull() 0 7 3
A canBeString() 0 7 4
A isIn() 0 9 3
A isCustom() 0 3 2
A equals() 0 6 2
A isValueType() 0 7 3
A isScalar() 0 17 5
A toString() 0 7 2
B __construct() 0 34 6
B hasProperty() 0 27 7

How to fix   Complexity   

Complex Class

Complex classes like Type often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Type, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * PHP: Nelson Martell Library file
5
 *
6
 * Copyright © 2013-2021 Nelson Martell (http://nelson6e65.github.io)
7
 *
8
 * Licensed under The MIT License (MIT)
9
 * For full copyright and license information, please see the LICENSE
10
 * Redistributions of files must retain the above copyright notice.
11
 *
12
 * @copyright 2013-2021 Nelson Martell
13
 * @link      http://nelson6e65.github.io/php_nml/
14
 * @since     0.1.1
15
 * @license   http://www.opensource.org/licenses/mit-license.php The MIT License (MIT)
16
 * */
17
18
declare(strict_types=1);
19
20
namespace NelsonMartell;
21
22
use InvalidArgumentException;
23
use ReflectionClass;
24
use ReflectionException;
25
use ReflectionMethod;
26
use ReflectionProperty;
27
28
/**
29
 * Represents a PHP object type. Provides some properties and methods to describe some info about itself.
30
 *
31
 * @author Nelson Martell <[email protected]>
32
 * @since 0.1.1
33
 *
34
 * @property-read string        $name      Gets the name of this Type. This property is read-only.
35
 * @property-read string        $shortName Gets the abbreviated name of class, in other words, without the namespace.
36
 *   This property is read-only.
37
 * @property-read string        $namespace Gets the namespace name of this class. If the underlying type is not a class,
38
 *   this property is set to `''` (empty string). This property is read-only.
39
 * */
40
final class Type extends StrictObject implements IEquatable, IMagicPropertiesContainer
41
{
42
    /**
43
     * Gets the type of specified $obj and collect some info about itself.
44
     *
45
     * @param string|mixed  $obj        Target object or `class`|`interface`|`trait` name.
46
     * @param bool          $searchName Set this to `true` if `$obj` is a class name instead of an instance.
47
     *   This param is ignored if `obj` is not a `string`.
48
     *
49
     * @throws InvalidArgumentException If `$obj` is not a class name when `$searchName` is `true`.
50
     *
51
     * @since 1.0.0 Allow construct from a class name string.
52
     * */
53 351
    public function __construct($obj, bool $searchName = false)
54
    {
55 351
        parent::__construct();
56
57 351
        $type = (is_string($obj) && $searchName === true) ? 'object' : gettype($obj);
58
59 351
        $namespace = '';
60
61
        switch ($type) {
62 351
            case 'object':
63
                try {
64 223
                    $this->reflectionObject = new ReflectionClass($obj);
65 1
                } catch (ReflectionException $e) {
66 1
                    $msg  = msg('Invalid value.');
67 1
                    $msg .= msg(' `{0}` (position {1}) must to be a name of an existing class.', 'obj', 0);
68
69 1
                    throw new InvalidArgumentException($msg, 1, $e);
70
                }
71
72 222
                $name      = $this->reflectionObject->getName();
73 222
                $shortName = $this->reflectionObject->getShortName();
74 222
                $namespace = $this->reflectionObject->getNamespaceName();
75 222
                break;
76
77 302
            case 'resource':
78
                $shortName = get_resource_type($obj);
0 ignored issues
show
Bug introduced by
It seems like $obj can also be of type string; however, parameter $resource of get_resource_type() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

78
                $shortName = get_resource_type(/** @scrutinizer ignore-type */ $obj);
Loading history...
79
                $name      = 'resource: ' . $shortName;
80
                break;
81
82
            default:
83 302
                $shortName = $name = $type;
84
        }
85
86 351
        $this->basicMetadata = compact('namespace', 'name', 'shortName');
87
    }
88
89
    /**
90
     * Namespace, Name and ShortName storage of the underlying type.
91
     *
92
     * @var string[]
93
     */
94
    private $basicMetadata;
95
96
    /**
97
     * @var ReflectionClass
98
     */
99
    private $reflectionObject = null;
100
101
    /**
102
     * Getter for `$name` property.
103
     *
104
     * @return string
105
     * @see Type::$name
106
     * */
107 308
    protected function getName(): string
108
    {
109 308
        return $this->basicMetadata['name'];
110
    }
111
112
    /**
113
     * Getter for `$shortName` property.
114
     *
115
     * @return string
116
     * @see Type::$shortName
117
     * */
118 1
    public function getShortName(): string
119
    {
120 1
        return $this->basicMetadata['shortName'];
121
    }
122
123
    /**
124
     * Getter for `$namespace` property.
125
     *
126
     * @return string
127
     * @see    Type::$namespace
128
     * */
129 1
    public function getNamespace(): string
130
    {
131 1
        return $this->basicMetadata['namespace'];
132
    }
133
134
    /**
135
     *
136
     * @param int $filters
137
     *
138
     * @return ReflectionProperty[]|array
139
     *
140
     * @deprecated 1.0.0 Use `Type::getProperties()` instead.
141
     *
142
     * @see Type::getProperties()
143
     */
144
    public function getVars(int $filters = ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED): array
145
    {
146
        return $this->getProperties($filters);
147
    }
148
149
    /**
150
     * Gets the properties of underlying type of this instance
151
     *
152
     * @param int $filters Filter the results to include only properties with certain attributes. Defaults to
153
     *   `ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED`. Any combination of
154
     *   `ReflectionMethod::IS_STATIC`, `ReflectionMethod::IS_PUBLIC`, `ReflectionMethod::IS_PROTECTED`,
155
     *   `ReflectionMethod::IS_PRIVATE`.
156
     *
157
     * @return ReflectionProperty[]|array
158
     *
159
     * @since 1.0.0 Replacement for `Type::getVars()`
160
     */
161
    public function getProperties(
162
        int $filters = ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED
163
    ): array {
164
        if ($this->reflectionObject != null) {
165
            return $this->reflectionObject->getProperties($filters);
166
        }
167
168
        return [];
169
    }
170
171
172
    /**
173
     * Gets the public|protected methods (ReflectionMethod) of the underlying type of this instance.
174
     *
175
     * @param int $filters Filter the results to include only methods with certain attributes. Defaults to
176
     *   `ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED`. Any combination of
177
     *   `ReflectionMethod::IS_STATIC`, `ReflectionMethod::IS_PUBLIC`, `ReflectionMethod::IS_PROTECTED`,
178
     *   `ReflectionMethod::IS_PRIVATE`, `ReflectionMethod::IS_ABSTRACT` and `ReflectionMethod::IS_FINAL`.
179
     *
180
     * @return ReflectionMethod[]|array
181
     *
182
     * @since 1.0.0 Add `$filters` param.
183
     */
184
    public function getMethods(int $filters = ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED): array
185
    {
186
        if ($this->reflectionObject != null) {
187
            return $this->reflectionObject->getMethods($filters);
188
        }
189
190
        return [];
191
    }
192
193
    /**
194
     * Checks if the specified method is defined in the underlying type of this instance.
195
     *
196
     * @param string $name
197
     *
198
     * @return bool
199
     */
200 141
    public function hasMethod(string $name): bool
201
    {
202 141
        if ($this->reflectionObject !== null) {
203 138
            return $this->reflectionObject->hasMethod($name);
204
        }
205
206 6
        return false;
207
    }
208
209
    /**
210
     * Gets a list of all interfaces of the underlying type of this instance.
211
     *
212
     * @param bool $reflection If set to `true`, returns a list of interfaces as `ReflectionClass` (keyed by its names)
213
     *   instead of a list of names only (`string`).
214
     *
215
     * @return ReflectionClass[]|string[]|array
216
     *
217
     * @since 1.0.0
218
     */
219 6
    public function getInterfaces(bool $reflection = false): array
220
    {
221 6
        if ($this->reflectionObject !== null) {
222 5
            if ($reflection === true) {
223 5
                return $this->reflectionObject->getInterfaces();
224
            } else {
225 5
                return $this->reflectionObject->getInterfaceNames();
226
            }
227
        }
228
229 1
        return [];
230
    }
231
232
233
    /**
234
     * Gets a list of traits used by the underlying type of this instance.
235
     *
236
     * @param bool $reflection If set to `true`, returns a list of interfaces as `ReflectionClass` (keyed by its names)
237
     *   instead of a list of names only (`string`).
238
     * @param bool $recursive  If set to `true` will get all traits used by parent classes and used traits.
239
     *
240
     * @return array
241
     *
242
     * @since 1.0.0
243
     */
244 12
    public function getTraits(bool $reflection = false, bool $recursive = false): array
245
    {
246 12
        $traits = [];
247
248 12
        if ($this->reflectionObject !== null) {
249 10
            $traits += $this->reflectionObject->getTraits();
250
251 10
            if ($recursive === true) {
252
                // Search in sub-traits of this class --------------------------------------------
253 5
                foreach ($traits as $name => $traitClass) {
254 4
                    $traits += typeof($name, true)->getTraits(true, true);
255
                }
256
257
                // Search in parent class
258 5
                $parent = $this->reflectionObject->getParentClass();
259
260 5
                if ($parent) { // Search in parent class
261 3
                    $traits += typeof($parent->getName(), true)->getTraits(true, true);
262
                }
263
            }
264
        }
265
266 12
        if (count($traits) && !$reflection) {
267 6
            $traits = array_keys($traits); // Get only names if `$reflection == false`
268
        }
269
270 12
        return $traits;
271
    }
272
273
274
    /**
275
     * Checks if the specified property is defined in the underlying type of this instance.
276
     *
277
     * Unlike `property_exists()` function, this method returns `false` for dynamic attributes of an object.
278
     *
279
     * This method is ***case-sensitive***.
280
     *
281
     * @param string $name         Name of property.
282
     * @param bool   $recursive    Indicates if search for inherithed properties. Default: `true`.
283
     * @param bool   $includeMagic Include check for properties in class DocBlock definition.
284
     *
285
     * @return bool
286
     *
287
     * @since 1.0.0
288
     *
289
     * @see ReflectionClass::hasProperty()
290
     * @see \property_exists()
291
     */
292 161
    public function hasProperty(string $name, bool $recursive = true, bool $includeMagic = false): bool
293
    {
294 161
        if ($this->reflectionObject !== null) {
295 160
            $itHas = $this->reflectionObject->hasProperty($name);
296
297 160
            if (!$itHas && $includeMagic) {
298 66
                $pattern = '/\* @(?P<tag>property-read|property-write|property) +(?P<types>\S+) +(?P<property>\$'
299 66
                . $name
300 66
                . ')(?P<description>.*$)/m';
301
302
303 66
                $itHas = preg_match($pattern, $this->reflectionObject->getDocComment()) > 0;
304
            }
305
306 160
            if (!$itHas && $recursive) {
307
                /** @var ReflectionClass */
308 104
                $parentClass = $this->reflectionObject->getParentClass();
309
310 104
                if ($parentClass != false) {
311 94
                    $itHas = typeof($parentClass->getName(), true)->hasProperty($name, true, $includeMagic);
312
                }
313
            }
314
315 160
            return $itHas;
316
        }
317
318 1
        return false;
319
    }
320
321
    /**
322
     * Determines if instances of the underlying type can be converted to `string`.
323
     *
324
     * @return bool
325
     */
326 261
    public function canBeString(): bool
327
    {
328 261
        if ($this->isNull() || $this->isScalar() || $this->hasMethod('__toString')) {
329 257
            return true;
330
        }
331
332 11
        return false;
333
    }
334
335
    /**
336
     * Determines if the underlying type is `null`.
337
     *
338
     * @return bool `true` if this type is `null`; other case, `false`.
339
     * */
340 277
    public function isNull(): bool
341
    {
342 277
        if ($this->getName() == 'NULL' || $this->getName() == 'null') {
343 36
            return true;
344
        }
345
346 273
        return false;
347
    }
348
349
    /**
350
     * Determines if the underlying type is NOT `null`.
351
     *
352
     * @return bool `true` if this type is NOT `null`; other case, `false`.
353
     *
354
     * @deprecated 1.0.0 Use `!Type::isNull()` instead
355
     *
356
     * @see Type::isNull()
357
     * */
358 10
    public function isNotNull(): bool
359
    {
360 10
        return !$this->isNull();
361
    }
362
363
364
    /**
365
     * Determines if the underlying type of this instance is a custom `class`.
366
     *
367
     * @return bool `true`, if the underlying type is a custom class; another case, `false`.
368
     * */
369 49
    public function isCustom(): bool
370
    {
371 49
        return !$this->isValueType() && !$this->isNull();
372
    }
373
374
    /**
375
     * Determines if the underlying type of this instance is scalar.
376
     *
377
     * @return bool
378
     * @see    \is_scalar()
379
     * */
380 270
    public function isScalar(): bool
381
    {
382 270
        $r = false;
383
384 270
        switch ($this->getName()) {
385 270
            case 'boolean':
386 270
            case 'integer':
387 268
            case 'double':
388 266
            case 'string':
389 258
                $r = true;
390 258
                break;
391
392
            default:
393 49
                $r = false;
394
        }
395
396 270
        return $r;
397
    }
398
399
    /**
400
     * Determines if the underlying type of this instance is value type.
401
     *
402
     * @return bool
403
     * */
404 66
    public function isValueType(): bool
405
    {
406 66
        if ($this->isScalar() || $this->getName() === 'array') {
407 38
            return true;
408
        }
409
410 31
        return false;
411
    }
412
413
    /**
414
     * Determines if the underlying type of this instance is ref type.
415
     *
416
     * @return bool
417
     * */
418
    public function isReferenceType(): bool
419
    {
420
        return !$this->isValueType();
421
    }
422
423
    /**
424
     * Converts the current instance to its `string` representation.
425
     *
426
     * @return string
427
     * */
428 12
    public function toString(): string
429
    {
430 12
        if ($this->isCustom()) {
431 4
            return sprintf('object (%s)', $this->getName());
432
        }
433
434 9
        return $this->getName();
435
    }
436
437
438
    /**
439
     * Indicates whether the specified object is equal to the current instance.
440
     *
441
     * @param Type|mixed $other
442
     *
443
     * @return bool Returns always `false` if `$other` is not a `Type`.
444
     */
445 94
    public function equals($other): bool
446
    {
447 94
        if ($other instanceof Type) {
448 94
            return $this->getName() == $other->getName();
449
        } else {
450 11
            return false;
451
        }
452
    }
453
454
    /**
455
     * Detects if at least one of the objects are from the underlying type.
456
     *
457
     * **Usage:**
458
     *
459
     * ```php
460
     * $var1 = 'Hola, mundo';
461
     * $oneIsString = typeof((string) '')->isIn($var1, 1, 34); // true
462
     * ```
463
     *
464
     * Also works with 1st dimention of arrays:
465
     * ```php
466
     * $vars = ['Hola, mundo', 1, 34];
467
     * $oneIsString = typeof((string) '')->isIn($vars); // true
468
     * ```
469
     *
470
     * @param array $args List of object to check type.
471
     *
472
     * @return bool
473
     *
474
     * @see typeof()
475
     * @see Type::is()
476
     * @since 1.0.0
477
     */
478 70
    public function isIn(...$args): bool
479
    {
480 70
        foreach ($args as $obj) {
481 70
            if ($this->equals(typeof($obj))) {
482 57
                return true;
483
            }
484
        }
485
486 60
        return false;
487
    }
488
489
    /**
490
     * Detects if all object are from the underlying type.
491
     *
492
     * * **Usage:**
493
     *
494
     * ```php
495
     * $var1 = 'Hola, mundo';
496
     * $allAreString = typeof((string) '')->isIn($var1, 1, 34); // false
497
     * $allAreString = typeof((string) '')->isIn($var1, '1', '34'); // true
498
     * ```
499
     *
500
     * Also works with 1st dimention of arrays:
501
     * ```php
502
     * $vars = ['Hola, mundo', '1', '34'];
503
     * $allAreString = typeof((string) '')->isIn($vars); // true
504
     * ```
505
     *
506
     * @param array $args List of object to check type.
507
     *
508
     * @return bool
509
     *
510
     * @see typeof()
511
     * @see Type::isIn()
512
     * @since 1.0.0
513
     */
514 16
    public function is(...$args): bool
515
    {
516 16
        foreach ($args as $obj) {
517 16
            if (!$this->equals(typeof($obj))) {
518 8
                return false;
519
            }
520
        }
521
522 8
        return true;
523
    }
524
}
525