Completed
Push — milestone/2_0/react-ui ( 321798...c1e1b2 )
by
unknown
14:33 queued 10:17
created

Post_Meta_Container   D

Complexity

Total Complexity 88

Size/Duplication

Total Lines 561
Duplicated Lines 3.74 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 76.19%

Importance

Changes 0
Metric Value
dl 21
loc 561
ccs 16
cts 21
cp 0.7619
rs 4.8717
c 0
b 0
f 0
wmc 88
lcom 1
cbo 3

25 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 7 7 2
A init() 0 19 4
A is_valid_save() 0 11 4
A save() 14 14 2
C is_valid_attach_for_request() 0 41 14
B is_valid_attach_for_object() 0 25 6
A attach() 0 16 3
A add_postbox_classes() 0 4 1
A render() 0 3 1
A set_post_id() 0 4 1
C is_condition_fullfilled() 0 62 18
A condition_is_post_of_format() 0 12 4
A condition_is_post_on_level() 0 5 1
A condition_is_post_using_template() 0 5 2
A show_on_page() 0 19 4
A show_on_page_children() 0 13 2
A show_on_category() 0 5 1
A show_on_template() 0 18 4
A hide_on_template() 0 12 3
A show_on_level() 0 9 2
A show_on_taxonomy_term() 0 9 2
A show_on_post_format() 0 16 4
A show_on_post_type() 0 7 1
A set_context() 0 5 1
A set_priority() 0 5 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 Post_Meta_Container 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 Post_Meta_Container, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Carbon_Fields\Container;
4
5
use Carbon_Fields\Datastore\Datastore;
6
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
7
8
/**
9
 * Field container designed to extend WordPress custom fields functionality,
10
 * providing easier user interface to add, edit and delete text, media files,
11
 * location information and more.
12
 */
13
class Post_Meta_Container extends Container {
14
	/**
15
	 * ID of the post the container is working with
16
	 *
17
	 * @see init()
18
	 * @var int
19
	 */
20
	protected $post_id;
21
22
	/**
23
	 * List of default container settings
24
	 *
25
	 * @see init()
26
	 * @var array
27
	 */
28
	public $settings = array(
29
		'post_type' => array( 'post' ),
30
		'panel_context' => 'normal',
31
		'panel_priority' => 'high',
32
		'show_on' => array(
33
			'category' => null,
34
			'template_names' => array(),
35
			'not_in_template_names' => array(),
36
			'post_formats' => array(),
37
			'level_limit' => null,
38
			'tax_term_id' => null,
39
			'page_id' => null,
40
			'parent_page_id' => null,
41
			'post_path' => null,
42
		),
43
	);
44
45
	/**
46
	 * Create a new container
47
	 *
48
	 * @param string $unique_id Unique id of the container
49
	 * @param string $title title of the container
50
	 * @param string $type Type of the container
51
	 **/
52 View Code Duplication
	public function __construct( $unique_id, $title, $type ) {
53
		parent::__construct( $unique_id, $title, $type );
54
55
		if ( ! $this->get_datastore() ) {
56
			$this->set_datastore( Datastore::make( 'post_meta' ), $this->has_default_datastore() );
57
		}
58
	}
59
60
	/**
61
	 * Create DataStore instance, set post ID to operate with (if such exists).
62
	 * Bind attach() and save() to the appropriate WordPress actions.
63
	 **/
64
	public function init() {
65
		$input = stripslashes_deep( $_GET );
1 ignored issue
show
introduced by
Detected access of super global var $_GET, probably need manual inspection.
Loading history...
66
		$request_post_id = isset( $input['post'] ) ? intval( $input['post'] ) : 0;
67
		if ( $request_post_id > 0 ) {
68
			$this->set_post_id( $request_post_id );
69
		}
70
71
		// force post_type to be array
72
		if ( ! is_array( $this->settings['post_type'] ) ) {
73
			$this->settings['post_type'] = array( $this->settings['post_type'] );
74
		}
75
76
		add_action( 'admin_init', array( $this, '_attach' ) );
77
		add_action( 'save_post', array( $this, '_save' ) );
78
79
		// support for attachments
80
		add_action( 'add_attachment', array( $this, '_save' ) );
81
		add_action( 'edit_attachment', array( $this, '_save' ) );
82
	}
83
84
	/**
85
	 * Checks whether the current save request is valid
86
	 * Possible errors are triggering save() for autosave requests
87
	 * or performing post save outside of the post edit page (like Quick Edit)
88
	 *
89
	 * @param int $post_id ID of the post against which save() is ran
90
	 * @return bool
91
	 **/
92
	public function is_valid_save( $post_id = 0 ) {
93
		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
94
			return false;
95
		}
96
97
		if ( ! $this->verified_nonce_in_request() ) {
98
			return false;
99
		}
100
101
		return $this->is_valid_attach_for_object( $post_id );
102
	}
