1 | <?php |
||||||
2 | namespace PodloveSubscribeButton\Model; |
||||||
3 | |||||||
4 | abstract class Base { |
||||||
5 | /** |
||||||
6 | * Property dictionary for all tables |
||||||
7 | */ |
||||||
8 | private static $properties = array(); |
||||||
9 | |||||||
10 | private $is_new = true; |
||||||
11 | |||||||
12 | /** |
||||||
13 | * Contains property values |
||||||
14 | */ |
||||||
15 | private $data = array(); |
||||||
16 | |||||||
17 | public function __set( $name, $value ) { |
||||||
18 | if ( static::has_property( $name ) ) { |
||||||
19 | $this->set_property( $name, $value ); |
||||||
20 | } else { |
||||||
21 | $this->$name = $value; |
||||||
22 | } |
||||||
23 | } |
||||||
24 | |||||||
25 | private function set_property( $name, $value ) { |
||||||
26 | $this->data[ $name ] = $value; |
||||||
27 | } |
||||||
28 | |||||||
29 | public function __get( $name ) { |
||||||
30 | if ( static::has_property( $name ) ) { |
||||||
31 | return $this->get_property( $name ); |
||||||
32 | } elseif ( property_exists( $this, $name ) ) { |
||||||
33 | return $this->$name; |
||||||
34 | } else { |
||||||
35 | return null; |
||||||
36 | } |
||||||
37 | } |
||||||
38 | |||||||
39 | private function get_property( $name ) { |
||||||
40 | if ( isset( $this->data[ $name ] ) ) { |
||||||
41 | return $this->data[ $name ]; |
||||||
42 | } else { |
||||||
43 | return null; |
||||||
44 | } |
||||||
45 | } |
||||||
46 | |||||||
47 | private static function unserialize_property($property) { |
||||||
48 | if ( ! isset($property) ) |
||||||
49 | return; |
||||||
50 | |||||||
51 | if ( $unserialized_string = is_serialized($property) ) |
||||||
52 | return unserialize($property); |
||||||
53 | |||||||
54 | return $property; |
||||||
55 | } |
||||||
56 | |||||||
57 | /** |
||||||
58 | * Retrieves the database table name. |
||||||
59 | * |
||||||
60 | * The name is derived from the namespace an class name. Additionally, it |
||||||
61 | * is prefixed with the global WordPress database table prefix. |
||||||
62 | * @todo cache |
||||||
63 | * |
||||||
64 | * @return string database table name |
||||||
65 | */ |
||||||
66 | public static function table_name() { |
||||||
67 | global $wpdb; |
||||||
68 | |||||||
69 | // prefix with $wpdb prefix |
||||||
70 | return $wpdb->prefix . static::name(); |
||||||
71 | } |
||||||
72 | |||||||
73 | /** |
||||||
74 | * Define a property with name and type. |
||||||
75 | * |
||||||
76 | * Currently only supports basics. |
||||||
77 | * @todo enable additional options like NOT NULL, DEFAULT etc. |
||||||
78 | * |
||||||
79 | * @param string $name Name of the property / column |
||||||
80 | * @param string $type mySQL column type |
||||||
81 | */ |
||||||
82 | public static function property( $name, $type, $args = array() ) { |
||||||
83 | $class = get_called_class(); |
||||||
84 | |||||||
85 | if ( ! isset( static::$properties[ $class ] ) ) { |
||||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||||
86 | static::$properties[ $class ] = array(); |
||||||
87 | } |
||||||
88 | |||||||
89 | // "id" columns and those ending on "_id" get an index by default |
||||||
90 | $index = $name == 'id' || stripos( $name, '_id' ); |
||||||
91 | // but if the argument is set, it overrides the default |
||||||
92 | if (isset($args['index'])) { |
||||||
93 | $index = $args['index']; |
||||||
94 | } |
||||||
95 | |||||||
96 | static::$properties[ $class ][] = array( |
||||||
97 | 'name' => $name, |
||||||
98 | 'type' => $type, |
||||||
99 | 'index' => $index, |
||||||
100 | 'index_length' => isset($args['index_length']) ? $args['index_length'] : null, |
||||||
101 | 'unique' => isset($args['unique']) ? $args['unique'] : null |
||||||
102 | ); |
||||||
103 | } |
||||||
104 | |||||||
105 | /** |
||||||
106 | * Return a list of property dictionaries. |
||||||
107 | * |
||||||
108 | * @return array property list |
||||||
109 | */ |
||||||
110 | private static function properties() { |
||||||
111 | $class = get_called_class(); |
||||||
112 | |||||||
113 | if ( ! isset( static::$properties[ $class ] ) ) { |
||||||
0 ignored issues
–
show
|
|||||||
114 | static::$properties[ $class ] = array(); |
||||||
115 | } |
||||||
116 | |||||||
117 | return static::$properties[ $class ]; |
||||||
118 | } |
||||||
119 | |||||||
120 | /** |
||||||
121 | * Does the given property exist? |
||||||
122 | * |
||||||
123 | * @param string $name name of the property to test |
||||||
124 | * @return bool True if the property exists, else false. |
||||||
125 | */ |
||||||
126 | public static function has_property( $name ) { |
||||||
127 | return in_array( $name, static::property_names() ); |
||||||
128 | } |
||||||
129 | |||||||
130 | /** |
||||||
131 | * Return a list of property names. |
||||||
132 | * |
||||||
133 | * @return array property names |
||||||
134 | */ |
||||||
135 | public static function property_names() { |
||||||
136 | return array_map( function ( $p ) { return $p['name']; } , static::properties() ); |
||||||
137 | } |
||||||
138 | |||||||
139 | /** |
||||||
140 | * Does the table have any entries? |
||||||
141 | * |
||||||
142 | * @return bool True if there is at least one entry, else false. |
||||||
143 | */ |
||||||
144 | public static function has_entries() { |
||||||
145 | return static::count() > 0; |
||||||
146 | } |
||||||
147 | |||||||
148 | /** |
||||||
149 | * Return number of rows in the table. |
||||||
150 | * |
||||||
151 | * @return int number of rows |
||||||
152 | */ |
||||||
153 | public static function count() { |
||||||
154 | global $wpdb; |
||||||
155 | |||||||
156 | $sql = 'SELECT COUNT(*) FROM ' . static::table_name(); |
||||||
157 | return (int) $wpdb->get_var( $sql ); |
||||||
158 | } |
||||||
159 | |||||||
160 | public static function find_by_id( $id ) { |
||||||
161 | global $wpdb; |
||||||
162 | |||||||
163 | $class = get_called_class(); |
||||||
164 | $model = new $class(); |
||||||
165 | $model->flag_as_not_new(); |
||||||
166 | |||||||
167 | $row = $wpdb->get_row( 'SELECT * FROM ' . static::table_name() . ' WHERE id = ' . (int) $id ); |
||||||
168 | |||||||
169 | if ( ! $row ) { |
||||||
170 | return null; |
||||||
171 | } |
||||||
172 | |||||||
173 | foreach ( $row as $property => $value ) { |
||||||
174 | $model->$property = static::unserialize_property($value); |
||||||
175 | } |
||||||
176 | |||||||
177 | return $model; |
||||||
178 | } |
||||||
179 | |||||||
180 | public static function find_one_by_property( $property, $value ) { |
||||||
181 | global $wpdb; |
||||||
182 | |||||||
183 | $class = get_called_class(); |
||||||
184 | $model = new $class(); |
||||||
185 | $model->flag_as_not_new(); |
||||||
186 | |||||||
187 | $query = $wpdb->prepare('SELECT * FROM ' . static::table_name() . ' WHERE ' . $property . ' = \'%s\' LIMIT 0,1', $value); |
||||||
188 | $row = $wpdb->get_row($query); |
||||||
189 | |||||||
190 | if ( ! $row ) { |
||||||
191 | return null; |
||||||
192 | } |
||||||
193 | |||||||
194 | foreach ( $row as $property => $value ) { |
||||||
195 | $model->$property = static::unserialize_property($value); |
||||||
196 | } |
||||||
197 | |||||||
198 | return $model; |
||||||
199 | } |
||||||
200 | |||||||
201 | /** |
||||||
202 | * Retrieve all entries from the table. |
||||||
203 | * |
||||||
204 | * @return array list of model objects |
||||||
205 | */ |
||||||
206 | public static function all() { |
||||||
207 | global $wpdb; |
||||||
208 | |||||||
209 | $class = get_called_class(); |
||||||
210 | $models = array(); |
||||||
211 | |||||||
212 | $rows = $wpdb->get_results( 'SELECT * FROM ' . static::table_name() ); |
||||||
213 | |||||||
214 | foreach ( $rows as $row ) { |
||||||
215 | $model = new $class(); |
||||||
216 | $model->flag_as_not_new(); |
||||||
217 | foreach ( $row as $property => $value ) { |
||||||
218 | $model->$property = static::unserialize_property($value); |
||||||
219 | } |
||||||
220 | $models[] = $model; |
||||||
221 | } |
||||||
222 | |||||||
223 | return $models; |
||||||
224 | } |
||||||
225 | |||||||
226 | /** |
||||||
227 | * True if not yet saved to database. Else false. |
||||||
228 | */ |
||||||
229 | public function is_new() { |
||||||
230 | return $this->is_new; |
||||||
231 | } |
||||||
232 | |||||||
233 | public function flag_as_not_new() { |
||||||
234 | $this->is_new = false; |
||||||
235 | } |
||||||
236 | |||||||
237 | /** |
||||||
238 | * Rails-ish update_attributes for easy form handling. |
||||||
239 | * |
||||||
240 | * Takes an array of form values and takes care of serializing it. |
||||||
241 | * |
||||||
242 | * @param array $attributes |
||||||
243 | * @return bool |
||||||
244 | */ |
||||||
245 | public function update_attributes( $attributes ) { |
||||||
246 | |||||||
247 | if ( ! is_array( $attributes ) ) |
||||||
248 | return false; |
||||||
249 | |||||||
250 | $request = filter_input_array(INPUT_POST); // Do this for security reasons |
||||||
251 | |||||||
252 | foreach ( $attributes as $key => $value ) { |
||||||
253 | if ( is_array($value) ) { |
||||||
254 | $this->{$key} = serialize($value); |
||||||
255 | } else { |
||||||
256 | $this->{$key} = esc_sql($value); |
||||||
257 | } |
||||||
258 | } |
||||||
259 | |||||||
260 | if ( isset( $request['checkboxes'] ) && is_array( $request['checkboxes'] ) ) { |
||||||
261 | foreach ( $request['checkboxes'] as $checkbox ) { |
||||||
262 | if ( isset( $attributes[ $checkbox ] ) && $attributes[ $checkbox ] === 'on' ) { |
||||||
263 | $this->$checkbox = 1; |
||||||
264 | } else { |
||||||
265 | $this->$checkbox = 0; |
||||||
266 | } |
||||||
267 | } |
||||||
268 | } |
||||||
269 | |||||||
270 | // @todo this is the wrong place to do this! |
||||||
271 | // The feed password is the only "passphrase" which is saved. It is not encrypted! |
||||||
272 | // However, we keep this function for later use |
||||||
273 | if ( isset( $request['passwords'] ) && is_array( $request['passwords'] ) ) { |
||||||
274 | foreach ( $request['passwords'] as $password ) { |
||||||
275 | $this->$password = $attributes[ $password ]; |
||||||
276 | } |
||||||
277 | } |
||||||
278 | return $this->save(); |
||||||
279 | } |
||||||
280 | |||||||
281 | /** |
||||||
282 | * Update and save a single attribute. |
||||||
283 | * |
||||||
284 | * @param string $attribute attribute name |
||||||
285 | * @param mixed $value |
||||||
286 | * @return (bool) query success |
||||||
287 | */ |
||||||
288 | public function update_attribute($attribute, $value) { |
||||||
289 | global $wpdb; |
||||||
290 | |||||||
291 | $this->$attribute = $value; |
||||||
292 | |||||||
293 | $sql = sprintf( |
||||||
294 | "UPDATE %s SET %s = '%s' WHERE id = %s", |
||||||
295 | static::table_name(), |
||||||
296 | $attribute, |
||||||
297 | mysqli_real_escape_string($value), |
||||||
0 ignored issues
–
show
The call to
mysqli_real_escape_string() has too few arguments starting with string .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above. ![]() |
|||||||
298 | $this->id |
||||||
299 | ); |
||||||
300 | |||||||
301 | return $wpdb->query( $sql ); |
||||||
302 | } |
||||||
303 | |||||||
304 | /** |
||||||
305 | * Saves changes to database. |
||||||
306 | * |
||||||
307 | * @todo use wpdb::insert() |
||||||
308 | */ |
||||||
309 | public function save() { |
||||||
310 | global $wpdb; |
||||||
311 | |||||||
312 | if ( $this->is_new() ) { |
||||||
313 | |||||||
314 | $this->set_defaults(); |
||||||
315 | |||||||
316 | $sql = 'INSERT INTO ' |
||||||
317 | . static::table_name() |
||||||
318 | . ' ( ' |
||||||
319 | . implode( ',', static::property_names() ) |
||||||
320 | . ' ) ' |
||||||
321 | . 'VALUES' |
||||||
322 | . ' ( ' |
||||||
323 | . implode( ',', array_map( array( $this, 'property_name_to_sql_value' ), static::property_names() ) ) |
||||||
324 | . ' );' |
||||||
325 | ; |
||||||
326 | $success = $wpdb->query( $sql ); |
||||||
327 | if ( $success ) { |
||||||
328 | $this->id = $wpdb->insert_id; |
||||||
0 ignored issues
–
show
|
|||||||
329 | } |
||||||
330 | } else { |
||||||
331 | $sql = 'UPDATE ' . static::table_name() |
||||||
332 | . ' SET ' |
||||||
333 | . implode( ',', array_map( array( $this, 'property_name_to_sql_update_statement' ), static::property_names() ) ) |
||||||
334 | . ' WHERE id = ' . $this->id |
||||||
335 | ; |
||||||
336 | |||||||
337 | $success = $wpdb->query( $sql ); |
||||||
338 | } |
||||||
339 | |||||||
340 | $this->is_new = false; |
||||||
341 | |||||||
342 | do_action('podlove_model_save', $this); |
||||||
343 | do_action('podlove_model_change', $this); |
||||||
344 | |||||||
345 | return $success; |
||||||
346 | } |
||||||
347 | |||||||
348 | /** |
||||||
349 | * Sets default values. |
||||||
350 | * |
||||||
351 | * @return array |
||||||
352 | */ |
||||||
353 | private function set_defaults() { |
||||||
354 | |||||||
355 | $defaults = $this->default_values(); |
||||||
356 | |||||||
357 | if ( ! is_array( $defaults ) || empty( $defaults ) ) |
||||||
358 | return; |
||||||
359 | |||||||
360 | foreach ( $defaults as $property => $value ) { |
||||||
361 | if ( $this->$property === null ) |
||||||
362 | $this->$property = $value; |
||||||
363 | } |
||||||
364 | |||||||
365 | } |
||||||
366 | |||||||
367 | /** |
||||||
368 | * Return default values for properties. |
||||||
369 | * |
||||||
370 | * Can be overridden by inheriting model classes. |
||||||
371 | * |
||||||
372 | * @return array |
||||||
373 | */ |
||||||
374 | public function default_values() { |
||||||
375 | return array(); |
||||||
376 | } |
||||||
377 | |||||||
378 | public function delete() { |
||||||
379 | global $wpdb; |
||||||
380 | |||||||
381 | $sql = 'DELETE FROM ' |
||||||
382 | . static::table_name() |
||||||
383 | . ' WHERE id = ' . $this->id; |
||||||
384 | |||||||
385 | $rows_affected = $wpdb->query( $sql ); |
||||||
386 | |||||||
387 | do_action('podlove_model_delete', $this); |
||||||
388 | do_action('podlove_model_change', $this); |
||||||
389 | |||||||
390 | return $rows_affected !== false; |
||||||
391 | } |
||||||
392 | |||||||
393 | private function property_name_to_sql_update_statement( $p ) { |
||||||
394 | global $wpdb; |
||||||
395 | |||||||
396 | if ( $this->$p !== null && $this->$p !== '' ) { |
||||||
397 | return sprintf( "%s = '%s'", $p, ( is_array($this->$p) ? serialize($this->$p) : $this->$p ) ); |
||||||
398 | } else { |
||||||
399 | return "$p = NULL"; |
||||||
400 | } |
||||||
401 | } |
||||||
402 | |||||||
403 | private function property_name_to_sql_value( $p ) { |
||||||
404 | global $wpdb; |
||||||
405 | |||||||
406 | if ( $this->$p !== null && $this->$p !== '' ) { |
||||||
407 | return sprintf( "'%s'", $this->$p ); |
||||||
408 | } else { |
||||||
409 | return 'NULL'; |
||||||
410 | } |
||||||
411 | } |
||||||
412 | |||||||
413 | /** |
||||||
414 | * Create database table based on defined properties. |
||||||
415 | * |
||||||
416 | * Automatically includes an id column as auto incrementing primary key. |
||||||
417 | * @todo allow model changes |
||||||
418 | */ |
||||||
419 | public static function build() { |
||||||
420 | global $wpdb; |
||||||
421 | |||||||
422 | $property_sql = array(); |
||||||
423 | foreach ( static::properties() as $property ) |
||||||
424 | $property_sql[] = "`{$property['name']}` {$property['type']}"; |
||||||
425 | |||||||
426 | $sql = 'CREATE TABLE IF NOT EXISTS ' |
||||||
427 | . static::table_name() |
||||||
428 | . ' (' |
||||||
429 | . implode( ',', $property_sql ) |
||||||
430 | . ' ) CHARACTER SET utf8;' |
||||||
431 | ; |
||||||
432 | |||||||
433 | $wpdb->query( $sql ); |
||||||
434 | |||||||
435 | static::build_indices(); |
||||||
436 | } |
||||||
437 | |||||||
438 | /** |
||||||
439 | * Convention based index generation. |
||||||
440 | * |
||||||
441 | * Creates default indices for all columns matching both: |
||||||
442 | * - equals "id" or contains "_id" |
||||||
443 | * - doesn't have an index yet |
||||||
444 | */ |
||||||
445 | public static function build_indices() { |
||||||
446 | global $wpdb; |
||||||
447 | |||||||
448 | $indices_sql = 'SHOW INDEX FROM `' . static::table_name() . '`'; |
||||||
449 | $indices = $wpdb->get_results( $indices_sql ); |
||||||
450 | $index_columns = array_map( function($index){ return $index->Column_name; }, $indices ); |
||||||
451 | |||||||
452 | foreach ( static::properties() as $property ) { |
||||||
453 | |||||||
454 | if ( $property['index'] && ! in_array( $property['name'], $index_columns ) ) { |
||||||
455 | $length = isset($property['index_length']) ? '(' . (int) $property['index_length'] . ')' : ''; |
||||||
456 | $unique = isset($property['unique']) && $property['unique'] ? 'UNIQUE' : ''; |
||||||
457 | $sql = 'ALTER TABLE `' . static::table_name() . '` ADD ' . $unique . ' INDEX `' . $property['name'] . '` (' . $property['name'] . $length . ')'; |
||||||
458 | $wpdb->query( $sql ); |
||||||
459 | } |
||||||
460 | } |
||||||
461 | } |
||||||
462 | |||||||
463 | /** |
||||||
464 | * Model identifier. |
||||||
465 | */ |
||||||
466 | public static function name() { |
||||||
467 | // get name of implementing class |
||||||
468 | $table_name = get_called_class(); |
||||||
469 | // replace backslashes from namespace by underscores |
||||||
470 | $table_name = str_replace( '\\', '_', $table_name ); |
||||||
471 | // remove Models subnamespace from name |
||||||
472 | $table_name = str_replace( 'Model_', '', $table_name ); |
||||||
473 | // all lowercase |
||||||
474 | $table_name = strtolower( $table_name ); |
||||||
475 | |||||||
476 | return $table_name; |
||||||
477 | } |
||||||
478 | } |