Completed
Pull Request — master (#10259)
by Mike
08:16
created

WC_CLI_Command::format_datetime()   B

Complexity

Conditions 6
Paths 19

Size

Total Lines 28
Code Lines 17

Duplication

Lines 28
Ratio 100 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 28
loc 28
rs 8.439
cc 6
eloc 17
nc 19
nop 2
1
<?php
2
3
/**
4
 * WooCommerce CLI Command.
5
 *
6
 * Base class that must be extended by any WooCommerce sub commands.
7
 *
8
 * @class    WC_CLI_Command
9
 * @version  2.5.0
10
 * @package  WooCommerce/CLI
11
 * @category CLI
12
 * @author   WooThemes
13
 */
14
class WC_CLI_Command extends WP_CLI_Command {
15
16
	/**
17
	 * Add common cli arguments to argument list before WP_Query is run.
18
	 *
19
	 * @since  2.5.0
20
	 * @param  array $base_args  Required arguments for the query (e.g. `post_type`, etc)
21
	 * @param  array $assoc_args Arguments provided in when invoking the command
22
	 * @return array
23
	 */
24
	protected function merge_wp_query_args( $base_args, $assoc_args ) {
25
		$args = array();
26
27
		// date
28
		if ( ! empty( $assoc_args['created_at_min'] ) || ! empty( $assoc_args['created_at_max'] ) || ! empty( $assoc_args['updated_at_min'] ) || ! empty( $assoc_args['updated_at_max'] ) ) {
29
30
			$args['date_query'] = array();
31
32
			// resources created after specified date
33 View Code Duplication
			if ( ! empty( $assoc_args['created_at_min'] ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
34
				$args['date_query'][] = array( 'column' => 'post_date_gmt', 'after' => $this->parse_datetime( $assoc_args['created_at_min'] ), 'inclusive' => true );
35
			}
36
37
			// resources created before specified date
38 View Code Duplication
			if ( ! empty( $assoc_args['created_at_max'] ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
39
				$args['date_query'][] = array( 'column' => 'post_date_gmt', 'before' => $this->parse_datetime( $assoc_args['created_at_max'] ), 'inclusive' => true );
40
			}
41
42
			// resources updated after specified date
43 View Code Duplication
			if ( ! empty( $assoc_args['updated_at_min'] ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
44
				$args['date_query'][] = array( 'column' => 'post_modified_gmt', 'after' => $this->parse_datetime( $assoc_args['updated_at_min'] ), 'inclusive' => true );
45
			}
46
47
			// resources updated before specified date
48 View Code Duplication
			if ( ! empty( $assoc_args['updated_at_max'] ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
49
				$args['date_query'][] = array( 'column' => 'post_modified_gmt', 'before' => $this->parse_datetime( $assoc_args['updated_at_max'] ), 'inclusive' => true );
50
			}
51
		}
52
53
		// Search.
54
		if ( ! empty( $assoc_args['q'] ) ) {
55
			$args['s'] = $assoc_args['q'];
56
		}
57
58
		// Number of post to show per page.
59
		if ( ! empty( $assoc_args['limit'] ) ) {
60
			$args['posts_per_page'] = $assoc_args['limit'];
61
		}
62
63
		// Number of post to displace or pass over.
64
		if ( ! empty( $assoc_args['offset'] ) ) {
65
			$args['offset'] = $assoc_args['offset'];
66
		}
67
68
		// order (ASC or DESC, DESC by default).
69
		if ( ! empty( $assoc_args['order'] ) ) {
70
			$args['order'] = $assoc_args['order'];
71
		}
72
73
		// orderby.
74 View Code Duplication
		if ( ! empty( $assoc_args['orderby'] ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
75
			$args['orderby'] = $assoc_args['orderby'];
76
77
			// allow sorting by meta value
78
			if ( ! empty( $assoc_args['orderby_meta_key'] ) ) {
79
				$args['meta_key'] = $assoc_args['orderby_meta_key'];
80
			}
81
		}
82
83
		// allow post status change
84
		if ( ! empty( $assoc_args['post_status'] ) ) {
85
			$args['post_status'] = $assoc_args['post_status'];
86
			unset( $assoc_args['post_status'] );
87
		}
88
89
		// filter by a list of post ids
90 View Code Duplication
		if ( ! empty( $assoc_args['in'] ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
91
			$args['post__in'] = explode( ',', $assoc_args['in'] );
92
			unset( $assoc_args['in'] );
93
		}
94
95
		// exclude by a list of post ids
96 View Code Duplication
		if ( ! empty( $assoc_args['not_in'] ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
97
			$args['post__not_in'] = explode( ',', $assoc_args['not_in'] );
98
			unset( $assoc_args['not_in'] );
99
		}
100
101
		// posts page.
102
		$args['paged'] = ( isset( $assoc_args['page'] ) ) ? absint( $assoc_args['page'] ) : 1;
103
104
		$args = apply_filters( 'woocommerce_cli_query_args', $args, $assoc_args );
105
106
		return array_merge( $base_args, $args );
107
	}
108
109
	/**
110
	 * Add common cli arguments to argument list before WP_User_Query is run.
111
	 *
112
	 * @since  2.5.0
113
	 * @param  array $base_args required arguments for the query (e.g. `post_type`, etc)
114
	 * @param  array $assoc_args arguments provided in when invoking the command
115
	 * @return array
116
	 */
117
	protected function merge_wp_user_query_args( $base_args, $assoc_args ) {
118
		$args = array();
119
120
		// Custom Role
121
		if ( ! empty( $assoc_args['role'] ) ) {
122
			$args['role'] = $assoc_args['role'];
123
		}
124
125
		// Search
126
		if ( ! empty( $assoc_args['q'] ) ) {
127
			$args['search'] = $assoc_args['q'];
128
		}
129
130
		// Limit number of users returned.
131
		if ( ! empty( $assoc_args['limit'] ) ) {
132
			$args['number'] = absint( $assoc_args['limit'] );
133
		}
134
135
		// Offset
136
		if ( ! empty( $assoc_args['offset'] ) ) {
137
			$args['offset'] = absint( $assoc_args['offset'] );
138
		}
139
140
		// date
141
		if ( ! empty( $assoc_args['created_at_min'] ) || ! empty( $assoc_args['created_at_max'] ) ) {
142
143
			$args['date_query'] = array();
144
145
			// resources created after specified date
146
			if ( ! empty( $assoc_args['created_at_min'] ) ) {
147
				$args['date_query'][] = array( 'after' => $this->parse_datetime( $assoc_args['created_at_min'] ), 'inclusive' => true );
148
			}
149
150
			// resources created before specified date
151 View Code Duplication
			if ( ! empty( $assoc_args['created_at_max'] ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
152
				$args['date_query'][] = array( 'before' => $this->parse_datetime( $assoc_args['created_at_max'] ), 'inclusive' => true );
153
			}
154
		}
155
156
		// Order (ASC or DESC, ASC by default).
157
		if ( ! empty( $assoc_args['order'] ) ) {
158
			$args['order'] = $assoc_args['order'];
159
		}
160
161
		// Orderby.
162
		if ( ! empty( $assoc_args['orderby'] ) ) {
163
			$args['orderby'] = $assoc_args['orderby'];
164
		}
165
166
		$args = apply_filters( 'woocommerce_cli_user_query_args', $args, $assoc_args );
167
168
		return array_merge( $base_args, $args );
169
	}
170
171
	/**
172
	 * Parse an RFC3339 datetime into a MySQl datetime.
173
	 *
174
	 * Invalid dates default to unix epoch.
175
	 *
176
	 * @since  2.5.0
177
	 * @param  string $datetime RFC3339 datetime
178
	 * @return string MySQl datetime (YYYY-MM-DD HH:MM:SS)
179
	 */
180 View Code Duplication
	protected function parse_datetime( $datetime ) {
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...
181
		// Strip millisecond precision (a full stop followed by one or more digits)
182
		if ( strpos( $datetime, '.' ) !== false ) {
183
			$datetime = preg_replace( '/\.\d+/', '', $datetime );
184
		}
185
186
		// default timezone to UTC
187
		$datetime = preg_replace( '/[+-]\d+:+\d+$/', '+00:00', $datetime );
188
189
		try {
190
191
			$datetime = new DateTime( $datetime, new DateTimeZone( 'UTC' ) );
192
193
		} catch ( Exception $e ) {
194
195
			$datetime = new DateTime( '@0' );
196
197
		}
198
199
		return $datetime->format( 'Y-m-d H:i:s' );
200
	}
201
202
	/**
203
	 * Format a unix timestamp or MySQL datetime into an RFC3339 datetime.
204
	 *
205
	 * @since  2.5.0
206
	 * @param  int|string $timestamp unix timestamp or MySQL datetime
207
	 * @param  bool $convert_to_utc
208
	 * @return string RFC3339 datetime
209
	 */
210 View Code Duplication
	protected function format_datetime( $timestamp, $convert_to_utc = false ) {
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...
211
		if ( ! $timestamp ) {
212
			return '';
213
		}
214
		if ( $convert_to_utc ) {
215
			$timezone = new DateTimeZone( wc_timezone_string() );
216
		} else {
217
			$timezone = new DateTimeZone( 'UTC' );
218
		}
219
220
		try {
221
			if ( is_numeric( $timestamp ) ) {
222
				$date = new DateTime( "@{$timestamp}" );
223
			} else {
224
				$date = new DateTime( $timestamp, $timezone );
225
			}
226
227
			// convert to UTC by adjusting the time based on the offset of the site's timezone
228
			if ( $convert_to_utc ) {
229
				$date->modify( -1 * $date->getOffset() . ' seconds' );
230
			}
231
232
		} catch ( Exception $e ) {
233
			$date = new DateTime( '@0' );
234
		}
235
236
		return $date->format( 'Y-m-d\TH:i:s\Z' );
237
	}
238
239
	/**
240
	 * Get formatter object based on supplied arguments.
241
	 *
242
	 * @since  2.5.0
243
	 * @param  array $assoc_args Associative args from CLI to determine formatting
244
	 * @return \WP_CLI\Formatter
245
	 */
246
	protected function get_formatter( $assoc_args ) {
247
		$args = $this->get_format_args( $assoc_args );
248
		return new \WP_CLI\Formatter( $args );
249
	}
250
251
	/**
252
	 * Get default fields for formatter.
253
	 *
254
	 * Class that extends WC_CLI_Command should override this method.
255
	 *
256
	 * @since  2.5.0
257
	 * @return null|string|array
258
	 */
259
	protected function get_default_format_fields() {
260
		return null;
261
	}
262
263
	/**
264
	 * Get format args that will be passed into CLI Formatter.
265
	 *
266
	 * @since  2.5.0
267
	 * @param  array $assoc_args Associative args from CLI
268
	 * @return array Formatter args
269
	 */
270
	protected function get_format_args( $assoc_args ) {
271
		$format_args = array(
272
			'fields' => $this->get_default_format_fields(),
273
			'field'  => null,
274
			'format' => 'table',
275
		);
276
277
		if ( isset( $assoc_args['fields'] ) ) {
278
			$format_args['fields'] = $assoc_args['fields'];
279
		}
280
281
		if ( isset( $assoc_args['field'] ) ) {
282
			$format_args['field'] = $assoc_args['field'];
283
		}
284
285
		if ( ! empty( $assoc_args['format'] ) && in_array( $assoc_args['format'], array( 'count', 'ids', 'table', 'csv', 'json' ) ) ) {
286
			$format_args['format'] = $assoc_args['format'];
287
		}
288
289
		return $format_args;
290
	}
291
292
	/**
293
	 * Flatten multidimensional array in which nested array will be prefixed with
294
	 * parent keys separated with dot char, e.g. given an array:
295
	 *
296
	 *     array(
297
	 *         'a' => array(
298
	 *             'b' => array(
299
	 *                 'c' => ...
300
	 *             )
301
	 *         )
302
	 *     )
303
	 *
304
	 * a flatten array would contain key 'a.b.c' => ...
305
	 *
306
	 * @since 2.5.0
307
	 * @param array  $arr    Array that may contains nested array
308
	 * @param string $prefix Prefix
309
	 *
310
	 * @return array Flattened array
311
	 */
312
	protected function flatten_array( $arr, $prefix = '' ) {
313
		$flattened = array();
314
		foreach ( $arr as $key => $value ) {
315
			if ( is_array( $value ) ) {
316
				if ( sizeof( $value ) > 0 ) {
317
318
					// Full access to whole elements if indices are numerical.
319
					$flattened[ $prefix . $key ] = $value;
320
321
					// This is naive assumption that if element with index zero
322
					// exists then array indices are numberical.
323
					if ( ! empty( $value[0] ) ) {
324
325
						// Allow size of array to be accessed, i.e., a.b.arr.size
326
						$flattened[ $prefix . $key . '.size' ] = sizeof( $value );
327
					}
328
329
					$flattened = array_merge( $flattened, $this->flatten_array( $value, $prefix . $key . '.' ) );
330
				} else {
331
					$flattened[ $prefix . $key ] = '';
332
333
					// Tells user that size of this array is zero.
334
					$flattened[ $prefix . $key . '.size' ] = 0;
335
				}
336
			} else {
337
				$flattened[ $prefix . $key ] = $value;
338
			}
339
		}
340
341
		return $flattened;
342
	}
343
344
	/**
345
	 * Unflatten array will make key 'a.b.c' becomes nested array:
346
	 *
347
	 *     array(
348
	 *         'a' => array(
349
	 *             'b' => array(
350
	 *                 'c' => ...
351
	 *             )
352
	 *         )
353
	 *     )
354
	 *
355
	 * @since  2.5.0
356
	 * @param  array $arr Flattened array
357
	 * @return array
358
	 */
359
	protected function unflatten_array( $arr ) {
360
		$unflatten = array();
361
362
		foreach ( $arr as $key => $value ) {
363
			$key_list  = explode( '.', $key );
364
			$first_key = array_shift( $key_list );
365
			$first_key = $this->get_normalized_array_key( $first_key );
366
			if ( sizeof( $key_list ) > 0 ) {
367
				$remaining_keys = implode( '.', $key_list );
368
				$subarray       = $this->unflatten_array( array( $remaining_keys => $value ) );
369
370
				foreach ( $subarray as $sub_key => $sub_value ) {
371
					$sub_key = $this->get_normalized_array_key( $sub_key );
372
					if ( ! empty( $unflatten[ $first_key ][ $sub_key ] ) ) {
373
						$unflatten[ $first_key ][ $sub_key ] = array_merge( $unflatten[ $first_key ][ $sub_key ], $sub_value );
374
					} else {
375
						$unflatten[ $first_key ][ $sub_key ] = $sub_value;
376
					}
377
				}
378
			} else {
379
				$unflatten[ $first_key ] = $value;
380
			}
381
		}
382
383
		return $unflatten;
384
	}
385
386
	/**
387
	 * Get normalized array key. If key is a numeric one it will be converted
388
	 * as absolute integer.
389
	 *
390
	 * @since  2.5.0
391
	 * @param  string $key Array key
392
	 * @return string|int
393
	 */
394
	protected function get_normalized_array_key( $key ) {
395
		if ( is_numeric( $key ) ) {
396
			$key = absint( $key );
397
		}
398
		return $key;
399
	}
400
}
401