103
104
	/**
105
	 * Perform save operation after successful is_valid_save() check.
106
	 * The call is propagated to all fields in the container.
107
	 *
108
	 * @param int $post_id ID of the post against which save() is ran
109
	 **/
110 View Code Duplication
	public function save( $post_id = null ) {
111
		// Unhook action to garantee single save
112
		remove_action( 'save_post', array( $this, '_save' ) );
113
114
		$this->set_post_id( $post_id );
115
116
		foreach ( $this->fields as $field ) {
117
			$field->set_value_from_input( stripslashes_deep( $_POST ) );
1 ignored issue
show
introduced by
Detected access of super global var $_POST, probably need manual inspection.
Loading history...
118
			$field->save();
119
		}
120
121
		do_action( 'carbon_after_save_custom_fields', $post_id );
122
		do_action( 'carbon_after_save_post_meta', $post_id );
123
	}
124
125
	/**
126
	 * Perform checks whether the container should be attached during the current request
127
	 *
128
	 * @return bool True if the container is allowed to be attached
129
	 **/
130
	public function is_valid_attach_for_request() {
131
		global $pagenow;
132
133
		$input = stripslashes_deep( $_GET );
1 ignored issue
show
introduced by
Detected access of super global var $_GET, probably need manual inspection.
Loading history...
134
135
		if ( $pagenow !== 'post.php' && $pagenow !== 'post-new.php' ) {
136
			return false;
137
		}
138
139
		// Post types check
140
		if ( ! empty( $this->settings['post_type'] ) ) {
141
			$post_type = '';
142
			$request_post_type = isset( $input['post_type'] ) ? $input['post_type'] : '';
143
144
			if ( $this->post_id ) {
145
				$post_type = get_post_type( $this->post_id );
146
			} elseif ( ! empty( $request_post_type ) ) {
147
				$post_type = $request_post_type;
148
			} elseif ( $pagenow === 'post-new.php' ) {
149
				$post_type = 'post';
150
			}
151
152
			if ( ! $post_type || ! in_array( $post_type, $this->settings['post_type'] ) ) {
153
				return false;
154
			}
155
		}
156
157
		// Check show on conditions
158
		foreach ( $this->settings['show_on'] as $condition => $value ) {
159
			if ( is_null( $value ) || $condition !== 'page_id' ) {
160
				continue;
161
			}
162
163
			$post = get_post( $this->post_id );
164
			if ( ! $this->is_condition_fullfilled( $condition, $value, $post ) ) {
165
				return false;
166
			}
167
		}
168
169
		return true;
170
	}
171
172
	/**
173
	 * Check container attachment rules against object id
174
	 *
175
	 * @param int $object_id
176
	 * @return bool
177
	 **/
178
	public function is_valid_attach_for_object( $object_id = null ) {
179
		$post = get_post( intval( $object_id ) );
180
181
		if ( ! $post ) {
182
			return false;
183
		}
184
185
		// Check post type
186
		if ( ! in_array( $post->post_type, $this->settings['post_type'] ) ) {
187
			return false;
188
		}
189
190
		// Check show on conditions
191
		foreach ( $this->settings['show_on'] as $condition => $value ) {
192
			if ( is_null( $value ) ) {
193
				continue;
194
			}
195
196
			if ( ! $this->is_condition_fullfilled( $condition, $value, $post ) ) {
197
				return false;
198
			}
199
		}
200
201
		return true;
202
	}
