Test Setup Failed
Push — issue/3871 ( 31b6b8 )
by Ravinder
07:30
created

Give_Donor_Wall   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 479
Duplicated Lines 1.88 %

Coupling/Cohesion

Components 2
Dependencies 3

Importance

Changes 0
Metric Value
dl 9
loc 479
rs 8.72
c 0
b 0
f 0
wmc 46
lcom 2
cbo 3

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 2 1
A get_instance() 9 9 2
A setup_actions() 0 8 1
B render_shortcode() 0 55 8
A parse_atts() 0 53 3
A get_donors() 0 6 1
A ajax_handler() 0 25 2
A get_query_param() 0 16 3
B get_donation_data() 0 51 9
B get_donations() 0 51 6
B get_donor_comments() 0 51 9
A has_donations() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Give_Donor_Wall often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Give_Donor_Wall, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Donors Gravatars
4
 *
5
 * @package     Give
6
 * @subpackage  Classes/Give_Donors_Gravatars
7
 * @copyright   Copyright (c) 2016, GiveWP
8
 * @license     https://opensource.org/licenses/gpl-license GNU Public License
9
 * @since       1.0
10
 */
11
12
// Exit if accessed directly.
13
if ( ! defined( 'ABSPATH' ) ) {
14
	exit;
15
}
16
17
18
/**
19
 * Give_Donor_Wall Class
20
 *
21
 * This class handles donors.
22
 *
23
 * @since 2.2.0
24
 */
