GenericList::prepareHeaders()   F
last analyzed

Complexity

Conditions 14
Paths 578

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 14
eloc 12
nc 578
nop 0
dl 0
loc 18
rs 2.6861
c 0
b 0
f 0

How to fix   Complexity   

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
/**
4
 * This class implements a standard way of displaying lists.
5
 *
6
 * @package   ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
9
 *
10
 * This file contains code covered by:
11
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
12
 *
13
 * @version 2.0 dev
14
 *
15
 */
16
17
namespace ElkArte\Helper;
18
19
use ElkArte\Languages\Txt;
20
21
/**
22
 * This class implements a standard way of displaying lists.
23
 */
24
class GenericList
25
{
26
	/**
27
	 * List options, an array with the format:
28
	 * 'id'
29
	 * 'columns'
30
	 * 'items_per_page'
31
	 * 'no_items_label'
32
	 * 'no_items_align'
33
	 * 'base_href'
34
	 * 'default_sort_col'
35
	 * 'form'
36
	 *   'href'
37
	 *   'hidden_fields'
38
	 * 'list_menu'
39
	 * 'javascript'
40
	 * 'data_check'
41
	 * 'start_var_name'
42
	 * 'default_sort_dir'
43
	 * 'request_vars'
44
	 *   'sort'
45
	 *   'desc'
46
	 * 'get_items'
47
	 *   'function'
48
	 * 'get_count'
49
	 *   'file'
50
	 *   'function'
51
	 *
52
	 * @var array
53
	 */
54
	protected $listOptions = [];
55
56
	/** @var HttpReq Instance of \ElkArte\Helper\HttpReq */
57
	protected $req;
58
59
	/** @var array Will hold the created $context */
60
	protected $context = [];
61
62
	/** @var array */
63
	protected $listItems = [];
64
65
	/** @var string */
66
	protected $sort = '';
67
68
	/** @var string */
69
	protected $sortVar = '';
70
71
	/** @var string */
72
	protected $descVar = '';
73
74
	/**
75
	 * GenericList constructor, Starts a new list
76
	 * Makes sure the passed list contains the minimum needed options to create a list
77
	 * Loads the options in to this instance
78
	 *
79
	 * @param array $listOptions
80
	 */
81
	public function __construct(array $listOptions)
82
	{
83
		// Access to post/get data
84
		$this->req = HttpReq::instance();
85
86
		// First make sure the array is constructed properly.
87
		$this->validateListOptions($listOptions);
88
89
		// Now that we've done that, let's set it, we're gonna need it!
90
		$this->listOptions = $listOptions;
91
92
		// Be ready for those pesky errors
93
		Txt::load('Errors');
94
95
		// Load the template
96
		theme()->getTemplates()->load('GenericList');
97
	}
98
99
	/**
100
	 * Validate the options sent
101
	 *
102
	 * @param array $listOptions
103
	 */
104
	protected function validateListOptions($listOptions)
105
	{
106
		// @todo trigger error here?
107
		assert(isset($listOptions['id']));
108
		assert(isset($listOptions['columns']));
109
		assert(is_array($listOptions['columns']));
110
		assert((empty($listOptions['items_per_page']) || (isset($listOptions['get_count']['function'], $listOptions['base_href']) && is_numeric($listOptions['items_per_page']))));
111
		assert((empty($listOptions['default_sort_col']) || isset($listOptions['columns'][$listOptions['default_sort_col']])));
112
		assert((!isset($listOptions['form']) || isset($listOptions['form']['href'])));
113
	}
114
115
	/**
116
	 * Make the list.
117
	 * The list will be populated in $context.
118
	 */
119
	public function buildList()
120
	{
121
		$this->prepareSort();
122
		$this->calculatePages();
123
		$this->prepareHeaders();
124
		$this->prepareColumns();
125
		$this->setTitle();
126
		$this->prepareForm();
127
		$this->prepareNoItemsLabel();
128
		$this->prepareAdditionalRows();
129
		$this->prepareJavascript();
130
		$this->prepareMenu();
131
		$this->prepareContext();
132
	}
133
134
	/**
135
	 * Figure out the sorting method.
136
	 */
137
	protected function prepareSort()
138
	{
139
		if (empty($this->listOptions['default_sort_col']))
140
		{
141
			$this->context['sort'] = array();
142
			$this->sort = '1=1';
143
		}
144
		else
145
		{
146
			$this->sortVar = $this->listOptions['request_vars']['sort'] ?? 'sort';
147
			$this->descVar = $this->listOptions['request_vars']['desc'] ?? 'desc';
148
			$sortReq = $this->req->getQuery($this->sortVar);
149
150
			if (isset($this->listOptions['columns'][$sortReq]['sort']))
151
			{
152
				$this->context['sort'] = array(
153
					'id' => $sortReq,
154
					'desc' => isset($_REQUEST[$this->descVar], $this->listOptions['columns'][$sortReq]['sort']['reverse']),
155
				);
156
			}
157
			else
158
			{
159
				$this->context['sort'] = array(
160
					'id' => $this->listOptions['default_sort_col'],
161
					'desc' => (!empty($this->listOptions['default_sort_dir']) && $this->listOptions['default_sort_dir'] === 'desc') || (!empty($this->listOptions['columns'][$this->listOptions['default_sort_col']]['sort']['default']) && substr($this->listOptions['columns'][$this->listOptions['default_sort_col']]['sort']['default'], -4, 4) === 'desc'),
162
				);
163
			}
164
165
			// Set the database column sort.
166
			$this->sort = $this->listOptions['columns'][$this->context['sort']['id']]['sort'][$this->context['sort']['desc'] ? 'reverse' : 'default'];
167
		}
168
169
		$this->context['start_var_name'] = $this->listOptions['start_var_name'] ?? 'start';
170
	}
171
172
	/**
173
	 * Calculate the page index.
174
	 */
175
	protected function calculatePages()
176
	{
177
		// In some cases the full list must be shown, regardless of the amount of items.
178
		if (empty($this->listOptions['items_per_page']))
179
		{
180
			$this->context['start'] = 0;
181
			$this->context['items_per_page'] = 0;
182
		}
183
		// With items per page set, calculate total number of items and page index.
184
		else
185
		{
186
			// First get an impression of how many items to expect.
187
			if (isset($this->listOptions['get_count']['file']))
188
			{
189
				require_once($this->listOptions['get_count']['file']);
190
			}
191
192
			$this->context['total_num_items'] = call_user_func_array($this->listOptions['get_count']['function'], empty($this->listOptions['get_count']['params']) ? array() : $this->listOptions['get_count']['params']);
193
194
			// Default the start to the beginning... sounds logical, amirite?
195
			$this->context['start'] = $this->req->getQuery($this->context['start_var_name'], 'intval', 0);
196
			$this->context['items_per_page'] = $this->listOptions['items_per_page'];
197
198
			// Then create a page index.
199
			if ($this->context['total_num_items'] > $this->context['items_per_page'])
200
			{
201
				$this->context['page_index'] = constructPageIndex($this->listOptions['base_href'] . (empty($this->context['sort']) ? '' : ';' . $this->sortVar . '=' . $this->context['sort']['id'] . ($this->context['sort']['desc'] ? ';' . $this->descVar : '')) . ($this->context['start_var_name'] != 'start' ? ';' . $this->context['start_var_name'] . '=%1$d' : ''), $this->context['start'], $this->context['total_num_items'], $this->context['items_per_page'], $this->context['start_var_name'] != 'start');
202
			}
203
		}
204
	}
205
206
	/**
207
	 * Prepare the headers of the table.
208
	 */
209
	protected function prepareHeaders()
210
	{
211
		$this->context['headers'] = array();
212
		foreach ($this->listOptions['columns'] as $column_id => $column)
213
		{
214
			if (isset($column['evaluate']) && $column['evaluate'] === false)
215
			{
216
				continue;
217
			}
218
219
			$this->context['headers'][] = array(
220
				'id' => $column_id,
221
				'label' => $column['header']['value'] ?? '',
222
				'href' => empty($this->listOptions['default_sort_col']) || empty($column['sort']) ? '' : $this->listOptions['base_href'] . ';' . $this->sortVar . '=' . $column_id . ($column_id === $this->context['sort']['id'] && !$this->context['sort']['desc'] && isset($column['sort']['reverse']) ? ';' . $this->descVar : '') . (empty($this->context['start']) ? '' : ';' . $this->context['start_var_name'] . '=' . $this->context['start']),
223
				'sort_image' => empty($this->listOptions['default_sort_col']) || empty($column['sort']) || $column_id !== $this->context['sort']['id'] ? null : ($this->context['sort']['desc'] ? 'down' : 'up'),
224
				'class' => $column['header']['class'] ?? '',
225
				'style' => $column['header']['style'] ?? '',
226
				'colspan' => $column['header']['colspan'] ?? '',
227
			);
228
		}
229
	}
230
231
	/**
232
	 * Prepare columns.
233
	 */
234
	protected function prepareColumns()
235
	{
236
		// We know the amount of columns, might be useful for the template.
237
		$this->context['num_columns'] = count($this->listOptions['columns']);
238
		$this->context['width'] = $this->listOptions['width'] ?? '0';
239
240
		// Maybe we want this one to interact with jquery UI sortable
241
		$this->context['sortable'] = isset($this->listOptions['sortable']);
242
243
		// Get the file with the function for the item list.
244
		if (isset($this->listOptions['get_items']['file']))
245
		{
246
			require_once($this->listOptions['get_items']['file']);
247
		}
248
249
		// Call the function and include which items we want and in what order.
250
		$this->listItems = call_user_func_array($this->listOptions['get_items']['function'], array_merge(array($this->context['start'], $this->context['items_per_page'], $this->sort), empty($this->listOptions['get_items']['params']) ? array() : $this->listOptions['get_items']['params']));
251
		$this->listItems = empty($this->listItems) ? array() : $this->listItems;
252
253
		$this->loopItems();
254
	}
255
256
	/**
257
	 * Build the data values for the column
258
	 */
259
	protected function loopItems()
260
	{
261
		// Loop through the list items to be shown and construct the data values.
262
		$this->context['rows'] = array();
263
		foreach ($this->listItems as $item_id => $list_item)
264
		{
265
			$cur_row = array();
266
			foreach ($this->listOptions['columns'] as $column_id => $column)
267
			{
268
				if (isset($column['evaluate']) && $column['evaluate'] === false)
269
				{
270
					$this->context['num_columns']--;
271
					continue;
272
				}
273
274
				$cur_data = array();
275
276
				// A value straight from the database?
277
				if (isset($column['data']['db']))
278
				{
279
					$cur_data['value'] = $list_item[$column['data']['db']] ?? '';
280
				}
281
				// Take the value from the database and make it HTML safe.
282
				elseif (isset($column['data']['db_htmlsafe']))
283
				{
284
					$cur_data['value'] = htmlspecialchars($list_item[$column['data']['db_htmlsafe']], ENT_COMPAT, 'UTF-8');
285
				}
286
				// Using sprintf is probably the most readable way of injecting data.
287
				elseif (isset($column['data']['sprintf']))
288
				{
289
					$params = array();
290
					foreach ($column['data']['sprintf']['params'] as $sprintf_param => $htmlsafe)
291
					{
292
						$params[] = $htmlsafe ? htmlspecialchars($list_item[$sprintf_param], ENT_COMPAT, 'UTF-8') : $list_item[$sprintf_param];
293
					}
294
295
					$cur_data['value'] = vsprintf($column['data']['sprintf']['format'], $params);
296
				}
297
				// The most flexible way probably is applying a custom function.
298
				elseif (isset($column['data']['function']))
299
				{
300
					$cur_data['value'] = $column['data']['function']($list_item);
301
				}
302
				// A literal value.
303
				elseif (isset($column['data']['value']))
304
				{
305
					$cur_data['value'] = $column['data']['value'];
306
				}
307
				// Empty value.
308
				else
309
				{
310
					$cur_data['value'] = '';
311
				}
312
313
				// Allow for basic formatting.
314
				if (!empty($column['data']['comma_format']))
315
				{
316
					$cur_data['value'] = comma_format($cur_data['value']);
317
				}
318
				elseif (!empty($column['data']['timeformat']))
319
				{
320
					// Maybe we need a relative time?
321
					if ($column['data']['timeformat'] === 'html_time')
322
					{
323
						$cur_data['value'] = htmlTime($cur_data['value']);
324
					}
325
					else
326
					{
327
						$cur_data['value'] = standardTime($cur_data['value']);
328
					}
329
				}
330
331
				// Set a style class for this column?
332
				if (isset($column['data']['class']))
333
				{
334
					$cur_data['class'] = $column['data']['class'];
335
				}
336
337
				// Fully customized styling for the cells in this column only.
338
				if (isset($column['data']['style']))
339
				{
340
					$cur_data['style'] = $column['data']['style'];
341
				}
342
343
				// Add the data cell properties to the current row.
344
				$cur_row[$column_id] = $cur_data;
345
			}
346
347
			$this->context['rows'][$item_id]['class'] = '';
348
			$this->context['rows'][$item_id]['style'] = '';
349
350
			// Maybe we wat set a custom class for the row based on the data in the row itself
351
			if (isset($this->listOptions['data_check']))
352
			{
353
				if (isset($this->listOptions['data_check']['class']))
354
				{
355
					$this->context['rows'][$item_id]['class'] = ' ' . $this->listOptions['data_check']['class']($list_item);
356
				}
357
358
				if (isset($this->listOptions['data_check']['style']))
359
				{
360
					$this->context['rows'][$item_id]['style'] = ' style="' . $this->listOptions['data_check']['style']($list_item) . '"';
361
				}
362
			}
363
364
			// Insert the row into the list.
365
			$this->context['rows'][$item_id]['data'] = $cur_row;
366
		}
367
	}
368
369
	/**
370
	 * Prepare the title (optional).
371
	 */
372
	protected function setTitle()
373
	{
374
		// The title is currently optional.
375
		if (isset($this->listOptions['title']))
376
		{
377
			$this->context['title'] = $this->listOptions['title'];
378
379
			// And the icon is optional for the title
380
			if (isset($this->listOptions['icon']))
381
			{
382
				$this->context['icon'] = $this->listOptions['icon'];
383
			}
384
		}
385
	}
386
387
	/**
388
	 * Prepare a form (optional) with hidden fields.
389
	 *
390
	 * Session check is added automatically. Both a token and a page identifier are optional.
391
	 */
392
	protected function prepareForm()
393
	{
394
		global $context;
395
396
		// In case there's a form, share it with the template context.
397
		if (isset($this->listOptions['form']))
398
		{
399
			$this->context['form'] = $this->listOptions['form'];
400
401
			if (!isset($this->context['form']['hidden_fields']))
402
			{
403
				$this->context['form']['hidden_fields'] = array();
404
			}
405
406
			// Always add a session check field.
407
			$this->context['form']['hidden_fields'][$context['session_var']] = $context['session_id'];
408
409
			// Will this do a token check?
410
			if (isset($this->listOptions['form']['token']))
411
			{
412
				$this->context['form']['hidden_fields'][$context[$this->listOptions['form']['token'] . '_token_var']] = $context[$this->listOptions['form']['token'] . '_token'];
413
			}
414
415
			// Include the starting page as hidden field?
416
			if (!empty($this->context['form']['include_start']) && !empty($this->context['start']))
417
			{
418
				$this->context['form']['hidden_fields'][$this->context['start_var_name']] = $this->context['start'];
419
			}
420
421
			// If sorting needs to be the same after submitting, add the parameter.
422
			if (!empty($this->context['form']['include_sort']) && !empty($this->context['sort']))
423
			{
424
				$this->context['form']['hidden_fields']['sort'] = $this->context['sort']['id'];
425
426
				if ($this->context['sort']['desc'])
427
				{
428
					$this->context['form']['hidden_fields']['desc'] = 1;
429
				}
430
			}
431
		}
432
	}
433
434
	/**
435
	 * Say something nice in case there are no items.
436
	 */
437
	protected function prepareNoItemsLabel()
438
	{
439
		if (isset($this->listOptions['no_items_label']))
440
		{
441
			$this->context['no_items_label'] = $this->listOptions['no_items_label'];
442
			$this->context['no_items_align'] = $this->listOptions['no_items_align'] ?? '';
443
		}
444
	}
445
446
	/**
447
	 * A list can sometimes need a few extra rows above and below.
448
	 *
449
	 * Supported row positions: top_of_list, after_title, selectors,
450
	 * above_column_headers, below_table_data, bottom_of_list.
451
	 */
452
	protected function prepareAdditionalRows()
453
	{
454
		if (isset($this->listOptions['additional_rows']))
455
		{
456
			$this->context['additional_rows'] = array();
457
			foreach ($this->listOptions['additional_rows'] as $row)
458
			{
459
				if (empty($row))
460
				{
461
					continue;
462
				}
463
464
				if (!isset($this->context['additional_rows'][$row['position']]))
465
				{
466
					$this->context['additional_rows'][$row['position']] = array();
467
				}
468
469
				$this->context['additional_rows'][$row['position']][] = $row;
470
			}
471
		}
472
	}
473
474
	/**
475
	 * Add an option for inline JavaScript.
476
	 */
477
	protected function prepareJavascript()
478
	{
479
		if (isset($this->listOptions['javascript']))
480
		{
481
			theme()->addInlineJavascript($this->listOptions['javascript'], true);
482
		}
483
	}
484
485
	/**
486
	 * We want a menu.
487
	 */
488
	protected function prepareMenu()
489
	{
490
		if (isset($this->listOptions['list_menu']))
491
		{
492
			if (!isset($this->listOptions['list_menu']['position']))
493
			{
494
				$this->listOptions['list_menu']['position'] = 'left';
495
			}
496
497
			$this->context['list_menu'] = $this->listOptions['list_menu'];
498
		}
499
	}
500
501
	/**
502
	 * Prepare the template by loading context variables for each setting.
503
	 */
504
	protected function prepareContext()
505
	{
506
		global $context;
507
508
		$context[$this->listOptions['id']] = $this->context;
509
510
		// Let's set some default that could be useful to avoid repetitions
511
		if (!isset($context['sub_template']))
512
		{
513
			if (function_exists('template_' . $this->listOptions['id']))
514
			{
515
				$context['sub_template'] = $this->listOptions['id'];
516
			}
517
			else
518
			{
519
				$context['sub_template'] = 'show_list';
520
				if (!isset($context['default_list']))
521
				{
522
					$context['default_list'] = $this->listOptions['id'];
523
				}
524
			}
525
		}
526
	}
527
}
528