PageRenderTime 307ms CodeModel.GetById 141ms app.highlight 72ms RepoModel.GetById 89ms app.codeStats 0ms

/QuickApps/View/Helper/TableHelper.php

http://github.com/QuickAppsCMS/QuickApps-CMS
PHP | 383 lines | 205 code | 47 blank | 131 comment | 44 complexity | 8e0881a77e2e4b9e3055158d6c8d8f56 MD5 | raw file
  1<?php
  2/**
  3 * Table Helper
  4 *
  5 * PHP version 5
  6 *
  7 * @package  QuickApps.View.Helper
  8 * @version  1.0
  9 * @author   Christopher Castro <chris@quickapps.es>
 10 * @link	 http://www.quickappscms.org
 11 */
 12
 13/**
 14 * ### Expected data's structure
 15 *
 16 * `$data` MUST be a numeric array. For example, any list result of `Model::find()` or paginated result:
 17 *
 18 *    $data = array(
 19 *        0 => array(
 20 *            'Model' => array('field1' => 'data', ...),
 21 *            'Model2' => ...
 22 *        ),
 23 *        ....
 24 *    );
 25 *
 26 * ### Options
 27 *
 28 * `columns` (array): Information about each of the columns of your table:
 29 *
 30 *    array(
 31 *        'columns' => array(
 32 *            'Column Title' => array(
 33 *                'value' => , (string) Values to display when filling this column.
 34 *						       You can specify array paths to find in the $data array. e.g.: `{Model.field}`
 35 *						       See TableHelper::_renderCell() for more tags.
 36 *	              'thOptions' => , (array) <th> tag options for this column. This will affect table header only.
 37 *	              'tdOptions' => , (array) <td> tag options for this column. This will affect table body (result rows) only.
 38 *	              'sort' => Optional (string) `Model.field`
 39 *            ),
 40 *            'Other Column' => array( ... ),
 41 *            ...
 42 *    );
 43 *
 44 */
 45class TableHelper extends AppHelper {
 46/**
 47 * Helpers used by TableHelper.
 48 *
 49 * @var array
 50 */
 51	public $helpers = array('Html', 'Paginator');
 52
 53/**
 54 * Default table rendering options.
 55 * `columns` (array): Settings for each table's column. (see TableHelper::$__columnDefaults).
 56 * `headerPosition` (mixed): Column titles position. 'top', 'top&bottom', bottom'. Or (boolean) FALSE for no titles.
 57 * `noItemsMessage` (string): Message to show if there are no rows to display.
 58 * `tableOptions` (array): <table> tag attributes.
 59 * `trOptions` (array): <tr> tag attributes for every row of content (between <tbody></tbody>).
 60 * `paginate.position` (string): Pagination row position, 'top' or 'top&bottom' or 'bottom'.
 61 * `paginate.trOptions` (array): <tr> tags attributes.
 62 * `paginate.tdOptions` (array): <td> tags attributes.
 63 *
 64 * @var array
 65 */
 66	private $__defaults = array(
 67		'columns' => array(),
 68		'headerPosition' => 'top',
 69		'noItemsMessage' => 'There are no items to display',
 70		'tableOptions' => array('cellpadding' => 0, 'cellspacing' => 0, 'border' => 0),
 71		'trOptions' => array(),
 72		'paginate' => array(
 73			'options' => array(),
 74			'prev' => array(
 75				'title' => 'Ť Previous ',
 76				'options' => array(),
 77				'disabledTitle' => null,
 78				'disabledOptions' => array('class' => 'disabled')
 79			),
 80			'numbers' => array(
 81				'options' => array(
 82					'before' => ' &nbsp; ',
 83					'after' => ' &nbsp; ',
 84					'modulus' => 10,
 85					'separator' => ' &nbsp; ',
 86					'tag' => 'span',
 87					'first' => 'First ',
 88					'last' => ' Last',
 89					'ellipsis' => '...'
 90				)
 91			),
 92			'next' => array(
 93				'title' => ' Next ť',
 94				'options' => array(),
 95				'disabledTitle' => null,
 96				'disabledOptions' => array('class' => 'disabled')
 97			),
 98			'position' => 'bottom',
 99			'trOptions' => array('class' => 'paginator'),
100			'tdOptions' => array('align' => 'center')
101		)
102	);
103
104/**
105 * Column default options.
106 * `value` (string): Cell's content. You can use special tags, see TableHelper::_renderCell().
107 * `thOptions` (array): <th> tag attributes for header cells (between <thead></thead>).
108 * `tdOptions` (array): <td> tag attributes for body cells (for each row of content between <tbody></tbody>).
109 * `sort` (mixed): Set to a string indicating the column name (`Model.column`). Set to (boolean) FALSE for do not sort this column.
110 *
111 * @var array
112 */
113	private $__columnDefaults = array(
114		'value' => '',
115		'thOptions' => array('align' => 'left'),
116		'tdOptions' => array('align' => 'left'),
117		'sort' => false
118	);
119
120/**
121 * Holds the number of columns of the table being rendered.
122 *
123 * @var integer
124 */
125	private $__colsCount = 0;
126
127/**
128 * Renders out HTML table.
129 *
130 * @param array $data Data to fill table rows
131 * @param array $options Table options.
132 * @return string HTML table element
133 * @see TableHelper::$__defaults
134 */
135	public function create($data, $options) {
136		$this->__defaults['paginate']['prev']['title'] = __t('Ť Previous ');
137		$this->__defaults['paginate']['next']['title'] = __t(' Next ť');
138
139		if (isset($options['paginate']) && $options['paginate'] === true) {
140			unset($options['paginate']);
141		} else {
142			$this->__defaults['paginate'] = !isset($options['paginate']) ? false : $this->__defaults['paginate'];
143		}
144
145		$options = Hash::merge($this->__defaults, $options);
146		$this->__colsCount = count($options['columns']);
147		$out = sprintf('<table%s>', $this->Html->_parseAttributes($options['tableOptions'])) . "\n";
148
149		if (count($data) > 0) {
150
151			$print_header_top = ($options['headerPosition'] !== false && in_array($options['headerPosition'], array('top', 'top&bottom')));
152			$print_paginator_top = ($options['paginate'] !== false && in_array($options['paginate']['position'], array('top', 'top&bottom')));
153
154			if ($print_header_top ||  $print_paginator_top) {
155				$out .= "\t<thead>\n";
156				$out .= $print_header_top ? $this->_renderHeader($options) : '';
157				$out .= $print_paginator_top ? $this->_renderPaginator($options) : '';
158				$out .= "\n\t</thead>\n";
159			}
160
161			$out .= "\t<tbody>\n";
162			$count = 1;
163
164			foreach ($data as $i => $r_data) {
165				$td = '';
166
167				foreach ($options['columns'] as $name => $c_data) {
168					$c_data = array_merge($this->__columnDefaults, $c_data);
169
170					$td .= "\n\t";
171					$td .= $this->Html->useTag('tablecell',
172							$this->Html->_parseAttributes($c_data['tdOptions']),
173							$this->_renderCell($c_data['value'], $data[$i])
174					);
175					$td .= "\t";
176				}
177
178				$tr_options = array(
179					'class' => ($count%2 ? 'even' : 'odd')
180				);
181
182				if (!empty($options['trOptions']) && is_array($options['trOptions'])) {
183					foreach ($options['trOptions'] as $key => $val) {
184						$val = $this->_renderCell($val, $data[$i]);
185
186						if ($key == 'class') {
187							$tr_options['class'] = $tr_options['class'] . " {$val}";
188						} else {
189							$tr_options[$key] = $val;
190						}
191					}
192				}
193
194				$out .= $this->Html->useTag('tablerow', $this->Html->_parseAttributes($tr_options), $td);
195				$count++;
196			}
197
198			$out .= "\t</tbody>\n";
199			$print_header_bottom = ($options['headerPosition'] !== false && in_array($options['headerPosition'], array('bottom', 'top&bottom')));
200			$print_paginator_bottom = ($options['paginate'] != false && in_array($options['paginate']['position'], array('bottom', 'top&bottom')));
201
202			if ($print_header_bottom || $print_paginator_bottom) {
203				$out .= "\t<tfoot>\n";
204				$out .= $print_header_bottom ? $this->_renderHeader($options) : '';
205				$out .= $print_paginator_bottom ? $this->_renderPaginator($options) : '';
206				$out .= "\n\t</tfoot>\n";
207			}
208		} else {
209			$options['noItemsMessage'] = $options['noItemsMessage'] != 'There are no items to display' ? $options['noItemsMessage'] : __t($options['noItemsMessage']);
210			$td = $this->Html->useTag('tablecell', $this->Html->_parseAttributes(array('colspan' => $this->__colsCount)), $options['noItemsMessage']);
211			$out .= $this->Html->useTag('tablerow', $this->Html->_parseAttributes(array('class' => 'even')), $td);
212		}
213
214		$out .= "</table>\n";
215
216		return $out;
217	}
218
219/**
220 * Render the given cell.
221 * Looks for special tags to be replaced, valid tags are:
222 *  - URL. e.g.: {url}/my/url.html{url}
223 *  - Array path. e.g.: {Node.slug}
224 *  - Image. e.g.: {img class='width' border=0}/url/to/image.jpg{/img}
225 *  - Link. e.g.: {link class='css-class' title='Link title'}Link label|/link/url.html{/link}
226 *  - Translation __t(). e.g.: {t}Translate this{/t}
227 *  - Translation __d(). e.g.: {d|System}System module will translate this{/d}
228 *  - PHP code. e.g.: {php} return 'Testing'; {/php}
229 *
230 * @param string $value Cell content
231 * @param array $row_data Array of data of the row that cell belongs to
232 * @return string HTML table cell content
233 */
234	protected function _renderCell($value, $row_data) {
235		// look for urls. e.g.: {url}/my/url.html{url}
236		preg_match_all('/\{url\}(.+)\{\/url\}/iUs', $value, $url);
237		if (isset($url[1]) && !empty($url[1])) {
238			foreach ($url[0] as $i => $m) {
239				$value = str_replace($m, $this->Html->url(trim($url[1][$i]), true), $value);
240			}
241		}
242
243		// look for array paths. e.g.: {Node.slug}
244		preg_match_all('/\{([\{\}0-9a-zA-Z_\.]+)\}/iUs', $value, $path);
245		if (isset($path[1]) && !empty($path[1])) {
246			$exclude = array('{d}', '{/d}', '{t}', '{/t}', '{php}', '{/php}', '{img}', '{/img}', '{link}', '{/link}');
247
248			foreach ($path[0] as $i => $m) {
249				if (in_array($m, $exclude)) {
250					continue;
251				}
252
253				$value = str_replace($m, addslashes(array_pop(Hash::extract($row_data, trim($path[1][$i])))), $value);
254			}
255		}
256
257		// look for images. {img class='width' border=0}/url/to/image.jpg{/img}
258		preg_match_all('/\{img(.*?)\}(.+)\{\/img\}/i', $value, $img);
259		if (isset($img[1]) && !empty($img[1])) {
260			foreach ($img[0] as $i => $m) {
261				$opts = isset($img[1][$i]) ? QuickApps::parseHooktagAttributes(trim($img[1][$i])) : array();
262				$opts = empty($opts) ? array(): $opts;
263				$value = str_replace($m, $this->Html->image($img[2][$i], $opts), $value);
264			}
265		}
266
267		// look for links. e.g..: {link class='css-class' title='Link title'}Link label|/link/url.html{/link}
268		preg_match_all('/\{link(.*?)\}(.*)\|(.*)\{\/link\}/i', $value, $link);
269		if (isset($link[1]) && !empty($link[1])) {
270			foreach ($link[0] as $i => $m) {
271				$opts = isset($link[1][$i]) ? QuickApps::parseHooktagAttributes(trim($link[1][$i])) : array();
272				$opts = empty($opts) ? array(): $opts;
273
274				if (isset($opts['escape'])) {
275					$opts['escape'] = $opts['escape'] == "true";
276				}
277
278				$value = str_replace($m, $this->Html->link(trim($link[2][$i]), $link[3][$i], $opts), $value);
279			}
280		}
281
282		// look for __t(). e.g.: {t}Translate this{/t}
283		preg_match_all('/\{t\}(.+)\{\/t\}/i', $value, $t);
284		if (isset($t[1]) && !empty($t[1])) {
285			foreach ($t[0] as $i => $m) {
286				$value = str_replace($m, __t($t[1][$i]), $value);
287			}
288		}
289
290		// look for __d(). e.g.: {d|System}System module will translate this{/d}
291		preg_match_all('/\{d\|(.+)\}(.+)\{\/d\}/i', $value, $d);
292		if (isset($d[1]) && !empty($d[1])) {
293			foreach ($d[0] as $i => $m) {
294				$value = str_replace($m, __d($d[1][$i], $d[2][$i]), $value);
295			}
296		}
297
298		// look for php code. e.g.: {php} return 'Testing'; {/php}
299		preg_match_all('/\{php\}(.+)\{\/php\}/iUs', $value, $php);
300		if (isset($php[1]) && !empty($php[1])) {
301			foreach ($php[0] as $i => $m) {
302				$value = str_replace($m, $this->__php_eval("<?php {$php[1][$i]}", $row_data), $value);
303			}
304		}
305
306		return $value;
307	}
308
309/**
310 * Evaluate a string of PHP code.
311 *
312 * This is a wrapper around PHP's eval(). It uses output buffering to capture both
313 * returned and printed text. Unlike eval(), we require code to be surrounded by
314 * <?php ?> tags; in other words, we evaluate the code as if it were a stand-alone
315 * PHP file.
316 *
317 * Using this wrapper also ensures that the PHP code which is evaluated can not
318 * overwrite any variables in the calling code, unlike a regular eval() call.
319 *
320 * @param string $code The code to evaluate.
321 * @return
322 *   A string containing the printed output of the code, followed by the returned
323 *   output of the code.
324 *
325 */
326	private function __php_eval($code, $row_data = array()) {
327		ob_start();
328		print eval('?>' . $code);
329
330		$output = ob_get_contents();
331
332		ob_end_clean();
333
334		return $output;
335	}
336
337/**
338 * Renders table's header.
339 *
340 * @return string HTML
341 */
342	protected function _renderHeader($options, $footer = false) {
343		$th = $out ='';
344
345		if ($footer && $options['paginate'] !== false && in_array($options['paginate']['position'], array('top', 'top&bottom'))) {
346			@$out .= $this->_renderPaginator($options);
347		}
348
349		foreach ($options['columns'] as $name => $data) {
350			$data = array_merge($this->__columnDefaults, $data);
351
352			if ($options['paginate'] !== false && is_string($data['sort'])) {
353				@$name = $this->Paginator->sort($data['sort'], $name);
354			}
355
356			$th .= "\t\t". $this->Html->useTag('tableheader', $this->Html->_parseAttributes($data['thOptions']), $name) . "\n";
357		}
358
359		$out .= $this->Html->useTag('tablerow', null, $th);
360
361		return $out;
362	}
363
364/**
365 * Renders table's pagination-row.
366 *
367 * @return string HTML
368 */
369	protected function _renderPaginator($array) {
370		$out = $paginator = '';
371		$array = $array['paginate'];
372		$array['numbers']['options']['first'] = $array['numbers']['options']['first'] != 'First ' ? __t($array['numbers']['options']['first']) : $array['numbers']['options']['first'];
373		$array['numbers']['options']['last'] = $array['numbers']['options']['last'] != ' Last' ? __t($array['numbers']['options']['last']) : $array['numbers']['options']['last'];
374		$paginator .= $this->Paginator->options($array['options']);
375		$paginator .= $this->Paginator->prev($array['prev']['title'], $array['prev']['options'], $array['prev']['disabledTitle'], $array['prev']['disabledOptions']);
376		$paginator .= $this->Paginator->numbers($array['numbers']['options']);
377		$paginator .= $this->Paginator->next($array['next']['title'], $array['next']['options'], $array['next']['disabledTitle'], $array['next']['disabledOptions']);
378		$td = $this->Html->useTag('tablecell', $this->Html->_parseAttributes(array_merge(array('colspan' => $this->__colsCount), $array['tdOptions'])), $paginator);
379		$out .= $this->Html->useTag('tablerow', $this->Html->_parseAttributes($array['trOptions']), $td);
380
381		return $out;
382	}
383}