25
class Give_Donor_Wall {
0 ignored issues
show
Coding Style introduced by
Since you have declared the constructor as private, maybe you should also declare the class as final.
Loading history...
26
27
	/**
28
	 * Instance.
29
	 *
30
	 * @since  2.2.0
31
	 * @access private
32
	 * @var Give_Donor_Wall
33
	 */
34
	static private $instance;
35
36
	/**
37
	 * Singleton pattern.
38
	 *
39
	 * @since  2.2.0
40
	 * @access private
41
	 */
42
	private function __construct() {
43
	}
44
45
46
	/**
47
	 * Get instance.
48
	 *
49
	 * @since  2.2.0
50
	 * @access public
51
	 * @return Give_Donor_Wall
52
	 */
53 View Code Duplication
	public static function get_instance() {
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...
54
		if ( null === static::$instance ) {
55
			self::$instance = new static();
56
57
			self::$instance->setup_actions();
58
		}
59
60
		return self::$instance;
61
	}
62
63
	/**
64
	 * Setup the default hooks and actions
65
	 *
66
	 * @since  2.2.0
67
	 *
68
	 * @return void
69
	 */
70
	public function setup_actions() {
71
72
		add_shortcode( 'give_donor_wall', array( $this, 'render_shortcode' ) );
73
74
		add_action( 'wp_ajax_give_get_donor_comments', array( $this, 'ajax_handler' ) );
75
		add_action( 'wp_ajax_nopriv_give_get_donor_comments', array( $this, 'ajax_handler' ) );
76
77
	}
78
79
80
	/**
81
	 * Displays donors in a grid layout.
82
	 *
83
	 * @since  2.2.0
84
	 *
85
	 * @param array $atts                {
86
	 *                                   Optional. Attributes of the donor wall shortcode.
87
	 *
88
	 * @type int    $donors_per_page     Number of donors per page. Default '20'.
89
	 * @type int    $form_id             The donation form to filter donors by. Default is all forms (no filter).
90
	 * @type bool   $paged               Whether to paginate donors. Default 'true'.
91
	 * @type string $ids                 A comma-separated list of donor IDs to display. Default empty.
92
	 * @type string $columns             Maximum columns to display. Default 'best-fit'.
93
	 *                                   Accepts 'best-fit', '1', '2', '3', '4'.
94
	 * @type bool   $show_avatar         Whether to display the donor's gravatar image if available. Default 'true'.
95
	 * @type bool   $show_name           Whether to display the donor's full name, first and last. Default 'true'.
96
	 * @type bool   $show_total          Whether to display the donor's donation amount. Default 'true'.
97
	 * @type bool   $show_time           Whether to display date of the last donation. Default 'true'.
98
	 * @type bool   $show_comments       Whether to display the donor's comment if they left one. Default 'true'.
99
	 * @type int    $comment_length      The number of words to display for the comments before a "Read more" field
100
	 * @type int    $only_comments       Whether to display the donors only with comment. Default 'false'.
101
	 *
102
	 * @type string $readmore_text       Link label for modal in which donor can read full comment.
103
	 * @type string $loadmore_text       Button label which will load more donor comments.
104
	 * @type int    $avatar_size         Avatar image size in pixels without the "px". Default "60"
105
	 * @type string $orderby             The order in which you want the donations to appear.
106
	 *                                   Currently we are using this attribute internally and it will sort donations by created date.
107
	 * @type string $order               The order in which you want the donors to appear. Accepts "ASC". "DESC".
108
	 *
109
	 * }
110
	 * @return string|bool The markup of the form grid or false.
111
	 */
112
	public function render_shortcode( $atts ) {
113
114
		$give_settings = give_get_settings();
115
116
		$atts      = $this->parse_atts( $atts );
117
		$donations = $this->get_donation_data( $atts );
118
		$html      = '';
119
120
		if ( $donations ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $donations 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...
121
122
			ob_start();
123
124
			foreach ( $donations as $donation ) {
125
				// Give/templates/shortcode-donor-wall.php.
126
				give_get_template( 'shortcode-donor-wall', array( $donation, $give_settings, $atts ) );
127
			}
128
129
			$html = ob_get_clean();
130
131
			// Return only donor html.
132
			if (
133
				isset( $atts['only_donor_html'] )
134
				&& wp_doing_ajax()
135
				&& $atts['only_donor_html']
136
			) {
137
				return $html;
138
			}
139
		}
140
141
		$temp_atts          = $atts;
142
		$temp_atts['paged'] = $atts['paged'] + 1;
143
144
		$more_btn_html = sprintf(
145
			'<input type="hidden" class="give-donor-wall-shortcode-attrs" data-shortcode="%1$s">',
146
			rawurlencode( http_build_query( $atts ) )
147
		);
148
149
		if ( $this->has_donations( $temp_atts ) ) {
150
			$more_btn_html .= sprintf(
151
				'<button class="give-donor__load_more give-button-with-loader"><span class="give-loading-animation"></span>%1$s</button>',
152
				$atts['loadmore_text']
153
			);
154
		}
155
156
		$html = $html
157
			? sprintf(
158
				'<div class="give-wrap give-grid-ie-utility"><div class="give-grid give-grid--%1$s">%2$s</div>%3$s</div>',
159
				esc_attr( $atts['columns'] ),
160
				$html,
161
				$more_btn_html
162
			)
163
			: '';
164
165
		return $html;
166
	}
167
168
	/**
169
	 * Parse shortcode attributes
170
	 *
171
	 * @since  2.2.0
172
	 * @access public
173
	 *
174
	 * @param array $atts Shortcode attributes.
175
	 *
176
	 * @return array
177
	 */
178
	public function parse_atts( $atts ) {
179
		$atts = shortcode_atts(
180
			array(
181
				'donors_per_page' => 12,
182
				'form_id'         => 0,
183
				'paged'           => 1,
184
				'ids'             => '',
185
				'columns'         => 'best-fit',
186
				'anonymous'       => true,
187
				'show_avatar'     => true,
188
				'show_name'       => true,
189
				'show_total'      => true,
190
				'show_time'       => true,
191
				'show_comments'   => true,
192
				'comment_length'  => 140,
193
				'only_comments'   => false,
194
				'readmore_text'   => esc_html__( 'Read more', 'give' ),
195
				'loadmore_text'   => esc_html__( 'Load more', 'give' ),
196
				'avatar_size'     => 60,
197
				'orderby'         => 'post_date',
198
				'order'           => 'DESC',
199
				'hide_empty'      => true,  // Deprecated in 2.3.0
200
				'only_donor_html' => false, // Only for internal use.
201
			), $atts
202
		);
203
0 ignored issues
show
Coding Style introduced by
Functions must not contain multiple empty lines in a row; found 2 empty lines
Loading history...
204
205
		// Validate boolean attributes.
206
		$boolean_attributes = array(
207
			'anonymous',
208
			'show_avatar',
209
			'show_name',
210
			'show_total',
211
			'show_time',
212
			'show_comments',
213
			'show_comments',
214
			'hide_empty',
215
			'only_comments',
216
			'only_donor_html',
217
		);
218
219
		foreach ( $boolean_attributes as $att ) {
220
			// Convert numeric to boolean.
221
			// It will prevent condition check against boolean value.
222
			if ( is_numeric( $atts[ $att ] ) ) {
223
				$atts[ $att ] = (bool) $atts[ $att ];
224
			}
225
226
			$atts[ $att ] = filter_var( $atts[ $att ], FILTER_VALIDATE_BOOLEAN );
227
		}
228
229
		return $atts;
230
	}
231
232
	/**
233
	 * Get donors
234
	 *
235
	 * @since  2.2.0
236
	 * @access public
237
	 *
238
	 * @param array $donor_query Dorno query.
239
	 *
240
	 * @return array
241
	 */
242
	public function get_donors( $donor_query ) {
243
		$donor_query = new Give_Donors_Query( $donor_query );
244
		$donors      = $donor_query->get_donors();
245
246
		return $donors;
247
	}
248
249
250
	/**
251
	 * Ajax handler
252
	 *
253
	 * @since  2.2.0
254
	 * @access public
255
	 */
256
	public function ajax_handler() {
257
		$shortcode_atts = wp_parse_args( give_clean( rawurldecode( $_POST['data'] ) ) ); // @codingStandardsIgnoreLine
258
259
		// Get next page donor comments.
260
		$shortcode_atts['paged']           = $shortcode_atts['paged'] + 1;
261
		$shortcode_atts['only_donor_html'] = true;
262
263
		$donors_comment_html = $this->render_shortcode( $shortcode_atts );
264
265
		// Check if donor comment remaining.
266
		$temp_atts          = $shortcode_atts;
267
		$temp_atts['paged'] = $shortcode_atts['paged'] + 1;
268
		$has_donors         = $this->has_donations( $temp_atts ) ? 1 : 0;
269
270
		// Remove internal shortcode param.
271
		unset( $shortcode_atts['only_donor_html'] );
272
273
		wp_send_json(
274
			array(
275
				'shortcode' => rawurlencode( http_build_query( $shortcode_atts ) ),
276
				'html'      => $donors_comment_html,
277
				'remaining' => $has_donors,
278
			)
279
		);
280
	}
281
282
	/**
283
	 * Get query params
284
	 *
285
	 * @since 2.3.0
286
	 *
287
	 * @param  array $atts
288
	 *
289
	 * @return array
290
	 */
291
	private function get_query_param( $atts = array() ) {
292
		$valid_order   = array( 'ASC', 'DESC' );
293
		$valid_orderby = array( 'post_date', 'donation_amount' );
294
295
		$query_atts = array();
296
297
		$query_atts['order']         = in_array( $atts['order'], $valid_order ) ? $atts['order'] : 'DESC';
298
		$query_atts['orderby']       = in_array( $atts['orderby'], $valid_orderby ) ? $atts['orderby'] : 'post_date';
299
		$query_atts['limit']         = $atts['donors_per_page'];
300
		$query_atts['offset']        = $atts['donors_per_page'] * ( $atts['paged'] - 1 );
301
		$query_atts['form_id']       = $atts['form_id'];
302
		$query_atts['only_comments'] = ( true === $atts['only_comments'] );
303
		$query_atts['anonymous']     = ( true === $atts['anonymous'] );
304
305
		return $query_atts;
306
	}
307
308
	/**
309
	 * Get donation data.
310
	 *
311
	 * @since 2.3.0
312
	 *
313
	 * @param array $atts
314
	 *
315
	 * @return array
316
	 */
317
	private function get_donation_data( $atts = array() ) {
318
		global $wpdb;
319
320
		// Bailout if donation does not exist.
321
		if ( ! ( $donation_ids = $this->get_donations( $atts ) ) ) {
322
			return array();
323
		}
324
325
		$donation_ids = ! empty( $donation_ids )
326
			? '\'' . implode( '\',\'', $donation_ids ) . '\''
327
			: '';
328
329
		// Backward compatibility
330
		$donation_id_col = Give()->payment_meta->get_meta_type() . '_id';
331
332
		$sql = "SELECT * FROM {$wpdb->donationmeta} as m1
333
				INNER JOIN {$wpdb->posts} as p1 ON (m1.{$donation_id_col}=p1.ID)
334
				WHERE m1.{$donation_id_col} IN ( {$donation_ids} )
335
				ORDER BY FIELD( p1.ID, {$donation_ids} )
336
				";
337
338
		$results = (array) $wpdb->get_results( $sql );
0 ignored issues
show
introduced by
Usage of a direct database call is discouraged.
Loading history...
introduced by
Usage of a direct database call without caching is prohibited. Use wp_cache_get / wp_cache_set.
Loading history...
339
340
		if ( ! empty( $results ) ) {
341
			$temp = array();
342
343
			/* @var stdClass $result */
344
			foreach ( $results as $result ) {
345
				$temp[ $result->{$donation_id_col} ][ $result->meta_key ] = maybe_unserialize( $result->meta_value );
0 ignored issues
show
introduced by
Detected usage of meta_key, possible slow query.
Loading history...
346
			}
347
348
			$comments = $this->get_donor_comments( $temp );
349
350
			if ( ! empty( $temp ) ) {
351
				foreach ( $temp as $donation_id => $donation_data ) {
352
					$temp[ $donation_id ]['donation_id'] = $donation_id;
353
354
					$temp[ $donation_id ]['name_initial'] = give_get_name_initial( array(
355
						'firstname' => $donation_data['_give_donor_billing_first_name'],
356
						'lastname'  => $donation_data['_give_donor_billing_last_name'],
357
					) );
358
359
					$temp[ $donation_id ]['donor_comment'] = ! empty( $comments[ $donation_id ] ) ? $comments[ $donation_id ] : '';
360
				}
361
			}
362
363
			$results = ! empty( $temp ) ? $temp : array();
364
		}
365
366
		return $results;
367
	}
368
369
	/**
370
	 * Get donation list for specific query
371
	 *
372
	 * @since 2.3.0
373
	 *
374
	 * @param  array $atts
375
	 *
376
	 * @return array
377
	 */
378
	private function get_donations( $atts = array() ) {
379
		global $wpdb;
380
381
		// Backward compatibility
382
		$donation_id_col = Give()->payment_meta->get_meta_type() . '_id';
383
384
		$query_params = $this->get_query_param( $atts );
385
386
		$sql   = "SELECT p1.ID FROM {$wpdb->posts} as p1";
387
		$where = " WHERE p1.post_status IN ('publish') AND p1.post_type = 'give_payment'";
388
389
		// exclude donation with zero amount from result.
390
		$sql   .= " INNER JOIN {$wpdb->donationmeta} as m1 ON (p1.ID = m1.{$donation_id_col})";
391
		$where .= " AND m1.meta_key='_give_payment_total' AND m1.meta_value>0";
392
0 ignored issues
show
Coding Style introduced by
Functions must not contain multiple empty lines in a row; found 2 empty lines
Loading history...
393
394
		if ( $query_params['form_id'] ) {
395
			$sql   .= " INNER JOIN {$wpdb->donationmeta} as m2 ON (p1.ID = m2.{$donation_id_col})";
396
			$where .= " AND m2.meta_key='_give_payment_form_id' AND m2.meta_value={$query_params['form_id']}";
397
		}
398
399
		// exclude donations which does not has donor comment.
400
		if ( $query_params['only_comments'] ) {
401
			$sql   .= " INNER JOIN {$wpdb->give_comments} as gc1 ON (p1.ID = gc1.comment_parent)";
402
			$where .= " AND gc1.comment_type='donor_donation'";
403
		}
404
405
		// exclude anonymous donation form query based on query parameters.
406
		if (
407
			! $query_params['anonymous']
408
			|| $query_params['only_comments']
409
		) {
410
			$where .= " AND p1.ID NOT IN ( SELECT DISTINCT({$donation_id_col}) FROM {$wpdb->donationmeta} WHERE meta_key='_give_anonymous_donation' AND meta_value='1')";
411
		}
412
413
		// order by query based on parameter.
414
		if ( 'donation_amount' === $query_params['orderby'] ) {
415
			$order = " ORDER BY m1.meta_value+0 {$query_params['order']}";
416
		} else {
417
			$order = " ORDER BY p1.{$query_params['orderby']} {$query_params['order']}, p1.ID {$query_params['order']}";
418
		}
419
420
		$limit  = " LIMIT {$query_params['limit']}";
421
		$offset = " OFFSET {$query_params['offset']}";
422
423
		$sql = $sql . $where . $order . $limit . $offset;
424
425
		$donation_ids = $wpdb->get_col( $sql );
0 ignored issues
show
introduced by
Usage of a direct database call is discouraged.
Loading history...
introduced by
Usage of a direct database call without caching is prohibited. Use wp_cache_get / wp_cache_set.
Loading history...
426
427
		return $donation_ids;
428
	}
429
430
	/**
431
	 * Get donor comments
432
	 *
433
	 * @since 2.3.0
434
	 *
435
	 * @param array $donations_data
436
	 *
437
	 * @return array
438
	 */
439
	private function get_donor_comments( $donations_data = array() ) {
440
		global $wpdb;
441
		$comments = array();
442
443
		// Bailout.
444
		if ( empty( $donations_data ) ) {
445
			return $comments;
446
		}
447
448
		// Backward compatibility.
449
		if (
450
			! give_has_upgrade_completed( 'v230_move_donor_note' )
451
			|| ! give_has_upgrade_completed( 'v230_move_donation_note' )
452
		) {
453
			foreach ( $donations_data as $id => $data ) {
454
				$comment         = give_get_donor_donation_comment( $id, $data['_give_payment_donor_id'] );
455
				$comments[ $id ] = ! empty( $comment ) ? $comment->comment_content : '';
456
			}
457
458
			return $comments;
459
		}
460
461
		$sql   = "SELECT c1.comment_parent as donation_id, c1.comment_content as comment FROM {$wpdb->give_comments} as c1";
462
		$sql   .= " INNER JOIN {$wpdb->give_commentmeta} as cm1 ON (c1.comment_ID=cm1.give_comment_id)";
463
		$where = array();
464
465
		foreach ( $donations_data as $id => $data ) {
466
			// Do not fetch comment for anonymous donation.
467
			if( ! empty( $data['_give_anonymous_donation'] )  ) {
0 ignored issues
show
introduced by
Space after opening control structure is required
Loading history...
introduced by
No space before opening parenthesis is prohibited
Loading history...
468
				continue;
469
			}
470
471
			$where[] = "(c1.comment_parent={$id} AND cm1.meta_key='_give_donor_id' AND cm1.meta_value={$data['_give_payment_donor_id']})";
472
		}
473
474
		$where = ' WHERE ' . implode( ' OR ', $where );
475
		$where .= " AND c1.comment_type='donor_donation'";
476
477
		$sql = $sql . $where;
478
479
		$comments = (array) $wpdb->get_results( $sql );
0 ignored issues
show
introduced by
Usage of a direct database call is discouraged.
Loading history...
introduced by
Usage of a direct database call without caching is prohibited. Use wp_cache_get / wp_cache_set.
Loading history...
480
481
		if ( ! empty( $comments ) ) {
482
			$comments = array_combine(
483
				wp_list_pluck( $comments, 'donation_id' ),
484
				wp_list_pluck( $comments, 'comment' )
485
			);
486
		}
487
488
		return $comments;
489
	}
490
491
	/**
492
	 * Check if donation exist or not for specific query
493
	 *
494
	 * @since 2.3.0
495
	 *
496
	 * @param  array $atts
497
	 *
498
	 * @return bool
499
	 */
500
	private function has_donations( $atts = array() ) {
501
		return (bool) $this->get_donations( $atts );
502
	}
503
}
504
505
// Initialize shortcode.
506
Give_Donor_Wall::get_instance();
507