203
204
	/**
205
	 * Add meta box for each of the container post types
206
	 **/
207
	public function attach() {
208
		foreach ( $this->settings['post_type'] as $post_type ) {
209
			add_meta_box(
210
				$this->id,
211
				$this->title,
212
				array( $this, 'render' ),
213
				$post_type,
214
				$this->settings['panel_context'],
215
				$this->settings['panel_priority']
216
			);
217
		}
218
219
		foreach ( $this->settings['post_type'] as $post_type ) {
220
			add_filter( "postbox_classes_{$post_type}_{$this->id}", array( $this, 'add_postbox_classes' ) );
221
		}
222
	}
223
224
	/**
225
	 * Classes to add to the post meta box
226
	 */
227
	public function add_postbox_classes( $classes ) {
228
		$classes[] = 'carbon-box';
229
		return $classes;
230
	}
231
232
	/**
233
	 * Output the container markup
234
	 **/
235
	public function render() {
236
		include \Carbon_Fields\DIR . '/templates/Container/post_meta.php';
237
	}
238
239
	/**
240
	 * Set the post ID the container will operate with.
241
	 *
242
	 * @param int $post_id
243
	 **/
244
	public function set_post_id( $post_id ) {
245
		$this->post_id = $post_id;
246
		$this->get_datastore()->set_id( $post_id );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Carbon_Fields\Datastore\Datastore_Interface as the method set_id() does only exist in the following implementations of said interface: Carbon_Fields\Datastore\Comment_Meta_Datastore, Carbon_Fields\Datastore\Meta_Datastore, Carbon_Fields\Datastore\Nav_Menu_Item_Datastore, Carbon_Fields\Datastore\Post_Meta_Datastore, Carbon_Fields\Datastore\Term_Meta_Datastore, Carbon_Fields\Datastore\User_Meta_Datastore.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
247
	}
248
249
	/**
250
	 * CONDITION TOOLS
251
	 */
252
	
253
	/**
254
	 * Check a condition against all supported conditions
255
	 * 
256
	 * @param string $condition
257
	 * @param mixed $value
258
	 * @param WP_Post $post
259
	 * @return bool
260
	 */
261
	protected function is_condition_fullfilled( $condition, $value, $post ) {
262
		switch ( $condition ) {
263
			// show_on_post_format
264
			case 'post_formats':
265
				if ( ! $this->condition_is_post_of_format( $post, $value ) ) {
266
					return false;
267
				}
268
				break;
269
270
			// show_on_taxonomy_term or show_on_category
271
			case 'category':
272
				$this->show_on_category( $value );
273
274
				/* fall-through intended */
275
			case 'tax_term_id':
276
				$has_term = has_term(
277
					intval( $this->settings['show_on']['tax_term_id'] ),
278
					$this->settings['show_on']['tax_slug'],
279
					$post->ID
280
				);
281
				if ( ! $has_term ) {
282
					return false;
283
				}
284
				break;
285
286
			// show_on_level
287
			case 'level_limit':
288
				if ( ! $this->condition_is_post_on_level( $post, $value ) ) {
289
					return false;
290
				}
291
				break;
292
293
			// show_on_page
294
			case 'page_id':
295
				if ( $post->ID !== intval( $value ) ) {
296
					return false;
297
				}
298
				break;
299
300
			// show_on_page_children
301
			case 'parent_page_id':
302
				if ( $post->post_parent !== $value ) {
303
					return false;
304
				}
305
				break;
306
307
			// show_on_template
308
			case 'template_names':
309
				if ( ! empty( $value ) && ! $this->condition_is_post_using_template( $post, $value ) ) {
310
					return false;
311
				}
312
				break;
313
314
			// hide_on_template
315
			case 'not_in_template_names':
316
				if ( ! empty( $value ) && $this->condition_is_post_using_template( $post, $value ) ) {
317
					return false;
318
				}
319
				break;
320
		}
321
		return true;
322
	}
