Issues (1386)

model/base.php (3 issues)

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
Since $properties is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $properties to at least protected.
Loading history...
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
Since $properties is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $properties to at least protected.
Loading history...
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_all_by_property( $property, $value ) {
181
		global $wpdb;
182
		
183
		$class = get_called_class();
184
		$models = array();
185
		
186
		$rows = $wpdb->get_results(
187
			'SELECT * FROM ' . static::table_name() . ' WHERE ' . $property .  ' = \'' . $value . '\''
188
		);
189
		
190
		if ( ! $rows ) {
191
			return array();
192
		}
193
		
194
		foreach ( $rows as $row ) {
195
			$model = new $class();
196
			$model->flag_as_not_new();
197
			foreach ( $row as $property => $value ) {
198
				$model->$property = static::unserialize_property($value);
199
			}
200
			$models[] = $model;
201
		}
202
		
203
		return $models;
204
	}
205
206
	public static function find_one_by_property( $property, $value ) {
207
		global $wpdb;
208
		
209
		$class = get_called_class();
210
		$model = new $class();
211
		$model->flag_as_not_new();
212
		
213
		$row = $wpdb->get_row(
214
			'SELECT * FROM ' . static::table_name() . ' WHERE ' . $property .  ' = \'' . $value . '\' LIMIT 0,1'
215
		);
216
		
217
		if ( ! $row ) {
218
			return null;
219
		}
220
		
221
		foreach ( $row as $property => $value ) {
222
			$model->$property = static::unserialize_property($value);
223
		}
224
		
225
		return $model;
226
	}
227
228
	public static function find_all_by_where( $where ) {
229
		global $wpdb;
230
		
231
		$class = get_called_class();
232
		$models = array();
233
		
234
		$rows = $wpdb->get_results(
235
			'SELECT * FROM ' . static::table_name() . ' WHERE ' . $where
236
		);
237
		
238
		if ( ! $rows ) {
239
			return array();
240
		}
241
		
242
		foreach ( $rows as $row ) {
243
			$model = new $class();
244
			$model->flag_as_not_new();
245
			foreach ( $row as $property => $value ) {
246
				$model->$property = static::unserialize_property($value);
247
			}
248
			$models[] = $model;
249
		}
250
		
251
		return $models;
252
	}
253
	
254
	public static function find_one_by_where( $where ) {
255
		global $wpdb;
256
		
257
		$class = get_called_class();
258
		$model = new $class();
259
		$model->flag_as_not_new();
260
		
261
		$row = $wpdb->get_row(
262
			'SELECT * FROM ' . static::table_name() . ' WHERE ' . $where . ' LIMIT 0,1'
263
		);
264
		
265
		if ( ! $row ) {
266
			return null;
267
		}
268
		
269
		foreach ( $row as $property => $value ) {
270
			$model->$property = static::unserialize_property($value);
271
		}
272
		
273
		return $model;
274
	}
275
	/**
276
	 * Retrieve all entries from the table.
277
	 *
278
	 * @param  string $sql_suffix optional SQL, appended after FROM clause
279
	 * @return array list of model objects
280
	 */
281
	public static function all( $sql_suffix = '' ) {
282
		global $wpdb;
283
		
284
		$class = get_called_class();
285
		$models = array();
286
		
287
		$rows = $wpdb->get_results( 'SELECT * FROM ' . static::table_name() . ' ' . $sql_suffix );
288
289
		foreach ( $rows as $row ) {
290
			$model = new $class();
291
			$model->flag_as_not_new();
292
			foreach ( $row as $property => $value ) {
293
				$model->$property = static::unserialize_property($value);
294
			}
295
			$models[] = $model;
296
		}
297
		
298
		return $models;
299
	}
300
	
301
	/**
302
	 * True if not yet saved to database. Else false.
303
	 */
304
	public function is_new() {
305
		return $this->is_new;
306
	}
307
	
