1 <?php
2 3 4 5 6 7 8 9 10
11
12 namespace Neevo;
13
14 use Countable;
15 use DateTime;
16 use InvalidArgumentException;
17 use IteratorAggregate;
18 use Traversable;
19
20
21 22 23 24 25 26 27
28 class Result extends BaseStatement implements IteratorAggregate, Countable {
29
30
31
32 protected $grouping;
33
34
35 protected $columns = array();
36
37
38 protected $joins;
39
40
41 protected $tableAlias;
42
43
44 protected $resultSet;
45
46
47 protected $columnTypes = array();
48
49
50 protected $detectTypes;
51
52
53 private $rowClass = 'Neevo\\Row';
54
55
56 private $iterator;
57
58
59 60 61 62 63 64 65
66 public function __construct(Connection $connection, $columns = null, $source = null){
67 parent::__construct($connection);
68
69
70 if($columns === null && $source === null)
71 throw new InvalidArgumentException('Missing select source.');
72 if($source === null){
73 $source = $columns;
74 $columns = '*';
75 }
76 if(!is_string($source) && !$source instanceof self)
77 throw new InvalidArgumentException('Source must be a string or Neevo\\Result.');
78
79 $columns = is_string($columns)
80 ? explode(',', $columns) : ($columns instanceof Traversable
81 ? iterator_to_array($columns) : (array) $columns);
82
83 if(empty($columns))
84 throw new InvalidArgumentException('No columns given.');
85
86 if($source instanceof self)
87 $this->subqueries[] = $source;
88
89 $this->type = Manager::STMT_SELECT;
90 $this->columns = array_map('trim', $columns);
91 $this->source = $source;
92 $this->detectTypes = (bool) $connection['result']['detectTypes'];
93 $this->setRowClass($connection['rowClass']);
94 }
95
96
97 98 99
100 public function __destruct(){
101 try{
102 $this->connection->getDriver()->freeResultSet($this->resultSet);
103 } catch(ImplementationException $e){}
104
105 $this->resultSet = null;
106 }
107
108
109 public function __call($name, $args){
110 $name = strtolower($name);
111 if($name === 'as')
112 return $this->setAlias(isset($args[0]) ? $args[0] : null);
113 return parent::__call($name, $args);
114 }
115
116
117 118 119 120 121 122
123 public function group($rule, $having = null){
124 if($this->validateConditions())
125 return $this;
126
127 $this->resetState();
128 $this->grouping = array($rule, $having);
129 return $this;
130 }
131
132
133 134 135 136 137 138
139 public function join($source, $condition){
140 if($this->validateConditions())
141 return $this;
142
143 if(!(is_string($source) || $source instanceof self || $source instanceof Literal))
144 throw new InvalidArgumentException('Source must be a string, Neevo\\Literal or Neevo\\Result.');
145 if(!(is_string($condition) || $condition instanceof Literal))
146 throw new InvalidArgumentException('Condition must be a string or Neevo\\Literal.');
147
148 if($source instanceof self)
149 $this->subqueries[] = $source;
150
151 $this->resetState();
152 $type = (func_num_args() > 2) ? func_get_arg(2) : '';
153
154 $this->joins[] = array($source, $condition, $type);
155
156 return $this;
157 }
158
159
160 161 162 163 164 165
166 public function leftJoin($source, $condition){
167 return $this->join($source, $condition, Manager::JOIN_LEFT);
168 }
169
170
171 172 173 174 175 176
177 public function innerJoin($source, $condition){
178 return $this->join($source, $condition, Manager::JOIN_INNER);
179 }
180
181
182 183 184 185 186 187 188
189 public function page($page, $items){
190 if($page < 1 || $items < 1)
191 throw new InvalidArgumentException('Both arguments must be positive integers.');
192 return $this->limit((int) $items, (int) ($items * --$page));
193 }
194
195
196 197 198 199
200 public function fetch(){
201 $this->performed || $this->run();
202
203 $row = $this->connection->getDriver()->fetch($this->resultSet);
204 if(!is_array($row))
205 return false;
206
207
208 if($this->detectTypes)
209 $this->detectTypes();
210 if(!empty($this->columnTypes)){
211 foreach($this->columnTypes as $col => $type){
212 if(isset($row[$col]))
213 $row[$col] = $this->convertType($row[$col], $type);
214 }
215 }
216 return new $this->rowClass($row, $this);
217 }
218
219
220 221 222 223 224 225
226 public function fetchAll($limit = null, $offset = null){
227 $limit = $limit === null ? -1 : (int) $limit;
228 if($offset !== null)
229 $this->seek((int) $offset);
230
231 $row = $this->fetch();
232 if(!$row)
233 return array();
234
235 $rows = array();
236 do{
237 if($limit === 0)
238 break;
239 $rows[] = $row;
240 $limit--;
241 } while($row = $this->fetch());
242
243 return $rows;
244 }
245
246
247 248 249 250
251 public function fetchSingle(){
252 $this->performed || $this->run();
253 $row = $this->connection->getDriver()->fetch($this->resultSet);
254
255 if(!$row)
256 return false;
257 $value = reset($row);
258
259
260 if($this->detectTypes)
261 $this->detectTypes();
262 if(!empty($this->columnTypes)){
263 $key = key($row);
264 if(isset($this->columnTypes[$key]))
265 $value = $this->convertType($value, $this->columnTypes[$key]);
266 }
267
268 return $value;
269 }
270
271
272 273 274 275 276 277
278 public function fetchPairs($key, $value = null){
279 $clone = clone $this;
280
281
282 if(!in_array('*', $clone->columns)){
283 if($value !== null)
284 $clone->columns = array($key, $value);
285 elseif(!in_array($key, $clone->columns))
286 $clone->columns[] = $key;
287 }
288 $k = substr($key, ($pk = strrpos($key, '.')) ? $pk+1 : 0);
289 $v = $value === null ? null : substr($value, ($pv = strrpos($value, '.')) ? $pv+1 : 0);
290
291 $rows = array();
292 while($row = $clone->fetch()){
293 if(!$row)
294 return array();
295 $rows[$row[$k]] = $value === null ? $row : $row->$v;
296 }
297
298 return $rows;
299 }
300
301
302 303 304 305 306 307
308 public function seek($offset){
309 $this->performed || $this->run();
310 $seek = $this->connection->getDriver()->seek($this->resultSet, $offset);
311 if($seek)
312 return $seek;
313 throw new NeevoException("Cannot seek to offset $offset.");
314 }
315
316
317 public function rows(){
318 return $this->count();
319 }
320
321
322 323 324 325 326 327
328 public function count($column = null){
329 if($column === null){
330 $this->performed || $this->run();
331 return (int) $this->connection->getDriver()->getNumRows($this->resultSet);
332 }
333 return $this->aggregation("COUNT(:$column)");
334 }
335
336
337 338 339 340 341
342 public function aggregation($function){
343 $clone = clone $this;
344 $clone->columns = (array) $function;
345 return $clone->fetchSingle();
346 }
347
348
349 350 351 352 353
354 public function sum($column){
355 return $this->aggregation("SUM($column)");
356 }
357
358
359 360 361 362 363
364 public function min($column){
365 return $this->aggregation("MIN($column)");
366 }
367
368
369 370 371 372 373
374 public function max($column){
375 return $this->aggregation("MAX($column)");
376 }
377
378
379 380 381 382
383 public function explain(){
384 $driver = $this->getConnection()->getDriver();
385 $query = $driver->runQuery("EXPLAIN $this");
386
387 $rows = array();
388 while($row = $driver->fetch($query)){
389 $rows[] = $row;
390 }
391
392 return $rows;
393 }
394
395
396 397 398 399 400 401
402 public function setType($column, $type){
403 $this->columnTypes[$column] = $type;
404 return $this;
405 }
406
407
408 409 410 411 412
413 public function setTypes($types){
414 if(!($types instanceof Traversable || is_array($types)))
415 throw new InvalidArgumentException('Types must be an array or Traversable.');
416 foreach($types as $column => $type){
417 $this->setType($column, $type);
418 }
419 return $this;
420 }
421
422
423 424 425 426
427 public function detectTypes(){
428 $table = $this->getTable();
429 $this->performed || $this->run();
430
431
432 $types = (array) $this->connection->getCache()->fetch($table . '_detectedTypes');
433
434 if(empty($types)){
435 try{
436 $types = $this->connection->getDriver()->getColumnTypes($this->resultSet, $table);
437 } catch(NeevoException $e){
438 return $this;
439 }
440 }
441
442 foreach((array) $types as $col => $type){
443 $this->columnTypes[$col] = $this->resolveType($type);
444 }
445
446 $this->connection->getCache()->store($table . '_detectedTypes', $this->columnTypes);
447 return $this;
448 }
449
450
451 452 453 454 455
456 protected function resolveType($type){
457 static $patterns = array(
458 'bool|bit' => Manager::BOOL,
459 'bin|blob|bytea' => Manager::BINARY,
460 'string|char|text|bigint|longlong' => Manager::TEXT,
461 'int|long|byte|serial|counter' => Manager::INT,
462 'float|real|double|numeric|number|decimal|money|currency' => Manager::FLOAT,
463 'time|date|year' => Manager::DATETIME
464 );
465
466 foreach($patterns as $vendor => $universal){
467 if(preg_match("~$vendor~i", $type))
468 return $universal;
469 }
470 return Manager::TEXT;
471 }
472
473
474 475 476 477 478 479
480 protected function convertType($value, $type){
481 $dateFormat = $this->connection['result']['formatDate'];
482 if($value === null)
483 return null;
484 switch($type){
485 case Manager::TEXT:
486 return (string) $value;
487
488 case Manager::INT:
489 return (int) $value;
490
491 case Manager::FLOAT:
492 return (float) $value;
493
494 case Manager::BOOL:
495 return ((bool) $value) && $value !== 'f' && $value !== 'F';
496
497 case Manager::BINARY:
498 return $this->connection->getDriver()->unescape($value, $type);
499
500 case Manager::DATETIME:
501 if((int) $value === 0)
502 return null;
503 elseif(!$dateFormat)
504 return new DateTime(is_numeric($value) ? date('Y-m-d H:i:s', $value) : $value);
505 elseif($dateFormat == 'U')
506 return is_numeric($value) ? (int) $value : strtotime($value);
507 elseif(is_numeric($value))
508 return date($dateFormat, $value);
509 else{
510 $d = new DateTime($value);
511 return $d->format($dateFormat);
512 }
513
514 default:
515 return $value;
516 }
517 }
518
519
520 521 522 523 524
525 public function setAlias($alias){
526 $this->tableAlias = $alias;
527 return $this;
528 }
529
530
531 532 533 534
535 public function getAlias(){
536 return $this->tableAlias ? $this->tableAlias : null;
537 }
538
539
540 541 542 543 544 545
546 public function setRowClass($className){
547 if(!class_exists($className))
548 throw new NeevoException("Cannot set row class '$className'.");
549 $this->rowClass = $className;
550 return $this;
551 }
552
553
554 555 556 557
558 public function getIterator(){
559 if(!isset($this->iterator))
560 $this->iterator = new ResultIterator($this);
561 return $this->iterator;
562 }
563
564
565 public function getGrouping(){
566 return $this->grouping;
567 }
568
569
570 public function getColumns(){
571 return $this->columns;
572 }
573
574
575 public function getJoins(){
576 if(!empty($this->joins))
577 return $this->joins;
578 return array();
579 }
580
581
582 583 584 585
586 public function getTable(){
587 if($this->source instanceof self)
588 return null;
589 return parent::getTable();
590 }
591
592
593 594 595 596
597 public function getSource(){
598 return $this->source;
599 }
600
601
602 }
603