Completed
Push — develop ( 5c7568...6a9ba1 )
by David
14:52
created

Wordlift_Vocabulary_Shortcode   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 322
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 0
Metric Value
dl 0
loc 322
rs 10
c 0
b 0
f 0
wmc 26
lcom 1
cbo 5

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A are_requirements_satisfied() 0 8 5
C render() 0 91 9
A get_section() 0 20 2
A format_posts_as_list() 0 8 1
A get_posts() 0 32 3
A add_to_alphabet() 0 12 1
A get_first_letter_in_alphabet_or_hash() 0 12 3
A get_and_increment_vocabulary_id() 0 3 1
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
	 * The vocabulary id
48
	 *
49
	 * @since  3.18.3
50
	 * @access private
51
	 * @var int $vocabulary_id The vocabulary unique id.
52
	 */
53
	private static $vocabulary_id = 0;
54
55
	/**
56
	 * Create a {@link Wordlift_Glossary_Shortcode} instance.
57
	 *
58
	 * @since 3.16.0
59
	 *
60
	 * @param \Wordlift_Configuration_Service $configuration_service The {@link Wordlift_Configuration_Service} instance.
61
	 */
62
	public function __construct( $configuration_service ) {
63
		parent::__construct();
64
65
		$this->log = Wordlift_Log_Service::get_logger( get_class() );
66
67
		$this->configuration_service = $configuration_service;
68
69
	}
70
71
	/**
72
	 * Check whether the requirements for this shortcode to work are available.
73
	 *
74
	 * @since 3.17.0
75
	 * @return bool True if the requirements are satisfied otherwise false.
76
	 */
77
	private static function are_requirements_satisfied() {
78
79
		return function_exists( 'mb_strlen' ) &&
80
		       function_exists( 'mb_substr' ) &&
81
		       function_exists( 'mb_strtolower' ) &&
82
		       function_exists( 'mb_strtoupper' ) &&
83
		       function_exists( 'mb_convert_case' );
84
	}
85
86
	/**
87
	 * Render the shortcode.
88
	 *
89
	 * @since 3.16.0
90
	 *
91
	 * @param array $atts An array of shortcode attributes as set by the editor.
92
	 *
93
	 * @return string The output html code.
94
	 */
95
	public function render( $atts ) {
96
97
		// Bail out if the requirements aren't satisfied: we need mbstring for
98
		// the vocabulary widget to work.
99
		if ( ! self::are_requirements_satisfied() ) {
100
			$this->log->warn( "The vocabulary widget cannot be displayed because this WordPress installation doesn't satisfy its requirements." );
101
102
			return '';
103
		}
104
105
		wp_enqueue_style( 'wl-vocabulary-shortcode', dirname( plugin_dir_url( __FILE__ ) ) . '/public/css/wordlift-vocabulary-shortcode.css' );
106
107
		// Extract attributes and set default values.
108
		$atts = shortcode_atts(
109
			array(
110
				// 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...
111
				'type'    => 'all',
112
				// Limit the number of posts to 100 by default. Use -1 to remove the limit.
113
				'limit'   => 100,
114
				// Sort by title.
115
				'orderby' => 'post_date',
116
				// Sort DESC.
117
				'order'   => 'DESC',
118
				// Allow to specify the category ID.
119
				'cat'     => '',
120
			), $atts
121
		);
122
123
		// Get the posts. Note that if a `type` is specified before, then the
124
		// `tax_query` from the `add_criterias` call isn't added.
125
		$posts = $this->get_posts( $atts );
126
127
		// Get the alphabet.
128
		$language_code = $this->configuration_service->get_language_code();
129
		$alphabet      = Wordlift_Alphabet_Service::get( $language_code );
130
131
		// Add posts to the alphabet.
132
		foreach ( $posts as $post ) {
133
			$this->add_to_alphabet( $alphabet, $post->ID );
134
		}
135
136
		$header   = '';
137
		$sections = '';
138
139
		// Get unique id for each vocabulary shortcode.
140
		$vocabulary_id = self::get_and_increment_vocabulary_id();
141
142
		// Generate the header.
143
		foreach ( $alphabet as $item => $translations ) {
144
			$template = ( empty( $translations )
145
				? '<span class="wl-vocabulary-widget-disabled">%s</span>'
146
				: '<a href="#wl-vocabulary-%3$d-%2$s">%1$s</a>' );
147
148
			$header .= sprintf( $template, esc_html( $item ), esc_attr( $item ), $vocabulary_id );
149
		}
150
151
		// Generate the sections.
152
		foreach ( $alphabet as $item => $translations ) {
153
			// @since 3.19.3 we use `mb_strtolower` and `mb_strtoupper` with a custom function to handle sorting,
154
			// since we had `AB` being placed before `Aa` with `asort`.
155
			//
156
			// Order the translations alphabetically.
157
			// asort( $translations );
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% 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...
158
			uasort( $translations, function ( $a, $b ) {
159
				if ( mb_strtolower( $a ) === mb_strtolower( $b )
160
				     || mb_strtoupper( $a ) === mb_strtoupper( $b ) ) {
161
					return 0;
162
				}
163
164
				return ( mb_strtolower( $a ) < mb_strtolower( $b ) ) ? - 1 : 1;
165
			} );
166
			$sections .= $this->get_section( $item, $translations, $vocabulary_id );
167
		}
168
169
		// Return HTML template.
170
		ob_start();
171
		?>
172
        <div class='wl-vocabulary'>
173
            <nav class='wl-vocabulary-alphabet-nav'>
174
				<?php echo $header; ?>
175
            </nav>
176
            <div class='wl-vocabulary-grid'>
177
				<?php echo $sections; ?>
178
            </div>
179
        </div>
180
		<?php
181
		$html = ob_get_clean();
182
183
		return $html;
184
185
	}