323
	
324
	/**
325
	 * Check if a post is of a given post format
326
	 * 
327
	 * @param WP_Post $post
328
	 * @param string $format
329
	 * @return bool
330
	 */
331
	protected function condition_is_post_of_format( $post, $format ) {
332
		if ( empty( $format ) || $post->post_type !== 'post' ) {
333
			return true; // this doesn't make sense - returning true for a post that does not support formats (kept for backwards compatibility)
334
		}
335
336
		$current_format = get_post_format( $post->ID );
337
		if ( ! in_array( $current_format, $format ) ) {
338
			return false;
339
		}
340
341
		return true;
342
	}
343
	
344
	/**
345
	 * Check if a post is on a specific level in it's hierarchy
346
	 * 
347
	 * @param WP_Post $post
348
	 * @param int $level
349
	 * @return bool
350
	 */
351
	protected function condition_is_post_on_level( $post, $level ) {
352
		$level = intval( $level );
353
		$post_level = count( get_post_ancestors( $post->ID ) ) + 1;
354
		return ( $post_level === $level );
355
	}
356
	
357
	/**
358
	 * Check if a post uses one of a given array of templates
359
	 * 
360
	 * @param WP_Post $post
361
	 * @param string|array<string> $templates
362
	 * @return bool
363
	 */
364
	protected function condition_is_post_using_template( $post, $templates ) {
365
		$templates = is_array( $templates ) ? $templates : array( $templates );
366
		$current_template = get_post_meta( $post->ID, '_wp_page_template', true );
367
		return in_array( $current_template, $templates );
368
	}
369
370
	/**
371
	 * COMMON USAGE METHODS
372
	 */
373
374
	/**
375
	 * Show the container only on particular page referenced by it's path.
376
	 *
377
	 * @param int|string $page page ID or page path
378
	 * @return object $this
379
	 **/
380 4
	public function show_on_page( $page ) {
381
		$page_id = absint( $page );
382
383 4
		if ( $page_id && $page_id == $page ) {
384
			$page_obj = get_post( $page_id );
385
		} else {
386
			$page_obj = get_page_by_path( $page );
387 2
		}
388
389
		$this->show_on_post_type( 'page' );
390
391 4
		if ( $page_obj ) {
392 4
			$this->settings['show_on']['page_id'] = $page_obj->ID;
393
		} else {
394
			$this->settings['show_on']['page_id'] = -1;
395 4
		}
396
397 4
		return $this;
398
	}
399
400
	/**
401
	 * Show the container only on pages whose parent is referenced by $parent_page_path.
402
	 *
403
	 * @param string $parent_page_path
404
	 * @return object $this
405
	 **/
406 1
	public function show_on_page_children( $parent_page_path ) {
407
		$page = get_page_by_path( $parent_page_path );
408
409
		$this->show_on_post_type( 'page' );
410
411 1
		if ( $page ) {
412 1
			$this->settings['show_on']['parent_page_id'] = $page->ID;
413
		} else {
414
			$this->settings['show_on']['parent_page_id'] = -1;
415 1
		}
416
417 1
		return $this;
418
	}
419
420
	/**
421
	 * Show the container only on posts from the specified category.
422
	 *
423
	 * @see show_on_taxonomy_term()
424
	 *
425
	 * @param string $category_slug
426
	 * @return object $this
427
	 **/
428
	public function show_on_category( $category_slug ) {
429
		$this->settings['show_on']['category'] = $category_slug;
430
431
		return $this->show_on_taxonomy_term( $category_slug, 'category' );
432
	}
433
434
	/**
435
	 * Show the container only on pages whose template has filename $template_path.
436
	 *
437
	 * @param string|array $template_path
438
	 * @return object $this
439
	 **/
