1<?php 2/********************************************************************* 3 class.orm.php 4 5 Simple ORM (Object Relational Mapper) for PHP5 based on Django's ORM, 6 except that complex filter operations are not supported. The ORM simply 7 supports ANDed filter operations without any GROUP BY support. 8 9 Jared Hancock <jared@osticket.com> 10 Copyright (c) 2006-2013 osTicket 11 http://www.osticket.com 12 13 Released under the GNU General Public License WITHOUT ANY WARRANTY. 14 See LICENSE.TXT for details. 15 16 vim: expandtab sw=4 ts=4 sts=4: 17**********************************************************************/ 18require_once INCLUDE_DIR . 'class.util.php'; 19 20class OrmException extends Exception {} 21class OrmConfigurationException extends Exception {} 22// Database fields/tables do not match codebase 23class InconsistentModelException extends OrmException { 24 function __construct() { 25 // Drop the model cache (just incase) 26 ModelMeta::flushModelCache(); 27 call_user_func_array(array('parent', '__construct'), func_get_args()); 28 } 29} 30 31/** 32 * Meta information about a model including edges (relationships), table 33 * name, default sorting information, database fields, etc. 34 * 35 * This class is constructed and built automatically from the model's 36 * ::getMeta() method using a class's ::$meta array. 37 */ 38class ModelMeta implements ArrayAccess { 39 40 static $base = array( 41 'pk' => false, 42 'table' => false, 43 'defer' => array(), 44 'select_related' => array(), 45 'view' => false, 46 'joins' => array(), 47 'foreign_keys' => array(), 48 ); 49 static $model_cache; 50 51 var $model; 52 var $meta = array(); 53 var $new; 54 var $subclasses = array(); 55 var $fields; 56 57 function __construct($model) { 58 $this->model = $model; 59 60 // Merge ModelMeta from parent model (if inherited) 61 $parent = get_parent_class($this->model); 62 $meta = $model::$meta; 63 if ($model::$meta instanceof self) 64 $meta = $meta->meta; 65 if (is_subclass_of($parent, 'VerySimpleModel')) { 66 $this->parent = $parent::getMeta(); 67 $meta = $this->parent->extend($this, $meta); 68 } 69 else { 70 $meta = $meta + self::$base; 71 } 72 73 // Short circuit the meta-data processing if APCu is available. 74 // This is preferred as the meta-data is unlikely to change unless 75 // osTicket is upgraded, (then the upgrader calls the 76 // flushModelCache method to clear this cache). Also, GIT_VERSION is 77 // used in the APC key which should be changed if new code is 78 // deployed. 79 if (function_exists('apcu_store')) { 80 $loaded = false; 81 $apc_key = SECRET_SALT.GIT_VERSION."/orm/{$this->model}"; 82 $this->meta = apcu_fetch($apc_key, $loaded); 83 if ($loaded) 84 return; 85 } 86 87 if (!$meta['view']) { 88 if (!$meta['table']) 89 throw new OrmConfigurationException( 90 sprintf(__('%s: Model does not define meta.table'), $this->model)); 91 elseif (!$meta['pk']) 92 throw new OrmConfigurationException( 93 sprintf(__('%s: Model does not define meta.pk'), $this->model)); 94 } 95 96 // Ensure other supported fields are set and are arrays 97 foreach (array('pk', 'ordering', 'defer', 'select_related') as $f) { 98 if (!isset($meta[$f])) 99 $meta[$f] = array(); 100 elseif (!is_array($meta[$f])) 101 $meta[$f] = array($meta[$f]); 102 } 103 104 // Break down foreign-key metadata 105 foreach ($meta['joins'] as $field => &$j) { 106 $this->processJoin($j); 107 if ($j['local']) 108 $meta['foreign_keys'][$j['local']] = $field; 109 } 110 unset($j); 111 $this->meta = $meta; 112 113 if (function_exists('apcu_store')) { 114 apcu_store($apc_key, $this->meta, 1800); 115 } 116 } 117 118 /** 119 * Merge this class's meta-data into the recieved child meta-data. 120 * When a model extends another model, the meta data for the two models 121 * is merged to form the child's meta data. Returns the merged, child 122 * meta-data. 123 */ 124 function extend(ModelMeta $child, $meta) { 125 $this->subclasses[$child->model] = $child; 126 // Merge 'joins' settings (instead of replacing) 127 if (isset($this->meta['joins'])) { 128 $meta['joins'] = array_merge($meta['joins'] ?: array(), 129 $this->meta['joins']); 130 } 131 return $meta + $this->meta + self::$base; 132 } 133 134 function isSuperClassOf($model) { 135 if (isset($this->subclasses[$model])) 136 return true; 137 foreach ($this->subclasses as $M=>$meta) 138 if ($meta->isSuperClassOf($M)) 139 return true; 140 } 141 142 function isSubclassOf($model) { 143 if (!isset($this->parent)) 144 return false; 145 146 if ($this->parent->model === $model) 147 return true; 148 149 return $this->parent->isSubclassOf($model); 150 } 151 152 /** 153 * Adds some more information to a declared relationship. If the 154 * relationship is a reverse relation, then the information from the 155 * reverse relation is loaded into the local definition 156 * 157 * Compiled-Join-Structure: 158 * 'constraint' => array(local => array(foreign_field, foreign_class)), 159 * Constraint used to construct a JOIN in an SQL query 160 * 'list' => boolean 161 * TRUE if an InstrumentedList should be employed to fetch a list 162 * of related items 163 * 'broker' => Handler for the 'list' property. Usually a subclass of 164 * 'InstrumentedList' 165 * 'null' => boolean 166 * TRUE if relation is nullable 167 * 'fkey' => array(class, pk) 168 * Classname and field of the first item in the constraint that 169 * points to a PK field of a foreign model 170 * 'local' => string 171 * The local field corresponding to the 'fkey' property 172 */ 173 function processJoin(&$j) { 174 $constraint = array(); 175 if (isset($j['reverse'])) { 176 list($fmodel, $key) = explode('.', $j['reverse']); 177 // NOTE: It's ok if the forein meta data is not yet inspected. 178 $info = $fmodel::$meta['joins'][$key]; 179 if (!is_array($info['constraint'])) 180 throw new OrmConfigurationException(sprintf(__( 181 // `reverse` here is the reverse of an ORM relationship 182 '%s: Reverse does not specify any constraints'), 183 $j['reverse'])); 184 foreach ($info['constraint'] as $foreign => $local) { 185 list($L,$field) = is_array($local) ? $local : explode('.', $local); 186 $constraint[$field ?: $L] = array($fmodel, $foreign); 187 } 188 if (!isset($j['list'])) 189 $j['list'] = true; 190 if (!isset($j['null'])) 191 // By default, reverse releationships can be empty lists 192 $j['null'] = true; 193 } 194 else { 195 foreach ($j['constraint'] as $local => $foreign) { 196 list($class, $field) = $constraint[$local] 197 = is_array($foreign) ? $foreign : explode('.', $foreign); 198 } 199 } 200 if ($j['list'] && !isset($j['broker'])) { 201 $j['broker'] = 'InstrumentedList'; 202 } 203 if ($j['broker'] && !class_exists($j['broker'])) { 204 throw new OrmException($j['broker'] . ': List broker does not exist'); 205 } 206 foreach ($constraint as $local => $foreign) { 207 list($class, $field) = $foreign; 208 if ($local[0] == "'" || $field[0] == "'" || !class_exists($class)) 209 continue; 210 $j['fkey'] = $foreign; 211 $j['local'] = $local; 212 } 213 $j['constraint'] = $constraint; 214 } 215 216 function addJoin($name, array $join) { 217 $this->meta['joins'][$name] = $join; 218 $this->processJoin($this->meta['joins'][$name]); 219 } 220 221 /** 222 * Fetch ModelMeta instance by following a join path from this model 223 */ 224 function getByPath($path) { 225 if (is_string($path)) 226 $path = explode('__', $path); 227 $root = $this; 228 foreach ($path as $P) { 229 if (!($root = $root['joins'][$P]['fkey'][0])) 230 break; 231 $root = $root::getMeta(); 232 } 233 return $root; 234 } 235 236 function offsetGet($field) { 237 return $this->meta[$field]; 238 } 239 function offsetSet($field, $what) { 240 $this->meta[$field] = $what; 241 } 242 function offsetExists($field) { 243 return isset($this->meta[$field]); 244 } 245 function offsetUnset($field) { 246 throw new Exception('Model MetaData is immutable'); 247 } 248 249 /** 250 * Fetch the column names of the table used to persist instances of this 251 * model in the database. 252 */ 253 function getFieldNames() { 254 if (!isset($this->fields)) 255 $this->fields = self::inspectFields(); 256 return $this->fields; 257 } 258 259 /** 260 * Create a new instance of the model, optionally hydrating it with the 261 * given hash table. The constructor is not called, which leaves the 262 * default constructor free to assume new object status. 263 * 264 * Three methods were considered, with runtime for 10000 iterations 265 * * unserialze('O:9:"ModelBase":0:{}') - 0.0671s 266 * * new ReflectionClass("ModelBase")->newInstanceWithoutConstructor() 267 * - 0.0478s 268 * * and a hybrid by cloning the reflection class instance - 0.0335s 269 */ 270 function newInstance($props=false) { 271 if (!isset($this->new)) { 272 $rc = new ReflectionClass($this->model); 273 $this->new = $rc->newInstanceWithoutConstructor(); 274 } 275 $instance = clone $this->new; 276 // Hydrate if props were included 277 if (is_array($props)) { 278 $instance->ht = $props; 279 } 280 return $instance; 281 } 282 283 function inspectFields() { 284 if (!isset(self::$model_cache)) 285 self::$model_cache = function_exists('apcu_fetch'); 286 if (self::$model_cache) { 287 $key = SECRET_SALT.GIT_VERSION."/orm/{$this['table']}"; 288 if ($fields = apcu_fetch($key)) { 289 return $fields; 290 } 291 } 292 $fields = DbEngine::getCompiler()->inspectTable($this['table']); 293 if (self::$model_cache) { 294 apcu_store($key, $fields, 1800); 295 } 296 return $fields; 297 } 298 299 static function flushModelCache() { 300 if (self::$model_cache) 301 @apcu_clear_cache(); 302 } 303} 304 305class VerySimpleModel { 306 static $meta = array( 307 'table' => false, 308 'ordering' => false, 309 'pk' => false 310 ); 311 312 var $ht = array(); 313 var $dirty = array(); 314 var $__new__ = false; 315 var $__deleted__ = false; 316 var $__deferred__ = array(); 317 318 function __construct($row=false) { 319 if (is_array($row)) 320 foreach ($row as $field=>$value) 321 if (!is_array($value)) 322 $this->set($field, $value); 323 $this->__new__ = true; 324 } 325 326 /** 327 * Creates a new instance of the model without calling the constructor. 328 * If the constructor is required, consider using the PHP `new` keyword. 329 * The instance returned from this method will not be considered *new* 330 * and will now result in an INSERT when sent to the database. 331 */ 332 static function __hydrate($row=false) { 333 return static::getMeta()->newInstance($row); 334 } 335 336 function __wakeup() { 337 // If a model is stashed in a session, refresh the model from the database 338 $this->refetch(); 339 } 340 341 function get($field, $default=false) { 342 if (array_key_exists($field, $this->ht)) 343 return $this->ht[$field]; 344 elseif (($joins = static::getMeta('joins')) && isset($joins[$field])) { 345 $j = $joins[$field]; 346 // Support instrumented lists and such 347 if (isset($j['list']) && $j['list']) { 348 $class = $j['fkey'][0]; 349 $fkey = array(); 350 // Localize the foreign key constraint 351 foreach ($j['constraint'] as $local=>$foreign) { 352 list($_klas,$F) = $foreign; 353 $fkey[$F ?: $_klas] = ($local[0] == "'") 354 ? trim($local, "'") : $this->ht[$local]; 355 } 356 $v = $this->ht[$field] = new $j['broker']( 357 // Send Model, [Foriegn-Field => Local-Id] 358 array($class, $fkey) 359 ); 360 return $v; 361 } 362 // Support relationships 363 elseif (isset($j['fkey'])) { 364 $criteria = array(); 365 foreach ($j['constraint'] as $local => $foreign) { 366 list($klas,$F) = $foreign; 367 if (class_exists($klas)) 368 $class = $klas; 369 if ($local[0] == "'") { 370 $criteria[$F] = trim($local,"'"); 371 } 372 elseif ($F[0] == "'") { 373 // Does not affect the local model 374 continue; 375 } 376 else { 377 $criteria[$F] = $this->ht[$local]; 378 } 379 } 380 try { 381 $v = $this->ht[$field] = $class::lookup($criteria); 382 } 383 catch (DoesNotExist $e) { 384 $v = null; 385 } 386 return $v; 387 } 388 } 389 elseif (isset($this->__deferred__[$field])) { 390 // Fetch deferred field 391 $row = static::objects()->filter($this->getPk()) 392 // XXX: Seems like all the deferred fields should be fetched 393 ->values_flat($field) 394 ->one(); 395 if ($row) 396 return $this->ht[$field] = $row[0]; 397 } 398 elseif ($field == 'pk') { 399 return $this->getPk(); 400 } 401 402 if (isset($default)) 403 return $default; 404 405 // For new objects, assume the field is NULLable 406 if ($this->__new__) 407 return null; 408 409 // Check to see if the column referenced is actually valid 410 if (in_array($field, static::getMeta()->getFieldNames())) 411 return null; 412 413 throw new OrmException(sprintf(__('%s: %s: Field not defined'), 414 get_class($this), $field)); 415 } 416 function __get($field) { 417 return $this->get($field, null); 418 } 419 420 function getByPath($path) { 421 if (is_string($path)) 422 $path = explode('__', $path); 423 $root = $this; 424 foreach ($path as $P) 425 $root = $root->get($P); 426 return $root; 427 } 428 429 function __isset($field) { 430 return ($this->ht && array_key_exists($field, $this->ht)) 431 || isset(static::$meta['joins'][$field]); 432 } 433 434 function __unset($field) { 435 if ($this->__isset($field)) 436 unset($this->ht[$field]); 437 else 438 unset($this->{$field}); 439 } 440 441 function set($field, $value) { 442 // Update of foreign-key by assignment to model instance 443 $related = false; 444 $joins = static::getMeta('joins'); 445 if (isset($joins[$field])) { 446 $j = $joins[$field]; 447 if ($j['list'] && ($value instanceof InstrumentedList)) { 448 // Magic list property 449 $this->ht[$field] = $value; 450 return; 451 } 452 if ($value === null) { 453 $this->ht[$field] = $value; 454 if (in_array($j['local'], static::$meta['pk'])) { 455 // Reverse relationship — don't null out local PK 456 return; 457 } 458 // Pass. Set local field to NULL in logic below 459 } 460 elseif ($value instanceof VerySimpleModel) { 461 // Ensure that the model being assigned as a relationship is 462 // an instance of the foreign model given in the 463 // relationship, or is a super class thereof. The super 464 // class case is used primary for the xxxThread classes 465 // which all extend from the base Thread class. 466 if (!$value instanceof $j['fkey'][0] 467 && !$value::getMeta()->isSuperClassOf($j['fkey'][0]) 468 ) { 469 throw new InvalidArgumentException( 470 sprintf(__('Expecting NULL or instance of %s. Got a %s instead'), 471 $j['fkey'][0], is_object($value) ? get_class($value) : gettype($value))); 472 } 473 // Capture the object under the object's field name 474 $this->ht[$field] = $value; 475 $value = $value->get($j['fkey'][1]); 476 // Fall through to the standard logic below 477 } 478 // Capture the foreign key id value 479 $field = $j['local']; 480 } 481 // elseif $field is in a relationship, adjust the relationship 482 elseif (isset(static::$meta['foreign_keys'][$field])) { 483 // meta->foreign_keys->{$field} points to the property of the 484 // foreign object. For instance 'object_id' points to 'object' 485 $related = static::$meta['foreign_keys'][$field]; 486 } 487 $old = isset($this->ht[$field]) ? $this->ht[$field] : null; 488 if ($old != $value) { 489 // isset should not be used here, because `null` should not be 490 // replaced in the dirty array 491 if (!array_key_exists($field, $this->dirty)) 492 $this->dirty[$field] = $old; 493 if ($related) 494 // $related points to a foreign object propery. If setting a 495 // new object_id value, the relationship to object should be 496 // cleared and rebuilt 497 unset($this->ht[$related]); 498 } 499 $this->ht[$field] = $value; 500 } 501 function __set($field, $value) { 502 return $this->set($field, $value); 503 } 504 505 function setAll($props) { 506 foreach ($props as $field=>$value) 507 $this->set($field, $value); 508 } 509 510 function __onload() {} 511 512 function serialize() { 513 return $this->getPk(); 514 } 515 516 function unserialize($data) { 517 $this->ht = $data; 518 $this->refetch(); 519 } 520 521 static function getMeta($key=false) { 522 if (!static::$meta instanceof ModelMeta 523 || get_called_class() != static::$meta->model 524 ) { 525 static::$meta = new ModelMeta(get_called_class()); 526 } 527 $M = static::$meta; 528 return ($key) ? $M->offsetGet($key) : $M; 529 } 530 531 static function getOrmFields($recurse=false) { 532 $fks = $lfields = $fields = array(); 533 $myname = get_called_class(); 534 foreach (static::getMeta('joins') as $name=>$j) { 535 $fks[$j['local']] = true; 536 if (!$j['reverse'] && !$j['list'] && $recurse) { 537 foreach ($j['fkey'][0]::getOrmFields($recurse - 1) as $name2=>$f) { 538 $fields["{$name}__{$name2}"] = "{$name} / $f"; 539 } 540 } 541 } 542 foreach (static::getMeta('fields') as $f) { 543 if (isset($fks[$f])) 544 continue; 545 if (in_array($f, static::getMeta('pk'))) 546 continue; 547 $lfields[$f] = "{$f}"; 548 } 549 return $lfields + $fields; 550 } 551 552 /** 553 * objects 554 * 555 * Retrieve a QuerySet for this model class which can be used to fetch 556 * models from the connected database. Subclasses can override this 557 * method to apply forced constraints on the QuerySet. 558 */ 559 static function objects() { 560 return new QuerySet(get_called_class()); 561 } 562 563 /** 564 * lookup 565 * 566 * Retrieve a record by its primary key. This method may be short 567 * circuited by model caching if the record has already been loaded by 568 * the database. In such a case, the database will not be consulted for 569 * the model's data. 570 * 571 * This method can be called with an array of keyword arguments matching 572 * the PK of the object or the values of the primary key. Both of these 573 * usages are correct: 574 * 575 * >>> User::lookup(1) 576 * >>> User::lookup(array('id'=>1)) 577 * 578 * For composite primary keys and the first usage, pass the values in 579 * the order they are given in the Model's 'pk' declaration in its meta 580 * data. 581 * 582 * Parameters: 583 * $criteria - (mixed) primary key for the sought model either as 584 * arguments or key/value array as the function's first argument 585 * 586 * Returns: 587 * (Object<Model>|null) a single instance of the sought model or null if 588 * no such instance exists. 589 */ 590 static function lookup($criteria) { 591 // Model::lookup(1), where >1< is the pk value 592 $args = func_get_args(); 593 if (!is_array($criteria)) { 594 $criteria = array(); 595 $pk = static::getMeta('pk'); 596 foreach ($args as $i=>$f) 597 $criteria[$pk[$i]] = $f; 598 599 // Only consult cache for PK lookup, which is assumed if the 600 // values are passed as args rather than an array 601 if ($cached = ModelInstanceManager::checkCache(get_called_class(), 602 $criteria)) 603 return $cached; 604 } 605 try { 606 return static::objects()->filter($criteria)->one(); 607 } 608 catch (DoesNotExist $e) { 609 return null; 610 } 611 } 612 613 function delete() { 614 $ex = DbEngine::delete($this); 615 try { 616 $ex->execute(); 617 if ($ex->affected_rows() != 1) 618 return false; 619 620 $this->__deleted__ = true; 621 Signal::send('model.deleted', $this); 622 } 623 catch (OrmException $e) { 624 return false; 625 } 626 return true; 627 } 628 629 function save($refetch=false) { 630 if ($this->__deleted__) 631 throw new OrmException('Trying to update a deleted object'); 632 633 $pk = static::getMeta('pk'); 634 $wasnew = $this->__new__; 635 636 // First, if any foreign properties of this object are connected to 637 // another *new* object, then save those objects first and set the 638 // local foreign key field values 639 foreach (static::getMeta('joins') as $prop => $j) { 640 if (isset($this->ht[$prop]) 641 && ($foreign = $this->ht[$prop]) 642 && $foreign instanceof VerySimpleModel 643 && !in_array($j['local'], $pk) 644 && null === $this->get($j['local']) 645 ) { 646 if ($foreign->__new__ && !$foreign->save()) 647 return false; 648 $this->set($j['local'], $foreign->get($j['fkey'][1])); 649 } 650 } 651 652 // If there's nothing in the model to be saved, then we're done 653 if (count($this->dirty) === 0) 654 return true; 655 656 $ex = DbEngine::save($this); 657 try { 658 $ex->execute(); 659 if ($ex->affected_rows() != 1) { 660 // This doesn't really signify an error. It just means that 661 // the database believes that the row did not change. For 662 // inserts though, it's a deal breaker 663 if ($this->__new__) 664 return false; 665 else 666 // No need to reload the record if requested — the 667 // database didn't update anything 668 $refetch = false; 669 } 670 } 671 catch (OrmException $e) { 672 return false; 673 } 674 675 if ($wasnew) { 676 if (count($pk) == 1) 677 // XXX: Ensure AUTO_INCREMENT is set for the field 678 $this->ht[$pk[0]] = $ex->insert_id(); 679 $this->__new__ = false; 680 Signal::send('model.created', $this); 681 } 682 else { 683 $data = array('dirty' => $this->dirty); 684 Signal::send('model.updated', $this, $data); 685 foreach ($this->dirty as $key => $value) { 686 if ($key != 'value' && $key != 'updated') { 687 $type = array('type' => 'edited', 'key' => $key, 'orm_audit' => true); 688 Signal::send('object.edited', $this, $type); 689 } 690 } 691 } 692 # Refetch row from database 693 if ($refetch) { 694 // Preserve non database information such as list relationships 695 // across the refetch 696 $this->refetch(); 697 } 698 if ($wasnew) { 699 // Attempt to update foreign, unsaved objects with the PK of 700 // this newly created object 701 foreach (static::getMeta('joins') as $prop => $j) { 702 if (isset($this->ht[$prop]) 703 && ($foreign = $this->ht[$prop]) 704 && in_array($j['local'], $pk) 705 ) { 706 if ($foreign instanceof VerySimpleModel 707 && null === $foreign->get($j['fkey'][1]) 708 ) { 709 $foreign->set($j['fkey'][1], $this->get($j['local'])); 710 } 711 elseif ($foreign instanceof InstrumentedList) { 712 foreach ($foreign as $item) { 713 if (null === $item->get($j['fkey'][1])) 714 $item->set($j['fkey'][1], $this->get($j['local'])); 715 } 716 } 717 } 718 } 719 $this->__onload(); 720 } 721 $this->dirty = array(); 722 return true; 723 } 724 725 private function refetch() { 726 try { 727 $this->ht = 728 static::objects()->filter($this->getPk())->values()->one() 729 + $this->ht; 730 } catch (DoesNotExist $ex) {} 731 } 732 733 private function getPk() { 734 $pk = array(); 735 foreach ($this::getMeta('pk') as $f) 736 $pk[$f] = $this->ht[$f]; 737 return $pk; 738 } 739 740 function getDbFields() { 741 return $this->ht; 742 } 743 744 /** 745 * Create a new clone of this model. The primary key will be unset and the 746 * object will be set as __new__. The __clone() magic method is reserved 747 * by the buildModel() system, because it clone's a single instance when 748 * hydrating objects from the database. 749 */ 750 function copy() { 751 // Drop the PK and set as unsaved 752 $dup = clone $this; 753 foreach ($dup::getMeta('pk') as $f) 754 $dup->__unset($f); 755 $dup->__new__ = true; 756 return $dup; 757 } 758} 759 760/** 761 * AnnotatedModel 762 * 763 * Simple wrapper class which allows wrapping and write-protecting of 764 * annotated fields retrieved from the database. Instances of this class 765 * will delegate most all of the heavy lifting to the wrapped Model instance. 766 */ 767class AnnotatedModel { 768 static function wrap(VerySimpleModel $model, $extras=array(), $class=false) { 769 static $classes; 770 771 $class = $class ?: get_class($model); 772 773 if ($extras instanceof VerySimpleModel) { 774 $extra = "Writeable"; 775 } 776 if (!isset($classes[$class])) { 777 $classes[$class] = eval(<<<END_CLASS 778class {$extra}AnnotatedModel___{$class} 779extends {$class} { 780 protected \$__overlay__; 781 use {$extra}AnnotatedModelTrait; 782 783 static function __hydrate(\$ht=false, \$annotations=false) { 784 \$instance = parent::__hydrate(\$ht); 785 \$instance->__overlay__ = \$annotations; 786 return \$instance; 787 } 788} 789return "{$extra}AnnotatedModel___{$class}"; 790END_CLASS 791 ); 792 } 793 return $classes[$class]::__hydrate($model->ht, $extras); 794 } 795} 796 797trait AnnotatedModelTrait { 798 function get($what, $default=false) { 799 if (isset($this->__overlay__[$what])) 800 return $this->__overlay__[$what]; 801 return parent::get($what); 802 } 803 804 function set($what, $to) { 805 if (isset($this->__overlay__[$what])) 806 throw new OrmException('Annotated fields are read-only'); 807 return parent::set($what, $to); 808 } 809 810 function __isset($what) { 811 if (isset($this->__overlay__[$what])) 812 return true; 813 return parent::__isset($what); 814 } 815 816 function getDbFields() { 817 return $this->__overlay__ + parent::getDbFields(); 818 } 819} 820 821/** 822 * Slight variant on the AnnotatedModelTrait, except that the overlay is 823 * another model. Its fields are preferred over the wrapped model's fields. 824 * Updates to the overlayed fields are tracked in the overlay model and 825 * therefore kept separate from the annotated model's fields. ::save() will 826 * call save on both models. Delete will only delete the overlay model (that 827 * is, the annotated model will remain). 828 */ 829trait WriteableAnnotatedModelTrait { 830 function get($what, $default=false) { 831 if ($this->__overlay__->__isset($what)) 832 return $this->__overlay__->get($what); 833 return parent::get($what); 834 } 835 836 function set($what, $to) { 837 if (isset($this->__overlay__) 838 && $this->__overlay__->__isset($what)) { 839 return $this->__overlay__->set($what, $to); 840 } 841 return parent::set($what, $to); 842 } 843 844 function __isset($what) { 845 if (isset($this->__overlay__) && $this->__overlay__->__isset($what)) 846 return true; 847 return parent::__isset($what); 848 } 849 850 function getDbFields() { 851 return $this->__overlay__->getDbFields() + parent::getDbFields(); 852 } 853 854 function save($refetch=false) { 855 $this->__overlay__->save($refetch); 856 return parent::save($refetch); 857 } 858 859 function delete() { 860 if ($rv = $this->__overlay__->delete()) 861 // Mark the annotated object as deleted 862 $this->__deleted__ = true; 863 return $rv; 864 } 865} 866 867class SqlFunction { 868 var $alias; 869 870 function __construct($name) { 871 $this->func = $name; 872 $this->args = array_slice(func_get_args(), 1); 873 } 874 875 function input($what, $compiler, $model) { 876 if ($what instanceof SqlFunction) 877 $A = $what->toSql($compiler, $model); 878 elseif ($what instanceof Q) 879 $A = $compiler->compileQ($what, $model); 880 else 881 $A = $compiler->input($what); 882 return $A; 883 } 884 885 function toSql($compiler, $model=false, $alias=false) { 886 $args = array(); 887 foreach ($this->args as $A) { 888 $args[] = $this->input($A, $compiler, $model); 889 } 890 return sprintf('%s(%s)%s', $this->func, implode(', ', $args), 891 $alias && $this->alias ? ' AS '.$compiler->quote($this->alias) : ''); 892 } 893 894 function getAlias() { 895 return $this->alias; 896 } 897 function setAlias($alias) { 898 $this->alias = $alias; 899 } 900 901 static function __callStatic($func, $args) { 902 $I = new static($func); 903 $I->args = $args; 904 return $I; 905 } 906 907 function __call($operator, $other) { 908 array_unshift($other, $this); 909 return SqlExpression::__callStatic($operator, $other); 910 } 911} 912 913class SqlCase extends SqlFunction { 914 var $cases = array(); 915 var $else = false; 916 917 static function N() { 918 return new static('CASE'); 919 } 920 921 function when($expr, $result) { 922 if (is_array($expr)) 923 $expr = new Q($expr); 924 $this->cases[] = array($expr, $result); 925 return $this; 926 } 927 function otherwise($result) { 928 $this->else = $result; 929 return $this; 930 } 931 932 function toSql($compiler, $model=false, $alias=false) { 933 $cases = array(); 934 foreach ($this->cases as $A) { 935 list($expr, $result) = $A; 936 $expr = $this->input($expr, $compiler, $model); 937 $result = $this->input($result, $compiler, $model); 938 $cases[] = "WHEN {$expr} THEN {$result}"; 939 } 940 if ($this->else) { 941 $else = $this->input($this->else, $compiler, $model); 942 $cases[] = "ELSE {$else}"; 943 } 944 return sprintf('CASE %s END%s', implode(' ', $cases), 945 $alias && $this->alias ? ' AS '.$compiler->quote($this->alias) : ''); 946 } 947} 948 949class SqlExpr extends SqlFunction { 950 function __construct($args) { 951 $this->args = func_get_args(); 952 if (count($this->args) == 1 && is_array($this->args[0])) 953 $this->args = $this->args[0]; 954 } 955 956 function toSql($compiler, $model=false, $alias=false) { 957 $O = array(); 958 foreach ($this->args as $field=>$value) { 959 if ($value instanceof Q) { 960 $ex = $compiler->compileQ($value, $model, false); 961 $O[] = $ex->text; 962 } 963 else { 964 list($field, $op) = $compiler->getField($field, $model); 965 if (is_callable($op)) 966 $O[] = call_user_func($op, $field, $value, $model); 967 else 968 $O[] = sprintf($op, $field, $compiler->input($value)); 969 } 970 } 971 return implode(' ', $O) . ($alias ? ' AS ' . $compiler->quote($alias) : ''); 972 } 973} 974 975class SqlExpression extends SqlFunction { 976 var $operator; 977 var $operands; 978 979 function toSql($compiler, $model=false, $alias=false) { 980 $O = array(); 981 foreach ($this->args as $operand) { 982 $O[] = $this->input($operand, $compiler, $model); 983 } 984 return '('.implode(' '.$this->func.' ', $O) 985 . ($alias ? ' AS '.$compiler->quote($alias) : '') 986 . ')'; 987 } 988 989 static function __callStatic($operator, $operands) { 990 switch ($operator) { 991 case 'minus': 992 $operator = '-'; break; 993 case 'plus': 994 $operator = '+'; break; 995 case 'times': 996 $operator = '*'; break; 997 case 'bitand': 998 $operator = '&'; break; 999 case 'bitor': 1000 $operator = '|'; break; 1001 default: 1002 throw new InvalidArgumentException($operator.': Invalid operator specified'); 1003 } 1004 return parent::__callStatic($operator, $operands); 1005 } 1006 1007 function __call($operator, $operands) { 1008 array_unshift($operands, $this); 1009 return SqlExpression::__callStatic($operator, $operands); 1010 } 1011} 1012 1013class SqlInterval extends SqlFunction { 1014 var $type; 1015 1016 function toSql($compiler, $model=false, $alias=false) { 1017 $A = $this->args[0]; 1018 if ($A instanceof SqlFunction) 1019 $A = $A->toSql($compiler, $model); 1020 else 1021 $A = $compiler->input($A); 1022 return sprintf('INTERVAL %s %s', 1023 $A, 1024 $this->func) 1025 . ($alias ? ' AS '.$compiler->quote($alias) : ''); 1026 } 1027 1028 static function __callStatic($interval, $args) { 1029 if (count($args) != 1) { 1030 throw new InvalidArgumentException("Interval expects a single interval value"); 1031 } 1032 return parent::__callStatic($interval, $args); 1033 } 1034} 1035 1036class SqlField extends SqlExpression { 1037 var $level; 1038 1039 function __construct($field, $level=0) { 1040 $this->field = $field; 1041 $this->level = $level; 1042 } 1043 1044 function toSql($compiler, $model=false, $alias=false) { 1045 $L = $this->level; 1046 while ($L--) 1047 $compiler = $compiler->getParent(); 1048 list($field) = $compiler->getField($this->field, $model); 1049 return $field; 1050 } 1051} 1052 1053class SqlCode extends SqlFunction { 1054 function __construct($code) { 1055 $this->code = $code; 1056 } 1057 1058 function toSql($compiler, $model=false, $alias=false) { 1059 return $this->code.($alias ? ' AS '.$alias : ''); 1060 } 1061} 1062 1063class SqlAggregate extends SqlFunction { 1064 1065 var $func; 1066 var $expr; 1067 var $distinct=false; 1068 var $constraint=false; 1069 1070 function __construct($func, $expr, $distinct=false, $constraint=false) { 1071 $this->func = $func; 1072 $this->expr = $expr; 1073 $this->distinct = $distinct; 1074 if ($constraint instanceof Q) 1075 $this->constraint = $constraint; 1076 elseif ($constraint) 1077 $this->constraint = new Q($constraint); 1078 } 1079 1080 static function __callStatic($func, $args) { 1081 $distinct = @$args[1] ?: false; 1082 $constraint = @$args[2] ?: false; 1083 return new static($func, $args[0], $distinct, $constraint); 1084 } 1085 1086 function toSql($compiler, $model=false, $alias=false) { 1087 $options = array('constraint' => $this->constraint, 'model' => true); 1088 1089 // For DISTINCT, require a field specification — not a relationship 1090 // specification. 1091 $E = $this->expr; 1092 if ($E instanceof SqlFunction) { 1093 $field = $E->toSql($compiler, $model); 1094 } 1095 else { 1096 list($field, $rmodel) = $compiler->getField($E, $model, $options); 1097 if ($this->distinct) { 1098 $pk = false; 1099 $fpk = $rmodel::getMeta('pk'); 1100 foreach ($fpk as $f) { 1101 $pk |= false !== strpos($field, $f); 1102 } 1103 if (!$pk) { 1104 // Try and use the foriegn primary key 1105 if (count($fpk) == 1) { 1106 list($field) = $compiler->getField( 1107 $this->expr . '__' . $fpk[0], 1108 $model, $options); 1109 } 1110 else { 1111 throw new OrmException( 1112 sprintf('%s :: %s', $rmodel, $field) . 1113 ': DISTINCT aggregate expressions require specification of a single primary key field of the remote model' 1114 ); 1115 } 1116 } 1117 } 1118 } 1119 1120 return sprintf('%s(%s%s)%s', $this->func, 1121 $this->distinct ? 'DISTINCT ' : '', $field, 1122 $alias && $this->alias ? ' AS '.$compiler->quote($this->alias) : ''); 1123 } 1124 1125 function getFieldName() { 1126 return strtolower(sprintf('%s__%s', $this->args[0], $this->func)); 1127 } 1128} 1129 1130class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countable { 1131 var $model; 1132 1133 var $constraints = array(); 1134 var $path_constraints = array(); 1135 var $ordering = array(); 1136 var $limit = false; 1137 var $offset = 0; 1138 var $related = array(); 1139 var $values = array(); 1140 var $defer = array(); 1141 var $aggregated = false; 1142 var $annotations = array(); 1143 var $extra = array(); 1144 var $distinct = array(); 1145 var $lock = false; 1146 var $chain = array(); 1147 var $options = array(); 1148 1149 const LOCK_EXCLUSIVE = 1; 1150 const LOCK_SHARED = 2; 1151 1152 const ASC = 'ASC'; 1153 const DESC = 'DESC'; 1154 1155 const OPT_NOSORT = 'nosort'; 1156 const OPT_NOCACHE = 'nocache'; 1157 const OPT_MYSQL_FOUND_ROWS = 'found_rows'; 1158 const OPT_INDEX_HINT = 'indexhint'; 1159 1160 const ITER_MODELS = 1; 1161 const ITER_HASH = 2; 1162 const ITER_ROW = 3; 1163 1164 var $iter = self::ITER_MODELS; 1165 1166 var $compiler = 'MySqlCompiler'; 1167 1168 var $query; 1169 var $count; 1170 var $total; 1171 1172 function __construct($model) { 1173 $this->model = $model; 1174 } 1175 1176 function filter() { 1177 // Multiple arrays passes means OR 1178 foreach (func_get_args() as $Q) { 1179 $this->constraints[] = $Q instanceof Q ? $Q : new Q($Q); 1180 } 1181 return $this; 1182 } 1183 1184 function exclude() { 1185 foreach (func_get_args() as $Q) { 1186 $this->constraints[] = $Q instanceof Q ? $Q->negate() : Q::not($Q); 1187 } 1188 return $this; 1189 } 1190 1191 /** 1192 * Add a path constraint for the query. This is different from ::filter 1193 * in that the constraint is added to a join clause which is normally 1194 * built from the model meta data. The ::filter() method on the other 1195 * hand adds the constraint to the where clause. This is generally useful 1196 * for aggregate queries and left join queries where multiple rows might 1197 * match a filter in the where clause and would produce incorrect results. 1198 * 1199 * Example: 1200 * Find users with personal email hosted with gmail. 1201 * >>> $Q = User::objects(); 1202 * >>> $Q->constrain(['user__emails' => new Q(['type' => 'personal'])) 1203 * >>> $Q->filter(['user__emails__address__contains' => '@gmail.com']) 1204 */ 1205 function constrain() { 1206 foreach (func_get_args() as $I) { 1207 foreach ($I as $path => $Q) { 1208 if (!is_array($Q) && !$Q instanceof Q) { 1209 // ->constrain(array('field__path__op' => val)); 1210 $Q = array($path => $Q); 1211 list(, $path) = SqlCompiler::splitCriteria($path); 1212 $path = implode('__', $path); 1213 } 1214 $this->path_constraints[$path][] = $Q instanceof Q ? $Q : Q::all($Q); 1215 } 1216 } 1217 return $this; 1218 } 1219 1220 function defer() { 1221 foreach (func_get_args() as $f) 1222 $this->defer[$f] = true; 1223 return $this; 1224 } 1225 function order_by($order, $direction=false) { 1226 if ($order === false) 1227 return $this->options(array('nosort' => true)); 1228 1229 $args = func_get_args(); 1230 if (in_array($direction, array(self::ASC, self::DESC))) { 1231 $args = array($args[0]); 1232 } 1233 else 1234 $direction = false; 1235 1236 $new = is_array($order) ? $order : $args; 1237 if ($direction) { 1238 foreach ($new as $i=>$x) { 1239 $new[$i] = array($x, $direction); 1240 } 1241 } 1242 $this->ordering = array_merge($this->ordering, $new); 1243 return $this; 1244 } 1245 function getSortFields() { 1246 $ordering = $this->ordering; 1247 if ($this->extra['order_by']) 1248 $ordering = array_merge($ordering, $this->extra['order_by']); 1249 return $ordering; 1250 } 1251 1252 function lock($how=false) { 1253 $this->lock = $how ?: self::LOCK_EXCLUSIVE; 1254 return $this; 1255 } 1256 1257 function limit($count) { 1258 $this->limit = $count; 1259 return $this; 1260 } 1261 1262 function offset($at) { 1263 $this->offset = $at; 1264 return $this; 1265 } 1266 1267 function isWindowed() { 1268 return $this->limit || $this->offset || (count($this->values) + count($this->annotations) + @count($this->extra['select'])) > 1; 1269 } 1270 1271 /** 1272 * Fetch related fields with the query. This will result in better 1273 * performance as related items are fetched with the root model with 1274 * only one trip to the database. 1275 * 1276 * Either an array of fields can be sent as one argument, or the list of 1277 * fields can be sent as the arguments to the function. 1278 * 1279 * Example: 1280 * >>> $q = User::objects()->select_related('role'); 1281 */ 1282 function select_related() { 1283 $args = func_get_args(); 1284 if (is_array($args[0])) 1285 $args = $args[0]; 1286 1287 $this->related = array_merge($this->related, $args); 1288 return $this; 1289 } 1290 1291 function extra(array $extra) { 1292 foreach ($extra as $section=>$info) { 1293 $this->extra[$section] = array_merge($this->extra[$section] ?: array(), $info); 1294 } 1295 return $this; 1296 } 1297 1298 function addExtraJoin(array $join) { 1299 return $this->extra(array('joins' => array($join))); 1300 } 1301 1302 function distinct() { 1303 foreach (func_get_args() as $D) 1304 $this->distinct[] = $D; 1305 return $this; 1306 } 1307 1308 function models() { 1309 $this->iter = self::ITER_MODELS; 1310 $this->values = $this->related = array(); 1311 return $this; 1312 } 1313 1314 function values() { 1315 foreach (func_get_args() as $A) 1316 $this->values[$A] = $A; 1317 $this->iter = self::ITER_HASH; 1318 // This disables related models 1319 $this->related = false; 1320 return $this; 1321 } 1322 1323 function values_flat() { 1324 $this->values = func_get_args(); 1325 $this->iter = self::ITER_ROW; 1326 // This disables related models 1327 $this->related = false; 1328 return $this; 1329 } 1330 1331 function copy() { 1332 return clone $this; 1333 } 1334 1335 function all() { 1336 return $this->getIterator()->asArray(); 1337 } 1338 1339 function first() { 1340 $list = $this->limit(1)->all(); 1341 return $list[0]; 1342 } 1343 1344 /** 1345 * one 1346 * 1347 * Finds and returns a single model instance based on the criteria in 1348 * this QuerySet instance. 1349 * 1350 * Throws: 1351 * DoesNotExist - if no such model exists with the given criteria 1352 * ObjectNotUnique - if more than one model matches the given criteria 1353 * 1354 * Returns: 1355 * (Object<Model>) a single instance of the sought model is guarenteed. 1356 * If no such model or multiple models exist, an exception is thrown. 1357 */ 1358 function one() { 1359 $list = $this->all(); 1360 if (count($list) == 0) 1361 throw new DoesNotExist(); 1362 elseif (count($list) > 1) 1363 throw new ObjectNotUnique('One object was expected; however ' 1364 .'multiple objects in the database matched the query. ' 1365 .sprintf('In fact, there are %d matching objects.', count($list)) 1366 ); 1367 return $list[0]; 1368 } 1369 1370 function count() { 1371 // Defer to the iterator if fetching already started 1372 if (isset($this->_iterator)) { 1373 return $this->_iterator->count(); 1374 } 1375 elseif (isset($this->count)) { 1376 return $this->count; 1377 } 1378 $class = $this->compiler; 1379 $compiler = new $class(); 1380 return $this->count = $compiler->compileCount($this); 1381 } 1382 1383 /** 1384 * Similar to count, except that the LIMIT and OFFSET parts are not 1385 * considered in the counts. That is, this will return the count of rows 1386 * if the query were not windowed with limit() and offset(). 1387 * 1388 * For MySQL, the query will be submitted and fetched and the 1389 * SQL_CALC_FOUND_ROWS hint will be sent in the query. Afterwards, the 1390 * result of FOUND_ROWS() is fetched and is the result of this function. 1391 * 1392 * The result of this function is cached. If further changes are made 1393 * after this is run, the changes should be made in a clone. 1394 */ 1395 function total() { 1396 if (isset($this->total)) 1397 return $this->total; 1398 1399 // Optimize the query with the CALC_FOUND_ROWS if 1400 // - the compiler supports it 1401 // - the iterator hasn't yet been built, that is, the query for this 1402 // statement has not yet been sent to the database 1403 $compiler = $this->compiler; 1404 if ($compiler::supportsOption(self::OPT_MYSQL_FOUND_ROWS) 1405 && !isset($this->_iterator) 1406 ) { 1407 // This optimization requires caching 1408 $this->options(array( 1409 self::OPT_MYSQL_FOUND_ROWS => 1, 1410 self::OPT_NOCACHE => null, 1411 )); 1412 $this->exists(true); 1413 $compiler = new $compiler(); 1414 return $this->total = $compiler->getFoundRows(); 1415 } 1416 1417 $query = clone $this; 1418 $query->limit(false)->offset(false)->order_by(false); 1419 return $this->total = $query->count(); 1420 } 1421 1422 function toSql($compiler, $model, $alias=false) { 1423 // FIXME: Force root model of the compiler to $model 1424 $exec = $this->getQuery(array('compiler' => get_class($compiler), 1425 'parent' => $compiler, 'subquery' => true)); 1426 // Rewrite the parameter numbers so they fit the parameter numbers 1427 // of the current parameters of the $compiler 1428 $sql = preg_replace_callback("/:(\d+)/", 1429 function($m) use ($compiler, $exec) { 1430 $compiler->params[] = $exec->params[$m[1]-1]; 1431 return ':'.count($compiler->params); 1432 }, $exec->sql); 1433 return "({$sql})".($alias ? " AS {$alias}" : ''); 1434 } 1435 1436 /** 1437 * exists 1438 * 1439 * Determines if there are any rows in this QuerySet. This can be 1440 * achieved either by evaluating a SELECT COUNT(*) query or by 1441 * attempting to fetch the first row from the recordset and return 1442 * boolean success. 1443 * 1444 * Parameters: 1445 * $fetch - (bool) TRUE if a compile and fetch should be attempted 1446 * instead of a SELECT COUNT(*). This would be recommended if an 1447 * accurate count is not required and the records would be fetched 1448 * if this method returns TRUE. 1449 * 1450 * Returns: 1451 * (bool) TRUE if there would be at least one record in this QuerySet 1452 */ 1453 function exists($fetch=false) { 1454 if ($fetch) { 1455 return (bool) $this[0]; 1456 } 1457 return $this->count() > 0; 1458 } 1459 1460 function annotate($annotations) { 1461 if (!is_array($annotations)) 1462 $annotations = func_get_args(); 1463 foreach ($annotations as $name=>$A) { 1464 if ($A instanceof SqlFunction) { 1465 if (is_int($name)) 1466 $name = $A->getFieldName(); 1467 $A->setAlias($name); 1468 } 1469 $this->annotations[$name] = $A; 1470 } 1471 return $this; 1472 } 1473 1474 function aggregate($annotations) { 1475 // Aggregate works like annotate, except that it sets up values 1476 // fetching which will disable model creation 1477 $this->annotate($annotations); 1478 $this->values(); 1479 // Disable other fields from being fetched 1480 $this->aggregated = true; 1481 $this->related = false; 1482 return $this; 1483 } 1484 1485 function options($options) { 1486 // Make an array with $options as the only key 1487 if (!is_array($options)) 1488 $options = array($options => 1); 1489 1490 $this->options = array_merge($this->options, $options); 1491 return $this; 1492 } 1493 1494 function hasOption($option) { 1495 return isset($this->options[$option]); 1496 } 1497 1498 function getOption($option) { 1499 return @$this->options[$option] ?: false; 1500 } 1501 1502 function setOption($option, $value) { 1503 $this->options[$option] = $value; 1504 } 1505 1506 function clearOption($option) { 1507 unset($this->options[$option]); 1508 } 1509 1510 function countSelectFields() { 1511 $count = count($this->values) + count($this->annotations); 1512 if (isset($this->extra['select'])) 1513 foreach (@$this->extra['select'] as $S) 1514 $count += count($S); 1515 return $count; 1516 } 1517 1518 function union(QuerySet $other, $all=true) { 1519 // Values and values_list _must_ match for this to work 1520 if ($this->countSelectFields() != $other->countSelectFields()) 1521 throw new OrmException('Union queries must have matching values counts'); 1522 1523 // TODO: Clear OFFSET and LIMIT in the $other query 1524 1525 $this->chain[] = array($other, $all); 1526 return $this; 1527 } 1528 1529 function delete() { 1530 $class = $this->compiler; 1531 $compiler = new $class(); 1532 // XXX: Mark all in-memory cached objects as deleted 1533 $ex = $compiler->compileBulkDelete($this); 1534 $ex->execute(); 1535 return $ex->affected_rows(); 1536 } 1537 1538 function update(array $what) { 1539 $class = $this->compiler; 1540 $compiler = new $class; 1541 $ex = $compiler->compileBulkUpdate($this, $what); 1542 $ex->execute(); 1543 return $ex->affected_rows(); 1544 } 1545 1546 function __clone() { 1547 unset($this->_iterator); 1548 unset($this->query); 1549 unset($this->count); 1550 unset($this->total); 1551 } 1552 1553 function __call($name, $args) { 1554 1555 if (!is_callable(array($this->getIterator(), $name))) 1556 throw new OrmException('Call to undefined method QuerySet::'.$name); 1557 1558 return $args 1559 ? call_user_func_array(array($this->getIterator(), $name), $args) 1560 : call_user_func(array($this->getIterator(), $name)); 1561 } 1562 1563 // IteratorAggregate interface 1564 function getIterator($iterator=false) { 1565 if (!isset($this->_iterator)) { 1566 $class = $iterator ?: $this->getIteratorClass(); 1567 $it = new $class($this); 1568 if (!$this->hasOption(self::OPT_NOCACHE)) { 1569 if ($this->iter == self::ITER_MODELS) 1570 // Add findFirst() and such 1571 $it = new ModelResultSet($it); 1572 else 1573 $it = new CachedResultSet($it); 1574 } 1575 else { 1576 $it = $it->getIterator(); 1577 } 1578 $this->_iterator = $it; 1579 } 1580 return $this->_iterator; 1581 } 1582 1583 function getIteratorClass() { 1584 switch ($this->iter) { 1585 case self::ITER_MODELS: 1586 return 'ModelInstanceManager'; 1587 case self::ITER_HASH: 1588 return 'HashArrayIterator'; 1589 case self::ITER_ROW: 1590 return 'FlatArrayIterator'; 1591 } 1592 } 1593 1594 // ArrayAccess interface 1595 function offsetExists($offset) { 1596 return $this->getIterator()->offsetExists($offset); 1597 } 1598 function offsetGet($offset) { 1599 return $this->getIterator()->offsetGet($offset); 1600 } 1601 function offsetUnset($a) { 1602 throw new Exception(__('QuerySet is read-only')); 1603 } 1604 function offsetSet($a, $b) { 1605 throw new Exception(__('QuerySet is read-only')); 1606 } 1607 1608 function __toString() { 1609 return (string) $this->getQuery(); 1610 } 1611 1612 function getQuery($options=array()) { 1613 if (isset($this->query)) 1614 return $this->query; 1615 1616 // Load defaults from model 1617 $model = $this->model; 1618 $meta = $model::getMeta(); 1619 $query = clone $this; 1620 $options += $this->options; 1621 if ($options['nosort']) 1622 $query->ordering = array(); 1623 elseif (!$query->ordering && $meta['ordering']) 1624 $query->ordering = $meta['ordering']; 1625 if (false !== $query->related && !$query->related && !$query->values && $meta['select_related']) 1626 $query->related = $meta['select_related']; 1627 if (!$query->defer && $meta['defer']) 1628 $query->defer = $meta['defer']; 1629 1630 $class = $options['compiler'] ?: $this->compiler; 1631 $compiler = new $class($options); 1632 $this->query = $compiler->compileSelect($query); 1633 1634 return $this->query; 1635 } 1636 1637 /** 1638 * Fetch a model class which can be used to render the QuerySet as a 1639 * subquery to be used as a JOIN. 1640 */ 1641 function asView() { 1642 $unique = spl_object_hash($this); 1643 $classname = "QueryView{$unique}"; 1644 1645 if (class_exists($classname)) 1646 return $classname; 1647 1648 $class = <<<EOF 1649class {$classname} extends VerySimpleModel { 1650 static \$meta = array( 1651 'view' => true, 1652 ); 1653 static \$queryset; 1654 1655 static function getQuery(\$compiler) { 1656 return ' ('.static::\$queryset->getQuery().') '; 1657 } 1658 1659 static function getSqlAddParams(\$compiler) { 1660 return static::\$queryset->toSql(\$compiler, self::\$queryset->model); 1661 } 1662} 1663EOF; 1664 eval($class); // Ugh 1665 $classname::$queryset = $this; 1666 return $classname; 1667 } 1668 1669 function serialize() { 1670 $info = get_object_vars($this); 1671 unset($info['query']); 1672 unset($info['limit']); 1673 unset($info['offset']); 1674 unset($info['_iterator']); 1675 unset($info['count']); 1676 unset($info['total']); 1677 return serialize($info); 1678 } 1679 1680 function unserialize($data) { 1681 $data = unserialize($data); 1682 foreach ($data as $name => $val) { 1683 $this->{$name} = $val; 1684 } 1685 } 1686} 1687 1688class DoesNotExist extends Exception {} 1689class ObjectNotUnique extends Exception {} 1690 1691class CachedResultSet 1692extends BaseList 1693implements ArrayAccess { 1694 protected $inner; 1695 protected $eoi = false; 1696 1697 function __construct(IteratorAggregate $iterator) { 1698 $this->inner = $iterator->getIterator(); 1699 } 1700 1701 function fillTo($level) { 1702 while (!$this->eoi && count($this->storage) < $level) { 1703 if (!$this->inner->valid()) { 1704 $this->eoi = true; 1705 break; 1706 } 1707 $this->storage[] = $this->inner->current(); 1708 $this->inner->next(); 1709 } 1710 } 1711 1712 function asArray() { 1713 $this->fillTo(PHP_INT_MAX); 1714 return $this->getCache(); 1715 } 1716 1717 function getCache() { 1718 return $this->storage; 1719 } 1720 1721 function reset() { 1722 $this->eoi = false; 1723 $this->storage = array(); 1724 // XXX: Should the inner be recreated to refetch? 1725 $this->inner->rewind(); 1726 } 1727 1728 function getIterator() { 1729 $this->asArray(); 1730 return new ArrayIterator($this->storage); 1731 } 1732 1733 function offsetExists($offset) { 1734 $this->fillTo($offset+1); 1735 return count($this->storage) > $offset; 1736 } 1737 function offsetGet($offset) { 1738 $this->fillTo($offset+1); 1739 return $this->storage[$offset]; 1740 } 1741 function offsetUnset($a) { 1742 throw new Exception(__('QuerySet is read-only')); 1743 } 1744 function offsetSet($a, $b) { 1745 throw new Exception(__('QuerySet is read-only')); 1746 } 1747 1748 function count($mode=COUNT_NORMAL) { 1749 $this->asArray(); 1750 return count($this->storage); 1751 } 1752 1753 /** 1754 * Sort the instrumented list in place. This would be useful to change the 1755 * sorting order of the items in the list without fetching the list from 1756 * the database again. 1757 * 1758 * Parameters: 1759 * $key - (callable|int) A callable function to produce the sort keys 1760 * or one of the SORT_ constants used by the array_multisort 1761 * function 1762 * $reverse - (bool) true if the list should be sorted descending 1763 * 1764 * Returns: 1765 * This instrumented list for chaining and inlining. 1766 */ 1767 function sort($key=false, $reverse=false) { 1768 // Fetch all records into the cache 1769 $this->asArray(); 1770 parent::sort($key, $reverse); 1771 return $this; 1772 } 1773 1774 /** 1775 * Reverse the list item in place. Returns this object for chaining 1776 */ 1777 function reverse() { 1778 $this->asArray(); 1779 return parent::reverse(); 1780 } 1781} 1782 1783class ModelResultSet 1784extends CachedResultSet { 1785 /** 1786 * Find the first item in the current set which matches the given criteria. 1787 * This would be used in favor of ::filter() which might trigger another 1788 * database query. The criteria is intended to be quite simple and should 1789 * not traverse relationships which have not already been fetched. 1790 * Otherwise, the ::filter() or ::window() methods would provide better 1791 * performance. 1792 * 1793 * Example: 1794 * >>> $a = new User(); 1795 * >>> $a->roles->add(Role::lookup(['name' => 'administator'])); 1796 * >>> $a->roles->findFirst(['roles__name__startswith' => 'admin']); 1797 * <Role: administrator> 1798 */ 1799 function findFirst($criteria) { 1800 $records = $this->findAll($criteria, 1); 1801 return count($records) > 0 ? $records[0] : null; 1802 } 1803 1804 /** 1805 * Find all the items in the current set which match the given criteria. 1806 * This would be used in favor of ::filter() which might trigger another 1807 * database query. The criteria is intended to be quite simple and should 1808 * not traverse relationships which have not already been fetched. 1809 * Otherwise, the ::filter() or ::window() methods would provide better 1810 * performance, as they can provide results with one more trip to the 1811 * database. 1812 */ 1813 function findAll($criteria, $limit=false) { 1814 $records = new ListObject(); 1815 foreach ($this as $record) { 1816 $matches = true; 1817 foreach ($criteria as $field=>$check) { 1818 if (!SqlCompiler::evaluate($record, $check, $field)) { 1819 $matches = false; 1820 break; 1821 } 1822 } 1823 if ($matches) 1824 $records[] = $record; 1825 if ($limit && count($records) == $limit) 1826 break; 1827 } 1828 return $records; 1829 } 1830} 1831 1832class ModelInstanceManager 1833implements IteratorAggregate { 1834 var $model; 1835 var $map; 1836 var $resource; 1837 var $annnotations; 1838 var $defer; 1839 1840 static $objectCache = array(); 1841 1842 function __construct(QuerySet $queryset) { 1843 $this->model = $queryset->model; 1844 $this->resource = $queryset->getQuery(); 1845 $cache = !$queryset->hasOption(QuerySet::OPT_NOCACHE); 1846 $this->resource->setBuffered($cache); 1847 $this->map = $this->resource->getMap(); 1848 $this->annotations = $queryset->annotations; 1849 $this->defer = $queryset->defer; 1850 } 1851 1852 function cache($model) { 1853 $key = sprintf('%s.%s', 1854 $model::$meta->model, implode('.', $model->get('pk'))); 1855 self::$objectCache[$key] = $model; 1856 } 1857 1858 /** 1859 * uncache 1860 * 1861 * Drop the cached reference to the model. If the model is deleted 1862 * database-side. Lookups for the same model should not be short 1863 * circuited to retrieve the cached reference. 1864 */ 1865 static function uncache($model) { 1866 $key = sprintf('%s.%s', 1867 $model::$meta->model, implode('.', $model->pk)); 1868 unset(self::$objectCache[$key]); 1869 } 1870 1871 static function flushCache() { 1872 self::$objectCache = array(); 1873 } 1874 1875 static function checkCache($modelClass, $fields) { 1876 $key = $modelClass::$meta->model; 1877 foreach ($modelClass::getMeta('pk') as $f) 1878 $key .= '.'.$fields[$f]; 1879 return @self::$objectCache[$key]; 1880 } 1881 1882 /** 1883 * getOrBuild 1884 * 1885 * Builds a new model from the received fields or returns the model 1886 * already stashed in the model cache. Caching helps to ensure that 1887 * multiple lookups for the same model identified by primary key will 1888 * fetch the exact same model. Therefore, changes made to the model 1889 * anywhere in the project will be reflected everywhere. 1890 * 1891 * For annotated models (models build from querysets with annotations), 1892 * the built or cached model is wrapped in an AnnotatedModel instance. 1893 * The annotated fields are in the AnnotatedModel instance and the 1894 * database-backed fields are managed by the Model instance. 1895 */ 1896 function getOrBuild($modelClass, $fields, $cache=true) { 1897 // Check for NULL primary key, used with related model fetching. If 1898 // the PK is NULL, then consider the object to also be NULL 1899 foreach ($modelClass::getMeta('pk') as $pkf) { 1900 if (!isset($fields[$pkf])) { 1901 return null; 1902 } 1903 } 1904 $annotations = $this->annotations; 1905 $extras = array(); 1906 // For annotations, drop them from the $fields list and add them to 1907 // an $extras list. The fields passed to the root model should only 1908 // be the root model's fields. The annotated fields will be wrapped 1909 // using an AnnotatedModel instance. 1910 if ($annotations && $modelClass == $this->model) { 1911 foreach ($annotations as $name=>$A) { 1912 if (array_key_exists($name, $fields)) { 1913 $extras[$name] = $fields[$name]; 1914 unset($fields[$name]); 1915 } 1916 } 1917 } 1918 // Check the cache for the model instance first 1919 if (!($m = self::checkCache($modelClass, $fields))) { 1920 // Construct and cache the object 1921 $m = $modelClass::__hydrate($fields); 1922 // XXX: defer may refer to fields not in this model 1923 $m->__deferred__ = $this->defer; 1924 $m->__onload(); 1925 if ($cache) 1926 $this->cache($m); 1927 } 1928 // Wrap annotations in an AnnotatedModel 1929 if ($extras) { 1930 $m = AnnotatedModel::wrap($m, $extras, $modelClass); 1931 } 1932 // TODO: If the model has deferred fields which are in $fields, 1933 // those can be resolved here 1934 return $m; 1935 } 1936 1937 /** 1938 * buildModel 1939 * 1940 * This method builds the model including related models from the record 1941 * received. For related recordsets, a $map should be setup inside this 1942 * object prior to using this method. The $map is assumed to have this 1943 * configuration: 1944 * 1945 * array(array(<fieldNames>, <modelClass>, <relativePath>)) 1946 * 1947 * Where $modelClass is the name of the foreign (with respect to the 1948 * root model ($this->model), $fieldNames is the number and names of 1949 * fields in the row for this model, $relativePath is the path that 1950 * describes the relationship between the root model and this model, 1951 * 'user__account' for instance. 1952 */ 1953 function buildModel($row, $cache=true) { 1954 // TODO: Traverse to foreign keys 1955 if ($this->map) { 1956 if ($this->model != $this->map[0][1]) 1957 throw new OrmException('Internal select_related error'); 1958 1959 $offset = 0; 1960 foreach ($this->map as $info) { 1961 @list($fields, $model_class, $path) = $info; 1962 $values = array_slice($row, $offset, count($fields)); 1963 $record = array_combine($fields, $values); 1964 if (!$path) { 1965 // Build the root model 1966 $model = $this->getOrBuild($this->model, $record, $cache); 1967 } 1968 elseif ($model) { 1969 $i = 0; 1970 // Traverse the declared path and link the related model 1971 $tail = array_pop($path); 1972 $m = $model; 1973 foreach ($path as $field) { 1974 if (!($m = $m->get($field))) 1975 break; 1976 } 1977 if ($m) { 1978 // Only apply cache setting to the root model. 1979 // Reference models should use caching 1980 $m->set($tail, $this->getOrBuild($model_class, $record, $cache)); 1981 } 1982 } 1983 $offset += count($fields); 1984 } 1985 } 1986 else { 1987 $model = $this->getOrBuild($this->model, $row, $cache); 1988 } 1989 return $model; 1990 } 1991 1992 function getIterator() { 1993 $func = ($this->map) ? 'getRow' : 'getArray'; 1994 $func = array($this->resource, $func); 1995 1996 return new CallbackSimpleIterator(function() use ($func, $cache) { 1997 global $StopIteration; 1998 1999 if ($row = $func()) 2000 return $this->buildModel($row, $cache); 2001 2002 $this->resource->close(); 2003 throw $StopIteration; 2004 }); 2005 } 2006} 2007 2008class CallbackSimpleIterator 2009implements Iterator { 2010 var $current; 2011 var $eoi; 2012 var $callback; 2013 var $key = -1; 2014 2015 function __construct($callback) { 2016 assert(is_callable($callback)); 2017 $this->callback = $callback; 2018 } 2019 2020 function rewind() { 2021 $this->eoi = false; 2022 $this->next(); 2023 } 2024 2025 function key() { 2026 return $this->key; 2027 } 2028 2029 function valid() { 2030 if (!isset($this->eoi)) 2031 $this->rewind(); 2032 return !$this->eoi; 2033 } 2034 2035 function current() { 2036 if ($this->eoi) return false; 2037 return $this->current; 2038 } 2039 2040 function next() { 2041 try { 2042 $cbk = $this->callback; 2043 $this->current = $cbk(); 2044 $this->key++; 2045 } 2046 catch (StopIteration $x) { 2047 $this->eoi = true; 2048 } 2049 } 2050} 2051 2052// Use a global variable, as constructing exceptions is expensive 2053class StopIteration extends Exception {} 2054$StopIteration = new StopIteration(); 2055 2056class FlatArrayIterator 2057implements IteratorAggregate { 2058 var $queryset; 2059 var $resource; 2060 2061 function __construct(QuerySet $queryset) { 2062 $this->queryset = $queryset; 2063 } 2064 2065 function getIterator() { 2066 $this->resource = $this->queryset->getQuery(); 2067 return new CallbackSimpleIterator(function() { 2068 global $StopIteration; 2069 2070 if ($row = $this->resource->getRow()) 2071 return $row; 2072 2073 $this->resource->close(); 2074 throw $StopIteration; 2075 }); 2076 } 2077} 2078 2079class HashArrayIterator 2080implements IteratorAggregate { 2081 var $queryset; 2082 var $resource; 2083 2084 function __construct(QuerySet $queryset) { 2085 $this->queryset = $queryset; 2086 } 2087 2088 function getIterator() { 2089 $this->resource = $this->queryset->getQuery(); 2090 return new CallbackSimpleIterator(function() { 2091 global $StopIteration; 2092 2093 if ($row = $this->resource->getArray()) 2094 return $row; 2095 2096 $this->resource->close(); 2097 throw $StopIteration; 2098 }); 2099 } 2100} 2101 2102class InstrumentedList 2103extends ModelResultSet { 2104 var $key; 2105 2106 function __construct($fkey, $queryset=false, 2107 $iterator='ModelInstanceManager' 2108 ) { 2109 list($model, $this->key) = $fkey; 2110 if (!$queryset) { 2111 $queryset = $model::objects()->filter($this->key); 2112 if ($related = $model::getMeta('select_related')) 2113 $queryset->select_related($related); 2114 } 2115 parent::__construct(new $iterator($queryset)); 2116 $this->model = $model; 2117 $this->queryset = $queryset; 2118 } 2119 2120 function add($object, $save=true, $at=false) { 2121 // NOTE: Attempting to compare $object to $this->model will likely 2122 // be problematic, and limits creative applications of the ORM 2123 if (!$object) { 2124 throw new Exception(sprintf( 2125 'Attempting to add invalid object to list. Expected <%s>, but got <NULL>', 2126 $this->model 2127 )); 2128 } 2129 2130 foreach ($this->key as $field=>$value) 2131 $object->set($field, $value); 2132 2133 if (!$object->__new__ && $save) 2134 $object->save(); 2135 2136 if ($at !== false) 2137 $this->storage[$at] = $object; 2138 else 2139 $this->storage[] = $object; 2140 2141 return $object; 2142 } 2143 2144 function merge(InstrumentedList $list, $save=false) { 2145 foreach ($list as $object) 2146 $this->add($object, $save); 2147 2148 return $this; 2149 } 2150 2151 function remove($object, $delete=true) { 2152 if ($delete) 2153 $object->delete(); 2154 else 2155 foreach ($this->key as $field=>$value) 2156 $object->set($field, null); 2157 } 2158 2159 /** 2160 * Slight edit to the standard iteration method which will skip deleted 2161 * items. 2162 */ 2163 function getIterator() { 2164 return new CallbackFilterIterator(parent::getIterator(), 2165 function($i) { return !$i->__deleted__; } 2166 ); 2167 } 2168 2169 /** 2170 * Reduce the list to a subset using a simply key/value constraint. New 2171 * items added to the subset will have the constraint automatically 2172 * added to all new items. 2173 * 2174 * Parameters: 2175 * $criteria - (<Traversable>) criteria by which this list will be 2176 * constrained and filtered. 2177 * $evaluate - (<bool>) if set to TRUE, the criteria will be evaluated 2178 * without making any more trips to the database. NOTE this may yield 2179 * unexpected results if this list does not contain all the records 2180 * from the database which would be matched by another query. 2181 */ 2182 function window($constraint, $evaluate=false) { 2183 $model = $this->model; 2184 $fields = $model::getMeta()->getFieldNames(); 2185 $key = $this->key; 2186 foreach ($constraint as $field=>$value) { 2187 if (!is_string($field) || false === in_array($field, $fields)) 2188 throw new OrmException('InstrumentedList windowing must be performed on local fields only'); 2189 $key[$field] = $value; 2190 } 2191 $list = new static(array($this->model, $key), $this->filter($constraint)); 2192 if ($evaluate) { 2193 $list->setCache($this->findAll($constraint)); 2194 } 2195 return $list; 2196 } 2197 2198 /** 2199 * Disable database fetching on this list by providing a static list of 2200 * objects. ::add() and ::remove() are still supported. 2201 * XXX: Move this to a parent class? 2202 */ 2203 function setCache(array $cache) { 2204 if (count($this->storage) > 0) 2205 throw new Exception('Cache must be set before fetching records'); 2206 // Set cache and disable fetching 2207 $this->reset(); 2208 $this->storage = $cache; 2209 } 2210 2211 // Save all changes made to any list items 2212 function saveAll() { 2213 foreach ($this as $I) 2214 if (!$I->save()) 2215 return false; 2216 return true; 2217 } 2218 2219 // QuerySet delegates 2220 function exists() { 2221 return $this->queryset->exists(); 2222 } 2223 function expunge() { 2224 if ($this->queryset->delete()) 2225 $this->reset(); 2226 } 2227 function update(array $what) { 2228 return $this->queryset->update($what); 2229 } 2230 2231 // Fetch a new QuerySet 2232 function objects() { 2233 return clone $this->queryset; 2234 } 2235 2236 function offsetUnset($a) { 2237 $this->fillTo($a); 2238 $this->storage[$a]->delete(); 2239 } 2240 function offsetSet($a, $b) { 2241 $this->fillTo($a); 2242 if ($obj = $this->storage[$a]) 2243 $obj->delete(); 2244 $this->add($b, true, $a); 2245 } 2246 2247 // QuerySet overriedes 2248 function __call($what, $how) { 2249 return call_user_func_array(array($this->objects(), $what), $how); 2250 } 2251} 2252 2253class SqlCompiler { 2254 var $options = array(); 2255 var $params = array(); 2256 var $joins = array(); 2257 var $aliases = array(); 2258 var $alias_num = 1; 2259 2260 static $operators = array( 2261 'exact' => '%$1s = %$2s' 2262 ); 2263 2264 function __construct($options=false) { 2265 if (is_array($options)) { 2266 $this->options = array_merge($this->options, $options); 2267 if (isset($options['subquery'])) 2268 $this->alias_num += 150; 2269 } 2270 } 2271 2272 function getParent() { 2273 return $this->options['parent']; 2274 } 2275 2276 /** 2277 * Split a criteria item into the identifying pieces: path, field, and 2278 * operator. 2279 */ 2280 static function splitCriteria($criteria) { 2281 static $operators = array( 2282 'exact' => 1, 'isnull' => 1, 2283 'gt' => 1, 'lt' => 1, 'gte' => 1, 'lte' => 1, 'range' => 1, 2284 'contains' => 1, 'like' => 1, 'startswith' => 1, 'endswith' => 1, 'regex' => 1, 2285 'in' => 1, 'intersect' => 1, 2286 'hasbit' => 1, 2287 ); 2288 $path = explode('__', $criteria); 2289 if (!isset($options['table'])) { 2290 $field = array_pop($path); 2291 if (isset($operators[$field])) { 2292 $operator = $field; 2293 $field = array_pop($path); 2294 } 2295 } 2296 return array($field, $path, $operator ?: 'exact'); 2297 } 2298 2299 /** 2300 * Check if the values match given the operator. 2301 * 2302 * Parameters: 2303 * $record - <ModelBase> An model instance representing a row from the 2304 * database 2305 * $field - Field path including operator used as the evaluated 2306 * expression base. To check if field `name` startswith something, 2307 * $field would be `name__startswith`. 2308 * $check - <mixed> value used as the comparison. This would be the RHS 2309 * of the condition expressed with $field. This can also be a Q 2310 * instance, in which case, $field is not considered, and the Q 2311 * will be used to evaluate the $record directly. 2312 * 2313 * Throws: 2314 * OrmException - if $operator is not supported 2315 */ 2316 static function evaluate($record, $check, $field) { 2317 static $ops; if (!isset($ops)) { $ops = array( 2318 'exact' => function($a, $b) { return is_string($a) ? strcasecmp($a, $b) == 0 : $a == $b; }, 2319 'isnull' => function($a, $b) { return is_null($a) == $b; }, 2320 'gt' => function($a, $b) { return $a > $b; }, 2321 'gte' => function($a, $b) { return $a >= $b; }, 2322 'lt' => function($a, $b) { return $a < $b; }, 2323 'lte' => function($a, $b) { return $a <= $b; }, 2324 'in' => function($a, $b) { return in_array($a, is_array($b) ? $b : array($b)); }, 2325 'contains' => function($a, $b) { return stripos($a, $b) !== false; }, 2326 'startswith' => function($a, $b) { return stripos($a, $b) === 0; }, 2327 'endswith' => function($a, $b) { return iEndsWith($a, $b); }, 2328 'regex' => function($a, $b) { return preg_match("/$a/iu", $b); }, 2329 'hasbit' => function($a, $b) { return ($a & $b) == $b; }, 2330 ); } 2331 // TODO: Support Q expressions 2332 if ($check instanceof Q) 2333 return $check->evaluate($record); 2334 2335 list($field, $path, $operator) = self::splitCriteria($field); 2336 if (!isset($ops[$operator])) 2337 throw new OrmException($operator.': Unsupported operator'); 2338 2339 if ($path) 2340 $record = $record->getByPath($path); 2341 2342 return $ops[$operator]($record->get($field), $check); 2343 } 2344 2345 /** 2346 * Handles breaking down a field or model search descriptor into the 2347 * model search path, field, and operator parts. When used in a queryset 2348 * filter, an expression such as 2349 * 2350 * user__email__hostname__contains => 'foobar' 2351 * 2352 * would be broken down to search from the root model (passed in, 2353 * perhaps a ticket) to the user and email models by inspecting the 2354 * model metadata 'joins' property. The 'constraint' value found there 2355 * will be used to build the JOIN sql clauses. 2356 * 2357 * The 'hostname' will be the field on 'email' model that should be 2358 * compared in the WHERE clause. The comparison should be made using a 2359 * 'contains' function, which in MySQL, might be implemented using 2360 * something like "<lhs> LIKE '%foobar%'" 2361 * 2362 * This function will rely heavily on the pushJoin() function which will 2363 * handle keeping track of joins made previously in the query and 2364 * therefore prevent multiple joins to the same table for the same 2365 * reason. (Self joins are still supported). 2366 * 2367 * Comparison functions supported by this function are defined for each 2368 * respective SqlCompiler subclass; however at least these functions 2369 * should be defined: 2370 * 2371 * function a__function => b 2372 * ----------+------------------------------------------------ 2373 * exact | a is exactly equal to b 2374 * gt | a is greater than b 2375 * lte | b is greater than a 2376 * lt | a is less than b 2377 * gte | b is less than a 2378 * ----------+------------------------------------------------ 2379 * contains | (string) b is contained within a 2380 * statswith | (string) first len(b) chars of a are exactly b 2381 * endswith | (string) last len(b) chars of a are exactly b 2382 * like | (string) a matches pattern b 2383 * ----------+------------------------------------------------ 2384 * in | a is in the list or the nested queryset b 2385 * ----------+------------------------------------------------ 2386 * isnull | a is null (if b) else a is not null 2387 * 2388 * If no comparison function is declared in the field descriptor, 2389 * 'exact' is assumed. 2390 * 2391 * Parameters: 2392 * $field - (string) name of the field to join 2393 * $model - (VerySimpleModel) root model for references in the $field 2394 * parameter 2395 * $options - (array) extra options for the compiler 2396 * 'table' => return the table alias rather than the field-name 2397 * 'model' => return the target model class rather than the operator 2398 * 'constraint' => extra constraint for join clause 2399 * 2400 * Returns: 2401 * (mixed) Usually array<field-name, operator> where field-name is the 2402 * name of the field in the destination model, and operator is the 2403 * requestion comparison method. 2404 */ 2405 function getField($field, $model, $options=array()) { 2406 // Break apart the field descriptor by __ (double-underbars). The 2407 // first part is assumed to be the root field in the given model. 2408 // The parts after each of the __ pieces are links to other tables. 2409 // The last item (after the last __) is allowed to be an operator 2410 // specifiction. 2411 list($field, $parts, $op) = static::splitCriteria($field); 2412 $operator = static::$operators[$op]; 2413 $path = ''; 2414 $rootModel = $model; 2415 2416 // Call pushJoin for each segment in the join path. A new JOIN 2417 // fragment will need to be emitted and/or cached 2418 $joins = array(); 2419 $null = false; 2420 $push = function($p, $model) use (&$joins, &$path, &$null) { 2421 $J = $model::getMeta('joins'); 2422 if (!($info = $J[$p])) { 2423 throw new OrmException(sprintf( 2424 'Model `%s` does not have a relation called `%s`', 2425 $model, $p)); 2426 } 2427 // Propogate LEFT joins through other joins. That is, if a 2428 // multi-join expression is used, the first LEFT join should 2429 // result in further joins also being LEFT 2430 if (isset($info['null'])) 2431 $null = $null || $info['null']; 2432 $info['null'] = $null; 2433 $crumb = $path; 2434 $path = ($path) ? "{$path}__{$p}" : $p; 2435 $joins[] = array($crumb, $path, $model, $info); 2436 // Roll to foreign model 2437 return $info['fkey']; 2438 }; 2439 2440 foreach ($parts as $p) { 2441 list($model) = $push($p, $model); 2442 } 2443 2444 // If comparing a relationship, join the foreign table 2445 // This is a comparison with a relationship — use the foreign key 2446 $J = $model::getMeta('joins'); 2447 if (isset($J[$field])) { 2448 list($model, $field) = $push($field, $model); 2449 } 2450 2451 // Apply the joins list to $this->pushJoin 2452 $last = count($joins) - 1; 2453 $constraint = false; 2454 foreach ($joins as $i=>$A) { 2455 // Add the conststraint as the last arg to the last join 2456 if ($i == $last) 2457 $constraint = $options['constraint']; 2458 $alias = $this->pushJoin($A[0], $A[1], $A[2], $A[3], $constraint); 2459 } 2460 2461 if (!isset($alias)) { 2462 // Determine the alias for the root model table 2463 $alias = (isset($this->joins[''])) 2464 ? $this->joins['']['alias'] 2465 : $this->quote($rootModel::getMeta('table')); 2466 } 2467 2468 if (isset($options['table']) && $options['table']) 2469 $field = $alias; 2470 elseif (isset($this->annotations[$field])) 2471 $field = $this->annotations[$field]; 2472 elseif ($alias) 2473 $field = $alias.'.'.$this->quote($field); 2474 else 2475 $field = $this->quote($field); 2476 2477 if (isset($options['model']) && $options['model']) 2478 $operator = $model; 2479 return array($field, $operator); 2480 } 2481 2482 /** 2483 * Uses the compiler-specific `compileJoin` function to compile the join 2484 * statement fragment, and caches the result in the local $joins list. A 2485 * new alias is acquired using the `nextAlias` function which will be 2486 * associated with the join. If the same path is requested again, the 2487 * algorithm is short-circuited and the originally-assigned table alias 2488 * is returned immediately. 2489 */ 2490 function pushJoin($tip, $path, $model, $info, $constraint=false) { 2491 // TODO: Build the join statement fragment and return the table 2492 // alias. The table alias will be useful where the join is used in 2493 // the WHERE and ORDER BY clauses 2494 2495 // If the join already exists for the statement-being-compiled, just 2496 // return the alias being used. 2497 if (!$constraint && isset($this->joins[$path])) 2498 return $this->joins[$path]['alias']; 2499 2500 // TODO: Support only using aliases if necessary. Use actual table 2501 // names for everything except oddities like self-joins 2502 2503 $alias = $this->nextAlias(); 2504 // Keep an association between the table alias and the model. This 2505 // will make model construction much easier when we have the data 2506 // and the table alias from the database. 2507 $this->aliases[$alias] = $model; 2508 2509 // TODO: Stash joins and join constraints into local ->joins array. 2510 // This will be useful metadata in the executor to construct the 2511 // final models for fetching 2512 // TODO: Always use a table alias. This will further help with 2513 // coordination between the data returned from the database (where 2514 // table alias is available) and the corresponding data. 2515 $T = array('alias' => $alias); 2516 $this->joins[$path] = $T; 2517 $this->joins[$path]['sql'] = $this->compileJoin($tip, $model, $alias, $info, $constraint); 2518 return $alias; 2519 } 2520 2521 /** 2522 * compileQ 2523 * 2524 * Build a constraint represented in an arbitrarily nested Q instance. 2525 * The placement of the compiled constraint is also considered and 2526 * represented in the resulting CompiledExpression instance. 2527 * 2528 * Parameters: 2529 * $Q - (Q) constraint represented in a Q instance 2530 * $model - (VerySimpleModel) root model for all the field references in 2531 * the Q instance 2532 * 2533 * Returns: 2534 * (CompiledExpression) object containing the compiled expression (with 2535 * AND, OR, and NOT operators added). Furthermore, the $type attribute 2536 * of the CompiledExpression will allow the compiler to place the 2537 * constraint properly in the WHERE or HAVING clause appropriately. 2538 */ 2539 function compileQ(Q $Q, $model, $parens=true) { 2540 $filter = array(); 2541 $type = CompiledExpression::TYPE_WHERE; 2542 foreach ($Q->constraints as $field=>$value) { 2543 $fieldName = $field; 2544 // Handle nested constraints 2545 if ($value instanceof Q) { 2546 $filter[] = $T = $this->compileQ($value, $model, 2547 !$Q->isCompatibleWith($value)); 2548 // Bubble up HAVING constraints 2549 if ($T instanceof CompiledExpression 2550 && $T->type == CompiledExpression::TYPE_HAVING) 2551 $type = $T->type; 2552 } 2553 // Handle relationship comparisons with model objects 2554 elseif ($value instanceof VerySimpleModel) { 2555 $criteria = array(); 2556 // Avoid a join if possible. Use the local side of the 2557 // relationship 2558 if (count($value->pk) === 1) { 2559 $path = explode('__', $field); 2560 $relationship = array_pop($path); 2561 $lmodel = $model::getMeta()->getByPath($path); 2562 $local = $lmodel['joins'][$relationship]['local']; 2563 $path = $path ? (implode('__', $path) . '__') : ''; 2564 foreach ($value->pk as $v) { 2565 $criteria["{$path}{$local}"] = $v; 2566 } 2567 } 2568 else { 2569 foreach ($value->pk as $f=>$v) { 2570 $criteria["{$field}__{$f}"] = $v; 2571 } 2572 } 2573 // New criteria here is joined with AND, so if the outer 2574 // criteria is joined with OR, then parentheses are 2575 // necessary 2576 $filter[] = $this->compileQ(new Q($criteria), $model, $Q->ored); 2577 } 2578 // Handle simple field = <value> constraints 2579 else { 2580 list($field, $op) = $this->getField($field, $model); 2581 if ($field instanceof SqlAggregate) { 2582 // This constraint has to go in the HAVING clause 2583 $field = $field->toSql($this, $model); 2584 $type = CompiledExpression::TYPE_HAVING; 2585 } elseif ($field instanceof QuerySet) { 2586 // Constraint on a subquery goes to HAVING clause 2587 list($field) = static::splitCriteria($fieldName); 2588 $type = CompiledExpression::TYPE_HAVING; 2589 } 2590 2591 if ($value === null) 2592 $filter[] = sprintf('%s IS NULL', $field); 2593 elseif ($value instanceof SqlField) 2594 $filter[] = sprintf($op, $field, $value->toSql($this, $model)); 2595 // Allow operators to be callable rather than sprintf 2596 // strings 2597 elseif (is_callable($op)) 2598 $filter[] = call_user_func($op, $field, $value, $model); 2599 else 2600 $filter[] = sprintf($op, $field, $this->input($value)); 2601 } 2602 } 2603 $glue = $Q->ored ? ' OR ' : ' AND '; 2604 $filter = array_filter($filter); 2605 $clause = implode($glue, $filter); 2606 if (($Q->negated || $parens) && count($filter) > 1) 2607 $clause = '(' . $clause . ')'; 2608 if ($Q->negated) 2609 $clause = 'NOT '.$clause; 2610 return new CompiledExpression($clause, $type); 2611 } 2612 2613 function compileConstraints($where, $model) { 2614 $constraints = array(); 2615 foreach ($where as $Q) { 2616 // Constraints are joined by AND operators, so if they have 2617 // internal OR operators, then they need to be parenthesized 2618 $constraints[] = $this->compileQ($Q, $model, $Q->ored); 2619 } 2620 return $constraints; 2621 } 2622 2623 function getParams() { 2624 return $this->params; 2625 } 2626 2627 function getJoins($queryset) { 2628 $sql = ''; 2629 foreach ($this->joins as $path => $j) { 2630 if (!$j['sql']) 2631 continue; 2632 list($base, $constraints) = $j['sql']; 2633 // Add in path-specific constraints, if any 2634 if (isset($queryset->path_constraints[$path])) { 2635 foreach ($queryset->path_constraints[$path] as $Q) { 2636 $constraints[] = $this->compileQ($Q, $queryset->model); 2637 } 2638 } 2639 $sql .= $base; 2640 if ($constraints) 2641 $sql .= ' ON ('.implode(' AND ', $constraints).')'; 2642 } 2643 // Add extra items from QuerySet 2644 if (isset($queryset->extra['tables'])) { 2645 foreach ($queryset->extra['tables'] as $S) { 2646 $join = ' JOIN '; 2647 // Left joins require an ON () clause 2648 // TODO: Have a way to indicate a LEFT JOIN 2649 $sql .= $join.$S; 2650 } 2651 } 2652 2653 // Add extra joins from QuerySet 2654 if (isset($queryset->extra['joins'])) { 2655 foreach ($queryset->extra['joins'] as $J) { 2656 list($base, $constraints, $alias) = $J; 2657 $join = $constraints ? ' LEFT JOIN ' : ' JOIN '; 2658 $sql .= "{$join}{$base} $alias"; 2659 if ($constraints instanceof Q) 2660 $sql .= ' ON ('.$this->compileQ($constraints, $queryset->model).')'; 2661 } 2662 } 2663 2664 return $sql; 2665 } 2666 2667 function nextAlias() { 2668 // Use alias A1-A9,B1-B9,... 2669 $alias = chr(65 + (int)($this->alias_num / 9)) . $this->alias_num % 9; 2670 $this->alias_num++; 2671 return $alias; 2672 } 2673} 2674 2675class CompiledExpression /* extends SplString */ { 2676 const TYPE_WHERE = 0x0001; 2677 const TYPE_HAVING = 0x0002; 2678 2679 var $text = ''; 2680 2681 function __construct($clause, $type=self::TYPE_WHERE) { 2682 $this->text = $clause; 2683 $this->type = $type; 2684 } 2685 2686 function __toString() { 2687 return $this->text; 2688 } 2689} 2690 2691class DbEngine { 2692 2693 static $compiler = 'MySqlCompiler'; 2694 2695 function __construct($info) { 2696 } 2697 2698 function connect() { 2699 } 2700 2701 // Gets a compiler compatible with this database engine that can compile 2702 // and execute a queryset or DML request. 2703 static function getCompiler() { 2704 $class = static::$compiler; 2705 return new $class(); 2706 } 2707 2708 static function delete(VerySimpleModel $model) { 2709 ModelInstanceManager::uncache($model); 2710 return static::getCompiler()->compileDelete($model); 2711 } 2712 2713 static function save(VerySimpleModel $model) { 2714 $compiler = static::getCompiler(); 2715 if ($model->__new__) 2716 return $compiler->compileInsert($model); 2717 else 2718 return $compiler->compileUpdate($model); 2719 } 2720} 2721 2722class MySqlCompiler extends SqlCompiler { 2723 2724 static $operators = array( 2725 'exact' => '%1$s = %2$s', 2726 'contains' => array('self', '__contains'), 2727 'startswith' => array('self', '__startswith'), 2728 'endswith' => array('self', '__endswith'), 2729 'gt' => '%1$s > %2$s', 2730 'lt' => '%1$s < %2$s', 2731 'gte' => '%1$s >= %2$s', 2732 'lte' => '%1$s <= %2$s', 2733 'range' => array('self', '__range'), 2734 'isnull' => array('self', '__isnull'), 2735 'like' => '%1$s LIKE %2$s', 2736 'hasbit' => '%1$s & %2$s != 0', 2737 'in' => array('self', '__in'), 2738 'intersect' => array('self', '__find_in_set'), 2739 'regex' => array('self', '__regex'), 2740 ); 2741 2742 // Thanks, http://stackoverflow.com/a/3683868 2743 function like_escape($what, $e='\\') { 2744 return str_replace(array($e, '%', '_'), array($e.$e, $e.'%', $e.'_'), $what); 2745 } 2746 2747 function __contains($a, $b) { 2748 # {%a} like %{$b}% 2749 # Escape $b 2750 $b = $this->like_escape($b); 2751 return sprintf('%s LIKE %s', $a, $this->input("%$b%")); 2752 } 2753 function __startswith($a, $b) { 2754 $b = $this->like_escape($b); 2755 return sprintf('%s LIKE %s', $a, $this->input("$b%")); 2756 } 2757 function __endswith($a, $b) { 2758 $b = $this->like_escape($b); 2759 return sprintf('%s LIKE %s', $a, $this->input("%$b")); 2760 } 2761 2762 function __in($a, $b) { 2763 if (is_array($b)) { 2764 $vals = array_map(array($this, 'input'), $b); 2765 $b = '('.implode(', ', $vals).')'; 2766 } 2767 // MySQL is almost always faster with a join. Use one if possible 2768 // MySQL doesn't support LIMIT or OFFSET in subqueries. Instead, add 2769 // the query as a JOIN and add the join constraint into the WHERE 2770 // clause. 2771 elseif ($b instanceof QuerySet 2772 && ($b->isWindowed() || $b->countSelectFields() > 1 || $b->chain) 2773 ) { 2774 $f1 = $b->values[0]; 2775 $view = $b->asView(); 2776 $alias = $this->pushJoin($view, $a, $view, array('constraint'=>array())); 2777 return sprintf('%s = %s.%s', $a, $alias, $this->quote($f1)); 2778 } 2779 else { 2780 $b = $this->input($b); 2781 } 2782 return sprintf('%s IN %s', $a, $b); 2783 } 2784 2785 function __isnull($a, $b) { 2786 return $b 2787 ? sprintf('%s IS NULL', $a) 2788 : sprintf('%s IS NOT NULL', $a); 2789 } 2790 2791 function __find_in_set($a, $b) { 2792 if (is_array($b)) { 2793 $sql = array(); 2794 foreach (array_map(array($this, 'input'), $b) as $b) { 2795 $sql[] = sprintf('FIND_IN_SET(%s, %s)', $b, $a); 2796 } 2797 $parens = count($sql) > 1; 2798 $sql = implode(' OR ', $sql); 2799 return $parens ? ('('.$sql.')') : $sql; 2800 } 2801 return sprintf('FIND_IN_SET(%s, %s)', $b, $a); 2802 } 2803 2804 function __regex($a, $b) { 2805 // Strip slashes and options 2806 if ($b[0] == '/') 2807 $b = preg_replace('`/[^/]*$`', '', substr($b, 1)); 2808 return sprintf('%s REGEXP %s', $a, $this->input($b)); 2809 } 2810 2811 function __range($a, $b) { 2812 return sprintf('%s BETWEEN %s AND %s', 2813 $a, 2814 $b[2] ? $b[0] : $this->input($b[0]), 2815 $b[2] ? $b[1] : $this->input($b[1])); 2816 } 2817 2818 function compileJoin($tip, $model, $alias, $info, $extra=false) { 2819 $constraints = array(); 2820 $join = ' JOIN '; 2821 if (isset($info['null']) && $info['null']) 2822 $join = ' LEFT'.$join; 2823 if (isset($this->joins[$tip])) 2824 $table = $this->joins[$tip]['alias']; 2825 else 2826 $table = $this->quote($model::getMeta('table')); 2827 foreach ($info['constraint'] as $local => $foreign) { 2828 list($rmodel, $right) = $foreign; 2829 // Support a constant constraint with 2830 // "'constant'" => "Model.field_name" 2831 if ($local[0] == "'") { 2832 $constraints[] = sprintf("%s.%s = %s", 2833 $alias, $this->quote($right), 2834 $this->input(trim($local, '\'"')) 2835 ); 2836 } 2837 // Support local constraint 2838 // field_name => "'constant'" 2839 elseif ($rmodel[0] == "'" && !$right) { 2840 $constraints[] = sprintf("%s.%s = %s", 2841 $table, $this->quote($local), 2842 $this->input(trim($rmodel, '\'"')) 2843 ); 2844 } 2845 else { 2846 $constraints[] = sprintf("%s.%s = %s.%s", 2847 $table, $this->quote($local), $alias, 2848 $this->quote($right) 2849 ); 2850 } 2851 } 2852 // Support extra join constraints 2853 if ($extra instanceof Q) { 2854 $constraints[] = $this->compileQ($extra, $model); 2855 } 2856 if (!isset($rmodel)) 2857 $rmodel = $model; 2858 // Support inline views 2859 $rmeta = $rmodel::getMeta(); 2860 $table = ($rmeta['view']) 2861 // XXX: Support parameters from the nested query 2862 ? $rmodel::getSqlAddParams($this) 2863 : $this->quote($rmeta['table']); 2864 $base = "{$join}{$table} {$alias}"; 2865 return array($base, $constraints); 2866 } 2867 2868 /** 2869 * input 2870 * 2871 * Generate a parameterized input for a database query. 2872 * 2873 * Parameters: 2874 * $what - (mixed) value to be sent to the database. No escaping is 2875 * necessary. Pass a raw value here. 2876 * 2877 * Returns: 2878 * (string) token to be placed into the compiled SQL statement. This 2879 * is a colon followed by a number 2880 */ 2881 function input($what, $model=false) { 2882 if ($what instanceof QuerySet) { 2883 $q = $what->getQuery(array('nosort'=>!($what->limit || $what->offset))); 2884 // Rewrite the parameter numbers so they fit the parameter numbers 2885 // of the current parameters of the $compiler 2886 $self = $this; 2887 $sql = preg_replace_callback("/:(\d+)/", 2888 function($m) use ($self, $q) { 2889 $self->params[] = $q->params[$m[1]-1]; 2890 return ':'.count($self->params); 2891 }, $q->sql); 2892 return "({$sql})"; 2893 } 2894 elseif ($what instanceof SqlFunction) { 2895 return $what->toSql($this, $model); 2896 } 2897 elseif (!isset($what)) { 2898 return 'NULL'; 2899 } 2900 else { 2901 $this->params[] = $what; 2902 return ':'.(count($this->params)); 2903 } 2904 } 2905 2906 function quote($what) { 2907 return sprintf("`%s`", str_replace("`", "``", $what)); 2908 } 2909 2910 function supportsOption($option) { 2911 return true; 2912 } 2913 2914 /** 2915 * getWhereClause 2916 * 2917 * This builds the WHERE ... part of a DML statement. This should be 2918 * called before ::getJoins(), because it may add joins into the 2919 * statement based on the relationships used in the where clause 2920 */ 2921 protected function getWhereHavingClause($queryset) { 2922 $model = $queryset->model; 2923 $constraints = $this->compileConstraints($queryset->constraints, $model); 2924 $where = $having = array(); 2925 foreach ($constraints as $C) { 2926 if ($C->type == CompiledExpression::TYPE_WHERE) 2927 $where[] = $C; 2928 else 2929 $having[] = $C; 2930 } 2931 if (isset($queryset->extra['where'])) { 2932 foreach ($queryset->extra['where'] as $S) { 2933 $where[] = "($S)"; 2934 } 2935 } 2936 if ($where) 2937 $where = ' WHERE '.implode(' AND ', $where); 2938 if ($having) 2939 $having = ' HAVING '.implode(' AND ', $having); 2940 return array($where ?: '', $having ?: ''); 2941 } 2942 2943 function compileCount($queryset) { 2944 $q = clone $queryset; 2945 // Drop extra fields from the queryset 2946 $q->related = $q->anotations = false; 2947 $model = $q->model; 2948 $q->values = $model::getMeta('pk'); 2949 $q->annotations = false; 2950 $exec = $q->getQuery(array('nosort' => true)); 2951 $exec->sql = 'SELECT COUNT(*) FROM ('.$exec->sql.') __'; 2952 $row = $exec->getRow(); 2953 return is_array($row) ? (int) $row[0] : null; 2954 } 2955 2956 function getFoundRows() { 2957 $exec = new MysqlExecutor('SELECT FOUND_ROWS()', array()); 2958 $row = $exec->getRow(); 2959 return is_array($row) ? (int) $row[0] : null; 2960 } 2961 2962 function compileSelect($queryset) { 2963 $model = $queryset->model; 2964 // Use an alias for the root model table 2965 $this->joins[''] = array('alias' => ($rootAlias = $this->nextAlias())); 2966 2967 // Compile the WHERE clause 2968 $this->annotations = $queryset->annotations ?: array(); 2969 list($where, $having) = $this->getWhereHavingClause($queryset); 2970 2971 // Compile the ORDER BY clause 2972 $sort = ''; 2973 if ($columns = $queryset->getSortFields()) { 2974 $orders = array(); 2975 foreach ($columns as $sort) { 2976 $dir = 'ASC'; 2977 if (is_array($sort)) { 2978 list($sort, $dir) = $sort; 2979 } 2980 if ($sort instanceof SqlFunction) { 2981 $field = $sort->toSql($this, $model); 2982 } 2983 else { 2984 if ($sort[0] === '-') { 2985 $dir = 'DESC'; 2986 $sort = substr($sort, 1); 2987 } 2988 // If the field is already an annotation, then don't 2989 // compile the annotation again below. It's included in 2990 // the select clause, which is sufficient 2991 if (isset($this->annotations[$sort])) 2992 $field = $this->quote($sort); 2993 else 2994 list($field) = $this->getField($sort, $model); 2995 } 2996 if ($field instanceof SqlFunction) 2997 $field = $field->toSql($this, $model); 2998 // TODO: Throw exception if $field can be indentified as 2999 // invalid 3000 3001 $orders[] = "{$field} {$dir}"; 3002 } 3003 $sort = ' ORDER BY '.implode(', ', $orders); 3004 } 3005 3006 // Compile the field listing 3007 $fields = $group_by = array(); 3008 $meta = $model::getMeta(); 3009 $table = $this->quote($meta['table']).' '.$rootAlias; 3010 // Handle related tables 3011 $need_group_by = false; 3012 if ($queryset->related) { 3013 $count = 0; 3014 $fieldMap = $theseFields = array(); 3015 $defer = $queryset->defer ?: array(); 3016 // Add local fields first 3017 foreach ($meta->getFieldNames() as $f) { 3018 // Handle deferreds 3019 if (isset($defer[$f])) 3020 continue; 3021 $fields[$rootAlias . '.' . $this->quote($f)] = true; 3022 $theseFields[] = $f; 3023 } 3024 $fieldMap[] = array($theseFields, $model); 3025 // Add the JOINs to this query 3026 foreach ($queryset->related as $sr) { 3027 // XXX: Sort related by the paths so that the shortest paths 3028 // are resolved first when building out the models. 3029 $full_path = ''; 3030 $parts = array(); 3031 // Track each model traversal and fetch data for each of the 3032 // models in the path of the related table 3033 foreach (explode('__', $sr) as $field) { 3034 $full_path .= $field; 3035 $parts[] = $field; 3036 $theseFields = array(); 3037 list($alias, $fmodel) = $this->getField($full_path, $model, 3038 array('table'=>true, 'model'=>true)); 3039 foreach ($fmodel::getMeta()->getFieldNames() as $f) { 3040 // Handle deferreds 3041 if (isset($defer[$sr . '__' . $f])) 3042 continue; 3043 elseif (isset($fields[$alias.'.'.$this->quote($f)])) 3044 continue; 3045 $fields[$alias . '.' . $this->quote($f)] = true; 3046 $theseFields[] = $f; 3047 } 3048 if ($theseFields) { 3049 $fieldMap[] = array($theseFields, $fmodel, $parts); 3050 } 3051 $full_path .= '__'; 3052 } 3053 } 3054 } 3055 // Support retrieving only a list of values rather than a model 3056 elseif ($queryset->values) { 3057 $additional_group_by = array(); 3058 foreach ($queryset->values as $alias=>$v) { 3059 list($f) = $this->getField($v, $model); 3060 $unaliased = $f; 3061 if ($f instanceof SqlFunction) { 3062 $fields[$f->toSql($this, $model, $alias)] = true; 3063 if ($f instanceof SqlAggregate) { 3064 // Don't group_by aggregate expressions, but if there is an 3065 // aggergate expression, then we need a GROUP BY clause. 3066 $need_group_by = true; 3067 continue; 3068 } 3069 } 3070 else { 3071 if (!is_int($alias) && $unaliased != $alias) 3072 $f .= ' AS '.$this->quote($alias); 3073 $fields[$f] = true; 3074 } 3075 // If there are annotations, add in these fields to the 3076 // GROUP BY clause 3077 if ($queryset->annotations && !$queryset->distinct) 3078 $additional_group_by[] = $unaliased; 3079 } 3080 if ($need_group_by && $additional_group_by) 3081 $group_by = array_merge($group_by, $additional_group_by); 3082 } 3083 // Simple selection from one table 3084 elseif (!$queryset->aggregated) { 3085 if ($queryset->defer) { 3086 foreach ($meta->getFieldNames() as $f) { 3087 if (isset($queryset->defer[$f])) 3088 continue; 3089 $fields[$rootAlias .'.'. $this->quote($f)] = true; 3090 } 3091 } 3092 else { 3093 $fields[$rootAlias.'.*'] = true; 3094 } 3095 } 3096 $fields = array_keys($fields); 3097 // Add in annotations 3098 if ($queryset->annotations) { 3099 foreach ($queryset->annotations as $alias=>$A) { 3100 // The root model will receive the annotations, add in the 3101 // annotation after the root model's fields 3102 if ($A instanceof SqlAggregate) 3103 $need_group_by = true; 3104 $T = $A->toSql($this, $model, $alias); 3105 if ($fieldMap) { 3106 array_splice($fields, count($fieldMap[0][0]), 0, array($T)); 3107 $fieldMap[0][0][] = $alias; 3108 } 3109 else { 3110 // No field map — just add to end of field list 3111 $fields[] = $T; 3112 } 3113 } 3114 // If no group by has been set yet, use the root model pk 3115 if (!$group_by && !$queryset->aggregated && !$queryset->distinct && $need_group_by) { 3116 foreach ($meta['pk'] as $pk) 3117 $group_by[] = $rootAlias .'.'. $pk; 3118 } 3119 } 3120 // Add in SELECT extras 3121 if (isset($queryset->extra['select'])) { 3122 foreach ($queryset->extra['select'] as $name=>$expr) { 3123 if ($expr instanceof SqlFunction) 3124 $expr = $expr->toSql($this, false, $name); 3125 else 3126 $expr = sprintf('%s AS %s', $expr, $this->quote($name)); 3127 $fields[] = $expr; 3128 } 3129 } 3130 if (isset($queryset->distinct)) { 3131 foreach ($queryset->distinct as $d) 3132 list($group_by[]) = $this->getField($d, $model); 3133 } 3134 $group_by = $group_by ? ' GROUP BY '.implode(', ', $group_by) : ''; 3135 3136 $joins = $this->getJoins($queryset); 3137 if ($hint = $queryset->getOption(QuerySet::OPT_INDEX_HINT)) { 3138 $hint = " USE INDEX ({$hint})"; 3139 } 3140 3141 $sql = 'SELECT '; 3142 if ($queryset->hasOption(QuerySet::OPT_MYSQL_FOUND_ROWS)) 3143 $sql .= 'SQL_CALC_FOUND_ROWS '; 3144 $sql .= implode(', ', $fields).' FROM ' 3145 .$table.$hint.$joins.$where.$group_by.$having.$sort; 3146 // UNIONS 3147 if ($queryset->chain) { 3148 // If the main query is sorted, it will need parentheses 3149 if ($parens = (bool) $sort) 3150 $sql = "($sql)"; 3151 foreach ($queryset->chain as $qs) { 3152 list($qs, $all) = $qs; 3153 $q = $qs->getQuery(array('nosort' => true)); 3154 // Rewrite the parameter numbers so they fit the parameter numbers 3155 // of the current parameters of the $compiler 3156 $self = $this; 3157 $S = preg_replace_callback("/:(\d+)/", 3158 function($m) use ($self, $q) { 3159 $self->params[] = $q->params[$m[1]-1]; 3160 return ':'.count($self->params); 3161 }, $q->sql); 3162 // Wrap unions in parentheses if they are windowed or sorted 3163 if ($parens || $qs->isWindowed() || count($qs->getSortFields())) 3164 $S = "($S)"; 3165 $sql .= ' UNION '.($all ? 'ALL ' : '').$S; 3166 } 3167 } 3168 3169 if ($queryset->limit) 3170 $sql .= ' LIMIT '.$queryset->limit; 3171 if ($queryset->offset) 3172 $sql .= ' OFFSET '.$queryset->offset; 3173 switch ($queryset->lock) { 3174 case QuerySet::LOCK_EXCLUSIVE: 3175 $sql .= ' FOR UPDATE'; 3176 break; 3177 case QuerySet::LOCK_SHARED: 3178 $sql .= ' LOCK IN SHARE MODE'; 3179 break; 3180 } 3181 3182 return new MysqlExecutor($sql, $this->params, $fieldMap); 3183 } 3184 3185 function __compileUpdateSet($model, array $pk) { 3186 $fields = array(); 3187 foreach ($model->dirty as $field=>$old) { 3188 if ($model->__new__ or !in_array($field, $pk)) { 3189 $fields[] = sprintf('%s = %s', $this->quote($field), 3190 $this->input($model->get($field))); 3191 } 3192 } 3193 return ' SET '.implode(', ', $fields); 3194 } 3195 3196 function compileUpdate(VerySimpleModel $model) { 3197 $pk = $model::getMeta('pk'); 3198 $sql = 'UPDATE '.$this->quote($model::getMeta('table')); 3199 $sql .= $this->__compileUpdateSet($model, $pk); 3200 // Support PK updates 3201 $criteria = array(); 3202 foreach ($pk as $f) { 3203 $criteria[$f] = @$model->dirty[$f] ?: $model->get($f); 3204 } 3205 $sql .= ' WHERE '.$this->compileQ(new Q($criteria), $model); 3206 $sql .= ' LIMIT 1'; 3207 3208 return new MySqlExecutor($sql, $this->params); 3209 } 3210 3211 function compileInsert(VerySimpleModel $model) { 3212 $pk = $model::getMeta('pk'); 3213 $sql = 'INSERT INTO '.$this->quote($model::getMeta('table')); 3214 $sql .= $this->__compileUpdateSet($model, $pk); 3215 3216 return new MySqlExecutor($sql, $this->params); 3217 } 3218 3219 function compileDelete($model) { 3220 $table = $model::getMeta('table'); 3221 3222 $where = ' WHERE '.implode(' AND ', 3223 $this->compileConstraints(array(new Q($model->pk)), $model)); 3224 $sql = 'DELETE FROM '.$this->quote($table).$where.' LIMIT 1'; 3225 return new MySqlExecutor($sql, $this->params); 3226 } 3227 3228 function compileBulkDelete($queryset) { 3229 $model = $queryset->model; 3230 $table = $model::getMeta('table'); 3231 list($where, $having) = $this->getWhereHavingClause($queryset); 3232 $joins = $this->getJoins($queryset); 3233 $sql = 'DELETE '.$this->quote($table).'.* FROM ' 3234 .$this->quote($table).$joins.$where; 3235 return new MysqlExecutor($sql, $this->params); 3236 } 3237 3238 function compileBulkUpdate($queryset, array $what) { 3239 $model = $queryset->model; 3240 $table = $model::getMeta('table'); 3241 $set = array(); 3242 foreach ($what as $field=>$value) 3243 $set[] = sprintf('%s = %s', $this->quote($field), $this->input($value, $model)); 3244 $set = implode(', ', $set); 3245 list($where, $having) = $this->getWhereHavingClause($queryset); 3246 $joins = $this->getJoins($queryset); 3247 $sql = 'UPDATE '.$this->quote($table).$joins.' SET '.$set.$where; 3248 return new MysqlExecutor($sql, $this->params); 3249 } 3250 3251 // Returns meta data about the table used to build queries 3252 function inspectTable($table) { 3253 static $cache = array(); 3254 3255 // XXX: Assuming schema is not changing — add support to track 3256 // current schema 3257 if (isset($cache[$table])) 3258 return $cache[$table]; 3259 3260 $sql = 'SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS ' 3261 .'WHERE TABLE_NAME = '.db_input($table).' AND TABLE_SCHEMA = DATABASE() ' 3262 .'ORDER BY ORDINAL_POSITION'; 3263 $ex = new MysqlExecutor($sql, array()); 3264 $columns = array(); 3265 while (list($column) = $ex->getRow()) { 3266 $columns[] = $column; 3267 } 3268 return $cache[$table] = $columns; 3269 } 3270} 3271 3272class MySqlPreparedExecutor { 3273 3274 var $stmt; 3275 var $fields = array(); 3276 var $sql; 3277 var $params; 3278 // Array of [count, model] values representing which fields in the 3279 // result set go with witch model. Useful for handling select_related 3280 // queries 3281 var $map; 3282 3283 var $unbuffered = false; 3284 3285 function __construct($sql, $params, $map=null) { 3286 $this->sql = $sql; 3287 $this->params = $params; 3288 $this->map = $map; 3289 } 3290 3291 function getMap() { 3292 return $this->map; 3293 } 3294 3295 function setBuffered($buffered) { 3296 $this->unbuffered = !$buffered; 3297 } 3298 3299 function fixupParams() { 3300 $self = $this; 3301 $params = array(); 3302 $sql = preg_replace_callback("/:(\d+)/", 3303 function($m) use ($self, &$params) { 3304 $params[] = $self->params[$m[1]-1]; 3305 return '?'; 3306 }, $this->sql); 3307 return array($sql, $params); 3308 } 3309 3310 function _prepare() { 3311 $this->execute(); 3312 $this->_setup_output(); 3313 } 3314 3315 function execute() { 3316 list($sql, $params) = $this->fixupParams(); 3317 if (!($this->stmt = db_prepare($sql))) 3318 throw new InconsistentModelException( 3319 'Unable to prepare query: '.db_error().' '.$sql); 3320 if (count($params)) 3321 $this->_bind($params); 3322 if (!$this->stmt->execute() || !($this->unbuffered || $this->stmt->store_result())) { 3323 throw new OrmException('Unable to execute query: ' . $this->stmt->error); 3324 } 3325 return true; 3326 } 3327 3328 function _bind($params) { 3329 if (count($params) != $this->stmt->param_count) 3330 throw new Exception(__('Parameter count does not match query')); 3331 3332 $types = ''; 3333 $ps = array(); 3334 foreach ($params as $i=>&$p) { 3335 if (is_int($p) || is_bool($p)) 3336 $types .= 'i'; 3337 elseif (is_float($p)) 3338 $types .= 'd'; 3339 elseif (is_string($p)) 3340 $types .= 's'; 3341 elseif ($p instanceof DateTime) { 3342 $types .= 's'; 3343 $p = $p->format('Y-m-d H:i:s'); 3344 } 3345 elseif (is_object($p)) { 3346 $types .= 's'; 3347 $p = (string) $p; 3348 } 3349 // TODO: Emit error if param is null 3350 $ps[] = &$p; 3351 } 3352 unset($p); 3353 array_unshift($ps, $types); 3354 call_user_func_array(array($this->stmt,'bind_param'), $ps); 3355 } 3356 3357 function _setup_output() { 3358 if (!($meta = $this->stmt->result_metadata())) 3359 throw new OrmException('Unable to fetch statment metadata: ', $this->stmt->error); 3360 $this->fields = $meta->fetch_fields(); 3361 $meta->free_result(); 3362 } 3363 3364 // Iterator interface 3365 function rewind() { 3366 if (!isset($this->stmt)) 3367 $this->_prepare(); 3368 $this->stmt->data_seek(0); 3369 } 3370 3371 function next() { 3372 $status = $this->stmt->fetch(); 3373 if ($status === false) 3374 throw new OrmException($this->stmt->error); 3375 elseif ($status === null) { 3376 $this->close(); 3377 return false; 3378 } 3379 return true; 3380 } 3381 3382 function getArray() { 3383 $output = array(); 3384 $variables = array(); 3385 3386 if (!isset($this->stmt)) 3387 $this->_prepare(); 3388 3389 foreach ($this->fields as $f) 3390 $variables[] = &$output[$f->name]; // pass by reference 3391 3392 if (!call_user_func_array(array($this->stmt, 'bind_result'), $variables)) 3393 throw new OrmException('Unable to bind result: ' . $this->stmt->error); 3394 3395 if (!$this->next()) 3396 return false; 3397 return $output; 3398 } 3399 3400 function getRow() { 3401 $output = array(); 3402 $variables = array(); 3403 3404 if (!isset($this->stmt)) 3405 $this->_prepare(); 3406 3407 foreach ($this->fields as $f) 3408 $variables[] = &$output[]; // pass by reference 3409 3410 if (!call_user_func_array(array($this->stmt, 'bind_result'), $variables)) 3411 throw new OrmException('Unable to bind result: ' . $this->stmt->error); 3412 3413 if (!$this->next()) 3414 return false; 3415 return $output; 3416 } 3417 3418 function close() { 3419 if (!$this->stmt) 3420 return; 3421 3422 $this->stmt->close(); 3423 $this->stmt = null; 3424 } 3425 3426 function affected_rows() { 3427 return $this->stmt->affected_rows; 3428 } 3429 3430 function insert_id() { 3431 return $this->stmt->insert_id; 3432 } 3433 3434 function __toString() { 3435 $self = $this; 3436 return preg_replace_callback("/:(\d+)(?=([^']*'[^']*')*[^']*$)/", 3437 function($m) use ($self) { 3438 $p = $self->params[$m[1]-1]; 3439 switch (true) { 3440 case is_bool($p): 3441 $p = (int) $p; 3442 case is_int($p): 3443 case is_float($p): 3444 return $p; 3445 case $p instanceof DateTime: 3446 $p = $p->format('Y-m-d H:i:s'); 3447 default: 3448 return db_real_escape((string) $p, true); 3449 } 3450 }, $this->sql); 3451 } 3452} 3453 3454/** 3455 * Simplified executor which uses the mysqli_query() function to process 3456 * queries. This method is faster on MySQL as it doesn't require the PREPARE 3457 * overhead, nor require two trips to the database per query. All parameters 3458 * are escaped and placed directly into the SQL statement. With this style, 3459 * it is possible that multiple parameters could compile a statement which 3460 * exceeds the MySQL max_allowed_packet setting. 3461 */ 3462class MySqlExecutor 3463extends MySqlPreparedExecutor { 3464 function execute() { 3465 $sql = $this->__toString(); 3466 if (!($this->stmt = db_query($sql, true, !$this->unbuffered))) 3467 throw new InconsistentModelException( 3468 'Unable to prepare query: '.db_error().' '.$sql); 3469 // mysqli_query() return TRUE for UPDATE queries and friends 3470 if ($this->stmt !== true) 3471 $this->_setupCast(); 3472 return true; 3473 } 3474 3475 function _setupCast() { 3476 $fields = $this->stmt->fetch_fields(); 3477 $this->types = array(); 3478 foreach ($fields as $F) { 3479 $this->types[] = $F->type; 3480 } 3481 } 3482 3483 function _cast($record) { 3484 $i=0; 3485 foreach ($record as &$f) { 3486 switch ($this->types[$i++]) { 3487 case MYSQLI_TYPE_DECIMAL: 3488 case MYSQLI_TYPE_NEWDECIMAL: 3489 case MYSQLI_TYPE_LONGLONG: 3490 case MYSQLI_TYPE_FLOAT: 3491 case MYSQLI_TYPE_DOUBLE: 3492 $f = isset($f) ? (double) $f : $f; 3493 break; 3494 3495 case MYSQLI_TYPE_BIT: 3496 case MYSQLI_TYPE_TINY: 3497 case MYSQLI_TYPE_SHORT: 3498 case MYSQLI_TYPE_LONG: 3499 case MYSQLI_TYPE_INT24: 3500 $f = isset($f) ? (int) $f : $f; 3501 break; 3502 3503 default: 3504 // No change (leave as string) 3505 } 3506 } 3507 unset($f); 3508 return $record; 3509 } 3510 3511 function getArray() { 3512 if (!isset($this->stmt)) 3513 $this->execute(); 3514 3515 if (null === ($record = $this->stmt->fetch_assoc())) 3516 return false; 3517 return $this->_cast($record); 3518 } 3519 3520 function getRow() { 3521 if (!isset($this->stmt)) 3522 $this->execute(); 3523 3524 if (null === ($record = $this->stmt->fetch_row())) 3525 return false; 3526 return $this->_cast($record); 3527 } 3528 3529 function affected_rows() { 3530 return db_affected_rows(); 3531 } 3532 3533 function insert_id() { 3534 return db_insert_id(); 3535 } 3536} 3537 3538class Q implements Serializable { 3539 const NEGATED = 0x0001; 3540 const ANY = 0x0002; 3541 3542 var $constraints; 3543 var $negated = false; 3544 var $ored = false; 3545 3546 function __construct($filter=array(), $flags=0) { 3547 if (!is_array($filter)) 3548 $filter = array($filter); 3549 $this->constraints = $filter; 3550 $this->negated = $flags & self::NEGATED; 3551 $this->ored = $flags & self::ANY; 3552 } 3553 3554 function isNegated() { 3555 return $this->negated; 3556 } 3557 3558 function isOred() { 3559 return $this->ored; 3560 } 3561 3562 function negate() { 3563 $this->negated = !$this->negated; 3564 return $this; 3565 } 3566 3567 function union() { 3568 $this->ored = true; 3569 } 3570 3571 /** 3572 * Two neighboring Q's are compatible in a where clause if they have 3573 * the same boolean AND / OR operator. Negated Q's should always use 3574 * parentheses if there is more than one criterion. 3575 */ 3576 function isCompatibleWith(Q $other) { 3577 return $this->ored == $other->ored; 3578 } 3579 3580 function add($constraints) { 3581 if (is_array($constraints)) 3582 $this->constraints = array_merge($this->constraints, $constraints); 3583 elseif ($constraints instanceof static) 3584 $this->constraints[] = $constraints; 3585 else 3586 throw new InvalidArgumentException('Expected an instance of Q or an array thereof'); 3587 return $this; 3588 } 3589 3590 static function not($constraints) { 3591 return new static($constraints, self::NEGATED); 3592 } 3593 3594 static function any($constraints) { 3595 return new static($constraints, self::ANY); 3596 } 3597 3598 static function all($constraints) { 3599 return new static($constraints); 3600 } 3601 3602 function evaluate($record) { 3603 // Start with FALSE for OR and TRUE for AND 3604 $result = !$this->ored; 3605 foreach ($this->constraints as $field=>$check) { 3606 $R = SqlCompiler::evaluate($record, $check, $field); 3607 if ($this->ored) { 3608 if ($result |= $R) 3609 break; 3610 } 3611 elseif (!$R) { 3612 // Anything AND false 3613 $result = false; 3614 break; 3615 } 3616 } 3617 if ($this->negated) 3618 $result = !$result; 3619 return $result; 3620 } 3621 3622 function serialize() { 3623 return serialize(array($this->negated, $this->ored, $this->constraints)); 3624 } 3625 3626 function unserialize($data) { 3627 list($this->negated, $this->ored, $this->constraints) = unserialize($data); 3628 } 3629} 3630?> 3631