Completed
Push — master ( e64151...e90524 )
by David
08:25
created

Wordlift_Vocabulary_Shortcode::render()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 63
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 28
nc 3
nop 1
dl 0
loc 63
rs 8.8945
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Shortcodes: Glossary Shortcode.
4
 *
5
 * `wl_vocabulary` implementation.
6
 *
7
 * @since      3.16.0
8
 * @package    Wordlift
9
 * @subpackage Wordlift/includes
10
 */
11
12
/**
13
 * Define the {@link Wordlift_Glossary_Shortcode} class.
14
 *
15
 * @since      3.16.0
16
 * @package    Wordlift
17
 * @subpackage Wordlift/includes
18
 */
19
class Wordlift_Vocabulary_Shortcode extends Wordlift_Shortcode {
20
21
	/**
22
	 * The shortcode.
23
	 *
24
	 * @since  3.17.0
25
	 */
26
	const SHORTCODE = 'wl_vocabulary';
27
28
	/**
29
	 * The {@link Wordlift_Configuration_Service} instance.
30
	 *
31
	 * @since  3.11.0
32
	 * @access private
33
	 * @var \Wordlift_Configuration_Service $configuration_service The {@link Wordlift_Configuration_Service} instance.
34
	 */
35
	private $configuration_service;
36
37
	/**
38
	 * A {@link Wordlift_Log_Service} instance.
39
	 *
40
	 * @since  3.17.0
41
	 * @access private
42
	 * @var \Wordlift_Log_Service $log A {@link Wordlift_Log_Service} instance.
43
	 */
44
	private $log;
45
46
	/**
47
	 * Create a {@link Wordlift_Glossary_Shortcode} instance.
48
	 *
49
	 * @since 3.16.0
50
	 *
51
	 * @param \Wordlift_Configuration_Service $configuration_service The {@link Wordlift_Configuration_Service} instance.
52
	 */
53
	public function __construct( $configuration_service ) {
54
		parent::__construct();
55
56
		$this->log = Wordlift_Log_Service::get_logger( get_class() );
57
58
		$this->configuration_service = $configuration_service;
59
60
	}
61
62
	/**
63
	 * Check whether the requirements for this shortcode to work are available.
64
	 *
65
	 * @since 3.17.0
66
	 * @return bool True if the requirements are satisfied otherwise false.
67
	 */
68
	private static function are_requirements_satisfied() {
69
70
		return function_exists( 'mb_strlen' ) &&
71
			   function_exists( 'mb_substr' ) &&
72
			   function_exists( 'mb_convert_case' );
73
	}
74
75
	/**
76
	 * Render the shortcode.
77
	 *
78
	 * @since 3.16.0
79
	 *
80
	 * @param array $atts An array of shortcode attributes as set by the editor.
81
	 *
82
	 * @return string The output html code.
83
	 */
84
	public function render( $atts ) {
85
86
		// Bail out if the requirements aren't satisfied: we need mbstring for
87
		// the vocabulary widget to work.
88
		if ( ! self::are_requirements_satisfied() ) {
89
			$this->log->warn( "The vocabulary widget cannot be displayed because this WordPress installation doesn't satisfy its requirements." );
90
91
			return '';
92
		}
93
94
		wp_enqueue_style( 'wl-vocabulary-shortcode', dirname( plugin_dir_url( __FILE__ ) ) . '/public/css/wordlift-vocabulary-shortcode.css' );
95
96
		// Extract attributes and set default values.
97
		$atts = shortcode_atts( array(
98
			// The entity type, such as `person`, `organization`, ...
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
99
			'type'         => 'all',
100
			// Limit the number of posts to 100 by default. Use -1 to remove the limit.
101
			'limit'        => 100,
102
			// Sort by title.
103
			'orderby'      => 'title',
104
		), $atts );
105
106
		// Get the posts. Note that if a `type` is specified before, then the
107
		// `tax_query` from the `add_criterias` call isn't added.
108
		$posts = $this->get_posts( $atts );
109
110
		// Get the alphabet.
111
		$language_code = $this->configuration_service->get_language_code();
112
		$alphabet      = Wordlift_Alphabet_Service::get( $language_code );
113
114
		// Add posts to the alphabet.
115
		foreach ( $posts as $post ) {
116
			$this->add_to_alphabet( $alphabet, $post->ID );
117
		}
118
119
		// Generate the header.
120
		$header = array_reduce( array_keys( $alphabet ), function ( $carry, $item ) use ( $alphabet ) {
121
			$template = ( 0 === count( $alphabet[ $item ] )
122
				? '<span class="wl-vocabulary-widget-disabled">%s</span>'
123
				: '<a href="#wl-vocabulary-widget-%2$s">%1$s</a>' );
124
125
			return $carry . sprintf( $template, esc_html( $item ), esc_attr( $item ) );
126
		}, '' );
127
128
		// Generate the sections.
129
		$that     = $this;
130
		$sections = array_reduce( array_keys( $alphabet ), function ( $carry, $item ) use ( $alphabet, $that ) {
131
			return $carry . $that->get_section( $item, $alphabet[ $item ] );
132
		}, '' );
133
134
		// Return HTML template.
135
		return "
136
<div class='wl-vocabulary'>
137
	<nav class='wl-vocabulary-alphabet-nav'>
138
		$header
139
	</nav>
140
	<div class='wl-vocabulary-grid'>
141
		$sections
142
	</div>
143
</div>
144
		";
145
146
	}
147
148
	/**
149
	 * Generate the html code for the section.
150
	 *
151
	 * @since 3.17.0
152
	 *
153
	 * @param string $letter The section's letter.
154
	 * @param array  $posts  An array of `$post_id => $post_title` associated with
155
	 *                       the section.
156
	 *
157
	 * @return string The section html code (or an empty string if the section has
158
	 *                no posts).
159
	 */
160
	private function get_section( $letter, $posts ) {
161
162
		// Return an empty string if there are no posts.
163
		if ( 0 === count( $posts ) ) {
164
			return '';
165
		}
166
167
		return sprintf( '
168
			<div class="wl-vocabulary-letter-block" id="wl-vocabulary-widget-%s">
169
				<aside class="wl-vocabulary-left-column">%s</aside>
170
				<div class="wl-vocabulary-right-column">
171
					<ul class="wl-vocabulary-items-list">
172
						%s
173
					</ul>
174
				</div>
175
			</div>
176
		', esc_attr( $letter ), esc_html( $letter ), $this->format_posts_as_list( $posts ) );
177
	}
178
179
	/**
180
	 * Format an array post `$post_id => $post_title` as a list.
181
	 *
182
	 * @since 3.17.0
183
	 *
184
	 * @param array $posts An array of `$post_id => $post_title` key, value pairs.
185
	 *
186
	 * @return string A list.
187
	 */
188
	private function format_posts_as_list( $posts ) {
189
190
		return array_reduce( array_keys( $posts ), function ( $carry, $item ) use ( $posts ) {
191
			return $carry . sprintf( '<li><a href="%s">%s</a></li>', esc_attr( get_permalink( $item ) ), esc_html( $posts[ $item ] ) );
192
		}, '' );
193
	}
194
195
	/**
196
	 * Get the posts from WordPress using the provided attributes.
197
	 *
198
	 * @since 3.17.0
199
	 *
200
	 * @param array $atts The shortcode attributes.
201
	 *
202
	 * @return array An array of {@link WP_Post}s.
203
	 */
204
	private function get_posts( $atts ) {
205
206
		// The default arguments for the query.
207
		$args = array(
208
			'numberposts'            => intval( $atts['limit'] ),
209
			'update_post_meta_cache' => false,
210
			'update_post_term_cache' => false,
211
			// Exclude the publisher.
212
			'post__not_in' => array( $this->configuration_service->get_publisher_id() ),
213
		);
214
215
		// Limit the based entity type if needed.
216
		if ( 'all' !== $atts['type'] ) {
217
			$args['tax_query'] = array(
218
				array(
219
					'taxonomy' => Wordlift_Entity_Types_Taxonomy_Service::TAXONOMY_NAME,
220
					'field'    => 'slug',
221
					'terms'    => $atts['type'],
222
				),
223
			);
224
		}
225
226
		// Get the posts. Note that if a `type` is specified before, then the
227
		// `tax_query` from the `add_criterias` call isn't added.
228
		return get_posts( Wordlift_Entity_Service::add_criterias( $args ) );
229
230
	}
231
232
	/**
233
	 * Populate the alphabet with posts.
234
	 *
235
	 * @since 3.17.0
236
	 *
237
	 * @param array $alphabet An array of letters.
238
	 * @param int   $post_id  The {@link WP_Post} id.
239
	 */
240
	private function add_to_alphabet( &$alphabet, $post_id ) {
241
242
		// Get the title without accents.
243
		$title = remove_accents( get_the_title( $post_id ) );
244
245
		// Get the initial letter.
246
		$letter = $this->get_first_letter_in_alphabet_or_hash( $alphabet, $title );
247
248
		// Add the post.
249
		$alphabet[ $letter ][ $post_id ] = $title;
250
251
	}
252
253
	/**
254
	 * Find the first letter in the alphabet.
255
	 *
256
	 * In some alphabets a letter is a compound of letters, therefore this function
257
	 * will look for groups of 2 or 3 letters in the alphabet before looking for a
258
	 * single letter. In case the letter is not found a # (hash) key is returned.
259
	 *
260
	 * @since 3.17.0
261
	 *
262
	 * @param array  $alphabet An array of alphabet letters.
263
	 * @param string $title    The title to match.
264
	 *
265
	 * @return string The initial letter or a `#` key.
266
	 */
267
	private function get_first_letter_in_alphabet_or_hash( $alphabet, $title ) {
268
269
		// Need to handle letters which consist of 3 and 2 characters.
270
		for ( $i = 3; $i > 0; $i -- ) {
271
			$letter = mb_convert_case( mb_substr( $title, 0, $i ), MB_CASE_UPPER );
272
			if ( isset( $alphabet[ $letter ] ) ) {
273
				return $letter;
274
			}
275
		}
276
277
		return '#';
278
	}
279
280
}
281