1<?php 2 3namespace plugin\struct\meta; 4 5use plugin\struct\types\Text; 6 7class Search { 8 /** 9 * This separator will be used to concat multi values to flatten them in the result set 10 */ 11 const CONCAT_SEPARATOR = "\n!_-_-_-_-_!\n"; 12 13 /** 14 * The list of known and allowed comparators 15 */ 16 static public $COMPARATORS = array( 17 '<', '>', '<=', '>=', '!=', '!~', '~' 18 ); 19 20 /** @var \helper_plugin_sqlite */ 21 protected $sqlite; 22 23 /** @var Schema[] list of schemas to query */ 24 protected $schemas = array(); 25 26 /** @var Column[] list of columns to select */ 27 protected $columns = array(); 28 29 /** @var array the sorting of the result */ 30 protected $sortby = array(); 31 32 /** @var array the filters */ 33 protected $filter = array(); 34 35 /** @var array list of aliases tables can be referenced by */ 36 protected $aliases = array(); 37 38 /** 39 * Search constructor. 40 */ 41 public function __construct() { 42 /** @var \helper_plugin_struct_db $plugin */ 43 $plugin = plugin_load('helper', 'struct_db'); 44 $this->sqlite = $plugin->getDB(); 45 } 46 47 /** 48 * Add a schema to be searched 49 * 50 * Call multiple times for multiple schemas. 51 * 52 * @param string $table 53 * @param string $alias 54 */ 55 public function addSchema($table, $alias = '') { 56 $this->schemas[$table] = new Schema($table); 57 if($alias) $this->aliases[$alias] = $table; 58 } 59 60 /** 61 * Add a column to be returned by the search 62 * 63 * Call multiple times for multiple columns. Be sure the referenced tables have been 64 * added before 65 * 66 * @param string $colname may contain an alias 67 */ 68 public function addColumn($colname) { 69 if(!$this->schemas) throw new StructException('noschemas'); 70 if($colname == '%pid%') { // Special column 71 $col = new PageColumn(0, new Text(), array_shift(array_keys($this->schemas))); //FIXME the type should be Page 72 // FIXME %title% needs to be handled here, too (later) 73 } else { 74 $col = $this->findColumn($colname); 75 } 76 if(!$col) return; //FIXME do we really want to ignore missing columns? 77 $this->columns[] = $col; 78 } 79 80 /** 81 * Add sorting options 82 * 83 * Call multiple times for multiple columns. Be sure the referenced tables have been 84 * added before 85 * 86 * @param string $colname may contain an alias 87 * @param bool $asc sort direction (ASC = true, DESC = false) 88 */ 89 public function addSort($colname, $asc = true) { 90 $col = $this->findColumn($colname); 91 if(!$col) return; //FIXME do we really want to ignore missing columns? 92 93 $this->sortby[] = array($col, $asc); 94 } 95 96 /** 97 * Adds a filter 98 * 99 * @param string $colname may contain an alias 100 * @param string $value 101 * @param string $comp @see self::COMPARATORS 102 * @param string $type either 'OR' or 'AND' 103 */ 104 public function addFilter($colname, $value, $comp, $type = 'OR') { 105 if(!in_array($comp, self::$COMPARATORS)) throw new StructException("Bad comperator. Use " . join(',', self::$COMPARATORS)); 106 if($type != 'OR' && $type != 'AND') throw new StructException('Bad filter type . Only AND or OR allowed'); 107 108 $col = $this->findColumn($colname); 109 if(!$col) return; //FIXME do we really want to ignore missing columns? 110 111 $this->filter[] = array($col, $value, $comp, $type); 112 } 113 114 /** 115 * Execute this search and return the result 116 * 117 * The result is a two dimensional array of array. Each cell contains an array with 118 * the keys 'col' (containing a Column object) and 'val' containing the value(s) 119 */ 120 public function execute() { 121 list($sql, $opts) = $this->getSQL(); 122 123 $res = $this->sqlite->query($sql, $opts); 124 $data = $this->sqlite->res2arr($res); 125 $this->sqlite->res_close($res); 126 127 $result = array(); 128 foreach($data as $row) { 129 $C = 0; 130 $resrow = array(); 131 foreach($this->columns as $col) { 132 $rescol = array(); 133 $rescol['col'] = $col; 134 $rescol['val'] = $row["C$C"]; 135 if($col->isMulti()) { 136 $rescol['val'] = explode(self::CONCAT_SEPARATOR,$rescol['val']); 137 } 138 $resrow[] = $rescol; 139 $C++; 140 } 141 $result[] = $resrow; 142 } 143 return $result; 144 } 145 146 /** 147 * Transform the set search parameters into a statement 148 * 149 * @return array ($sql, $opts) The SQL and parameters to execute 150 */ 151 public function getSQL() { 152 if(!$this->columns) throw new StructException('nocolname'); 153 154 $from = ''; 155 $select = ''; 156 $order = ''; 157 $grouping = array(); 158 $opts = array(); 159 $where = '1 = 1'; 160 161 // basic tables 162 $first = ''; 163 foreach($this->schemas as $schema) { 164 if($first) { 165 // follow up tables 166 $from .= "\nLEFT OUTER JOIN data_{$schema->getTable()} ON data_$first.pid = data_{$schema->getTable()}.pid"; 167 } else { 168 // first table 169 $select .= "data_{$schema->getTable()}.pid as PID, "; 170 $from .= "data_{$schema->getTable()}"; 171 $first = $schema->getTable(); 172 } 173 174 $where .= "\nAND data_{$schema->getTable()}.latest = 1"; 175 } 176 177 // columns to select, handling multis 178 $sep = self::CONCAT_SEPARATOR; 179 $n = 0; 180 foreach($this->columns as $col) { 181 $CN = 'C' . $n++; 182 183 if(is_a($col, 'plugin\struct\meta\PageColumn')) { 184 $select .= 'data_' . $col->getTable() . ".pid AS $CN, "; 185 $grouping[] = $CN; 186 } else if($col->isMulti()) { 187 $tn = 'M' . $col->getColref(); 188 $select .= "GROUP_CONCAT($tn.value, '$sep') AS $CN, "; 189 $from .= "\nLEFT OUTER JOIN multi_{$col->getTable()} AS $tn"; 190 $from .= " ON data_{$col->getTable()}.pid = $tn.pid AND data_{$col->getTable()}.rev = $tn.rev"; 191 $from .= " AND $tn.colref = {$col->getColref()}\n"; 192 } else { 193 $select .= 'data_' . $col->getTable() . '.col' . $col->getColref() . " AS $CN, "; 194 $grouping[] = $CN; 195 } 196 } 197 $select = rtrim($select, ', '); 198 199 // where clauses 200 foreach($this->filter as $filter) { 201 list($col, $value, $comp, $type) = $filter; 202 203 /** @var $col Column */ 204 if($col->isMulti()) { 205 $tn = 'MN' . $col->getColref(); // FIXME this joins a second time if the column was selected before 206 $from .= "\nLEFT OUTER JOIN multi_{$col->getTable()} AS $tn"; 207 $from .= " ON data_{$col->getTable()}.pid = $tn.pid AND data_{$col->getTable()}.rev = $tn.rev"; 208 $from .= " AND $tn.colref = {$col->getColref()}\n"; 209 210 $column = "$tn.value"; 211 } else { 212 $column = "data_{$col->getTable()}.col{$col->getColref()}"; 213 } 214 215 list($wsql, $wopt) = $col->getType()->compare($column, $comp, $value); 216 $opts = array_merge($opts, $wopt); 217 218 $where .= " $type $wsql"; 219 } 220 221 // sorting 222 foreach($this->sortby as $sort) { 223 list($col, $asc) = $sort; 224 225 /** @var $col Column */ 226 if($col->isMulti()) { 227 // FIXME how to sort by multival? 228 // FIXME what if sort by non merged multival? 229 } else { 230 $order .= "data_{$col->getTable()}.col{$col->getColref()} "; 231 $order .= ($asc) ? 'ASC' : 'DESC'; 232 $order .= ', '; 233 } 234 } 235 $order = rtrim($order, ', '); 236 237 $sql = "SELECT $select\n FROM $from\nWHERE $where\nGROUP BY " . join(', ', $grouping); 238 if($order) $sql .= "\nORDER BY $order"; 239 240 return array($sql, $opts); 241 } 242 243 /** 244 * Find a column to be used in the search 245 * 246 * @param string $colname may contain an alias 247 * @return bool|Column 248 */ 249 protected function findColumn($colname) { 250 if(!$this->schemas) throw new StructException('noschemas'); 251 252 // resolve the alias or table name 253 list($table, $colname) = explode('.', $colname, 2); 254 if(!$colname) { 255 $colname = $table; 256 $table = ''; 257 } 258 if($table && isset($this->aliases[$table])) { 259 $table = $this->aliases[$table]; 260 } 261 262 if(!$colname) throw new StructException('nocolname'); 263 264 // if table name given search only that, otherwiese try all for matching column name 265 if($table) { 266 $schemas = array($table => $this->schemas[$table]); 267 } else { 268 $schemas = $this->schemas; 269 } 270 271 // find it 272 $col = false; 273 foreach($schemas as $schema) { 274 $col = $schema->findColumn($colname); 275 if($col) break; 276 } 277 278 return $col; 279 } 280 281} 282 283 284