Completed
Pull Request — develop (#1184)
by Naveen
03:08
created

Wordlift_Relation_Service::order_by()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 1
dl 0
loc 13
rs 9.8333
c 0
b 0
f 0
1
<?php
2
/**
3
 * Services: Relation Service.
4
 *
5
 * The Relation Service provides helpful function to get related posts/entities.
6
 *
7
 * @since      3.15.0
8
 * @package    Wordlift
9
 * @subpackage Wordlift/includes
10
 */
11
12
/**
13
 * Define the {@link Wordlift_Relation_Service} class.
14
 *
15
 * @since      3.15.0
16
 * @package    Wordlift
17
 * @subpackage Wordlift/includes
18
 */
19
class Wordlift_Relation_Service {
20
21
	/**
22
	 * The singleton instance.
23
	 *
24
	 * @since  3.15.0
25
	 * @access private
26
	 * @var \Wordlift_Relation_Service $instance The singleton instance.
27
	 */
28
	private static $instance;
29
30
	/**
31
	 * The relation table name in MySQL, set during instantiation.
32
	 *
33
	 * @since  3.15.0
34
	 * @access private
35
	 * @var string $relation_table The relation table name.
36
	 */
37
	private $relation_table;
38
39
	/**
40
	 * A {@link Wordlift_Log_Service} instance.
41
	 *
42
	 * @since 3.15.3
43
	 *
44
	 * @var Wordlift_Log_Service $log A {@link Wordlift_Log_Service} instance.
45
	 */
46
	private static $log;
47
48
	/**
49
	 * Create a {@link Wordlift_Relation_Service} instance.
50
	 *
51
	 * @since 3.15.0
52
	 */
53
	public function __construct() {
54
55
		self::$log = Wordlift_Log_Service::get_logger( get_class() );
56
57
		global $wpdb;
58
59
		// The relations table.
60
		$this->relation_table = "{$wpdb->prefix}wl_relation_instances";
61
62
		self::$instance = $this;
63
64
	}
65
66
	/**
67
	 * Get the singleton instance.
68
	 *
69
	 * @return \Wordlift_Relation_Service The {@link Wordlift_Relation_Service}
70
	 *                                    singleton instance.
71
	 * @since  3.15.0
72
	 * @access public
73
	 */
74
	public static function get_instance() {
75
76
		return self::$instance;
77
	}
78
79
	/**
80
	 * Get the articles referencing the specified entity {@link WP_Post}.
81
	 *
82
	 * @param int|array $object_id The entity {@link WP_Post}'s id.
83
	 * @param string $fields The fields to return, 'ids' to only return ids or
84
	 *                               '*' to return all fields, by default '*'.
85
	 * @param null|string $predicate The predicate (who|what|...), by default all.
86
	 * @param null|string $status The status, by default all.
87
	 * @param array $excludes An array of ids to exclude from the results.
88
	 * @param null|int $limit The maximum number of results, by default
89
	 *                               no limit.
90
	 * @param null|array $include The {@link WP_Post}s' ids to include.
91
	 *
92
	 * @param null | string $order_by
93
	 *
94
	 * @return array|object|null Database query results
95
	 * @since 3.15.0
96
	 *
97
	 */
98
	public function get_article_subjects( $object_id, $fields = '*', $predicate = null, $status = null, $excludes = array(), $limit = null, $include = null, $order_by = null ) {
99
		global $wpdb;
100
101
		// The output fields.
102
		$actual_fields = self::fields( $fields );
103
104
		self::$log->trace( 'Getting article subjects for object ' . implode( ', ', (array) $object_id ) . '...' );
105
106
		$objects = $this->article_id_to_entity_id( $object_id );
107
108
		// If there are no related objects, return an empty array.
109
		if ( empty( $objects ) ) {
110
			self::$log->debug( 'No entities found for object ' . implode( ', ', (array) $object_id ) . '.' );
111
112
			return array();
113
		}
114
115
		self::$log->debug( count( $objects ) . ' entity id(s) found for object ' . implode( ', ', (array) $object_id ) . '.' );
116
117
		$sql =
118
			"
119
			SELECT DISTINCT p.$actual_fields
120
			FROM {$this->relation_table} r
121
			INNER JOIN $wpdb->posts p
122
				ON p.id = r.subject_id
123
			"
124
			// Add the status clause.
125
			. self::and_status( $status )
126
			. self::inner_join_is_article()
127
			. self::where_object_id( $objects )
128
			// Since `object_id` can be an article ID we need to exclude it from
129
			// the results.
130
			. self::and_article_not_in( array_merge( $excludes, (array) $object_id ) )
131
			. self::and_article_in( $include )
132
			. self::and_post_type_in()
133
			. self::and_predicate( $predicate )
134
			. self::order_by( $order_by )
135
			. self::limit( $limit );
136
137
		return '*' === $actual_fields ? $wpdb->get_results( $sql ) : $wpdb->get_col( $sql );
138
	}
139
140
	/**
141
	 * The `post_type IN` clause.
142
	 *
143
	 * @return string The `post_type IN` clause.
144
	 * @since 3.15.3
145
	 *
146
	 */
147
	private static function and_post_type_in() {
148
149
		return " AND p.post_type IN ( '"
150
		       . implode(
151
			       "','",
152
			       array_map( 'esc_sql', Wordlift_Entity_Service::valid_entity_post_types() )
153
		       )
154
		       . "' )";
155
	}
156
157
	/**
158
	 * Add the limit clause if specified.
159
	 *
160
	 * @param null|int $limit The maximum number of results.
161
	 *
162
	 * @return string The limit clause (empty if no limit has been specified).
163
	 * @since 3.15.0
164
	 *
165
	 */
166
	private static function limit( $limit = null ) {
167
168
		if ( null === $limit ) {
169
			return '';
170
		}
171
172
		return "LIMIT $limit";
173
	}
174
175
	/**
176
	 * @param $order_by string | null
177
	 *
178
	 * @return string
179
	 */
180
	private static function order_by( $order_by ) {
181
		if ( ! $order_by ) {
182
			return '';
183
		}
184
		$order_by         = (string) $order_by;
185
		$order_by_clauses = array( 'DESC', 'ASC' );
186
187
		if ( in_array( $order_by, $order_by_clauses, true ) ) {
188
			return " ORDER BY p.ID ${order_by} ";
189
		} else {
190
			return ' ORDER BY p.ID DESC ';
191
		}
192
	}
193
194
	/**
195
	 * Map the provided ids into entities (i.e. return the id if it's an entity
196
	 * or get the entities if it's a post).
197
	 *
198
	 * @param int|array $object_id An array of posts/entities' ids.
199
	 *
200
	 * @return array An array of entities' ids.
201
	 * @since 3.15.0
202
	 *
203
	 */
204
	private function article_id_to_entity_id( $object_id ) {
205
206
		$entity_service = Wordlift_Entity_Service::get_instance();
207
208
		$relation_service = $this;
209
210
		return array_reduce( (array) $object_id, function ( $carry, $item ) use ( $entity_service, $relation_service ) {
211
			if ( $entity_service->is_entity( $item ) ) {
212
				return array_merge( $carry, (array) $item );
213
			}
214
215
			return array_merge( $carry, $relation_service->get_objects( $item, 'ids' ) );
216
		}, array() );
217
218
	}
219
220
	/**
221
	 * Add the WHERE clause.
222
	 *
223
	 * @param int|array $object_id An array of {@link WP_Post}s' ids.
224
	 *
225
	 * @return string The WHERE clause.
226
	 * @since 3.15.0
227
	 *
228
	 */
229
	private static function where_object_id( $object_id ) {
230
231
		if ( empty( $object_id ) ) {
232
			// self::$log->warn( sprintf( "%s `where_object_id` called with empty `object_id`.", var_export( debug_backtrace( false, 3 ), true ) ) );
233
234
			return ' WHERE 1 = 1';
235
		}
236
237
		return ' WHERE r.object_id IN ( ' . implode( ',', wp_parse_id_list( (array) $object_id ) ) . ' )';
238
	}
239
240
	/**
241
	 * Add the exclude clause.
242
	 *
243
	 * @param int|array $exclude An array of {@link WP_Post}s' ids to exclude.
244
	 *
245
	 * @return string The exclude clause.
246
	 * @since 3.15.0
247
	 *
248
	 */
249
	private static function and_article_not_in( $exclude ) {
250
251
		return ' AND NOT p.ID IN ( ' . implode( ',', wp_parse_id_list( (array) $exclude ) ) . ' )';
252
	}
253
254
	/**
255
	 * Add the include clause.
256
	 *
257
	 * @param null|int|array $include An array of {@link WP_Post}s' ids.
258
	 *
259
	 * @return string An empty string if $include is null otherwise the include
260
	 *                clause.
261
	 * @since 3.15.0
262
	 *
263
	 */
264
	private static function and_article_in( $include = null ) {
265
266
		if ( null === $include ) {
267
			return '';
268
		}
269
270
		return ' AND p.ID IN ( ' . implode( ',', wp_parse_id_list( (array) $include ) ) . ' )';
271
	}
272
273
	/**
274
	 * Get the entities' {@link WP_Post}s' ids referencing the specified {@link WP_Post}.
275
	 *
276
	 * @param int $object_id The object {@link WP_Post}'s id.
277
	 * @param string $fields The fields to return, 'ids' to only return ids or
278
	 *                               '*' to return all fields, by default '*'.
279
	 * @param null|string $status The status, by default all.
280
	 *
281
	 * @return array|object|null Database query results
282
	 * @since 3.15.0
283
	 *
284
	 */
285 View Code Duplication
	public function get_non_article_subjects( $object_id, $fields = '*', $status = null ) {
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...
286
		global $wpdb;
287
288
		// The output fields.
289
		$actual_fields = self::fields( $fields );
290
291
		$sql = $wpdb->prepare(
292
			"
293
			SELECT p.$actual_fields
294
			FROM {$this->relation_table} r
295
			INNER JOIN $wpdb->posts p
296
				ON p.id = r.subject_id
297
			"
298
			// Add the status clause.
299
			. self::and_status( $status )
300
			. self::inner_join_is_not_article()
301
			. " WHERE r.object_id = %d "
302
			. self::and_post_type_in()
303
			,
304
			$object_id
305
		);
306
307
		return '*' === $actual_fields ? $wpdb->get_results( $sql ) : $wpdb->get_col( $sql );
308
	}
309
310
	/**
311
	 * Get the entities referenced by the specified {@link WP_Post}.
312
	 *
313
	 * @param int $subject_id The {@link WP_Post}'s id.
314
	 * @param string $fields The fields to return, 'ids' to only return ids or
315
	 *                                '*' to return all fields, by default '*'.
316
	 * @param null|string $predicate The predicate (who|what|...), by default all.
317
	 * @param null|string $status The status, by default all.
318
	 *
319
	 * @return array|object|null Database query results
320
	 * @since 3.15.0
321
	 *
322
	 */
323 View Code Duplication
	public function get_objects( $subject_id, $fields = '*', $predicate = null, $status = null ) {
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...
324
		global $wpdb;
325
326
		// The output fields.
327
		$actual_fields = self::fields( $fields );
328
329
		$sql = $wpdb->prepare(
330
			"
331
			SELECT p.$actual_fields
332
			FROM {$this->relation_table} r
333
			INNER JOIN $wpdb->posts p
334
				ON p.id = r.object_id
335
			"
336
			// Add the status clause.
337
			. self::and_status( $status )
338
			. self::inner_join_is_not_article()
339
			. " WHERE r.subject_id = %d "
340
			. self::and_post_type_in()
341
			. self::and_predicate( $predicate )
342
			,
343
			$subject_id
344
		);
345
346
		return '*' === $actual_fields ? $wpdb->get_results( $sql ) : $wpdb->get_col( $sql );
347
	}
348
349
	/**
350
	 * Add the `post_status` clause.
351
	 *
352
	 * @param null|string|array $status The status values.
353
	 *
354
	 * @return string An empty string if $status is null, otherwise the status clause.
355
	 * @since 3.15.0
356
	 *
357
	 */
358 View Code Duplication
	private static function and_status( $status = null ) {
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...
359
360
		if ( null === $status ) {
361
			return '';
362
		}
363
364
		return " AND p.post_status IN ('" . implode( "', '", array_map( 'esc_sql', (array) $status ) ) . "')";
365
	}
366
367
	/**
368
	 * Add the `predicate` clause.
369
	 *
370
	 * @param null|string|array $predicate An array of predicates.
371
	 *
372
	 * @return string An empty string if $predicate is null otherwise the predicate
373
	 *                clause.
374
	 * @since 3.15.0
375
	 *
376
	 */
377 View Code Duplication
	private static function and_predicate( $predicate = null ) {
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...
378
379
		if ( null === $predicate ) {
380
			return '';
381
		}
382
383
		return " AND r.predicate IN ('" . implode( "', '", array_map( 'esc_sql', (array) $predicate ) ) . "')";
384
	}
385
386
	/**
387
	 * The select fields.
388
	 *
389
	 * @param string $fields Either 'ids' or '*', by default '*'.
390
	 *
391
	 * @return string The `id` field if `ids` otherwise `*`.
392
	 * @since 3.15.0
393
	 *
394
	 */
395
	private static function fields( $fields = '*' ) {
396
397
		// The output fields.
398
		return 'ids' === $fields ? 'id' : '*';
399
	}
400
401
	/**
402
	 * The inner join clause for articles.
403
	 *
404
	 * @return string The articles inner join clause.
405
	 * @since 3.15.0
406
	 *
407
	 */
408
	private static function inner_join_is_article() {
409
		global $wpdb;
410
411
		return $wpdb->prepare(
412
			"
413
			INNER JOIN $wpdb->term_relationships tr
414
			 ON p.id = tr.object_id
415
			INNER JOIN $wpdb->term_taxonomy tt
416
			 ON tt.term_taxonomy_id = tr.term_taxonomy_id
417
			  AND tt.taxonomy = %s
418
			INNER JOIN $wpdb->terms t
419
			 ON t.term_id = tt.term_id
420
			  AND t.slug = %s
421
			",
422
			'wl_entity_type',
423
			'article'
424
		);
425
	}
426
427
	/**
428
	 * The inner join clause for non-articles.
429
	 *
430
	 * @return string The non-articles inner join clause.
431
	 * @since 3.15.0
432
	 *
433
	 */
434
	private static function inner_join_is_not_article() {
435
		global $wpdb;
436
437
		return $wpdb->prepare(
438
			"
439
			INNER JOIN $wpdb->term_relationships tr
440
			 ON p.id = tr.object_id
441
			INNER JOIN $wpdb->term_taxonomy tt
442
			 ON tt.term_taxonomy_id = tr.term_taxonomy_id
443
			  AND tt.taxonomy = %s
444
			INNER JOIN $wpdb->terms t
445
			 ON t.term_id = tt.term_id
446
			  AND NOT t.slug = %s
447
			",
448
			'wl_entity_type',
449
			'article'
450
		);
451
	}
452
453
	/**
454
	 * Find all the subject IDs and their referenced/related object IDs. The
455
	 * object IDs are returned as comma separated IDs in the `object_ids` key.
456
	 *
457
	 * @return mixed Database query results
458
	 * @since 3.18.0
459
	 */
460
	public function find_all_grouped_by_subject_id() {
461
		global $wpdb;
462
463
		return $wpdb->get_results(
464
			"
465
			SELECT subject_id, GROUP_CONCAT( DISTINCT object_id ORDER BY object_id SEPARATOR ',' ) AS object_ids
466
			FROM $this->relation_table
467
			GROUP BY subject_id
468
			"
469
		);
470
471
	}
472
473
}
474