array( 'label' => '', 'db' => '', 'db_agg' => '', 'unset' => false, 'replace' => null, 'formatter' => null, 'manual' => false, 'hidden' => false, 'editable' => false, 'search' => true, 'search_op' => 'auto', 'classes' => '', 'align' => 'left', 'null' => null, 'encode' => true, ), 'nav' => array( 'add' => false, 'edit' => false, 'del' => false, 'refresh' => true, 'search' => false, 'view' => false, ), 'options' => array( 'datatype' => 'json', 'mtype' => 'POST', 'loadui' => 'block', 'prmNames' => array( 'id' => '_id', ), ), 'reserved_col_names' => array('page', 'sidx', 'sord', 'nd', 'oper', 'filters', 'rd'), ); protected $reserved_col_names; protected $internal_col_prop = array('db', 'db_agg', 'unset', 'manual', 'search_op'); protected $query_placeholders = array('fields' => '{fields}', 'where' => '{where}'); protected $render_data = array(); protected $render_extend = 'opts'; protected $render_suffix_col = null; protected $render_filter_toolbar = false; /** * Class constructor, initializes basic properties * * @param jqGridLoader $loader */ public function __construct(jqGridLoader $loader) { //------------------ // Globals //------------------ $this->grid_id = get_class($this); $this->Loader = $loader; $this->input = $this->getInput(); $this->DB = $loader->loadDB(); //---------------- // Init //---------------- $this->beforeInit(); $this->init(); $this->afterInit(); //---------------- // Primary key //---------------- reset($this->cols); $this->primary_key = $this->primary_key ? (array)$this->primary_key : array(key($this->cols)); if(is_null($this->primary_key_auto_increment)) { $this->primary_key_auto_increment = count($this->primary_key) == 1; } //---------------- // Prepare columns //---------------- $this->reserved_col_names = $this->getReservedColNames(); foreach($this->cols as $k => &$c) { $c = $this->initColumn($k, $c); } } /** * Abstract function for setting grid properties * * @abstract * @return void */ abstract protected function init(); /** * MAIN ACTION (1): Output data * * @throws jqGrid_Exception * @return void */ public function output() { //---------------- // Setup essential vars //---------------- $this->getOutputParams(); //---------------- // Input to search //---------------- if($this->do_search) { $this->search(); } if($this->do_search_advanced) { $this->searchAdvanced(); } //---------------- // Try to guess the basic query //---------------- if(!$this->query) { $this->query = $this->getDefaultQuery(); } //---------------- // Get agg data //---------------- if($this->do_agg) { $this->getDataAgg(); } //---------------- // Get rows data //---------------- $this->getDataRows(); //---------------- // Build userdata //---------------- $this->userdata = $this->getDataUser($this->userdata); //---------------- // Set count automatically //---------------- if(is_null($this->count)) { $this->setRowCount(); } //---------------- // Do output //---------------- $callback = array($this, jqGrid_Utils::uscore2camel('out', $this->out)); if(!is_callable($callback)) { throw new jqGrid_Exception("Output type '{$this->out}' is not defined"); } call_user_func($callback); } /** * MAIN ACTION (2): Perform operation to change data is any way * * @param $oper - operation name * @return void */ public function oper($oper) { $id = $this->input('_id'); $oper = strval($oper); switch($oper) { case 'add': $data = array_intersect_key($this->input, $this->cols); $data = $this->operData($data); $id = $this->opAdd($data); #Not auto increment -> build new_id from data if(empty($this->primary_key_auto_increment)) { $id = $this->implodePrimaryKey($data); } $this->response['new_id'] = $id; $this->operAfterAddEdit($id); break; case 'edit': $data = array_intersect_key($this->input, $this->cols); $data = $this->operData($data); $this->opEdit($id, $data); $this->operAfterAddEdit($id); break; case 'del': $this->opDel($id); break; default: $callback = array($this, jqGrid_Utils::uscore2camel('op', $oper)); if(is_callable($callback)) { call_user_func($callback); } else { throw new jqGrid_Exception("Oper $oper is not defined"); } break; } $this->response = array_merge(array('success' => 1), $this->response); $this->operComplete($oper); //---------------- // Output result //---------------- $this->json($this->response); } /** * MAIN ACTION (3): Render grid * * $jq_loader->render('jq_example'); * * @param array $render_data * @return string */ public function render($render_data = array()) { if(!is_array($render_data)) { throw new jqGrid_Exception_Render('Render data must be an array'); } $this->render_data = $render_data; //------------------ // Basic data //------------------ $data = array(); $data['extend'] = $this->render_extend; $data['suffix'] = $this->renderGridSuffix($render_data); //------------------ // Render ids //------------------ $data['id'] = $this->grid_id . $data['suffix']; $data['pager_id'] = $this->grid_id . $data['suffix'] . '_p'; //----------------- // Render colModel //----------------- foreach($this->cols as $k => $c) { if(isset($c['unset']) and $c['unset']) continue; #Remove internal column properties $c = array_diff_key($c, array_flip($this->internal_col_prop)); $colModel[] = $this->renderColumn($c); } //----------------- // Render options //----------------- $options = array( 'colModel' => $colModel, 'pager' => '#' . $data['pager_id'], ); #URL's $options['url'] = $options['editurl'] = $options['cellurl'] = $this->renderGridUrl(); #Any postData? if($post_data = $this->renderPostData()) { $options['postData'] = $post_data; } $data['options'] = $this->renderOptions(array_merge($this->default['options'], $options, $this->options)); //----------------- // Render navigator //----------------- if(is_array($this->nav)) { $data['nav'] = $this->renderNav(array_merge($this->default['nav'], $this->nav)); } //------------------ // Render base html //------------------ $data['html'] = $this->renderHtml($data); //----------------- // Compile the final string //----------------- return $this->renderComplete($data); } /** * All exceptions comes here * Override this method for custom exception handling * * @param jqGrid_Exception $e * @return mixed */ public function catchException(jqGrid_Exception $e) { #More output types will be added switch($e->getOutputType()) { case 'json': $r = array( 'error' => 1, 'error_msg' => $e->getMessage(), 'error_code' => $e->getCode(), 'error_data' => $e->getData(), 'error_type' => $e->getExceptionType(), ); if($this->Loader->get('debug_output')) { $r['error_string'] = (string)$e; } else { if($e instanceof jqGrid_Exception_DB) { unset($r['error_data']['query']); } } $this->json($r); break; case 'trigger_error': trigger_error($e->getMessage(), E_USER_ERROR); break; } return $e; } /** * (Output) Add new row to result set * And also do related stuff * * @param array $row - raw data from database * @return void */ protected function addRow($row) { #Allow modification of result row without annoying overloading of 'addRow' $row = $this->parseRow($row); $id = $this->implodePrimaryKey($row); $cell = array(); //---------------- // Fill cells //---------------- foreach($this->cols as $k => $c) { #Discard this column in output? if($c['unset']) continue; #Parse each cell $cell[] = $this->addRowCell($c, $row); } #Prase whole row $cell = $this->addRowComplete($id, $cell, $row); $this->rows[] = array('id' => $id, 'cell' => $cell); } /** * (Output) Populate cell * * @param array $c - column options * @param array $row - data row * @return mixed - final cell value */ protected function addRowCell($c, $row) { $val = isset($row[$c['name']]) ? $row[$c['name']] : null; #Handle nulls if($val === null and $c['null'] !== null) $val = $c['null']; #Easy replace values if($c['replace']) $val = isset($c['replace'][$val]) ? $c['replace'][$val] : $val; #Encode before output if($c['encode']) $val = $this->outputEncodeValue($c, $val); return $val; } /** * (Output) Per-row result modification * * @param integer $id row_id * @param array $cell - parsed cells * @param array $row - data row * @return array - modified $cell */ protected function addRowComplete($id, $cell, $row) { //---------------- // TreeGrid handling //---------------- if($this->treegrid == 'adjacency') { $cell[] = $row['level']; $cell[] = $row['parent']; $cell[] = $row['isLeaf']; $cell[] = $row['expanded']; $cell[] = isset($row['loaded']) ? $row['loaded'] : false; } if($this->treegrid == 'nested') { $cell[] = $row['level']; $cell[] = $row['lft']; $cell[] = $row['rgt']; $cell[] = $row['isLeaf']; $cell[] = $row['expanded']; $cell[] = isset($row['loaded']) ? $row['loaded'] : false; } //---------------- // Send all the vars beginning with '_' to userdata! //---------------- foreach($row as $k => $v) { if(strpos($k, '_') === 0) { $this->userdata[$k][$id] = $v; } } return $cell; } /** * Custom actions after 'init', but before columns init * You may also overload __construct to add something in the very end */ protected function afterInit() { //empty } /** * Custom actions before 'init' * Place your singletons init here */ protected function beforeInit() { //empty } //----------------- // Query builder //----------------- /** * (Output) Build complete AGG-query * * @param string $q - base query * @return string - final query */ protected function buildQueryAgg($q) { //----------------- // Placeholders //----------------- $replace = array( $this->query_placeholders['where'] => $this->buildWhere($this->where, $this->where_glue, 'agg'), $this->query_placeholders['fields'] => $this->buildFieldsAgg($this->cols), ); $q = strtr($q, $replace); return $q; } /** * (Output) Build complete ROWS-query * * @param string $q - base query * @return string - final query */ protected function buildQueryRows($q) { //----------------- // Placeholders //----------------- $replace = array( $this->query_placeholders['where'] => $this->buildWhere($this->where, $this->where_glue, 'rows'), $this->query_placeholders['fields'] => $this->buildFields($this->cols), ); $q = strtr($q, $replace); //----------------- // ORDER BY, LIMIT, OFFSET //----------------- if($this->do_sort) $q .= $this->buildOrderBy($this->sidx, $this->sord) . "\n"; if($this->do_limit) $q .= $this->buildLimitOffset($this->limit, $this->page) . "\n"; return $q; } /** * (Output) Implode {fields} for ROWS-query using 'db' property * * @param array $cols - grid columns * @return string - imploded list */ protected function buildFields($cols) { $fields = array(); foreach($cols as $k => &$c) { if($c['manual']) continue; $fields[] = ($k == $c['db']) ? $c['db'] : ($c['db'] . ' AS ' . $k); } return implode(', ', $fields); } /** * (Output) Implode {fields} for AGG-query using 'db_agg' property * * @param array $cols - grid columns * @return string - imploded list */ protected function buildFieldsAgg($cols) { #Count should always be here! $fields = array('count(*) AS _count'); foreach($cols as $k => $c) { if(!$c['db_agg']) continue; switch($c['db_agg']) { #Common case 'sum': case 'avg': case 'count': case 'min': case 'max': $fields[] = $c['db_agg'] . '(' . $c['db'] . ') AS ' . $k; break; #Custom default: $fields[] = $c['db_agg'] . ' AS ' . $k; break; } } return implode(', ', $fields); } /** * (Output) Builds paging for ROWS-query * LIMIT x OFFSET y * * Alter this if your database requires other syntax * * @param integer $limit * @param integer $page * @return string */ protected function buildLimitOffset($limit, $page) { $limit = intval($limit); $page = intval($page); if($limit > 0 and $page > 0) { $offset = ($page * $limit) - $limit; return "LIMIT $limit OFFSET $offset"; } return ''; } /** * (Output) Builds sorting for ROWS-query * Overload it to introduce more complex * * Input is checked in 'getOutputParams', so we trust it * * @param string $sidx - field name * @param string $sord - order (asc, desc) * @return string */ protected function buildOrderBy($sidx, $sord) { if($sidx and $sord) { return "ORDER BY $sidx $sord"; } return ''; } /** * (Output) Builds {where} both for ROWS and AGG queries * * @param array $where - array with conditions * @param string $glue - glue string (' AND ', ' OR ') * @param string $type - query type ('agg', 'rows') * @return string - imploded condition string */ protected function buildWhere($where, $glue, $type) { return $where ? implode($glue, $where) : $this->where_empty; } /** * (Output) Generate default query if only $this->table was set * * @return string */ protected function getDefaultQuery() { if(empty($this->table)) { return ''; } return " SELECT {fields} FROM {$this->table} WHERE {where} "; } /** * (Output) Get and validate params needed for output */ protected function getOutputParams() { $this->page = max(1, intval($this->input('page', 1))); $this->limit = max(-1, intval($this->input['rows'])); $this->sidx = $this->input('sidx') ? jqGrid_Utils::checkAlphanum($this->input('sidx')) : $this->primary_key[0]; $this->sord = in_array($this->input('sord'), array('asc', 'desc')) ? $this->input('sord') : 'asc'; $this->out = $this->input('_out', 'json'); } /** * (Output) Get AGG data - over the whole result set * Save it to $this->agg */ protected function getDataAgg() { $query = $this->buildQueryAgg($this->query_agg ? $this->query_agg : $this->query); $result = $this->DB->query($query); $this->agg = $this->DB->fetch($result); $this->debug['query_agg'] = $query; } /** * (Output) Get ROWS data - the current page * Save it to $this->rows via 'addRow' */ protected function getDataRows() { $query = $this->buildQueryRows($this->query); $result = $this->DB->query($query); while($r = $this->DB->fetch($result)) { $this->addRow($r); } $this->debug['query_rows'] = $query; } /** * (Output) Build 'userdata' * * @param array $userdata - orignal value * @return array - final value */ protected function getDataUser($userdata) { if($this->agg) { $userdata['agg'] = $this->agg; } return $userdata; } /** * It is the ONLY entry-point for external data * * @return array */ protected function getInput() { $req = array_merge($_GET, $_POST); #Ajax input is always utf-8 -> convert it if($this->Loader->get('encoding') != 'utf-8' and isset($_SERVER['HTTP_X_REQUESTED_WITH'])) { $req = jqGrid_Utils::arrayIconv($req, 'utf-8', $this->Loader->get('encoding')); } return $req; } /** * Get reserved column names specific to this grid * * @return array - array of names */ protected function getReservedColNames() { $names = $this->default['reserved_col_names']; if($this->treegrid == 'adjacency') { $names = array_merge($names, array('parent', 'level', 'isLeaf', 'expanded', 'loaded')); } if($this->treegrid == 'nested') { $names = array_merge($names, array('level', 'lft', 'rgt', 'isLeaf', 'expanded', 'loaded')); } return $names; } /** * Get row from DB * * @param $primary_key * @return array */ protected function getRowByPrimaryKey($primary_key) { $where = array(); foreach($this->explodePrimaryKey($primary_key) as $k => $v) { $where[] = "$k = " . $this->DB->quote($v); } $result = $this->DB->query("SELECT * FROM {$this->table} WHERE " . implode(' AND ', $where)); return $this->DB->fetch($result); } /** * Init one column * Apply defaults, check name etc. * * @param string $k - column name * @param array $c - non-default column params * @return array - complete column */ protected function initColumn($k, $c) { #Check reserved keys if(in_array($k, $this->reserved_col_names)) { throw new jqGrid_Exception("Column name '$k' reserved for internal usage!"); } if(strpos($k, '_') === 0) { throw new jqGrid_Exception("Column name '$k' must NOT begin with underscore!"); } #Name and index always match the array key $c['name'] = $k; $c['index'] = $k; #Label = column key if not set if(!isset($c['label'])) $c['label'] = $k; #Merge with defaults $c = array_merge($this->default['cols'], $this->cols_default, $c); #DB = column key if not set $c['db'] = $c['db'] ? $c['db'] : $k; return $c; } //---------------- // OPERATIONS PART //---------------- /** * (Oper) Insert * * Please note: this is the only "Oper" function, which must return new row id * * @param array $ins - form data * @return integer - new_id */ protected function opAdd($ins) { if(empty($this->table)) { throw new jqGrid_Exception('Table is not defined'); } return $this->DB->insert($this->table, $ins, true); } /** * (Oper) Update * * @param integer $id - id to update * @param array $upd - form data * @return void */ protected function opEdit($id, $upd) { if(empty($this->table)) { throw new jqGrid_Exception('Table is not defined'); } $this->DB->update($this->table, $upd, $this->explodePrimaryKey($id)); } /** * (Oper) Delete * * @param integer|string $id - one or multiple id's to delete * @return void */ protected function opDel($id) { if(empty($this->table)) { throw new jqGrid_Exception('Table is not defined'); } $where = array(); foreach(explode(',', $id) as $pk) { $wh = array(); foreach($this->explodePrimaryKey($pk) as $kk => $vv) { $wh[] = "$kk = " . $this->DB->quote($vv); } $where[] = '(' . implode(' AND ', $wh) . ')'; } $this->DB->delete($this->table, implode(' OR ', $where)); } //---------------- // OPER HOOKS PART //---------------- /** * (Oper) Modify form data for opAdd and opEdit only * These operations usually need the same modifications, so i made a stand-alone hook for them * * @param array $r - form data * @return array - modified form data */ protected function operData($r) { return $r; } /** * (Oper) Hook after opAdd and opEdit * Useful for uploading images after processing other data etc. * * @param $id - id of updated or inserted row * @return void */ protected function operAfterAddEdit($id) { return; } /** * (Oper) Hook after ALL oper's * Useful for cleanup and dropping caches * * @param $oper - operation name * @return void */ protected function operComplete($oper) { return; } //---------------- // OUTPUT PART //---------------- /** * Output json */ protected function outJson() { $data = array( 'page' => $this->page, 'total' => $this->total, 'records' => $this->count, 'rows' => $this->rows, 'userdata' => $this->userdata, ); if($this->Loader->get('debug_output')) { $data['debug'] = $this->debug; } $this->json($data); } /** * Output XML ... not supported */ protected function outXml() { //unsupported } /** * Export data using plugin */ protected function outExport() { $type = jqGrid_Utils::checkAlphanum($this->input('export')); $class = 'jqGrid_Export_' . ucfirst($type); if(!class_exists($class)) { throw new jqGrid_Exception("Export type $type does not exist"); } #Weird >__< $lib = new $class($this->Loader); $this->setExportData($lib); $lib->doExport(); } /** * (Output) Encode each row value before output * * @param $c - column * @param $val - value * @return string - encoded value */ protected function outputEncodeValue($c, $val) { return htmlspecialchars($val, ENT_QUOTES); } /** * (Output) Modify row data before output * * @param array $r - row data * @return array - modified row data */ protected function parseRow($r) { return $r; } //---------------- // RENDER PART //---------------- /** * (Render) Render single column * * @param array $c - col properties * @return array - modified col properties */ protected function renderColumn($c) { return $c; } /** * Get grid_id suffix when multiple instances are used on one page * * @param $render_data * @return string */ protected function renderGridSuffix($render_data) { if(isset($this->render_suffix_col) and isset($render_data[$this->render_suffix_col])) { return jqGrid_Utils::checkAlphanum($render_data[$this->render_suffix_col]); } return ''; } /** * (Render) Base url is used into 'url', 'editurl', 'cellurl' options * * @return string */ protected function renderGridUrl() { $params[$this->Loader->get('input_grid')] = $this->grid_id; foreach($this->render_data as $k => $v) { $params[$k] = $v; } return $this->base_url . '?' . http_build_query($params); } /** * (Render) There are few ways to create basic html markup for jqGrid * You may alter this if you want yours * * @param $data - render data * @return string - string to be added before .jqGrid call */ protected function renderHtml($data) { return '