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) |
|
|
|
|
195
|
|
|
return call_user_func($type, $value, $this); |
196
|
|
|
} |
197
|
|
|
if (in_array($type, self::TYPES)) { |
198
|
|
|
return $this->typeCheck($type, $value); |
|
|
|
|
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
|
|
|
|
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.