Completed
Push — master ( 1637a0...eab955 )
by Ben
38:26 queued 23:27
created

Property::checkType()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
dl 0
loc 25
ccs 0
cts 0
cp 0
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 14
nc 7
nop 2
crap 56
1
<?php declare(strict_types=1);
2
3
namespace Benrowe\Properties;
4
5
use Closure;
6
7
/**
8
 * Defines a unique property.
9
 * As a base, the property must have a a name. Additionally
10
 *
11
 * @package Benrowe\Properties
12
 * @todo add support for validating a property's value when being set
13
 */
14
class Property
15
{
16
    /**
17
     * @var string property name
18
     */
19
    private $name;
20
21
    /**
22
     * @var string|Closure|null the value type, {@see setType} for more details
23
     */
24
    private $type = null;
25
26
    /**
27
     * @var mixed the default value
28
     */
29
    private $default = null;
30
31
    /**
32 33
     * The currently set value
33
     */
34 33
    private $value = null;
35 33
36 33
    /**
37 33
     * @var Closure|string|null the setter mutator
38
     */
39
    private $setter;
40
41
    /**
42
     * @var Closure|string|null the getter mutator
43 33
     */
44
    private $getter;
45
46 33
    /**
47 33
     * @var string[] the base types the component will allow
48
     */
49
    const TYPES = [
50
            'string',
51
            'integer',
52
            'int',
53
            'float',
54 3
            'boolean',
55
            'bool',
56 3
            'array',
57
            'object',
58
            'null',
59
            'resource',
60
        ];
61
62
    const DOCBLOCK_PARAM_PATTERN = "/^(([a-z\\\])+(\[\])?\|?)+$/i";
63
64
    /**
65 33
     * Create a new Property Instance
66
     *
67 33
     * @param string $name the name of the property
68
     * @param string|Closure|null $type {@see setType}
69 30
     * @param string|null $default the default value
70 30
     */
71
    public function __construct(string $name, $type = null, $default = null)
72
    {
73
        $this->setName($name);
74 6
        $this->setType($type);
75
        $this->setDefault($default);
76
    }
77
78
    /**
79
     * Set the property name
80
     *
81
     * @param string $name the name of the property
82
     */
83 6
    public function setName(string $name)
84 6
    {
85
        $this->name = $name;
86
    }
87 6
88 6
    /**
89
     * Get the property name
90
     *
91
     * @return string
92
     */
93
    public function getName(): string
94 3
    {
95
        return $this->name;
96 3
    }
97
98
    /**
99
     * Set the type for the property
100
     * The type acts as a validator for when the {@see setValue} is called.
101
     * Properties are strict so the type specified here must be exact
102
     *
103
     * The following types are supported:
104 33
     * - php primitative types {@see self::TYPES} for a list
105
     * - docblock style string
106 33
     * - fully quantified class name of instanceof
107 33
     * - Closure: bool determines if the value is acceptable
108
     * - null. no set checking, effectively treated as 'mixed'
109
     *
110
     * @param string|Closure|null $type
111
     * @return void
112
     * @throws PropertyException if type is unsupported
113
     */
114 3
    public function setType($type): void
115
    {
116 3
        // null/callable
117
        if (is_callable($type) || $type === null) {
118
            $this->type = $type;
119
            return;
120
        }
121
122
        // primitaves
123
        if (in_array(strtolower($type), self::TYPES, true)) {
124 12
            $this->type = strtolower($type);
125
            return;
126 12
        }
127 6
128
        if (preg_match(self::DOCBLOCK_PARAM_PATTERN, $type)) {
129 12
            $this->type = $type;
130 12
            return;
131
        }
132
133
        // unknown, drop
134
        throw new PropertyException(PropertyException::UNKNOWN_TYPE);
135
    }
136
137 15
    /**
138
     * Get the property type
139 15
     * @return closure|string|null
140 9
     */
141
    public function getType()
142 12
    {
143 12
        return $this->type;
144 3
    }
145
146
    /**
147 12
     * Set the default value of the property if nothing is explicitly set
148
     *
149
     * @param mixed $default
150
     */
151
    public function setDefault($default)
152
    {
153
        $this->default = $default;
154
    }
155
156
    /**
157 6
     * Get the default value
158
     *
159 6
     * @return mixed
160
     */
161 6
    public function getDefault()
162
    {
163
        return $this->default;
164
    }
165
166
    /**
167
     * Set the value against the property
168
     *
169
     * @param mixed $value
170
     */
171 3
    public function setValue($value)
172
    {
173 3
        if ($this->setter) {
174
            $value = call_user_func($this->setter, $value);
175 3
        }
176
        // check the value against the type specified
177
        if ($this->type !== null && !$this->checkType($this->type, $value)) {
178
            throw new PropertyException("Value specified for \"{$this->name}\" is not of the correct type");
179
        }
180
        $this->value = $value;
181
    }
182
183
    /**
184
     * Check the the value against the type and see if we have a match
185
     *
186
     * @param string|Closure $type the type
187
     * @param mixed $value the value to check
188
     *
189
     * @return bool
190
     */
191
    private function checkType($type, $value): bool
192
    {
193
        if (is_callable($type)) {
194
            // call the type closure as function ($value, $property)
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
195
            return call_user_func($type, $value, $this);
196
        }
197
        if (in_array($type, self::TYPES)) {
198
            return $this->typeCheck($type, $value);
0 ignored issues
show
Bug introduced by
It seems like $type defined by parameter $type on line 191 can also be of type object<Closure>; however, Benrowe\Properties\Property::typeCheck() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
199
        }
200
        // docblock style type
201
        $types = explode('|', $type);
202
        foreach ($types as $type) {
203
            if (substr($type, -2) === '[]') {
204
                if ($this->arrayOf(substr($type, 0, -2), $value)) {
205
                    return true;
206
                }
207
            } else {
208
                if ($this->typeCheck($type, $value)) {
209
                    return true;
210
                }
211
            }
212
        }
213
214
        return false;
215
    }
216
217
    /**
218
     * Determine if the value is an array of the type specified
219
     *
220
     * @param string $type
221
     * @param mixed $value
222
     *
223
     * @return bool
224
     */
225
    private function arrayOf(string $type, $value): bool
226
    {
227
        if (!is_array($value)) {
228
            return false;
229
        }
230
        foreach ($value as $val) {
231
            if (!$this->typeCheck($type, $val)) {
232
                return false;
233
            }
234
        }
235
        return true;
236
    }
237
238
    /**
239
     * Check the type against the value (either a base type, or a instance of a class)
240
     *
241
     * @param string $type
242
     * @param mixed  $value
243
     *
244
     * @return bool
245
     */
246
    private function typeCheck(string $type, $value): bool
247
    {
248
        if (in_array($type, self::TYPES)) {
249
            return gettype($value) === $type;
250
        }
251
        // at this point, we assume the type is a FQCN..
252
        return (bool)($value instanceof $type);
253
    }
254
255
    /**
256
     * Get the currently set value, if no value is set the default is used
257
     *
258
     * @param mixed $default runtime default value. specify the default value for this
259
     *                       property when you call this method
260
     * @return mixed
261
     */
262
    public function getValue($default = null)
263
    {
264
        if ($this->value === null) {
265
            return $default !== null ? $default : $this->default;
266
        }
267
        $value = $this->value;
268
        if ($this->getter) {
269
            $value = call_user_func($this->getter, $value);
270
        }
271
272
        return $value;
273
    }
274
275
    /**
276
     * Register a closure to mutate the properties value before being stored.
277
     * This can be to cast the value to the $type specified
278
     *
279
     * @param  Closure $setter the custom function to run when the value is
280
     * being set
281
     * @return self
282
     */
283
    public function setter(Closure $setter)
284
    {
285
        $this->setter = $setter;
286
287
        return $this;
288
    }
289
290
    /**
291
     * Specify a custom closer to handle the retreival of the value stored
292
     * against this property
293
     *
294
     * @param  Closure $getter [description]
295
     * @return self
296
     */
297
    public function getter(Closure $getter)
298
    {
299
        $this->getter = $getter;
300
301
        return $this;
302
    }
303
}
304