createClassAttribute($this->getRowClasses($row)); } protected function createClassAttribute($classes) { $str = $this->createClassesString($classes); if (strlen($str) > 0) { return ' class="' . $str . '"'; } else { return ''; } } private function createClassesString($classes) { if (is_string($classes)) { $classes = array($classes); } if (empty($classes)) { return ''; } else { return implode(' ', $classes); } } protected function getMultiselectProperties() { /* array( * 'url' => 'director/hosts/edit', * 'sourceUrl' => 'director/hosts', * 'keys' => 'name' * ) */ return array(); } protected function renderMultiselectAttributes() { $props = $this->getMultiselectProperties(); if (empty($props)) { return ''; } $prefix = 'data-icinga-multiselect-'; $view = $this->view(); $parts = array(); $multi = array( 'url' => $view->href($props['url']), 'controllers' => $view->href($props['sourceUrl']), 'data' => implode(',', $props['keys']), ); foreach ($multi as $k => $v) { $parts[] = $prefix . $k . '="' . $v . '"'; } return ' ' . implode(' ', $parts); } protected function renderRow($row) { $htm = " getRowClassesString($row) . ">\n"; $firstCol = true; foreach ($this->getTitles() as $key => $title) { // Support missing columns if (property_exists($row, $key)) { $val = $row->$key; } else { $val = null; } $value = null; if ($firstCol) { if ($val !== null && $url = $this->getActionUrl($row)) { $value = $this->view()->qlink($val, $this->getActionUrl($row)); } $firstCol = false; } if ($value === null) { if ($val === null) { $value = '-'; } elseif (is_array($val) || $val instanceof stdClass || is_bool($val)) { $value = '
'
                           . $this->view()->escape(PlainObjectRenderer::render($val))
                           . '
'; } else { $value = $this->view()->escape($val); } } $htm .= ' ' . $value . "\n"; } if ($this->hasAdditionalActions()) { $htm .= ' ' . $this->renderAdditionalActions($row) . "\n"; } return $htm . " \n"; } abstract protected function getTitles(); protected function getActionUrl($row) { return false; } public function setConnection(Selectable $connection) { $this->connection = $connection; return $this; } /** * @return ZfDbSelect */ abstract protected function getBaseQuery(); public function fetchData() { $db = $this->db(); $query = $this->getBaseQuery()->columns($this->getColumns()); if ($this->hasLimit() || $this->hasOffset()) { $query->limit($this->getLimit(), $this->getOffset()); } $this->applyFiltersToQuery($query); return $db->fetchAll($query); } protected function applyFiltersToQuery(ZfDbSelect $query) { $filter = null; $enforced = $this->enforcedFilters; if ($this->filter && ! $this->filter->isEmpty()) { $filter = $this->filter; } elseif (! empty($enforced)) { $filter = array_shift($enforced); } if ($filter) { foreach ($enforced as $f) { $filter = $filter->andFilter($f); } $query->where($this->renderFilter($filter)); } return $query; } public function getPaginator() { $paginator = new Paginator(); $paginator->setQuery($this); return $paginator; } #[\ReturnTypeWillChange] public function count() { $db = $this->db(); $query = clone($this->getBaseQuery()); $query->reset('order')->columns(array('COUNT(*)')); $this->applyFiltersToQuery($query); return $db->fetchOne($query); } public function limit($count = null, $offset = null) { $this->limit = $count; $this->offset = $offset; return $this; } public function hasLimit() { return $this->limit !== null; } public function getLimit() { return $this->limit; } public function hasOffset() { return $this->offset !== null; } public function getOffset() { return $this->offset; } public function hasAdditionalActions() { return method_exists($this, 'renderAdditionalActions'); } /** @return Db */ protected function connection() { // TODO: Fail if missing? Require connection in constructor? return $this->connection; } protected function db() { return $this->connection()->getDbAdapter(); } protected function renderTitles($row) { $view = $this->view(); $htm = "\n \n"; foreach ($row as $title) { $htm .= ' ' . $view->escape($title) . "\n"; } if ($this->hasAdditionalActions()) { $htm .= ' ' . $view->translate('Actions') . "\n"; } return $htm . " \n\n"; } protected function url($url, $params) { return Url::fromPath($url, $params); } protected function listTableClasses() { $classes = array('simple', 'common-table', 'table-row-selectable'); $multi = $this->getMultiselectProperties(); if (! empty($multi)) { $classes[] = 'multiselect'; } return $classes; } public function render() { $data = $this->fetchData(); $htm = 'createClassAttribute($this->listTableClasses()) . $this->renderMultiselectAttributes() . '>' . "\n" . $this->renderTitles($this->getTitles()) . $this->beginTableBody(); foreach ($data as $row) { $htm .= $this->renderRow($row); } return $htm . $this->endTableBody() . $this->endTable(); } protected function beginTableBody() { return "\n"; } protected function endTableBody() { return "\n"; } protected function endTable() { return "\n"; } /** * @return View */ protected function view() { if ($this->view === null) { $this->view = Icinga::app()->getViewRenderer()->view; } return $this->view; } public function setView($view) { $this->view = $view; } public function __toString() { return $this->render(); } protected function getSearchColumns() { return $this->searchColumns; } abstract public function getColumns(); public function getFilterColumns() { $keys = array_keys($this->getColumns()); return array_combine($keys, $keys); } public function setFilter($filter) { $this->filter = $filter; return $this; } public function enforceFilter($filter, $expression = null) { if (! $filter instanceof Filter) { $filter = Filter::where($filter, $expression); } $this->enforcedFilters[] = $filter; return $this; } public function getFilterEditor(Request $request) { $filterEditor = Widget::create('filterEditor') ->setColumns(array_keys($this->getColumns())) ->setSearchColumns($this->getSearchColumns()) ->preserveParams('limit', 'sort', 'dir', 'view', 'backend', '_dev') ->ignoreParams('page') ->handleRequest($request); $filter = $filterEditor->getFilter(); $this->setFilter($filter); return $filterEditor; } protected function mapFilterColumn($col) { $cols = $this->getColumns(); return $cols[$col]; } protected function renderFilter(Filter $filter, $level = 0) { $str = ''; if ($filter instanceof FilterChain) { if ($filter instanceof FilterAnd) { $op = ' AND '; } elseif ($filter instanceof FilterOr) { $op = ' OR '; } elseif ($filter instanceof FilterNot) { $op = ' AND '; $str .= ' NOT '; } else { throw new QueryException( 'Cannot render filter: %s', $filter ); } $parts = array(); if (! $filter->isEmpty()) { foreach ($filter->filters() as $f) { $filterPart = $this->renderFilter($f, $level + 1); if ($filterPart !== '') { $parts[] = $filterPart; } } if (! empty($parts)) { if ($level > 0) { $str .= ' (' . implode($op, $parts) . ') '; } else { $str .= implode($op, $parts); } } } } else { /** @var FilterExpression $filter */ $str .= $this->whereToSql( $this->mapFilterColumn($filter->getColumn()), $filter->getSign(), $filter->getExpression() ); } return $str; } protected function escapeForSql($value) { // bindParam? bindValue? if (is_array($value)) { $ret = array(); foreach ($value as $val) { $ret[] = $this->escapeForSql($val); } return implode(', ', $ret); } else { //if (preg_match('/^\d+$/', $value)) { // return $value; //} else { return $this->db()->quote($value); //} } } protected function escapeWildcards($value) { return preg_replace('/\*/', '%', $value); } protected function valueToTimestamp($value) { // We consider integers as valid timestamps. Does not work for URL params if (! is_string($value) || ctype_digit($value)) { return $value; } $value = strtotime($value); if (! $value) { /* NOTE: It's too late to throw exceptions, we might finish in __toString throw new QueryException(sprintf( '"%s" is not a valid time expression', $value )); */ } return $value; } protected function timestampForSql($value) { // TODO: do this db-aware return $this->escapeForSql(date('Y-m-d H:i:s', $value)); } /** * Check for timestamp fields * * TODO: This is not here to do automagic timestamp stuff. One may * override this function for custom voodoo, IdoQuery right now * does. IMO we need to split whereToSql functionality, however * I'd prefer to wait with this unless we understood how other * backends will work. We probably should also rename this * function to isTimestampColumn(). * * @param string $field Field Field name to checked * @return bool Whether this field expects timestamps */ public function isTimestamp($field) { return false; } public function whereToSql($col, $sign, $expression) { if ($this->isTimestamp($col)) { $expression = $this->valueToTimestamp($expression); } if (is_array($expression) && $sign === '=') { // TODO: Should we support this? Doesn't work for blub* return $col . ' IN (' . $this->escapeForSql($expression) . ')'; } elseif ($sign === '=' && strpos($expression, '*') !== false) { if ($expression === '*') { // We'll ignore such filters as it prevents index usage and because "*" means anything, anything means // all whereas all means that whether we use a filter to match anything or no filter at all makes no // difference, except for performance reasons... return ''; } return $col . ' LIKE ' . $this->escapeForSql($this->escapeWildcards($expression)); } elseif ($sign === '!=' && strpos($expression, '*') !== false) { if ($expression === '*') { // We'll ignore such filters as it prevents index usage and because "*" means nothing, so whether we're // using a real column with a valid comparison here or just an expression which cannot be evaluated to // true makes no difference, except for performance reasons... return $this->escapeForSql(0); } return $col . ' NOT LIKE ' . $this->escapeForSql($this->escapeWildcards($expression)); } else { return $col . ' ' . $sign . ' ' . $this->escapeForSql($expression); } } }