308
	public function flag_as_not_new() {
309
		$this->is_new = false;
310
	}
311
312
	/**
313
	 * Rails-ish update_attributes for easy form handling.
314
	 *
315
	 * Takes an array of form values and takes care of serializing it.
316
	 * 
317
	 * @param  array $attributes
318
	 * @return bool
319
	 */
320
	public function update_attributes( $attributes ) {
321
322
		if ( ! is_array( $attributes ) )
323
			return false;
324
325
		$request = filter_input_array(INPUT_POST); // Do this for security reasons
326
			
327
		foreach ( $attributes as $key => $value ) {
328
			if ( is_array($value) ) {
329
				$this->{$key} = serialize($value);
330
			} else {
331
				$this->{$key} = esc_sql($value);
332
			}
333
		}
334
		
335
		if ( isset( $request['checkboxes'] ) && is_array( $request['checkboxes'] ) ) {
336
			foreach ( $request['checkboxes'] as $checkbox ) {
337
				if ( isset( $attributes[ $checkbox ] ) && $attributes[ $checkbox ] === 'on' ) {
338
					$this->$checkbox = 1;
339
				} else {
340
					$this->$checkbox = 0;
341
				}
342
			}
343
		}
344
345
		// @todo this is the wrong place to do this!
346
		// The feed password is the only "passphrase" which is saved. It is not encrypted!
347
		// However, we keep this function for later use
348
		if ( isset( $request['passwords'] ) && is_array( $request['passwords'] ) ) {
349
			foreach ( $request['passwords'] as $password ) {
350
				$this->$password = $attributes[ $password ];
351
			}
352
		}
353
		return $this->save();
354
	}
355
356
	/**
357
	 * Update and save a single attribute.
358
	 * 	
359
	 * @param  string $attribute attribute name
360
	 * @param  mixed  $value
361
	 * @return (bool) query success
362
	 */
363
	public function update_attribute($attribute, $value) {
364
		global $wpdb;
365
366
		$this->$attribute = $value;
367
368
		$sql = sprintf(
369
			"UPDATE %s SET %s = '%s' WHERE id = %s",
370
			static::table_name(),
371
			$attribute,
372
			mysqli_real_escape_string($value),
373
			$this->id
374
		);
375
376
		return $wpdb->query( $sql );
377
	}
378
	
379
	/**
380
	 * Saves changes to database.
381
	 * 
382
	 * @todo use wpdb::insert()
383
	 */
384
	public function save() {
385
		global $wpdb;
386
387
		if ( $this->is_new() ) {
388
389
			$this->set_defaults();
390
391
			$sql = 'INSERT INTO '
392
			     . static::table_name()
393
			     . ' ( '
394
			     . implode( ',', static::property_names() )
395
			     . ' ) '
396
			     . 'VALUES'
397
			     . ' ( '
398
			     . implode( ',', array_map( array( $this, 'property_name_to_sql_value' ), static::property_names() ) )
399
			     . ' );'
400
			;
401
			$success = $wpdb->query( $sql );
402
			if ( $success ) {
403
				$this->id = $wpdb->insert_id;
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
404
			}
405
		} else {
406
			$sql = 'UPDATE ' . static::table_name()
407
			     . ' SET '
408
			     . implode( ',', array_map( array( $this, 'property_name_to_sql_update_statement' ), static::property_names() ) )
409
			     . ' WHERE id = ' . $this->id
410
			;
411
412
			$success = $wpdb->query( $sql );
413
		}
414
415
		$this->is_new = false;
416
417
		do_action('podlove_model_save', $this);
418
		do_action('podlove_model_change', $this);
419
420
		return $success;
421
	}
422
423
	/**
424
	 * Sets default values.
425
	 * 
426
	 * @return array
427
	 */
428
	private function set_defaults() {
429
		
430
		$defaults = $this->default_values();
431
432
		if ( ! is_array( $defaults ) || empty( $defaults ) )
433
			return;
434
435
		foreach ( $defaults as $property => $value ) {
436
			if ( $this->$property === null )
437
				$this->$property = $value;
438
		}
439
440
	}
