Completed
Push — master ( f6d20e...4e14c4 )
by Mike
11:10
created

WC_Data::exclude_internal_meta_keys()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 1
eloc 2
c 1
b 1
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
if ( ! defined( 'ABSPATH' ) ) {
3
	exit;
4
}
5
6
/**
7
* Abstract WC Data Class
8
*
9
* Implemented by classes using the same CRUD(s) pattern.
10
*
11
* @version  2.6.0
12
* @package  WooCommerce/Abstracts
13
* @category Abstract Class
14
* @author   WooThemes
15
*/
16
abstract class WC_Data {
17
18
	/**
19
	 * Core data for this object. Name value pairs (name + default value).
20
	 * @var array
21
	 */
22
	protected $_data = array();
23
24
	/**
25
	 * Set to _data on construct so we can track and reset data if needed.
26
	 * @var array
27
	 */
28
	protected $_default_data = array();
29
30
	/**
31
	 * Stores meta in cache for future reads.
32
	 * A group must be set to to enable caching.
33
	 * @var string
34
	 */
35
	protected $_cache_group = '';
36
37
	/**
38
	 * Meta type. This should match up with
39
	 * the types avaiable at https://codex.wordpress.org/Function_Reference/add_metadata.
40
	 * WP defines 'post', 'user', 'comment', and 'term'.
41
	 */
42
	protected $_meta_type = 'post';
43
44
	/**
45
	 * This only needs set if you are using a custom metadata type (for example payment tokens.
46
	 * This should be the name of the field your table uses for associating meta with objects.
47
	 * For example, in payment_tokenmeta, this would be payment_token_id.
48
	 * @var string
49
	 */
50
	protected $object_id_field_for_meta = '';
51
52
	/**
53
	 * Stores additonal meta data.
54
	 * @var array
55
	 */
56
	protected $_meta_data = array();
57
58
	/**
59
	 * Internal meta keys we don't want exposed for the object.
60
	 * @var array
61
	 */
62
	protected $_internal_meta_keys = array();
63
64
	/**
65
	 * Default constructor.
66
	 * @param int|object|array $read ID to load from the DB (optional) or already queried data.
67
	 */
68
	public function __construct( $read = 0 ) {
69
		$this->_default_data = $this->_data;
70
	}
71
72
	/**
73
	 * Returns the unique ID for this object.
74
	 * @return int
75
	 */
76
	abstract public function get_id();
77
78
	/**
79
	 * Creates new object in the database.
80
	 */
81
	abstract public function create();
82
83
	/**
84
	 * Read object from the database.
85
	 * @param int ID of the object to load.
86
	 */
87
	abstract public function read( $id );
88
89
	/**
90
	 * Updates object data in the database.
91
	 */
92
	abstract public function update();
93
94
	/**
95
	 * Updates object data in the database.
96
	 */
97
	abstract public function delete();
98
99
	/**
100
	 * Save should create or update based on object existance.
101
	 */
102
	abstract public function save();
103
104
	/**
105
	 * Change data to JSON format.
106
	 * @return string Data in JSON format.
107
	 */
108
	public function __toString() {
109
		return json_encode( $this->get_data() );
110
	}
111
112
	/**
113
	 * Returns all data for this object.
114
	 * @return array
115
	 */
116
	public function get_data() {
117
		return array_merge( $this->_data, array( 'meta_data' => $this->get_meta_data() ) );
118
	}
119
120
	/**
121
	 * Filter null meta values from array.
122
	 * @return bool
123
	 */
124
	protected function filter_null_meta( $meta ) {
125
		return ! is_null( $meta->value );
126
	}
127
128
	/**
129
	 * Get All Meta Data.
130
	 * @since 2.6.0
131
	 * @return array
132
	 */
133
	public function get_meta_data() {
134
		return array_filter( $this->_meta_data, array( $this, 'filter_null_meta' ) );
135
	}
136
137
	/**
138
	 * Internal meta keys we don't want exposed as part of meta_data. This is in
139
	 * addition to all data props with _ prefix.
140
	 * @since 2.6.0
141
	 * @return array
142
	 */
143
	protected function prefix_key( $key ) {
144
		return '_' === substr( $key, 0, 1 ) ? $key : '_' . $key;
145
	}
146
147
	/**
148
	 * Internal meta keys we don't want exposed as part of meta_data. This is in
149
	 * addition to all data props with _ prefix.
150
	 * @since 2.6.0
151
	 * @return array
152
	 */
153
	protected function get_internal_meta_keys() {
154
		return array_merge( array_map( array( $this, 'prefix_key' ), array_keys( $this->_data ) ), $this->_internal_meta_keys );
155
	}
156
157
	/**
158
	 * Get Meta Data by Key.
159
	 * @since 2.6.0
160
	 * @param  string $key
161
	 * @param  bool $single return first found meta with key, or all with $key
162
	 * @return mixed
163
	 */
164
	public function get_meta( $key = '', $single = true ) {
165
		$array_keys = array_keys( wp_list_pluck( $this->get_meta_data(), 'key' ), $key );
166
		$value    = '';
167
168
		if ( ! empty( $array_keys ) ) {
169
			if ( $single ) {
170
				$value = $this->_meta_data[ current( $array_keys ) ]->value;
171
			} else {
172
				$value = array_intersect_key( $this->_meta_data, array_flip( $array_keys ) );
173
			}
174
		}
175
176
		return $value;
177
	}
178
179
	/**
180
	 * Set all meta data from array.
181
	 * @since 2.6.0
182
	 * @param array $data Key/Value pairs
183
	 */
184
	public function set_meta_data( $data ) {
185
		if ( ! empty( $data ) && is_array( $data ) ) {
186
			foreach ( $data as $meta ) {
187
				$meta = (array) $meta;
188
				if ( isset( $meta['key'], $meta['value'], $meta['id'] ) ) {
189
					$this->_meta_data[] = (object) array(
190
						'id'    => $meta['id'],
191
						'key'   => $meta['key'],
192
						'value' => $meta['value'],
193
					);
194
				}
195
			}
196
		}
197
	}
198
199
	/**
200
	 * Add meta data.
201
	 * @since 2.6.0
202
	 * @param string $key Meta key
203
	 * @param string $value Meta value
204
	 * @param bool $unique Should this be a unique key?
205
	 */
206
	public function add_meta_data( $key, $value, $unique = false ) {
207
		if ( $unique ) {
208
			$this->delete_meta_data( $key );
0 ignored issues
show
Documentation introduced by
$key is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
209
		}
210
		$this->_meta_data[] = (object) array(
211
			'key'   => $key,
212
			'value' => $value,
213
		);
214
	}
215
216
	/**
217
	 * Update meta data by key or ID, if provided.
218
	 * @since 2.6.0
219
	 * @param  string $key
220
	 * @param  string $value
221
	 * @param  int $meta_id
222
	 */
223
	public function update_meta_data( $key, $value, $meta_id = '' ) {
224
		$array_key = '';
225
		if ( $meta_id ) {
226
			$array_key = array_keys( wp_list_pluck( $this->_meta_data, 'id' ), $meta_id );
227
		}
228
		if ( $array_key ) {
229
			$this->_meta_data[ current( $array_key ) ] = (object) array(
230
				'id'    => $meta_id,
231
				'key'   => $key,
232
				'value' => $value,
233
			);
234
		} else {
235
			$this->add_meta_data( $key, $value, true );
236
		}
237
	}
238
239
	/**
240
	 * Delete meta data.
241
	 * @since 2.6.0
242
	 * @param array $key Meta key
243
	 */
244 View Code Duplication
	public function delete_meta_data( $key ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
245
		$array_keys = array_keys( wp_list_pluck( $this->_meta_data, 'key' ), $key );
246
		if ( $array_keys ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $array_keys of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
247
			foreach ( $array_keys as $array_key ) {
248
				$this->_meta_data[ $array_key ]->value = null;
249
			}
250
		}
251
	}
252
253
	/**
254
	 * Delete meta data.
255
	 * @since 2.6.0
256
	 * @param int $mid Meta ID
257
	 */
258 View Code Duplication
	public function delete_meta_data_by_mid( $mid ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
259
		$array_keys         = array_keys( wp_list_pluck( $this->_meta_data, 'id' ), $mid );
260
		if ( $array_keys ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $array_keys of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
261
			foreach ( $array_keys as $array_key ) {
262
				$this->_meta_data[ $array_key ]->value = null;
263
			}
264
		}
265
	}
266
267
	/**
268
	 * Callback to remove unwanted meta data.
269
	 *
270
	 * @param object $meta
271
	 * @return bool
272
	 */
273
	protected function exclude_internal_meta_keys( $meta ) {
274
		return ! in_array( $meta->meta_key, $this->get_internal_meta_keys() );
275
	}
276
277
	/**
278
	 * Read Meta Data from the database. Ignore any internal properties.
279
	 * @since 2.6.0
280
	 */
281
	protected function read_meta_data() {
282
		$this->_meta_data = array();
283
		$cache_loaded     = false;
284
285
		if ( ! $this->get_id() ) {
286
			return;
287
		}
288
289
		if ( ! empty( $this->_cache_group ) ) {
290
			$cache_key   = WC_Cache_Helper::get_cache_prefix( $this->_cache_group ) . $this->get_id();
291
			$cached_meta = wp_cache_get( $cache_key, $this->_cache_group );
292
293
			if ( false !== $cached_meta ) {
294
				$this->_meta_data = $cached_meta;
295
				$cache_loaded = true;
296
			}
297
		}
298
299
		if ( ! $cache_loaded ) {
300
			global $wpdb;
301
			$db_info = $this->_get_db_info();
302
			$raw_meta_data = $wpdb->get_results( $wpdb->prepare( "
303
				SELECT " . $db_info['meta_id_field'] . ", meta_key, meta_value
304
				FROM " . $db_info['table'] . "
305
				WHERE " . $db_info['object_id_field'] . "=%d AND meta_key NOT LIKE 'wp\_%%' ORDER BY " . $db_info['meta_id_field'] . "
306
			", $this->get_id() ) );
307
308
			if ( $raw_meta_data ) {
309
				$raw_meta_data = array_filter( $raw_meta_data, array( $this, 'exclude_internal_meta_keys' ) );
310
				foreach ( $raw_meta_data as $meta ) {
311
					$this->_meta_data[] = (object) array(
312
						'id'    => (int) $meta->{ $db_info['meta_id_field'] },
313
						'key'   => $meta->meta_key,
314
						'value' => maybe_unserialize( $meta->meta_value ),
315
					);
316
				}
317
			}
318
319
			if ( ! empty( $this->_cache_group ) ) {
320
				wp_cache_set( $cache_key, $this->_meta_data, $this->_cache_group );
321
			}
322
		}
323
	}
324
325
	/**
326
	 * Update Meta Data in the database.
327
	 * @since 2.6.0
328
	 */
329
	protected function save_meta_data() {
330
		foreach ( $this->_meta_data as $array_key => $meta ) {
331
			if ( is_null( $meta->value ) ) {
332
				if ( ! empty( $meta->id ) ) {
333
					delete_metadata_by_mid( $this->_meta_type, $meta->id );
334
				}
335
			} elseif ( empty( $meta->id ) ) {
336
				$new_meta_id = add_metadata( $this->_meta_type, $this->get_id(), $meta->key, $meta->value, false );
337
				$this->_meta_data[ $array_key ]->id = $new_meta_id;
338
			} else {
339
				update_metadata_by_mid( $this->_meta_type, $meta->id, $meta->value, $meta->key );
340
			}
341
		}
342
343
		if ( ! empty( $this->_cache_group ) ) {
344
			WC_Cache_Helper::incr_cache_prefix( $this->_cache_group );
345
		}
346
		$this->read_meta_data();
347
	}
348
349
	/**
350
	 * Table structure is slightly different between meta types, this function will return what we need to know.
351
	 * @since 2.6.0
352
	 * @return array Array elements: table, object_id_field, meta_id_field
353
	 */
354
	protected function _get_db_info() {
355
		global $wpdb;
356
357
		$meta_id_field   = 'meta_id'; // for some reason users calls this umeta_id so we need to track this as well.
358
		$table           = $wpdb->prefix;
359
360
		// If we are dealing with a type of metadata that is not a core type, the table should be prefixed.
361
		if ( ! in_array( $this->_meta_type, array( 'post', 'user', 'comment', 'term' ) ) ) {
362
			$table .= 'woocommerce_';
363
		}
364
365
		$table .= $this->_meta_type . 'meta';
366
		$object_id_field = $this->_meta_type . '_id';
367
368
		// Figure out our field names.
369
		if ( 'user' === $this->_meta_type ) {
370
			$meta_id_field   = 'umeta_id';
371
		}
372
373
		if ( ! empty( $this->object_id_field_for_meta ) ) {
374
			$object_id_field = $this->object_id_field_for_meta;
375
		}
376
377
		return array(
378
			'table'           => $table,
379
			'object_id_field' => $object_id_field,
380
			'meta_id_field'   => $meta_id_field,
381
		);
382
	}
383
384
	/**
385
	 * Set all props to default values.
386
	 */
387
	protected function set_defaults() {
388
		$this->_data = $this->_default_data;
389
	}
390
391
	/**
392
	 * Set a collection of props in one go, collect any errors, and return the result.
393
	 * @param array $props Key value pairs to set. Key is the prop and should map to a setter function name.
394
	 * @return WP_Error|bool
395
	 */
396
	public function set_props( $props ) {
397
		$errors = new WP_Error();
398
399
		foreach ( $props as $prop => $value ) {
400
			try {
401
				if ( 'meta_data' === $prop ) {
402
					continue;
403
				}
404
				$setter = "set_$prop";
405
				if ( ! is_null( $value ) && is_callable( array( $this, $setter ) ) ) {
406
					$this->{$setter}( $value );
407
				}
408
			} catch ( WC_Data_Exception $e ) {
409
				$errors->add( $e->getErrorCode(), $e->getMessage() );
410
			}
411
		}
412
413
		return sizeof( $errors->get_error_codes() ) ? $errors : true;
414
	}
415
416
	/**
417
	 * When invalid data is found, throw an exception unless reading from the DB.
418
	 * @param string $error_code Error code.
419
	 * @param string $error_message Error message.
420
	 * @throws WC_Data_Exception
421
	 */
422
	protected function error( $error_code, $error_message ) {
423
		throw new WC_Data_Exception( $error_code, $error_message );
424
	}
425
}
426