186
187
	/**
188
	 * Generate the html code for the section.
189
	 *
190
	 * @since 3.17.0
191
	 *
192
	 * @param string $letter The section's letter.
193
	 * @param array  $posts An array of `$post_id => $post_title` associated with
194
	 *                               the section.
195
	 * @param int    $vocabulary_id Unique vocabulary id.
196
	 *
197
	 * @return string The section html code (or an empty string if the section has
198
	 *                no posts).
199
	 */
200
	private function get_section( $letter, $posts, $vocabulary_id ) {
201
202
		// Return an empty string if there are no posts.
203
		if ( 0 === count( $posts ) ) {
204
			return '';
205
		}
206
207
		return sprintf(
208
			'
209
			<div class="wl-vocabulary-letter-block" id="wl-vocabulary-%d-%s">
210
				<aside class="wl-vocabulary-left-column">%s</aside>
211
				<div class="wl-vocabulary-right-column">
212
					<ul class="wl-vocabulary-items-list">
213
						%s
214
					</ul>
215
				</div>
216
			</div>
217
		', $vocabulary_id, esc_attr( $letter ), esc_html( $letter ), $this->format_posts_as_list( $posts )
218
		);
219
	}
220
221
	/**
222
	 * Format an array post `$post_id => $post_title` as a list.
223
	 *
224
	 * @since 3.17.0
225
	 *
226
	 * @param array $posts An array of `$post_id => $post_title` key, value pairs.
227
	 *
228
	 * @return string A list.
229
	 */
230
	private function format_posts_as_list( $posts ) {
231
232
		return array_reduce(
233
			array_keys( $posts ), function ( $carry, $item ) use ( $posts ) {
234
			return $carry . sprintf( '<li><a href="%s">%s</a></li>', esc_attr( get_permalink( $item ) ), esc_html( $posts[ $item ] ) );
235
		}, ''
236
		);
237
	}
238
239
	/**
240
	 * Get the posts from WordPress using the provided attributes.
241
	 *
242
	 * @since 3.17.0
243
	 *
244
	 * @param array $atts The shortcode attributes.
245
	 *
246
	 * @return array An array of {@link WP_Post}s.
247
	 */
248
	private function get_posts( $atts ) {
249
		// The default arguments for the query.
250
		$args = array(
251
			'posts_per_page'         => intval( $atts['limit'] ),
252
			'update_post_meta_cache' => false,
253
			'update_post_term_cache' => false,
254
			'orderby'                => $atts['orderby'],
255
			'order'                  => $atts['order'],
256
			// Exclude the publisher.
257
			'post__not_in'           => array( $this->configuration_service->get_publisher_id() ),
258
		);
259
260
		// Limit the based entity type if needed.
261
		if ( 'all' !== $atts['type'] ) {
262
			$args['tax_query'] = array(
263
				array(
264
					'taxonomy' => Wordlift_Entity_Type_Taxonomy_Service::TAXONOMY_NAME,
265
					'field'    => 'slug',
266
					'terms'    => $atts['type'],
267
				),
268
			);
269
		}
270
271
		if ( ! empty( $atts['cat'] ) ) {
272
			$args['cat'] = $atts['cat'];
273
		}
274
275
		// Get the posts. Note that if a `type` is specified before, then the
276
		// `tax_query` from the `add_criterias` call isn't added.
277
		return get_posts( Wordlift_Entity_Service::add_criterias( $args ) );
278
279
	}
280
281
	/**
282
	 * Populate the alphabet with posts.
283
	 *
284
	 * @since 3.17.0
285
	 *
286
	 * @param array $alphabet An array of letters.
287
	 * @param int   $post_id The {@link WP_Post} id.
288
	 */
289
	private function add_to_alphabet( &$alphabet, $post_id ) {
290
291
		// Get the title without accents.
292
		$title = remove_accents( get_the_title( $post_id ) );
293
294
		// Get the initial letter.
295
		$letter = $this->get_first_letter_in_alphabet_or_hash( $alphabet, $title );
296
297
		// Add the post.
298
		$alphabet[ $letter ][ $post_id ] = $title;
299
300
	}
301
302
	/**
303
	 * Find the first letter in the alphabet.
304
	 *
305
	 * In some alphabets a letter is a compound of letters, therefore this function
306
	 * will look for groups of 2 or 3 letters in the alphabet before looking for a
307
	 * single letter. In case the letter is not found a # (hash) key is returned.
308
	 *
309
	 * @since 3.17.0
310
	 *
311
	 * @param array  $alphabet An array of alphabet letters.
312
	 * @param string $title The title to match.
313
	 *
314
	 * @return string The initial letter or a `#` key.
315
	 */
316
	private function get_first_letter_in_alphabet_or_hash( $alphabet, $title ) {
317
318
		// Need to handle letters which consist of 3 and 2 characters.
319
		for ( $i = 3; $i > 0; $i -- ) {
320
			$letter = mb_convert_case( mb_substr( $title, 0, $i ), MB_CASE_UPPER );
321
			if ( isset( $alphabet[ $letter ] ) ) {
322
				return $letter;
323
			}
324
		}
325
326
		return '#';
327
	}
328
329
	/**
330
	 * Get and increment the `$vocabulary_id`.
331
	 *
332
	 * @since  3.18.3
333
	 *
334
	 * @return int The incremented vocabulary id.
335
	 */
336
	private static function get_and_increment_vocabulary_id() {
337
		return self::$vocabulary_id ++;
338
	}
339
340
}
341