class-model.php
6.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
<?php
declare( strict_types=1 );
namespace LearnDash\Hub\Framework;
/**
* This class will handle the data's sanitize and validation, it doesn't do the saving.
* Class Model
*
* @package LearnDash\Hub
* @property array attributes A magic property, use for mass assignment.
*/
abstract class Model extends Base implements \ArrayAccess {
/**
* The record ID
*
* @var int
* @property
*/
public $id = 0;
/**
* Contains all the validation rules, the format is
* [
* [['property1','property2],'validator','message'=>'Optional']
* ]
*
* @var array
*/
protected $rules;
/**
* After get validated, the validation errors will be stored in the property.
*
* @var array
*/
protected $errors;
/**
* Contain the annotation metadata.
*
* @var array
*/
protected $annotations;
/**
* Model constructor.
*/
public function __construct() {
$this->parse_annotations();
}
/**
* The validator engine
*
* @return bool
*/
public function validate(): bool {
}
/**
* Retrieve all validation errors;
*
* @return array
*/
public function get_errors(): array {
return $this->errors;
}
/**
* Retrieve the errors relate to a single property.
*
* @param string $key The error key.
*
* @return string
*/
public function get_error( string $key ): string {
return $this->errors[ $key ] ?? '';
}
/**
* Set value of an object property.
*
* @param string $name The property name.
* @param mixed $value The property value.
*
* @throws \Exception If data is not an array.
*/
public function __set( string $name, $value ) {
if ( 'attributes' === $name ) {
// massive assignments.
if ( ! is_array( $value ) ) {
throw new \Exception( 'Invalid data for a mass assignment.' );
}
foreach ( $value as $key => $val ) {
if ( ! property_exists( $this, $key ) ) {
throw new \Exception( "Property {$key} not exists." );
}
if ( isset( $this->annotations[ $key ] ) ) {
$type = $this->annotations[ $key ]['type'];
if ( 'boolean' === $type || 'bool' === $type ) {
$val = filter_var( $val, FILTER_VALIDATE_BOOLEAN );
} elseif ( null === $val && 'string' === $type ) {
$val = '';
} else {
settype( $val, $type );
}
}
// thanks to the ArrayAccess interface, so we can use object as array.
$this[ $key ] = $val;
}
}
}
/**
* Returns the element at the specified offset.
* This method is required by the SPL interface [[\ArrayAccess]].
* It is implicitly called when you use something like `$value = $model[$offset];`.
*
* @param mixed $offset the offset to retrieve element.
*
* @return mixed the element at the offset, null if no element is found at the offset
*/
public function offsetGet( $offset ) {
return $this->$offset;
}
/**
* Sets the element at the specified offset.
* This method is required by the SPL interface [[\ArrayAccess]].
* It is implicitly called when you use something like `$model[$offset] = $item;`.
*
* @param int $offset the offset to set element.
* @param mixed $value the element value.
*/
public function offsetSet( $offset, $value ) {
$this->$offset = $value;
}
/**
* Returns whether there is an element at the specified offset.
* This method is required by the SPL interface [[\ArrayAccess]].
* It is implicitly called when you use something like `isset($model[$offset])`.
*
* @param mixed $offset the offset to check on.
*
* @return bool whether or not an offset exists.
*/
public function offsetExists( $offset ): bool {
return isset( $this->$offset );
}
/**
* Sets the element value at the specified offset to null.
* This method is required by the SPL interface [[\ArrayAccess]].
* It is implicitly called when you use something like `unset($model[$offset])`.
*
* @param mixed $offset the offset to unset element.
*/
public function offsetUnset( $offset ) {
$this->$offset = null;
}
/**
* In model, we going to parse the annotations if any.
* @return array
*/
public function to_array(): array {
if ( empty( $this->annotations ) ) {
return parent::to_array();
}
$return = array();
foreach ( array_keys( $this->annotations ) as $property ) {
if ( isset( $this[ $property ] ) ) {
$return[ $property ] = $this[ $property ];
}
}
return $return;
}
/**
* Parse the annotations of the class, and cache it. The list should be
* - type: for casting
* - sanitize_*: the list of sanitize_ functions, which should be run on this property
* - rule: the rule that we use for validation
*/
protected function parse_annotations() {
$class = new \ReflectionClass( static::class );
$properties = $class->getProperties( \ReflectionProperty::IS_PUBLIC );
foreach ( $properties as $property ) {
$doc_block = $property->getDocComment();
if ( ! stristr( $doc_block, '@property' ) ) {
continue;
}
$this->annotations[ $property->getName() ] = array(
'type' => $this->parse_annotations_var( $doc_block ),
'sanitize' => $this->parse_annotation_sanitize( $doc_block ),
'rule' => $this->parse_annotation_rule( $doc_block ),
);
}
}
/**
* Get the variable type
*
* @param string $docblock
*
* @return false|mixed
*/
private function parse_annotations_var( string $docblock ) {
$pattern = '/@var\s(.+)/';
if ( preg_match( $pattern, $docblock, $matches ) ) {
$type = trim( $matches[1] );
// only allow right type.
if ( in_array(
$type,
array(
'boolean',
'bool',
'integer',
'int',
'float',
'double',
'string',
'array',
'object',
)
) ) {
return $type;
}
}
return false;
}
/**
* Get the sanitize function
*
* @param string $docblock
*
* @return false|mixed
*/
private function parse_annotation_sanitize( string $docblock ) {
$pattern = '/@(sanitize_.+)/';
if ( preg_match( $pattern, $docblock, $matches ) ) {
return trim( $matches[1] );
}
return false;
}
/**
* Get the validation rule
*
* @param string $docblock
*
* @return false|mixed
*/
private function parse_annotation_rule( string $docblock ) {
$pattern = '/@(rule_.+)/';
if ( preg_match( $pattern, $docblock, $matches ) ) {
return trim( $matches[1] );
}
return false;
}
}