440 2
	public function show_on_template( $template_path ) {
441
		// Backwards compatibility where only pages support templates
442
		if ( version_compare( get_bloginfo( 'version' ), '4.7', '<' ) ) {
443
			$this->show_on_post_type( 'page' );
444
		}
445
446
		if ( is_array( $template_path ) ) {
447
			foreach ( $template_path as $path ) {
448
				$this->show_on_template( $path );
449
			}
450
451 1
			return $this;
452
		}
453
454 2
		$this->settings['show_on']['template_names'][] = $template_path;
455
456 2
		return $this;
457
	}
458
459
	/**
460
	 * Hide the container from pages whose template has filename $template_path.
461
	 *
462
	 * @param string|array $template_path
463
	 * @return object $this
464
	 **/
465
	public function hide_on_template( $template_path ) {
466
		if ( is_array( $template_path ) ) {
467
			foreach ( $template_path as $path ) {
468
				$this->hide_on_template( $path );
469
			}
470
			return $this;
471
		}
472
473
		$this->settings['show_on']['not_in_template_names'][] = $template_path;
474
475
		return $this;
476
	}
477
478
	/**
479
	 * Show the container only on hierarchical posts of level $level.
480
	 * Levels start from 1 (top level post)
481
	 *
482
	 * @param int $level
483
	 * @return object $this
484
	 **/
485
	public function show_on_level( $level ) {
486
		if ( $level < 0 ) {
487
			Incorrect_Syntax_Exception::raise( 'Invalid level limitation (' . $level . ')' );
488
		}
489
490
		$this->settings['show_on']['level_limit'] = $level;
491
492
		return $this;
493
	}
494
495
	/**
496
	 * Show the container only on posts which have term $term_slug from the $taxonomy_slug taxonomy.
497
	 *
498
	 * @param string $taxonomy_slug
499
	 * @param string $term_slug
500
	 * @return object $this
501
	 **/
502
	public function show_on_taxonomy_term( $term_slug, $taxonomy_slug ) {
503
		$term = get_term_by( 'slug', $term_slug, $taxonomy_slug );
504
505
		$this->settings['show_on']['tax_slug'] = $taxonomy_slug;
506
		$this->settings['show_on']['tax_term'] = $term_slug;
507
		$this->settings['show_on']['tax_term_id'] = $term ? $term->term_id : null;
508
509
		return $this;
510
	}
511
512
	/**
513
	 * Show the container only on posts from the specified format.
514
	 * Learn more about {@link http://codex.wordpress.org/Post_Formats Post Formats (Codex)}
515
	 *
516
	 * @param string|array $post_format Name of the format as listed on Codex
517
	 * @return object $this
518
	 **/
519
	public function show_on_post_format( $post_format ) {
520
		if ( is_array( $post_format ) ) {
521
			foreach ( $post_format as $format ) {
522
				$this->show_on_post_format( $format );
523
			}
524
			return $this;
525
		}
526
527
		if ( $post_format === 'standard' ) {
528
			$post_format = 0;
529
		}
530
531
		$this->settings['show_on']['post_formats'][] = strtolower( $post_format );
532
533
		return $this;
534
	}
535
536
	/**
537
	 * Show the container only on posts from the specified type(s).
538
	 *
539
	 * @param string|array $post_types
540
	 * @return object $this
541
	 **/
542
	public function show_on_post_type( $post_types ) {
543
		$post_types = (array) $post_types;
544
545
		$this->settings['post_type'] = $post_types;
546
547
		return $this;
548
	}
549
550
	/**
551
	 * Sets the meta box container context
552
	 *
553
	 * @see https://codex.wordpress.org/Function_Reference/add_meta_box
554
	 * @param string $context ('normal', 'advanced' or 'side')
555
	 */
556
	public function set_context( $context ) {
557
		$this->settings['panel_context'] = $context;
558
559
		return $this;
560
	}
561
562
	/**
563
	 * Sets the meta box container priority
564
	 *
565
	 * @see https://codex.wordpress.org/Function_Reference/add_meta_box
566
	 * @param string $priority ('high', 'core', 'default' or 'low')
567
	 */
568
	public function set_priority( $priority ) {
569
		$this->settings['panel_priority'] = $priority;
570
571
		return $this;
572
	}
573
}
574