Completed
Push — master ( 4bf7a3...fdd65c )
by Lars
70:56 queued 65:43
created

Property::checkType()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 6
ccs 4
cts 4
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Arrayy;
4
5
use phpDocumentor\Reflection\Type;
6
7
/**
8
 * Class Property
9
 *
10
 * inspired by https://github.com/spatie/value-object
11
 */
12
class Property extends \ReflectionProperty
13
{
14
  /**
15
   * @var array
16
   */
17
  protected static $typeMapping = [
18
      'int'  => 'integer',
19
      'bool' => 'boolean',
20
  ];
21
22
  /**
23
   * @var bool
24
   */
25
  protected $hasTypeDeclaration = false;
26
27
  /**
28
   * @var bool
29
   */
30
  protected $isNullable = false;
31
32
  /**
33
   * @var array
34
   */
35
  protected $types = [];
36
37
  /**
38
   * Property constructor.
39
   *
40
   * @param null|\ReflectionProperty $reflectionProperty
41
   * @param object                   $fakeObject
42
   *
43
   * @throws \ReflectionException
44
   */
45 2
  public function __construct($reflectionProperty, $fakeObject = null)
46
  {
47 2
    parent::__construct($fakeObject, $reflectionProperty->getName());
0 ignored issues
show
Bug introduced by
It seems like $reflectionProperty is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
48 2
  }
49
50
  /**
51
   * @param string $type
52
   * @param mixed  $value
53
   *
54
   * @return bool
55
   */
56 6
  protected function assertTypeEquals(string $type, $value): bool
57
  {
58 6
    if (strpos($type, '[]') !== false) {
59
      return $this->isValidGenericCollection($type, $value);
60
    }
61
62 6
    if ($type === 'mixed' && $value !== null) {
63
      return true;
64
    }
65
66 6
    return $value instanceof $type
67
           ||
68 6
           \gettype($value) === (self::$typeMapping[$type] ?? $type);
69
  }
70
71 6
  public function checkType($value)
72
  {
73 6
    if (!$this->isValidType($value)) {
74 2
      $this->throwInvalidType($value);
75
    }
76 5
  }
77
78 2
  public static function fromPhpDocumentorProperty(\phpDocumentor\Reflection\DocBlock\Tags\Property $phpDocumentorReflectionProperty): self
79
  {
80 2
    $tmpProperty = $phpDocumentorReflectionProperty->getVariableName();
81 2
    $tmpObject = new \stdClass();
82 2
    $tmpObject->{$tmpProperty} = null;
83
84 2
    $tmpReflection = new self(new \ReflectionProperty($tmpObject, $tmpProperty), $tmpObject);
85
86 2
    $type = $phpDocumentorReflectionProperty->getType();
87
88 2
    if ($type) {
89 2
      $tmpReflection->hasTypeDeclaration = true;
90
91 2
      $docTypes = self::parseDocTypeObject($type);
92 2
      if (\is_array($docTypes) === true) {
93 2
        foreach ($docTypes as $docType) {
94 2
          $tmpReflection->types[] = $docType;
95
        }
96
      } else {
97 2
        $tmpReflection->types[] = $docTypes;
98
      }
99
100 2
      if (\in_array('null', $tmpReflection->types, true)) {
101 2
        $tmpReflection->isNullable = true;
102
      }
103
    }
104
105 2
    return $tmpReflection;
106
  }
107
108 2
  public function getTypes(): array
109
  {
110 2
    return $this->types;
111
  }
112
113
  protected function isValidGenericCollection(string $type, $collection): bool
114
  {
115
    if (!\is_array($collection)) {
116
      return false;
117
    }
118
119
    $valueType = \str_replace('[]', '', $type);
120
121
    foreach ($collection as $value) {
122
      if (!$this->assertTypeEquals($valueType, $value)) {
123
        return false;
124
      }
125
    }
126
127
    return true;
128
  }
129
130
  /**
131
   * @param mixed $value
132
   *
133
   * @return bool
134
   */
135 6
  protected function isValidType($value): bool
136
  {
137 6
    if (!$this->hasTypeDeclaration) {
138
      return true;
139
    }
140
141 6
    if ($this->isNullable && $value === null) {
142 3
      return true;
143
    }
144
145 6
    foreach ($this->types as $currentType) {
146 6
      $isValidType = $this->assertTypeEquals($currentType, $value);
147
148 6
      if ($isValidType) {
149 6
        return true;
150
      }
151
    }
152
153 2
    return false;
154
  }
155
156
  /**
157
   * @param Type $type
158
   *
159
   * @return string[]|string
160
   */
161 2
  protected static function parseDocTypeObject(Type $type)
162
  {
163 2
    if ($type instanceof \phpDocumentor\Reflection\Types\Object_) {
164 1
      return (string)$type->getFqsen();
165
    }
166
167 2
    if ($type instanceof \phpDocumentor\Reflection\Types\Array_) {
168
      $value = $type->getValueType();
169
      if ($value instanceof \phpDocumentor\Reflection\Types\Mixed_) {
170
        return 'mixed';
171
      }
172
173
      return 'array';
174
    }
175
176 2
    if ($type instanceof \phpDocumentor\Reflection\Types\Null_) {
177 2
      return 'null';
178
    }
179
180 2
    if ($type instanceof \phpDocumentor\Reflection\Types\Mixed_) {
181
      return 'mixed';
182
    }
183
184 2
    if ($type instanceof \phpDocumentor\Reflection\Types\Scalar) {
185
      return 'string|int|float|bool';
186
    }
187
188 2
    if ($type instanceof \phpDocumentor\Reflection\Types\Boolean) {
189
      return 'bool';
190
    }
191
192 2
    if ($type instanceof \phpDocumentor\Reflection\Types\Callable_) {
193
      return 'callable';
194
    }
195
196 2
    if ($type instanceof \phpDocumentor\Reflection\Types\Compound) {
197 2
      $types = [];
198 2
      foreach ($type as $subType) {
199 2
        $types[] = self::parseDocTypeObject($subType);
200
      }
201
202 2
      return $types;
203
    }
204
205 2
    if ($type instanceof \phpDocumentor\Reflection\Types\Float_) {
206
      return 'float';
207
    }
208
209 2
    if ($type instanceof \phpDocumentor\Reflection\Types\String_) {
210 2
      return 'string';
211
    }
212
213 1
    if ($type instanceof \phpDocumentor\Reflection\Types\Integer) {
214 1
      return 'int';
215
    }
216
217
    throw new \Exception('Unhandled PhpDoc type: ' . get_class($type));
218
  }
219
220
  /**
221
   * @param mixed $value
222
   */
223 2
  protected function throwInvalidType($value)
224
  {
225 2
    $type = \gettype($value);
226
227 2
    if ($type === 'NULL') {
228
      $value = 'null';
229 2
    } elseif ($type === 'object') {
230 1
      $value = \get_class($value);
231 1
    } elseif ($type === 'array') {
232
      $value = 'array';
233
    }
234
235 2
    $expectedTypes = \implode('|', $this->getTypes());
236
237 2
    throw new \InvalidArgumentException("Invalid type: expected {$this->name} to be of type {{$expectedTypes}}, instead got value `{$value}` with type {{$type}}.");
238
  }
239
}
240