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