441
442
	/**
443
	 * Return default values for properties.
444
	 * 
445
	 * Can be overridden by inheriting model classes.
446
	 * 
447
	 * @return array
448
	 */
449
	public function default_values() {
450
		return array();
451
	}
452
	
453
	public function delete() {
454
		global $wpdb;
455
		
456
		$sql = 'DELETE FROM '
457
		     . static::table_name()
458
		     . ' WHERE id = ' . $this->id;
459
460
		$rows_affected = $wpdb->query( $sql );
461
462
	    do_action('podlove_model_delete', $this);
463
	    do_action('podlove_model_change', $this);
464
465
		return $rows_affected !== false;
466
	}
467
468
	private function property_name_to_sql_update_statement( $p ) {
469
		global $wpdb;
470
471
		if ( $this->$p !== null && $this->$p !== '' ) {
472
			return sprintf( "%s = '%s'", $p, ( is_array($this->$p) ? serialize($this->$p) : $this->$p ) );
473
		} else {
474
			return "$p = NULL";
475
		}
476
	}
477
	
478
	private function property_name_to_sql_value( $p ) {
479
		global $wpdb;
480
481
		if ( $this->$p !== null && $this->$p !== '' ) {
482
			return sprintf( "'%s'", $this->$p );
483
		} else {
484
			return 'NULL';
485
		}
486
	}
487
	
488
	/**
489
	 * Create database table based on defined properties.
490
	 * 
491
	 * Automatically includes an id column as auto incrementing primary key.
492
	 * @todo allow model changes
493
	 */
494
	public static function build() {
495
		global $wpdb;
496
		
497
		$property_sql = array();
498
		foreach ( static::properties() as $property )
499
			$property_sql[] = "`{$property['name']}` {$property['type']}";
500
		
501
		$sql = 'CREATE TABLE IF NOT EXISTS '
502
		     . static::table_name()
503
		     . ' ('
504
		     . implode( ',', $property_sql )
505
		     . ' ) CHARACTER SET utf8;'
506
		;
507
		
508
		$wpdb->query( $sql );
509
510
		static::build_indices();
511
	}
512
	
513
	/**
514
	 * Convention based index generation.
515
	 *
516
	 * Creates default indices for all columns matching both:
517
	 * - equals "id" or contains "_id"
518
	 * - doesn't have an index yet
519
	 */
520
	public static function build_indices() {
521
		global $wpdb;
522
523
		$indices_sql = 'SHOW INDEX FROM `' . static::table_name() . '`';
524
		$indices = $wpdb->get_results( $indices_sql );
525
		$index_columns = array_map( function($index){ return $index->Column_name; }, $indices );
526
527
		foreach ( static::properties() as $property ) {
528
529
			if ( $property['index'] && ! in_array( $property['name'], $index_columns ) ) {
530
				$length = isset($property['index_length']) ? '(' . (int) $property['index_length'] . ')' : '';
531
				$unique = isset($property['unique']) && $property['unique'] ? 'UNIQUE' : '';
532
				$sql = 'ALTER TABLE `' . static::table_name() . '` ADD ' . $unique . ' INDEX `' . $property['name'] . '` (' . $property['name'] . $length . ')';
533
				$wpdb->query( $sql );
534
			}
535
		}
536
	}
537
538
	/**
539
	 * Model identifier.
540
	 */
541
	public static function name() {
542
		// get name of implementing class
543
		$table_name = get_called_class();
544
		// replace backslashes from namespace by underscores
545
		$table_name = str_replace( '\\', '_', $table_name );
546
		// remove Models subnamespace from name
547
		$table_name = str_replace( 'Model_', '', $table_name );
548
		// all lowercase
549
		$table_name = strtolower( $table_name );
550
551
		return $table_name;
552
	}
553
}