See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401]
1 <?php 2 /** 3 * Active Record implementation. Superset of Zend Framework's. 4 * 5 * This is "Active Record eXtended" to support JOIN, WORK and LAZY mode 6 * 7 * This file is part of ADOdb, a Database Abstraction Layer library for PHP. 8 * 9 * @package ADOdb 10 * @link https://adodb.org Project's web site and documentation 11 * @link https://github.com/ADOdb/ADOdb Source code and issue tracker 12 * 13 * The ADOdb Library is dual-licensed, released under both the BSD 3-Clause 14 * and the GNU Lesser General Public Licence (LGPL) v2.1 or, at your option, 15 * any later version. This means you can use it in proprietary products. 16 * See the LICENSE.md file distributed with this source code for details. 17 * @license BSD-3-Clause 18 * @license LGPL-2.1-or-later 19 * 20 * @copyright 2000-2013 John Lim 21 * @copyright 2014 Damien Regad, Mark Newnham and the ADOdb community 22 */ 23 24 // CFR: Active Records Definitions 25 define('ADODB_JOIN_AR', 0x01); 26 define('ADODB_WORK_AR', 0x02); 27 define('ADODB_LAZY_AR', 0x03); 28 29 30 global $_ADODB_ACTIVE_DBS; 31 global $ADODB_ACTIVE_CACHESECS; // set to true to enable caching of metadata such as field info 32 global $ACTIVE_RECORD_SAFETY; // set to false to disable safety checks 33 global $ADODB_ACTIVE_DEFVALS; // use default values of table definition when creating new active record. 34 35 // array of ADODB_Active_DB's, indexed by ADODB_Active_Record->_dbat 36 $_ADODB_ACTIVE_DBS = array(); 37 $ACTIVE_RECORD_SAFETY = true; // CFR: disabled while playing with relations 38 $ADODB_ACTIVE_DEFVALS = false; 39 40 class ADODB_Active_DB { 41 var $db; // ADOConnection 42 var $tables; // assoc array of ADODB_Active_Table objects, indexed by tablename 43 } 44 45 class ADODB_Active_Table { 46 var $name; // table name 47 var $flds; // assoc array of adofieldobjs, indexed by fieldname 48 var $keys; // assoc array of primary keys, indexed by fieldname 49 var $_created; // only used when stored as a cached file 50 var $_belongsTo = array(); 51 var $_hasMany = array(); 52 var $_colsCount; // total columns count, including relations 53 54 function updateColsCount() 55 { 56 $this->_colsCount = sizeof($this->flds); 57 foreach($this->_belongsTo as $foreignTable) 58 $this->_colsCount += sizeof($foreignTable->TableInfo()->flds); 59 foreach($this->_hasMany as $foreignTable) 60 $this->_colsCount += sizeof($foreignTable->TableInfo()->flds); 61 } 62 } 63 64 // returns index into $_ADODB_ACTIVE_DBS 65 function ADODB_SetDatabaseAdapter(&$db) 66 { 67 global $_ADODB_ACTIVE_DBS; 68 69 foreach($_ADODB_ACTIVE_DBS as $k => $d) { 70 if ($d->db === $db) { 71 return $k; 72 } 73 } 74 75 $obj = new ADODB_Active_DB(); 76 $obj->db = $db; 77 $obj->tables = array(); 78 79 $_ADODB_ACTIVE_DBS[] = $obj; 80 81 return sizeof($_ADODB_ACTIVE_DBS)-1; 82 } 83 84 85 class ADODB_Active_Record { 86 static $_changeNames = true; // dynamically pluralize table names 87 static $_foreignSuffix = '_id'; // 88 var $_dbat; // associative index pointing to ADODB_Active_DB eg. $ADODB_Active_DBS[_dbat] 89 var $_table; // tablename, if set in class definition then use it as table name 90 var $_sTable; // singularized table name 91 var $_pTable; // pluralized table name 92 var $_tableat; // associative index pointing to ADODB_Active_Table, eg $ADODB_Active_DBS[_dbat]->tables[$this->_tableat] 93 var $_where; // where clause set in Load() 94 var $_saved = false; // indicates whether data is already inserted. 95 var $_lasterr = false; // last error message 96 var $_original = false; // the original values loaded or inserted, refreshed on update 97 98 var $foreignName; // CFR: class name when in a relationship 99 100 static function UseDefaultValues($bool=null) 101 { 102 global $ADODB_ACTIVE_DEFVALS; 103 if (isset($bool)) { 104 $ADODB_ACTIVE_DEFVALS = $bool; 105 } 106 return $ADODB_ACTIVE_DEFVALS; 107 } 108 109 // should be static 110 static function SetDatabaseAdapter(&$db) 111 { 112 return ADODB_SetDatabaseAdapter($db); 113 } 114 115 116 public function __set($name, $value) 117 { 118 $name = str_replace(' ', '_', $name); 119 $this->$name = $value; 120 } 121 122 // php5 constructor 123 // Note: if $table is defined, then we will use it as our table name 124 // Otherwise we will use our classname... 125 // In our database, table names are pluralized (because there can be 126 // more than one row!) 127 // Similarly, if $table is defined here, it has to be plural form. 128 // 129 // $options is an array that allows us to tweak the constructor's behaviour 130 // if $options['refresh'] is true, we re-scan our metadata information 131 // if $options['new'] is true, we forget all relations 132 function __construct($table = false, $pkeyarr=false, $db=false, $options=array()) 133 { 134 global $_ADODB_ACTIVE_DBS; 135 136 if ($db == false && is_object($pkeyarr)) { 137 $db = $pkeyarr; 138 $pkeyarr = false; 139 } 140 141 if($table) { 142 // table argument exists. It is expected to be 143 // already plural form. 144 $this->_pTable = $table; 145 $this->_sTable = $this->_singularize($this->_pTable); 146 } 147 else { 148 // We will use current classname as table name. 149 // We need to pluralize it for the real table name. 150 $this->_sTable = strtolower(get_class($this)); 151 $this->_pTable = $this->_pluralize($this->_sTable); 152 } 153 $this->_table = &$this->_pTable; 154 155 $this->foreignName = $this->_sTable; // CFR: default foreign name (singular) 156 157 if ($db) { 158 $this->_dbat = ADODB_Active_Record::SetDatabaseAdapter($db); 159 } else 160 $this->_dbat = sizeof($_ADODB_ACTIVE_DBS)-1; 161 162 163 if ($this->_dbat < 0) { 164 $this->Error( 165 "No database connection set; use ADOdb_Active_Record::SetDatabaseAdapter(\$db)", 166 'ADODB_Active_Record::__constructor' 167 ); 168 } 169 170 $this->_tableat = $this->_table; # reserved for setting the assoc value to a non-table name, eg. the sql string in future 171 172 // CFR: Just added this option because UpdateActiveTable() can refresh its information 173 // but there was no way to ask it to do that. 174 $forceUpdate = (isset($options['refresh']) && true === $options['refresh']); 175 $this->UpdateActiveTable($pkeyarr, $forceUpdate); 176 if(isset($options['new']) && true === $options['new']) { 177 $table =& $this->TableInfo(); 178 unset($table->_hasMany); 179 unset($table->_belongsTo); 180 $table->_hasMany = array(); 181 $table->_belongsTo = array(); 182 } 183 } 184 185 function __wakeup() 186 { 187 $class = get_class($this); 188 new $class; 189 } 190 191 // CFR: Constants found in Rails 192 static $IrregularP = array( 193 'PERSON' => 'people', 194 'MAN' => 'men', 195 'WOMAN' => 'women', 196 'CHILD' => 'children', 197 'COW' => 'kine', 198 ); 199 200 static $IrregularS = array( 201 'PEOPLE' => 'PERSON', 202 'MEN' => 'man', 203 'WOMEN' => 'woman', 204 'CHILDREN' => 'child', 205 'KINE' => 'cow', 206 ); 207 208 static $WeIsI = array( 209 'EQUIPMENT' => true, 210 'INFORMATION' => true, 211 'RICE' => true, 212 'MONEY' => true, 213 'SPECIES' => true, 214 'SERIES' => true, 215 'FISH' => true, 216 'SHEEP' => true, 217 ); 218 219 function _pluralize($table) 220 { 221 if (!ADODB_Active_Record::$_changeNames) { 222 return $table; 223 } 224 $ut = strtoupper($table); 225 if(isset(self::$WeIsI[$ut])) { 226 return $table; 227 } 228 if(isset(self::$IrregularP[$ut])) { 229 return self::$IrregularP[$ut]; 230 } 231 $len = strlen($table); 232 $lastc = $ut[$len-1]; 233 $lastc2 = substr($ut,$len-2); 234 switch ($lastc) { 235 case 'S': 236 return $table.'es'; 237 case 'Y': 238 return substr($table,0,$len-1).'ies'; 239 case 'X': 240 return $table.'es'; 241 case 'H': 242 if ($lastc2 == 'CH' || $lastc2 == 'SH') { 243 return $table.'es'; 244 } 245 default: 246 return $table.'s'; 247 } 248 } 249 250 // CFR Lamest singular inflector ever - @todo Make it real! 251 // Note: There is an assumption here...and it is that the argument's length >= 4 252 function _singularize($table) 253 { 254 255 if (!ADODB_Active_Record::$_changeNames) { 256 return $table; 257 } 258 $ut = strtoupper($table); 259 if(isset(self::$WeIsI[$ut])) { 260 return $table; 261 } 262 if(isset(self::$IrregularS[$ut])) { 263 return self::$IrregularS[$ut]; 264 } 265 $len = strlen($table); 266 if($ut[$len-1] != 'S') { 267 return $table; // I know...forget oxen 268 } 269 if($ut[$len-2] != 'E') { 270 return substr($table, 0, $len-1); 271 } 272 switch($ut[$len-3]) { 273 case 'S': 274 case 'X': 275 return substr($table, 0, $len-2); 276 case 'I': 277 return substr($table, 0, $len-3) . 'y'; 278 case 'H'; 279 if($ut[$len-4] == 'C' || $ut[$len-4] == 'S') { 280 return substr($table, 0, $len-2); 281 } 282 default: 283 return substr($table, 0, $len-1); // ? 284 } 285 } 286 287 /* 288 * ar->foreignName will contain the name of the tables associated with this table because 289 * these other tables' rows may also be referenced by this table using theirname_id or the provided 290 * foreign keys (this index name is stored in ar->foreignKey) 291 * 292 * this-table.id = other-table-#1.this-table_id 293 * = other-table-#2.this-table_id 294 */ 295 function hasMany($foreignRef,$foreignKey=false) 296 { 297 $ar = new ADODB_Active_Record($foreignRef); 298 $ar->foreignName = $foreignRef; 299 $ar->UpdateActiveTable(); 300 $ar->foreignKey = ($foreignKey) ? $foreignKey : strtolower(get_class($this)) . self::$_foreignSuffix; 301 302 $table =& $this->TableInfo(); 303 if(!isset($table->_hasMany[$foreignRef])) { 304 $table->_hasMany[$foreignRef] = $ar; 305 $table->updateColsCount(); 306 } 307 # @todo Can I make this guy be lazy? 308 $this->$foreignRef = $table->_hasMany[$foreignRef]; // WATCHME Removed assignment by ref. to please __get() 309 } 310 311 /** 312 * ar->foreignName will contain the name of the tables associated with this table because 313 * this table's rows may also be referenced by those tables using thistable_id or the provided 314 * foreign keys (this index name is stored in ar->foreignKey) 315 * 316 * this-table.other-table_id = other-table.id 317 */ 318 function belongsTo($foreignRef,$foreignKey=false) 319 { 320 global $inflector; 321 322 $ar = new ADODB_Active_Record($this->_pluralize($foreignRef)); 323 $ar->foreignName = $foreignRef; 324 $ar->UpdateActiveTable(); 325 $ar->foreignKey = ($foreignKey) ? $foreignKey : $ar->foreignName . self::$_foreignSuffix; 326 327 $table =& $this->TableInfo(); 328 if(!isset($table->_belongsTo[$foreignRef])) { 329 $table->_belongsTo[$foreignRef] = $ar; 330 $table->updateColsCount(); 331 } 332 $this->$foreignRef = $table->_belongsTo[$foreignRef]; 333 } 334 335 /** 336 * __get Access properties - used for lazy loading 337 * 338 * @param mixed $name 339 * @access protected 340 * @return void 341 */ 342 function __get($name) 343 { 344 return $this->LoadRelations($name, '', -1. -1); 345 } 346 347 function LoadRelations($name, $whereOrderBy, $offset=-1, $limit=-1) 348 { 349 $extras = array(); 350 if($offset >= 0) { 351 $extras['offset'] = $offset; 352 } 353 if($limit >= 0) { 354 $extras['limit'] = $limit; 355 } 356 $table =& $this->TableInfo(); 357 358 if (strlen($whereOrderBy)) { 359 if (!preg_match('/^[ \n\r]*AND/i',$whereOrderBy)) { 360 if (!preg_match('/^[ \n\r]*ORDER[ \n\r]/i',$whereOrderBy)) { 361 $whereOrderBy = 'AND '.$whereOrderBy; 362 } 363 } 364 } 365 366 if(!empty($table->_belongsTo[$name])) { 367 $obj = $table->_belongsTo[$name]; 368 $columnName = $obj->foreignKey; 369 if(empty($this->$columnName)) { 370 $this->$name = null; 371 } 372 else { 373 if(($k = reset($obj->TableInfo()->keys))) { 374 $belongsToId = $k; 375 } 376 else { 377 $belongsToId = 'id'; 378 } 379 380 $arrayOfOne = 381 $obj->Find( 382 $belongsToId.'='.$this->$columnName.' '.$whereOrderBy, false, false, $extras); 383 $this->$name = $arrayOfOne[0]; 384 } 385 return $this->$name; 386 } 387 if(!empty($table->_hasMany[$name])) { 388 $obj = $table->_hasMany[$name]; 389 if(($k = reset($table->keys))) { 390 $hasManyId = $k; 391 } 392 else { 393 $hasManyId = 'id'; 394 } 395 396 $this->$name = 397 $obj->Find( 398 $obj->foreignKey.'='.$this->$hasManyId.' '.$whereOrderBy, false, false, $extras); 399 return $this->$name; 400 } 401 } 402 ////////////////////////////////// 403 404 // update metadata 405 function UpdateActiveTable($pkeys=false,$forceUpdate=false) 406 { 407 global $_ADODB_ACTIVE_DBS , $ADODB_CACHE_DIR, $ADODB_ACTIVE_CACHESECS; 408 global $ADODB_ACTIVE_DEFVALS, $ADODB_FETCH_MODE; 409 410 $activedb = $_ADODB_ACTIVE_DBS[$this->_dbat]; 411 412 $table = $this->_table; 413 $tables = $activedb->tables; 414 $tableat = $this->_tableat; 415 if (!$forceUpdate && !empty($tables[$tableat])) { 416 417 $tobj = $tables[$tableat]; 418 foreach($tobj->flds as $name => $fld) { 419 if ($ADODB_ACTIVE_DEFVALS && isset($fld->default_value)) { 420 $this->$name = $fld->default_value; 421 } 422 else { 423 $this->$name = null; 424 } 425 } 426 return; 427 } 428 429 $db = $activedb->db; 430 $fname = $ADODB_CACHE_DIR . '/adodb_' . $db->databaseType . '_active_'. $table . '.cache'; 431 if (!$forceUpdate && $ADODB_ACTIVE_CACHESECS && $ADODB_CACHE_DIR && file_exists($fname)) { 432 $fp = fopen($fname,'r'); 433 @flock($fp, LOCK_SH); 434 $acttab = unserialize(fread($fp,100000)); 435 fclose($fp); 436 if ($acttab->_created + $ADODB_ACTIVE_CACHESECS - (abs(rand()) % 16) > time()) { 437 // abs(rand()) randomizes deletion, reducing contention to delete/refresh file 438 // ideally, you should cache at least 32 secs 439 $activedb->tables[$table] = $acttab; 440 441 //if ($db->debug) ADOConnection::outp("Reading cached active record file: $fname"); 442 return; 443 } else if ($db->debug) { 444 ADOConnection::outp("Refreshing cached active record file: $fname"); 445 } 446 } 447 $activetab = new ADODB_Active_Table(); 448 $activetab->name = $table; 449 450 $save = $ADODB_FETCH_MODE; 451 $ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; 452 if ($db->fetchMode !== false) { 453 $savem = $db->SetFetchMode(false); 454 } 455 456 $cols = $db->MetaColumns($table); 457 458 if (isset($savem)) { 459 $db->SetFetchMode($savem); 460 } 461 $ADODB_FETCH_MODE = $save; 462 463 if (!$cols) { 464 $this->Error("Invalid table name: $table",'UpdateActiveTable'); 465 return false; 466 } 467 $fld = reset($cols); 468 if (!$pkeys) { 469 if (isset($fld->primary_key)) { 470 $pkeys = array(); 471 foreach($cols as $name => $fld) { 472 if (!empty($fld->primary_key)) { 473 $pkeys[] = $name; 474 } 475 } 476 } else { 477 $pkeys = $this->GetPrimaryKeys($db, $table); 478 } 479 } 480 if (empty($pkeys)) { 481 $this->Error("No primary key found for table $table",'UpdateActiveTable'); 482 return false; 483 } 484 485 $attr = array(); 486 $keys = array(); 487 488 switch (ADODB_ASSOC_CASE) { 489 case ADODB_ASSOC_CASE_LOWER: 490 foreach($cols as $name => $fldobj) { 491 $name = strtolower($name); 492 if ($ADODB_ACTIVE_DEFVALS && isset($fldobj->default_value)) { 493 $this->$name = $fldobj->default_value; 494 } 495 else { 496 $this->$name = null; 497 } 498 $attr[$name] = $fldobj; 499 } 500 foreach($pkeys as $k => $name) { 501 $keys[strtolower($name)] = strtolower($name); 502 } 503 break; 504 505 case ADODB_ASSOC_CASE_UPPER: 506 foreach($cols as $name => $fldobj) { 507 $name = strtoupper($name); 508 509 if ($ADODB_ACTIVE_DEFVALS && isset($fldobj->default_value)) { 510 $this->$name = $fldobj->default_value; 511 } 512 else { 513 $this->$name = null; 514 } 515 $attr[$name] = $fldobj; 516 } 517 518 foreach($pkeys as $k => $name) { 519 $keys[strtoupper($name)] = strtoupper($name); 520 } 521 break; 522 default: 523 foreach($cols as $name => $fldobj) { 524 $name = ($fldobj->name); 525 526 if ($ADODB_ACTIVE_DEFVALS && isset($fldobj->default_value)) { 527 $this->$name = $fldobj->default_value; 528 } 529 else { 530 $this->$name = null; 531 } 532 $attr[$name] = $fldobj; 533 } 534 foreach($pkeys as $k => $name) { 535 $keys[$name] = $cols[$name]->name; 536 } 537 break; 538 } 539 540 $activetab->keys = $keys; 541 $activetab->flds = $attr; 542 $activetab->updateColsCount(); 543 544 if ($ADODB_ACTIVE_CACHESECS && $ADODB_CACHE_DIR) { 545 $activetab->_created = time(); 546 $s = serialize($activetab); 547 if (!function_exists('adodb_write_file')) { 548 include_once (ADODB_DIR.'/adodb-csvlib.inc.php'); 549 } 550 adodb_write_file($fname,$s); 551 } 552 if (isset($activedb->tables[$table])) { 553 $oldtab = $activedb->tables[$table]; 554 555 if ($oldtab) { 556 $activetab->_belongsTo = $oldtab->_belongsTo; 557 $activetab->_hasMany = $oldtab->_hasMany; 558 } 559 } 560 $activedb->tables[$table] = $activetab; 561 } 562 563 function GetPrimaryKeys(&$db, $table) 564 { 565 return $db->MetaPrimaryKeys($table); 566 } 567 568 // error handler for both PHP4+5. 569 function Error($err,$fn) 570 { 571 global $_ADODB_ACTIVE_DBS; 572 573 $fn = get_class($this).'::'.$fn; 574 $this->_lasterr = $fn.': '.$err; 575 576 if ($this->_dbat < 0) { 577 $db = false; 578 } 579 else { 580 $activedb = $_ADODB_ACTIVE_DBS[$this->_dbat]; 581 $db = $activedb->db; 582 } 583 584 if (function_exists('adodb_throw')) { 585 if (!$db) { 586 adodb_throw('ADOdb_Active_Record', $fn, -1, $err, 0, 0, false); 587 } 588 else { 589 adodb_throw($db->databaseType, $fn, -1, $err, 0, 0, $db); 590 } 591 } else { 592 if (!$db || $db->debug) { 593 ADOConnection::outp($this->_lasterr); 594 } 595 } 596 597 } 598 599 // return last error message 600 function ErrorMsg() 601 { 602 if (!function_exists('adodb_throw')) { 603 if ($this->_dbat < 0) { 604 $db = false; 605 } 606 else { 607 $db = $this->DB(); 608 } 609 610 // last error could be database error too 611 if ($db && $db->ErrorMsg()) { 612 return $db->ErrorMsg(); 613 } 614 } 615 return $this->_lasterr; 616 } 617 618 function ErrorNo() 619 { 620 if ($this->_dbat < 0) { 621 return -9999; // no database connection... 622 } 623 $db = $this->DB(); 624 625 return (int) $db->ErrorNo(); 626 } 627 628 629 // retrieve ADOConnection from _ADODB_Active_DBs 630 function DB() 631 { 632 global $_ADODB_ACTIVE_DBS; 633 634 if ($this->_dbat < 0) { 635 $false = false; 636 $this->Error("No database connection set: use ADOdb_Active_Record::SetDatabaseAdaptor(\$db)", "DB"); 637 return $false; 638 } 639 $activedb = $_ADODB_ACTIVE_DBS[$this->_dbat]; 640 $db = $activedb->db; 641 return $db; 642 } 643 644 // retrieve ADODB_Active_Table 645 function &TableInfo() 646 { 647 global $_ADODB_ACTIVE_DBS; 648 649 $activedb = $_ADODB_ACTIVE_DBS[$this->_dbat]; 650 $table = $activedb->tables[$this->_tableat]; 651 return $table; 652 } 653 654 655 // I have an ON INSERT trigger on a table that sets other columns in the table. 656 // So, I find that for myTable, I want to reload an active record after saving it. -- Malcolm Cook 657 function Reload() 658 { 659 $db =& $this->DB(); 660 if (!$db) { 661 return false; 662 } 663 $table =& $this->TableInfo(); 664 $where = $this->GenWhere($db, $table); 665 return($this->Load($where)); 666 } 667 668 669 // set a numeric array (using natural table field ordering) as object properties 670 function Set(&$row) 671 { 672 global $ACTIVE_RECORD_SAFETY; 673 674 $db = $this->DB(); 675 676 if (!$row) { 677 $this->_saved = false; 678 return false; 679 } 680 681 $this->_saved = true; 682 683 $table = $this->TableInfo(); 684 $sizeofFlds = sizeof($table->flds); 685 $sizeofRow = sizeof($row); 686 if ($ACTIVE_RECORD_SAFETY && $table->_colsCount != $sizeofRow && $sizeofFlds != $sizeofRow) { 687 # <AP> 688 $bad_size = TRUE; 689 if($sizeofRow == 2 * $table->_colsCount || $sizeofRow == 2 * $sizeofFlds) { 690 // Only keep string keys 691 $keys = array_filter(array_keys($row), 'is_string'); 692 if (sizeof($keys) == sizeof($table->flds)) { 693 $bad_size = FALSE; 694 } 695 } 696 if ($bad_size) { 697 $this->Error("Table structure of $this->_table has changed","Load"); 698 return false; 699 } 700 # </AP> 701 } 702 else { 703 $keys = array_keys($row); 704 } 705 706 # <AP> 707 reset($keys); 708 $this->_original = array(); 709 foreach($table->flds as $name=>$fld) { 710 $value = $row[current($keys)]; 711 $this->$name = $value; 712 $this->_original[] = $value; 713 if(!next($keys)) { 714 break; 715 } 716 } 717 $table =& $this->TableInfo(); 718 foreach($table->_belongsTo as $foreignTable) { 719 $ft = $foreignTable->TableInfo(); 720 $propertyName = $ft->name; 721 foreach($ft->flds as $name=>$fld) { 722 $value = $row[current($keys)]; 723 $foreignTable->$name = $value; 724 $foreignTable->_original[] = $value; 725 if(!next($keys)) { 726 break; 727 } 728 } 729 } 730 foreach($table->_hasMany as $foreignTable) { 731 $ft = $foreignTable->TableInfo(); 732 foreach($ft->flds as $name=>$fld) { 733 $value = $row[current($keys)]; 734 $foreignTable->$name = $value; 735 $foreignTable->_original[] = $value; 736 if(!next($keys)) { 737 break; 738 } 739 } 740 } 741 # </AP> 742 743 return true; 744 } 745 746 // get last inserted id for INSERT 747 function LastInsertID(&$db,$fieldname) 748 { 749 if ($db->hasInsertID) { 750 $val = $db->Insert_ID($this->_table,$fieldname); 751 } 752 else { 753 $val = false; 754 } 755 756 if (is_null($val) || $val === false) { 757 // this might not work reliably in multi-user environment 758 return $db->GetOne("select max(".$fieldname.") from ".$this->_table); 759 } 760 return $val; 761 } 762 763 // quote data in where clause 764 function doquote(&$db, $val,$t) 765 { 766 switch($t) { 767 case 'D': 768 case 'T': 769 if (empty($val)) { 770 return 'null'; 771 } 772 case 'C': 773 case 'X': 774 if (is_null($val)) { 775 return 'null'; 776 } 777 if (strlen($val)>0 && 778 (strncmp($val,"'",1) != 0 || substr($val,strlen($val)-1,1) != "'") 779 ) { 780 return $db->qstr($val); 781 break; 782 } 783 default: 784 return $val; 785 break; 786 } 787 } 788 789 // generate where clause for an UPDATE/SELECT 790 function GenWhere(&$db, &$table) 791 { 792 $keys = $table->keys; 793 $parr = array(); 794 795 foreach($keys as $k) { 796 $f = $table->flds[$k]; 797 if ($f) { 798 $parr[] = $k.' = '.$this->doquote($db,$this->$k,$db->MetaType($f->type)); 799 } 800 } 801 return implode(' and ', $parr); 802 } 803 804 805 //------------------------------------------------------------ Public functions below 806 807 function Load($where=null,$bindarr=false) 808 { 809 $db = $this->DB(); 810 if (!$db) { 811 return false; 812 } 813 $this->_where = $where; 814 815 $save = $db->SetFetchMode(ADODB_FETCH_NUM); 816 $qry = "select * from ".$this->_table; 817 $table =& $this->TableInfo(); 818 819 if(($k = reset($table->keys))) { 820 $hasManyId = $k; 821 } 822 else { 823 $hasManyId = 'id'; 824 } 825 826 foreach($table->_belongsTo as $foreignTable) { 827 if(($k = reset($foreignTable->TableInfo()->keys))) { 828 $belongsToId = $k; 829 } 830 else { 831 $belongsToId = 'id'; 832 } 833 $qry .= ' LEFT JOIN '.$foreignTable->_table.' ON '. 834 $this->_table.'.'.$foreignTable->foreignKey.'='. 835 $foreignTable->_table.'.'.$belongsToId; 836 } 837 foreach($table->_hasMany as $foreignTable) 838 { 839 $qry .= ' LEFT JOIN '.$foreignTable->_table.' ON '. 840 $this->_table.'.'.$hasManyId.'='. 841 $foreignTable->_table.'.'.$foreignTable->foreignKey; 842 } 843 if($where) { 844 $qry .= ' WHERE '.$where; 845 } 846 847 // Simple case: no relations. Load row and return. 848 if((count($table->_hasMany) + count($table->_belongsTo)) < 1) { 849 $row = $db->GetRow($qry,$bindarr); 850 if(!$row) { 851 return false; 852 } 853 $db->SetFetchMode($save); 854 return $this->Set($row); 855 } 856 857 // More complex case when relations have to be collated 858 $rows = $db->GetAll($qry,$bindarr); 859 if(!$rows) { 860 return false; 861 } 862 $db->SetFetchMode($save); 863 if(count($rows) < 1) { 864 return false; 865 } 866 $class = get_class($this); 867 $isFirstRow = true; 868 869 if(($k = reset($this->TableInfo()->keys))) { 870 $myId = $k; 871 } 872 else { 873 $myId = 'id'; 874 } 875 $index = 0; $found = false; 876 /** @todo Improve by storing once and for all in table metadata */ 877 /** @todo Also re-use info for hasManyId */ 878 foreach($this->TableInfo()->flds as $fld) { 879 if($fld->name == $myId) { 880 $found = true; 881 break; 882 } 883 $index++; 884 } 885 if(!$found) { 886 $this->outp_throw("Unable to locate key $myId for $class in Load()",'Load'); 887 } 888 889 foreach($rows as $row) { 890 $rowId = intval($row[$index]); 891 if($rowId > 0) { 892 if($isFirstRow) { 893 $isFirstRow = false; 894 if(!$this->Set($row)) { 895 return false; 896 } 897 } 898 $obj = new $class($table,false,$db); 899 $obj->Set($row); 900 // TODO Copy/paste code below: bad! 901 if(count($table->_hasMany) > 0) { 902 foreach($table->_hasMany as $foreignTable) { 903 $foreignName = $foreignTable->foreignName; 904 if(!empty($obj->$foreignName)) { 905 if(!is_array($this->$foreignName)) { 906 $foreignObj = $this->$foreignName; 907 $this->$foreignName = array(clone($foreignObj)); 908 } 909 else { 910 $foreignObj = $obj->$foreignName; 911 array_push($this->$foreignName, clone($foreignObj)); 912 } 913 } 914 } 915 } 916 if(count($table->_belongsTo) > 0) { 917 foreach($table->_belongsTo as $foreignTable) { 918 $foreignName = $foreignTable->foreignName; 919 if(!empty($obj->$foreignName)) { 920 if(!is_array($this->$foreignName)) { 921 $foreignObj = $this->$foreignName; 922 $this->$foreignName = array(clone($foreignObj)); 923 } 924 else { 925 $foreignObj = $obj->$foreignName; 926 array_push($this->$foreignName, clone($foreignObj)); 927 } 928 } 929 } 930 } 931 } 932 } 933 return true; 934 } 935 936 // false on error 937 function Save() 938 { 939 if ($this->_saved) { 940 $ok = $this->Update(); 941 } 942 else { 943 $ok = $this->Insert(); 944 } 945 946 return $ok; 947 } 948 949 // CFR: Sometimes we may wish to consider that an object is not to be replaced but inserted. 950 // Sample use case: an 'undo' command object (after a delete()) 951 function Dirty() 952 { 953 $this->_saved = false; 954 } 955 956 // false on error 957 function Insert() 958 { 959 $db = $this->DB(); 960 if (!$db) { 961 return false; 962 } 963 $cnt = 0; 964 $table = $this->TableInfo(); 965 966 $valarr = array(); 967 $names = array(); 968 $valstr = array(); 969 970 foreach($table->flds as $name=>$fld) { 971 $val = $this->$name; 972 if(!is_null($val) || !array_key_exists($name, $table->keys)) { 973 $valarr[] = $val; 974 $names[] = $name; 975 $valstr[] = $db->Param($cnt); 976 $cnt += 1; 977 } 978 } 979 980 if (empty($names)){ 981 foreach($table->flds as $name=>$fld) { 982 $valarr[] = null; 983 $names[] = $name; 984 $valstr[] = $db->Param($cnt); 985 $cnt += 1; 986 } 987 } 988 $sql = 'INSERT INTO '.$this->_table."(".implode(',',$names).') VALUES ('.implode(',',$valstr).')'; 989 $ok = $db->Execute($sql,$valarr); 990 991 if ($ok) { 992 $this->_saved = true; 993 $autoinc = false; 994 foreach($table->keys as $k) { 995 if (is_null($this->$k)) { 996 $autoinc = true; 997 break; 998 } 999 } 1000 if ($autoinc && sizeof($table->keys) == 1) { 1001 $k = reset($table->keys); 1002 $this->$k = $this->LastInsertID($db,$k); 1003 } 1004 } 1005 1006 $this->_original = $valarr; 1007 return !empty($ok); 1008 } 1009 1010 function Delete() 1011 { 1012 $db = $this->DB(); 1013 if (!$db) { 1014 return false; 1015 } 1016 $table = $this->TableInfo(); 1017 1018 $where = $this->GenWhere($db,$table); 1019 $sql = 'DELETE FROM '.$this->_table.' WHERE '.$where; 1020 $ok = $db->Execute($sql); 1021 1022 return $ok ? true : false; 1023 } 1024 1025 // returns an array of active record objects 1026 function Find($whereOrderBy,$bindarr=false,$pkeysArr=false,$extra=array()) 1027 { 1028 $db = $this->DB(); 1029 if (!$db || empty($this->_table)) { 1030 return false; 1031 } 1032 $table =& $this->TableInfo(); 1033 $arr = $db->GetActiveRecordsClass(get_class($this),$this, $whereOrderBy,$bindarr,$pkeysArr,$extra, 1034 array('foreignName'=>$this->foreignName, 'belongsTo'=>$table->_belongsTo, 'hasMany'=>$table->_hasMany)); 1035 return $arr; 1036 } 1037 1038 // CFR: In introduced this method to ensure that inner workings are not disturbed by 1039 // subclasses...for instance when GetActiveRecordsClass invokes Find() 1040 // Why am I not invoking parent::Find? 1041 // Shockingly because I want to preserve PHP4 compatibility. 1042 function packageFind($whereOrderBy,$bindarr=false,$pkeysArr=false,$extra=array()) 1043 { 1044 $db = $this->DB(); 1045 if (!$db || empty($this->_table)) { 1046 return false; 1047 } 1048 $table =& $this->TableInfo(); 1049 $arr = $db->GetActiveRecordsClass(get_class($this),$this, $whereOrderBy,$bindarr,$pkeysArr,$extra, 1050 array('foreignName'=>$this->foreignName, 'belongsTo'=>$table->_belongsTo, 'hasMany'=>$table->_hasMany)); 1051 return $arr; 1052 } 1053 1054 // returns 0 on error, 1 on update, 2 on insert 1055 function Replace() 1056 { 1057 $db = $this->DB(); 1058 if (!$db) { 1059 return false; 1060 } 1061 $table = $this->TableInfo(); 1062 1063 $pkey = $table->keys; 1064 1065 foreach($table->flds as $name=>$fld) { 1066 $val = $this->$name; 1067 /* 1068 if (is_null($val)) { 1069 if (isset($fld->not_null) && $fld->not_null) { 1070 if (isset($fld->default_value) && strlen($fld->default_value)) { 1071 continue; 1072 } 1073 else { 1074 $this->Error("Cannot update null into $name","Replace"); 1075 return false; 1076 } 1077 } 1078 }*/ 1079 if (is_null($val) && !empty($fld->auto_increment)) { 1080 continue; 1081 } 1082 $t = $db->MetaType($fld->type); 1083 $arr[$name] = $this->doquote($db,$val,$t); 1084 $valarr[] = $val; 1085 } 1086 1087 if (!is_array($pkey)) { 1088 $pkey = array($pkey); 1089 } 1090 1091 1092 switch (ADODB_ASSOC_CASE) { 1093 case ADODB_ASSOC_CASE_LOWER: 1094 foreach($pkey as $k => $v) { 1095 $pkey[$k] = strtolower($v); 1096 } 1097 break; 1098 case ADODB_ASSOC_CASE_UPPER: 1099 foreach($pkey as $k => $v) { 1100 $pkey[$k] = strtoupper($v); 1101 } 1102 break; 1103 } 1104 1105 $ok = $db->Replace($this->_table,$arr,$pkey); 1106 if ($ok) { 1107 $this->_saved = true; // 1= update 2=insert 1108 if ($ok == 2) { 1109 $autoinc = false; 1110 foreach($table->keys as $k) { 1111 if (is_null($this->$k)) { 1112 $autoinc = true; 1113 break; 1114 } 1115 } 1116 if ($autoinc && sizeof($table->keys) == 1) { 1117 $k = reset($table->keys); 1118 $this->$k = $this->LastInsertID($db,$k); 1119 } 1120 } 1121 1122 $this->_original = $valarr; 1123 } 1124 return $ok; 1125 } 1126 1127 // returns 0 on error, 1 on update, -1 if no change in data (no update) 1128 function Update() 1129 { 1130 $db = $this->DB(); 1131 if (!$db) { 1132 return false; 1133 } 1134 $table = $this->TableInfo(); 1135 1136 $where = $this->GenWhere($db, $table); 1137 1138 if (!$where) { 1139 $this->error("Where missing for table $table", "Update"); 1140 return false; 1141 } 1142 $valarr = array(); 1143 $neworig = array(); 1144 $pairs = array(); 1145 $i = -1; 1146 $cnt = 0; 1147 foreach($table->flds as $name=>$fld) { 1148 $i += 1; 1149 $val = $this->$name; 1150 $neworig[] = $val; 1151 1152 if (isset($table->keys[$name])) { 1153 continue; 1154 } 1155 1156 if (is_null($val)) { 1157 if (isset($fld->not_null) && $fld->not_null) { 1158 if (isset($fld->default_value) && strlen($fld->default_value)) { 1159 continue; 1160 } 1161 else { 1162 $this->Error("Cannot set field $name to NULL","Update"); 1163 return false; 1164 } 1165 } 1166 } 1167 1168 if (isset($this->_original[$i]) && $val === $this->_original[$i]) { 1169 continue; 1170 } 1171 $valarr[] = $val; 1172 $pairs[] = $name.'='.$db->Param($cnt); 1173 $cnt += 1; 1174 } 1175 1176 1177 if (!$cnt) { 1178 return -1; 1179 } 1180 $sql = 'UPDATE '.$this->_table." SET ".implode(",",$pairs)." WHERE ".$where; 1181 $ok = $db->Execute($sql,$valarr); 1182 if ($ok) { 1183 $this->_original = $neworig; 1184 return 1; 1185 } 1186 return 0; 1187 } 1188 1189 function GetAttributeNames() 1190 { 1191 $table = $this->TableInfo(); 1192 if (!$table) { 1193 return false; 1194 } 1195 return array_keys($table->flds); 1196 } 1197 1198 }; 1199 1200 function adodb_GetActiveRecordsClass(&$db, $class, $tableObj,$whereOrderBy,$bindarr, $primkeyArr, 1201 $extra, $relations) 1202 { 1203 global $_ADODB_ACTIVE_DBS; 1204 1205 if (empty($extra['loading'])) { 1206 $extra['loading'] = ADODB_LAZY_AR; 1207 } 1208 $save = $db->SetFetchMode(ADODB_FETCH_NUM); 1209 $table = &$tableObj->_table; 1210 $tableInfo =& $tableObj->TableInfo(); 1211 if(($k = reset($tableInfo->keys))) { 1212 $myId = $k; 1213 } 1214 else { 1215 $myId = 'id'; 1216 } 1217 $index = 0; $found = false; 1218 /** @todo Improve by storing once and for all in table metadata */ 1219 /** @todo Also re-use info for hasManyId */ 1220 foreach($tableInfo->flds as $fld) 1221 { 1222 if($fld->name == $myId) { 1223 $found = true; 1224 break; 1225 } 1226 $index++; 1227 } 1228 if(!$found) { 1229 $db->outp_throw("Unable to locate key $myId for $class in GetActiveRecordsClass()",'GetActiveRecordsClass'); 1230 } 1231 1232 $qry = "select * from ".$table; 1233 if(ADODB_JOIN_AR == $extra['loading']) { 1234 if(!empty($relations['belongsTo'])) { 1235 foreach($relations['belongsTo'] as $foreignTable) { 1236 if(($k = reset($foreignTable->TableInfo()->keys))) { 1237 $belongsToId = $k; 1238 } 1239 else { 1240 $belongsToId = 'id'; 1241 } 1242 1243 $qry .= ' LEFT JOIN '.$foreignTable->_table.' ON '. 1244 $table.'.'.$foreignTable->foreignKey.'='. 1245 $foreignTable->_table.'.'.$belongsToId; 1246 } 1247 } 1248 if(!empty($relations['hasMany'])) { 1249 if(empty($relations['foreignName'])) { 1250 $db->outp_throw("Missing foreignName is relation specification in GetActiveRecordsClass()",'GetActiveRecordsClass'); 1251 } 1252 if(($k = reset($tableInfo->keys))) { 1253 $hasManyId = $k; 1254 } 1255 else { 1256 $hasManyId = 'id'; 1257 } 1258 1259 foreach($relations['hasMany'] as $foreignTable) { 1260 $qry .= ' LEFT JOIN '.$foreignTable->_table.' ON '. 1261 $table.'.'.$hasManyId.'='. 1262 $foreignTable->_table.'.'.$foreignTable->foreignKey; 1263 } 1264 } 1265 } 1266 if (!empty($whereOrderBy)) { 1267 $qry .= ' WHERE '.$whereOrderBy; 1268 } 1269 if(isset($extra['limit'])) { 1270 $rows = false; 1271 if(isset($extra['offset'])) { 1272 $rs = $db->SelectLimit($qry, $extra['limit'], $extra['offset']); 1273 } else { 1274 $rs = $db->SelectLimit($qry, $extra['limit']); 1275 } 1276 if ($rs) { 1277 while (!$rs->EOF) { 1278 $rows[] = $rs->fields; 1279 $rs->MoveNext(); 1280 } 1281 } 1282 } else 1283 $rows = $db->GetAll($qry,$bindarr); 1284 1285 $db->SetFetchMode($save); 1286 1287 $false = false; 1288 1289 if ($rows === false) { 1290 return $false; 1291 } 1292 1293 1294 if (!isset($_ADODB_ACTIVE_DBS)) { 1295 include_once (ADODB_DIR.'/adodb-active-record.inc.php'); 1296 } 1297 if (!class_exists($class)) { 1298 $db->outp_throw("Unknown class $class in GetActiveRecordsClass()",'GetActiveRecordsClass'); 1299 return $false; 1300 } 1301 $uniqArr = array(); // CFR Keep track of records for relations 1302 $arr = array(); 1303 // arrRef will be the structure that knows about our objects. 1304 // It is an associative array. 1305 // We will, however, return arr, preserving regular 0.. order so that 1306 // obj[0] can be used by app developers. 1307 $arrRef = array(); 1308 $bTos = array(); // Will store belongTo's indices if any 1309 foreach($rows as $row) { 1310 1311 $obj = new $class($table,$primkeyArr,$db); 1312 if ($obj->ErrorNo()){ 1313 $db->_errorMsg = $obj->ErrorMsg(); 1314 return $false; 1315 } 1316 $obj->Set($row); 1317 // CFR: FIXME: Insane assumption here: 1318 // If the first column returned is an integer, then it's a 'id' field 1319 // And to make things a bit worse, I use intval() rather than is_int() because, in fact, 1320 // $row[0] is not an integer. 1321 // 1322 // So, what does this whole block do? 1323 // When relationships are found, we perform JOINs. This is fast. But not accurate: 1324 // instead of returning n objects with their n' associated cousins, 1325 // we get n*n' objects. This code fixes this. 1326 // Note: to-many relationships mess around with the 'limit' parameter 1327 $rowId = intval($row[$index]); 1328 1329 if(ADODB_WORK_AR == $extra['loading']) { 1330 $arrRef[$rowId] = $obj; 1331 $arr[] = &$arrRef[$rowId]; 1332 if(!isset($indices)) { 1333 $indices = $rowId; 1334 } 1335 else { 1336 $indices .= ','.$rowId; 1337 } 1338 if(!empty($relations['belongsTo'])) { 1339 foreach($relations['belongsTo'] as $foreignTable) { 1340 $foreignTableRef = $foreignTable->foreignKey; 1341 // First array: list of foreign ids we are looking for 1342 if(empty($bTos[$foreignTableRef])) { 1343 $bTos[$foreignTableRef] = array(); 1344 } 1345 // Second array: list of ids found 1346 if(empty($obj->$foreignTableRef)) { 1347 continue; 1348 } 1349 if(empty($bTos[$foreignTableRef][$obj->$foreignTableRef])) { 1350 $bTos[$foreignTableRef][$obj->$foreignTableRef] = array(); 1351 } 1352 $bTos[$foreignTableRef][$obj->$foreignTableRef][] = $obj; 1353 } 1354 } 1355 continue; 1356 } 1357 1358 if($rowId>0) { 1359 if(ADODB_JOIN_AR == $extra['loading']) { 1360 $isNewObj = !isset($uniqArr['_'.$row[0]]); 1361 if($isNewObj) { 1362 $uniqArr['_'.$row[0]] = $obj; 1363 } 1364 1365 // TODO Copy/paste code below: bad! 1366 if(!empty($relations['hasMany'])) { 1367 foreach($relations['hasMany'] as $foreignTable) { 1368 $foreignName = $foreignTable->foreignName; 1369 if(!empty($obj->$foreignName)) { 1370 $masterObj = &$uniqArr['_'.$row[0]]; 1371 // Assumption: this property exists in every object since they are instances of the same class 1372 if(!is_array($masterObj->$foreignName)) { 1373 // Pluck! 1374 $foreignObj = $masterObj->$foreignName; 1375 $masterObj->$foreignName = array(clone($foreignObj)); 1376 } 1377 else { 1378 // Pluck pluck! 1379 $foreignObj = $obj->$foreignName; 1380 array_push($masterObj->$foreignName, clone($foreignObj)); 1381 } 1382 } 1383 } 1384 } 1385 if(!empty($relations['belongsTo'])) { 1386 foreach($relations['belongsTo'] as $foreignTable) { 1387 $foreignName = $foreignTable->foreignName; 1388 if(!empty($obj->$foreignName)) { 1389 $masterObj = &$uniqArr['_'.$row[0]]; 1390 // Assumption: this property exists in every object since they are instances of the same class 1391 if(!is_array($masterObj->$foreignName)) { 1392 // Pluck! 1393 $foreignObj = $masterObj->$foreignName; 1394 $masterObj->$foreignName = array(clone($foreignObj)); 1395 } 1396 else { 1397 // Pluck pluck! 1398 $foreignObj = $obj->$foreignName; 1399 array_push($masterObj->$foreignName, clone($foreignObj)); 1400 } 1401 } 1402 } 1403 } 1404 if(!$isNewObj) { 1405 unset($obj); // We do not need this object itself anymore and do not want it re-added to the main array 1406 } 1407 } 1408 else if(ADODB_LAZY_AR == $extra['loading']) { 1409 // Lazy loading: we need to give AdoDb a hint that we have not really loaded 1410 // anything, all the while keeping enough information on what we wish to load. 1411 // Let's do this by keeping the relevant info in our relationship arrays 1412 // but get rid of the actual properties. 1413 // We will then use PHP's __get to load these properties on-demand. 1414 if(!empty($relations['hasMany'])) { 1415 foreach($relations['hasMany'] as $foreignTable) { 1416 $foreignName = $foreignTable->foreignName; 1417 if(!empty($obj->$foreignName)) { 1418 unset($obj->$foreignName); 1419 } 1420 } 1421 } 1422 if(!empty($relations['belongsTo'])) { 1423 foreach($relations['belongsTo'] as $foreignTable) { 1424 $foreignName = $foreignTable->foreignName; 1425 if(!empty($obj->$foreignName)) { 1426 unset($obj->$foreignName); 1427 } 1428 } 1429 } 1430 } 1431 } 1432 1433 if(isset($obj)) { 1434 $arr[] = $obj; 1435 } 1436 } 1437 1438 if(ADODB_WORK_AR == $extra['loading']) { 1439 // The best of both worlds? 1440 // Here, the number of queries is constant: 1 + n*relationship. 1441 // The second query will allow us to perform a good join 1442 // while preserving LIMIT etc. 1443 if(!empty($relations['hasMany'])) { 1444 foreach($relations['hasMany'] as $foreignTable) { 1445 $foreignName = $foreignTable->foreignName; 1446 $className = ucfirst($foreignTable->_singularize($foreignName)); 1447 $obj = new $className(); 1448 $dbClassRef = $foreignTable->foreignKey; 1449 $objs = $obj->packageFind($dbClassRef.' IN ('.$indices.')'); 1450 foreach($objs as $obj) { 1451 if(!is_array($arrRef[$obj->$dbClassRef]->$foreignName)) { 1452 $arrRef[$obj->$dbClassRef]->$foreignName = array(); 1453 } 1454 array_push($arrRef[$obj->$dbClassRef]->$foreignName, $obj); 1455 } 1456 } 1457 1458 } 1459 if(!empty($relations['belongsTo'])) { 1460 foreach($relations['belongsTo'] as $foreignTable) { 1461 $foreignTableRef = $foreignTable->foreignKey; 1462 if(empty($bTos[$foreignTableRef])) { 1463 continue; 1464 } 1465 if(($k = reset($foreignTable->TableInfo()->keys))) { 1466 $belongsToId = $k; 1467 } 1468 else { 1469 $belongsToId = 'id'; 1470 } 1471 $origObjsArr = $bTos[$foreignTableRef]; 1472 $bTosString = implode(',', array_keys($bTos[$foreignTableRef])); 1473 $foreignName = $foreignTable->foreignName; 1474 $className = ucfirst($foreignTable->_singularize($foreignName)); 1475 $obj = new $className(); 1476 $objs = $obj->packageFind($belongsToId.' IN ('.$bTosString.')'); 1477 foreach($objs as $obj) 1478 { 1479 foreach($origObjsArr[$obj->$belongsToId] as $idx=>$origObj) 1480 { 1481 $origObj->$foreignName = $obj; 1482 } 1483 } 1484 } 1485 } 1486 } 1487 1488 return $arr; 1489 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body