Merge branch 'MDL-57742_master' of git://github.com/markn86/moodle
[moodle.git] / lib / adodb / adodb-active-recordx.inc.php
blob7f553f1e00a70e7f7de1c720798069aaa024b062
1 <?php
2 /*
4 @version v5.20.9 21-Dec-2016
5 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
6 @copyright (c) 2014 Damien Regad, Mark Newnham and the ADOdb community
7 Latest version is available at http://adodb.sourceforge.net
9 Released under both BSD license and Lesser GPL library license.
10 Whenever there is any discrepancy between the two licenses,
11 the BSD license will take precedence.
13 Active Record implementation. Superset of Zend Framework's.
15 This is "Active Record eXtended" to support JOIN, WORK and LAZY mode by Chris Ravenscroft chris#voilaweb.com
17 Version 0.9
19 See http://www-128.ibm.com/developerworks/java/library/j-cb03076/?ca=dgr-lnxw01ActiveRecord
20 for info on Ruby on Rails Active Record implementation
24 // CFR: Active Records Definitions
25 define('ADODB_JOIN_AR', 0x01);
26 define('ADODB_WORK_AR', 0x02);
27 define('ADODB_LAZY_AR', 0x03);
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.
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;
40 class ADODB_Active_DB {
41 var $db; // ADOConnection
42 var $tables; // assoc array of ADODB_Active_Table objects, indexed by tablename
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
54 function updateColsCount()
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);
64 // returns index into $_ADODB_ACTIVE_DBS
65 function ADODB_SetDatabaseAdapter(&$db)
67 global $_ADODB_ACTIVE_DBS;
69 foreach($_ADODB_ACTIVE_DBS as $k => $d) {
70 if (PHP_VERSION >= 5) {
71 if ($d->db === $db) {
72 return $k;
74 } else {
75 if ($d->db->_connectionID === $db->_connectionID && $db->database == $d->db->database) {
76 return $k;
81 $obj = new ADODB_Active_DB();
82 $obj->db = $db;
83 $obj->tables = array();
85 $_ADODB_ACTIVE_DBS[] = $obj;
87 return sizeof($_ADODB_ACTIVE_DBS)-1;
91 class ADODB_Active_Record {
92 static $_changeNames = true; // dynamically pluralize table names
93 static $_foreignSuffix = '_id'; //
94 var $_dbat; // associative index pointing to ADODB_Active_DB eg. $ADODB_Active_DBS[_dbat]
95 var $_table; // tablename, if set in class definition then use it as table name
96 var $_sTable; // singularized table name
97 var $_pTable; // pluralized table name
98 var $_tableat; // associative index pointing to ADODB_Active_Table, eg $ADODB_Active_DBS[_dbat]->tables[$this->_tableat]
99 var $_where; // where clause set in Load()
100 var $_saved = false; // indicates whether data is already inserted.
101 var $_lasterr = false; // last error message
102 var $_original = false; // the original values loaded or inserted, refreshed on update
104 var $foreignName; // CFR: class name when in a relationship
106 static function UseDefaultValues($bool=null)
108 global $ADODB_ACTIVE_DEFVALS;
109 if (isset($bool)) {
110 $ADODB_ACTIVE_DEFVALS = $bool;
112 return $ADODB_ACTIVE_DEFVALS;
115 // should be static
116 static function SetDatabaseAdapter(&$db)
118 return ADODB_SetDatabaseAdapter($db);
122 public function __set($name, $value)
124 $name = str_replace(' ', '_', $name);
125 $this->$name = $value;
128 // php5 constructor
129 // Note: if $table is defined, then we will use it as our table name
130 // Otherwise we will use our classname...
131 // In our database, table names are pluralized (because there can be
132 // more than one row!)
133 // Similarly, if $table is defined here, it has to be plural form.
135 // $options is an array that allows us to tweak the constructor's behaviour
136 // if $options['refresh'] is true, we re-scan our metadata information
137 // if $options['new'] is true, we forget all relations
138 function __construct($table = false, $pkeyarr=false, $db=false, $options=array())
140 global $ADODB_ASSOC_CASE,$_ADODB_ACTIVE_DBS;
142 if ($db == false && is_object($pkeyarr)) {
143 $db = $pkeyarr;
144 $pkeyarr = false;
147 if($table) {
148 // table argument exists. It is expected to be
149 // already plural form.
150 $this->_pTable = $table;
151 $this->_sTable = $this->_singularize($this->_pTable);
153 else {
154 // We will use current classname as table name.
155 // We need to pluralize it for the real table name.
156 $this->_sTable = strtolower(get_class($this));
157 $this->_pTable = $this->_pluralize($this->_sTable);
159 $this->_table = &$this->_pTable;
161 $this->foreignName = $this->_sTable; // CFR: default foreign name (singular)
163 if ($db) {
164 $this->_dbat = ADODB_Active_Record::SetDatabaseAdapter($db);
165 } else
166 $this->_dbat = sizeof($_ADODB_ACTIVE_DBS)-1;
169 if ($this->_dbat < 0) {
170 $this->Error(
171 "No database connection set; use ADOdb_Active_Record::SetDatabaseAdapter(\$db)",
172 'ADODB_Active_Record::__constructor'
176 $this->_tableat = $this->_table; # reserved for setting the assoc value to a non-table name, eg. the sql string in future
178 // CFR: Just added this option because UpdateActiveTable() can refresh its information
179 // but there was no way to ask it to do that.
180 $forceUpdate = (isset($options['refresh']) && true === $options['refresh']);
181 $this->UpdateActiveTable($pkeyarr, $forceUpdate);
182 if(isset($options['new']) && true === $options['new']) {
183 $table =& $this->TableInfo();
184 unset($table->_hasMany);
185 unset($table->_belongsTo);
186 $table->_hasMany = array();
187 $table->_belongsTo = array();
191 function __wakeup()
193 $class = get_class($this);
194 new $class;
197 // CFR: Constants found in Rails
198 static $IrregularP = array(
199 'PERSON' => 'people',
200 'MAN' => 'men',
201 'WOMAN' => 'women',
202 'CHILD' => 'children',
203 'COW' => 'kine',
206 static $IrregularS = array(
207 'PEOPLE' => 'PERSON',
208 'MEN' => 'man',
209 'WOMEN' => 'woman',
210 'CHILDREN' => 'child',
211 'KINE' => 'cow',
214 static $WeIsI = array(
215 'EQUIPMENT' => true,
216 'INFORMATION' => true,
217 'RICE' => true,
218 'MONEY' => true,
219 'SPECIES' => true,
220 'SERIES' => true,
221 'FISH' => true,
222 'SHEEP' => true,
225 function _pluralize($table)
227 if (!ADODB_Active_Record::$_changeNames) {
228 return $table;
230 $ut = strtoupper($table);
231 if(isset(self::$WeIsI[$ut])) {
232 return $table;
234 if(isset(self::$IrregularP[$ut])) {
235 return self::$IrregularP[$ut];
237 $len = strlen($table);
238 $lastc = $ut[$len-1];
239 $lastc2 = substr($ut,$len-2);
240 switch ($lastc) {
241 case 'S':
242 return $table.'es';
243 case 'Y':
244 return substr($table,0,$len-1).'ies';
245 case 'X':
246 return $table.'es';
247 case 'H':
248 if ($lastc2 == 'CH' || $lastc2 == 'SH') {
249 return $table.'es';
251 default:
252 return $table.'s';
256 // CFR Lamest singular inflector ever - @todo Make it real!
257 // Note: There is an assumption here...and it is that the argument's length >= 4
258 function _singularize($table)
261 if (!ADODB_Active_Record::$_changeNames) {
262 return $table;
264 $ut = strtoupper($table);
265 if(isset(self::$WeIsI[$ut])) {
266 return $table;
268 if(isset(self::$IrregularS[$ut])) {
269 return self::$IrregularS[$ut];
271 $len = strlen($table);
272 if($ut[$len-1] != 'S') {
273 return $table; // I know...forget oxen
275 if($ut[$len-2] != 'E') {
276 return substr($table, 0, $len-1);
278 switch($ut[$len-3]) {
279 case 'S':
280 case 'X':
281 return substr($table, 0, $len-2);
282 case 'I':
283 return substr($table, 0, $len-3) . 'y';
284 case 'H';
285 if($ut[$len-4] == 'C' || $ut[$len-4] == 'S') {
286 return substr($table, 0, $len-2);
288 default:
289 return substr($table, 0, $len-1); // ?
294 * ar->foreignName will contain the name of the tables associated with this table because
295 * these other tables' rows may also be referenced by this table using theirname_id or the provided
296 * foreign keys (this index name is stored in ar->foreignKey)
298 * this-table.id = other-table-#1.this-table_id
299 * = other-table-#2.this-table_id
301 function hasMany($foreignRef,$foreignKey=false)
303 $ar = new ADODB_Active_Record($foreignRef);
304 $ar->foreignName = $foreignRef;
305 $ar->UpdateActiveTable();
306 $ar->foreignKey = ($foreignKey) ? $foreignKey : strtolower(get_class($this)) . self::$_foreignSuffix;
308 $table =& $this->TableInfo();
309 if(!isset($table->_hasMany[$foreignRef])) {
310 $table->_hasMany[$foreignRef] = $ar;
311 $table->updateColsCount();
313 # @todo Can I make this guy be lazy?
314 $this->$foreignRef = $table->_hasMany[$foreignRef]; // WATCHME Removed assignment by ref. to please __get()
318 * ar->foreignName will contain the name of the tables associated with this table because
319 * this table's rows may also be referenced by those tables using thistable_id or the provided
320 * foreign keys (this index name is stored in ar->foreignKey)
322 * this-table.other-table_id = other-table.id
324 function belongsTo($foreignRef,$foreignKey=false)
326 global $inflector;
328 $ar = new ADODB_Active_Record($this->_pluralize($foreignRef));
329 $ar->foreignName = $foreignRef;
330 $ar->UpdateActiveTable();
331 $ar->foreignKey = ($foreignKey) ? $foreignKey : $ar->foreignName . self::$_foreignSuffix;
333 $table =& $this->TableInfo();
334 if(!isset($table->_belongsTo[$foreignRef])) {
335 $table->_belongsTo[$foreignRef] = $ar;
336 $table->updateColsCount();
338 $this->$foreignRef = $table->_belongsTo[$foreignRef];
342 * __get Access properties - used for lazy loading
344 * @param mixed $name
345 * @access protected
346 * @return void
348 function __get($name)
350 return $this->LoadRelations($name, '', -1. -1);
353 function LoadRelations($name, $whereOrderBy, $offset=-1, $limit=-1)
355 $extras = array();
356 if($offset >= 0) {
357 $extras['offset'] = $offset;
359 if($limit >= 0) {
360 $extras['limit'] = $limit;
362 $table =& $this->TableInfo();
364 if (strlen($whereOrderBy)) {
365 if (!preg_match('/^[ \n\r]*AND/i',$whereOrderBy)) {
366 if (!preg_match('/^[ \n\r]*ORDER[ \n\r]/i',$whereOrderBy)) {
367 $whereOrderBy = 'AND '.$whereOrderBy;
372 if(!empty($table->_belongsTo[$name])) {
373 $obj = $table->_belongsTo[$name];
374 $columnName = $obj->foreignKey;
375 if(empty($this->$columnName)) {
376 $this->$name = null;
378 else {
379 if(($k = reset($obj->TableInfo()->keys))) {
380 $belongsToId = $k;
382 else {
383 $belongsToId = 'id';
386 $arrayOfOne =
387 $obj->Find(
388 $belongsToId.'='.$this->$columnName.' '.$whereOrderBy, false, false, $extras);
389 $this->$name = $arrayOfOne[0];
391 return $this->$name;
393 if(!empty($table->_hasMany[$name])) {
394 $obj = $table->_hasMany[$name];
395 if(($k = reset($table->keys))) {
396 $hasManyId = $k;
398 else {
399 $hasManyId = 'id';
402 $this->$name =
403 $obj->Find(
404 $obj->foreignKey.'='.$this->$hasManyId.' '.$whereOrderBy, false, false, $extras);
405 return $this->$name;
408 //////////////////////////////////
410 // update metadata
411 function UpdateActiveTable($pkeys=false,$forceUpdate=false)
413 global $ADODB_ASSOC_CASE,$_ADODB_ACTIVE_DBS , $ADODB_CACHE_DIR, $ADODB_ACTIVE_CACHESECS;
414 global $ADODB_ACTIVE_DEFVALS, $ADODB_FETCH_MODE;
416 $activedb = $_ADODB_ACTIVE_DBS[$this->_dbat];
418 $table = $this->_table;
419 $tables = $activedb->tables;
420 $tableat = $this->_tableat;
421 if (!$forceUpdate && !empty($tables[$tableat])) {
423 $tobj = $tables[$tableat];
424 foreach($tobj->flds as $name => $fld) {
425 if ($ADODB_ACTIVE_DEFVALS && isset($fld->default_value)) {
426 $this->$name = $fld->default_value;
428 else {
429 $this->$name = null;
432 return;
435 $db = $activedb->db;
436 $fname = $ADODB_CACHE_DIR . '/adodb_' . $db->databaseType . '_active_'. $table . '.cache';
437 if (!$forceUpdate && $ADODB_ACTIVE_CACHESECS && $ADODB_CACHE_DIR && file_exists($fname)) {
438 $fp = fopen($fname,'r');
439 @flock($fp, LOCK_SH);
440 $acttab = unserialize(fread($fp,100000));
441 fclose($fp);
442 if ($acttab->_created + $ADODB_ACTIVE_CACHESECS - (abs(rand()) % 16) > time()) {
443 // abs(rand()) randomizes deletion, reducing contention to delete/refresh file
444 // ideally, you should cache at least 32 secs
445 $activedb->tables[$table] = $acttab;
447 //if ($db->debug) ADOConnection::outp("Reading cached active record file: $fname");
448 return;
449 } else if ($db->debug) {
450 ADOConnection::outp("Refreshing cached active record file: $fname");
453 $activetab = new ADODB_Active_Table();
454 $activetab->name = $table;
456 $save = $ADODB_FETCH_MODE;
457 $ADODB_FETCH_MODE = ADODB_FETCH_ASSOC;
458 if ($db->fetchMode !== false) {
459 $savem = $db->SetFetchMode(false);
462 $cols = $db->MetaColumns($table);
464 if (isset($savem)) {
465 $db->SetFetchMode($savem);
467 $ADODB_FETCH_MODE = $save;
469 if (!$cols) {
470 $this->Error("Invalid table name: $table",'UpdateActiveTable');
471 return false;
473 $fld = reset($cols);
474 if (!$pkeys) {
475 if (isset($fld->primary_key)) {
476 $pkeys = array();
477 foreach($cols as $name => $fld) {
478 if (!empty($fld->primary_key)) {
479 $pkeys[] = $name;
482 } else {
483 $pkeys = $this->GetPrimaryKeys($db, $table);
486 if (empty($pkeys)) {
487 $this->Error("No primary key found for table $table",'UpdateActiveTable');
488 return false;
491 $attr = array();
492 $keys = array();
494 switch($ADODB_ASSOC_CASE) {
495 case 0:
496 foreach($cols as $name => $fldobj) {
497 $name = strtolower($name);
498 if ($ADODB_ACTIVE_DEFVALS && isset($fldobj->default_value)) {
499 $this->$name = $fldobj->default_value;
501 else {
502 $this->$name = null;
504 $attr[$name] = $fldobj;
506 foreach($pkeys as $k => $name) {
507 $keys[strtolower($name)] = strtolower($name);
509 break;
511 case 1:
512 foreach($cols as $name => $fldobj) {
513 $name = strtoupper($name);
515 if ($ADODB_ACTIVE_DEFVALS && isset($fldobj->default_value)) {
516 $this->$name = $fldobj->default_value;
518 else {
519 $this->$name = null;
521 $attr[$name] = $fldobj;
524 foreach($pkeys as $k => $name) {
525 $keys[strtoupper($name)] = strtoupper($name);
527 break;
528 default:
529 foreach($cols as $name => $fldobj) {
530 $name = ($fldobj->name);
532 if ($ADODB_ACTIVE_DEFVALS && isset($fldobj->default_value)) {
533 $this->$name = $fldobj->default_value;
535 else {
536 $this->$name = null;
538 $attr[$name] = $fldobj;
540 foreach($pkeys as $k => $name) {
541 $keys[$name] = $cols[$name]->name;
543 break;
546 $activetab->keys = $keys;
547 $activetab->flds = $attr;
548 $activetab->updateColsCount();
550 if ($ADODB_ACTIVE_CACHESECS && $ADODB_CACHE_DIR) {
551 $activetab->_created = time();
552 $s = serialize($activetab);
553 if (!function_exists('adodb_write_file')) {
554 include(ADODB_DIR.'/adodb-csvlib.inc.php');
556 adodb_write_file($fname,$s);
558 if (isset($activedb->tables[$table])) {
559 $oldtab = $activedb->tables[$table];
561 if ($oldtab) {
562 $activetab->_belongsTo = $oldtab->_belongsTo;
563 $activetab->_hasMany = $oldtab->_hasMany;
566 $activedb->tables[$table] = $activetab;
569 function GetPrimaryKeys(&$db, $table)
571 return $db->MetaPrimaryKeys($table);
574 // error handler for both PHP4+5.
575 function Error($err,$fn)
577 global $_ADODB_ACTIVE_DBS;
579 $fn = get_class($this).'::'.$fn;
580 $this->_lasterr = $fn.': '.$err;
582 if ($this->_dbat < 0) {
583 $db = false;
585 else {
586 $activedb = $_ADODB_ACTIVE_DBS[$this->_dbat];
587 $db = $activedb->db;
590 if (function_exists('adodb_throw')) {
591 if (!$db) {
592 adodb_throw('ADOdb_Active_Record', $fn, -1, $err, 0, 0, false);
594 else {
595 adodb_throw($db->databaseType, $fn, -1, $err, 0, 0, $db);
597 } else {
598 if (!$db || $db->debug) {
599 ADOConnection::outp($this->_lasterr);
605 // return last error message
606 function ErrorMsg()
608 if (!function_exists('adodb_throw')) {
609 if ($this->_dbat < 0) {
610 $db = false;
612 else {
613 $db = $this->DB();
616 // last error could be database error too
617 if ($db && $db->ErrorMsg()) {
618 return $db->ErrorMsg();
621 return $this->_lasterr;
624 function ErrorNo()
626 if ($this->_dbat < 0) {
627 return -9999; // no database connection...
629 $db = $this->DB();
631 return (int) $db->ErrorNo();
635 // retrieve ADOConnection from _ADODB_Active_DBs
636 function DB()
638 global $_ADODB_ACTIVE_DBS;
640 if ($this->_dbat < 0) {
641 $false = false;
642 $this->Error("No database connection set: use ADOdb_Active_Record::SetDatabaseAdaptor(\$db)", "DB");
643 return $false;
645 $activedb = $_ADODB_ACTIVE_DBS[$this->_dbat];
646 $db = $activedb->db;
647 return $db;
650 // retrieve ADODB_Active_Table
651 function &TableInfo()
653 global $_ADODB_ACTIVE_DBS;
655 $activedb = $_ADODB_ACTIVE_DBS[$this->_dbat];
656 $table = $activedb->tables[$this->_tableat];
657 return $table;
661 // I have an ON INSERT trigger on a table that sets other columns in the table.
662 // So, I find that for myTable, I want to reload an active record after saving it. -- Malcolm Cook
663 function Reload()
665 $db =& $this->DB();
666 if (!$db) {
667 return false;
669 $table =& $this->TableInfo();
670 $where = $this->GenWhere($db, $table);
671 return($this->Load($where));
675 // set a numeric array (using natural table field ordering) as object properties
676 function Set(&$row)
678 global $ACTIVE_RECORD_SAFETY;
680 $db = $this->DB();
682 if (!$row) {
683 $this->_saved = false;
684 return false;
687 $this->_saved = true;
689 $table = $this->TableInfo();
690 $sizeofFlds = sizeof($table->flds);
691 $sizeofRow = sizeof($row);
692 if ($ACTIVE_RECORD_SAFETY && $table->_colsCount != $sizeofRow && $sizeofFlds != $sizeofRow) {
693 # <AP>
694 $bad_size = TRUE;
695 if($sizeofRow == 2 * $table->_colsCount || $sizeofRow == 2 * $sizeofFlds) {
696 // Only keep string keys
697 $keys = array_filter(array_keys($row), 'is_string');
698 if (sizeof($keys) == sizeof($table->flds)) {
699 $bad_size = FALSE;
702 if ($bad_size) {
703 $this->Error("Table structure of $this->_table has changed","Load");
704 return false;
706 # </AP>
708 else {
709 $keys = array_keys($row);
712 # <AP>
713 reset($keys);
714 $this->_original = array();
715 foreach($table->flds as $name=>$fld) {
716 $value = $row[current($keys)];
717 $this->$name = $value;
718 $this->_original[] = $value;
719 if(!next($keys)) {
720 break;
723 $table =& $this->TableInfo();
724 foreach($table->_belongsTo as $foreignTable) {
725 $ft = $foreignTable->TableInfo();
726 $propertyName = $ft->name;
727 foreach($ft->flds as $name=>$fld) {
728 $value = $row[current($keys)];
729 $foreignTable->$name = $value;
730 $foreignTable->_original[] = $value;
731 if(!next($keys)) {
732 break;
736 foreach($table->_hasMany as $foreignTable) {
737 $ft = $foreignTable->TableInfo();
738 foreach($ft->flds as $name=>$fld) {
739 $value = $row[current($keys)];
740 $foreignTable->$name = $value;
741 $foreignTable->_original[] = $value;
742 if(!next($keys)) {
743 break;
747 # </AP>
749 return true;
752 // get last inserted id for INSERT
753 function LastInsertID(&$db,$fieldname)
755 if ($db->hasInsertID) {
756 $val = $db->Insert_ID($this->_table,$fieldname);
758 else {
759 $val = false;
762 if (is_null($val) || $val === false) {
763 // this might not work reliably in multi-user environment
764 return $db->GetOne("select max(".$fieldname.") from ".$this->_table);
766 return $val;
769 // quote data in where clause
770 function doquote(&$db, $val,$t)
772 switch($t) {
773 case 'D':
774 case 'T':
775 if (empty($val)) {
776 return 'null';
778 case 'C':
779 case 'X':
780 if (is_null($val)) {
781 return 'null';
783 if (strlen($val)>0 &&
784 (strncmp($val,"'",1) != 0 || substr($val,strlen($val)-1,1) != "'")
786 return $db->qstr($val);
787 break;
789 default:
790 return $val;
791 break;
795 // generate where clause for an UPDATE/SELECT
796 function GenWhere(&$db, &$table)
798 $keys = $table->keys;
799 $parr = array();
801 foreach($keys as $k) {
802 $f = $table->flds[$k];
803 if ($f) {
804 $parr[] = $k.' = '.$this->doquote($db,$this->$k,$db->MetaType($f->type));
807 return implode(' and ', $parr);
811 //------------------------------------------------------------ Public functions below
813 function Load($where=null,$bindarr=false)
815 $db = $this->DB();
816 if (!$db) {
817 return false;
819 $this->_where = $where;
821 $save = $db->SetFetchMode(ADODB_FETCH_NUM);
822 $qry = "select * from ".$this->_table;
823 $table =& $this->TableInfo();
825 if(($k = reset($table->keys))) {
826 $hasManyId = $k;
828 else {
829 $hasManyId = 'id';
832 foreach($table->_belongsTo as $foreignTable) {
833 if(($k = reset($foreignTable->TableInfo()->keys))) {
834 $belongsToId = $k;
836 else {
837 $belongsToId = 'id';
839 $qry .= ' LEFT JOIN '.$foreignTable->_table.' ON '.
840 $this->_table.'.'.$foreignTable->foreignKey.'='.
841 $foreignTable->_table.'.'.$belongsToId;
843 foreach($table->_hasMany as $foreignTable)
845 $qry .= ' LEFT JOIN '.$foreignTable->_table.' ON '.
846 $this->_table.'.'.$hasManyId.'='.
847 $foreignTable->_table.'.'.$foreignTable->foreignKey;
849 if($where) {
850 $qry .= ' WHERE '.$where;
853 // Simple case: no relations. Load row and return.
854 if((count($table->_hasMany) + count($table->_belongsTo)) < 1) {
855 $row = $db->GetRow($qry,$bindarr);
856 if(!$row) {
857 return false;
859 $db->SetFetchMode($save);
860 return $this->Set($row);
863 // More complex case when relations have to be collated
864 $rows = $db->GetAll($qry,$bindarr);
865 if(!$rows) {
866 return false;
868 $db->SetFetchMode($save);
869 if(count($rows) < 1) {
870 return false;
872 $class = get_class($this);
873 $isFirstRow = true;
875 if(($k = reset($this->TableInfo()->keys))) {
876 $myId = $k;
878 else {
879 $myId = 'id';
881 $index = 0; $found = false;
882 /** @todo Improve by storing once and for all in table metadata */
883 /** @todo Also re-use info for hasManyId */
884 foreach($this->TableInfo()->flds as $fld) {
885 if($fld->name == $myId) {
886 $found = true;
887 break;
889 $index++;
891 if(!$found) {
892 $this->outp_throw("Unable to locate key $myId for $class in Load()",'Load');
895 foreach($rows as $row) {
896 $rowId = intval($row[$index]);
897 if($rowId > 0) {
898 if($isFirstRow) {
899 $isFirstRow = false;
900 if(!$this->Set($row)) {
901 return false;
904 $obj = new $class($table,false,$db);
905 $obj->Set($row);
906 // TODO Copy/paste code below: bad!
907 if(count($table->_hasMany) > 0) {
908 foreach($table->_hasMany as $foreignTable) {
909 $foreignName = $foreignTable->foreignName;
910 if(!empty($obj->$foreignName)) {
911 if(!is_array($this->$foreignName)) {
912 $foreignObj = $this->$foreignName;
913 $this->$foreignName = array(clone($foreignObj));
915 else {
916 $foreignObj = $obj->$foreignName;
917 array_push($this->$foreignName, clone($foreignObj));
922 if(count($table->_belongsTo) > 0) {
923 foreach($table->_belongsTo as $foreignTable) {
924 $foreignName = $foreignTable->foreignName;
925 if(!empty($obj->$foreignName)) {
926 if(!is_array($this->$foreignName)) {
927 $foreignObj = $this->$foreignName;
928 $this->$foreignName = array(clone($foreignObj));
930 else {
931 $foreignObj = $obj->$foreignName;
932 array_push($this->$foreignName, clone($foreignObj));
939 return true;
942 // false on error
943 function Save()
945 if ($this->_saved) {
946 $ok = $this->Update();
948 else {
949 $ok = $this->Insert();
952 return $ok;
955 // CFR: Sometimes we may wish to consider that an object is not to be replaced but inserted.
956 // Sample use case: an 'undo' command object (after a delete())
957 function Dirty()
959 $this->_saved = false;
962 // false on error
963 function Insert()
965 $db = $this->DB();
966 if (!$db) {
967 return false;
969 $cnt = 0;
970 $table = $this->TableInfo();
972 $valarr = array();
973 $names = array();
974 $valstr = array();
976 foreach($table->flds as $name=>$fld) {
977 $val = $this->$name;
978 if(!is_null($val) || !array_key_exists($name, $table->keys)) {
979 $valarr[] = $val;
980 $names[] = $name;
981 $valstr[] = $db->Param($cnt);
982 $cnt += 1;
986 if (empty($names)){
987 foreach($table->flds as $name=>$fld) {
988 $valarr[] = null;
989 $names[] = $name;
990 $valstr[] = $db->Param($cnt);
991 $cnt += 1;
994 $sql = 'INSERT INTO '.$this->_table."(".implode(',',$names).') VALUES ('.implode(',',$valstr).')';
995 $ok = $db->Execute($sql,$valarr);
997 if ($ok) {
998 $this->_saved = true;
999 $autoinc = false;
1000 foreach($table->keys as $k) {
1001 if (is_null($this->$k)) {
1002 $autoinc = true;
1003 break;
1006 if ($autoinc && sizeof($table->keys) == 1) {
1007 $k = reset($table->keys);
1008 $this->$k = $this->LastInsertID($db,$k);
1012 $this->_original = $valarr;
1013 return !empty($ok);
1016 function Delete()
1018 $db = $this->DB();
1019 if (!$db) {
1020 return false;
1022 $table = $this->TableInfo();
1024 $where = $this->GenWhere($db,$table);
1025 $sql = 'DELETE FROM '.$this->_table.' WHERE '.$where;
1026 $ok = $db->Execute($sql);
1028 return $ok ? true : false;
1031 // returns an array of active record objects
1032 function Find($whereOrderBy,$bindarr=false,$pkeysArr=false,$extra=array())
1034 $db = $this->DB();
1035 if (!$db || empty($this->_table)) {
1036 return false;
1038 $table =& $this->TableInfo();
1039 $arr = $db->GetActiveRecordsClass(get_class($this),$this, $whereOrderBy,$bindarr,$pkeysArr,$extra,
1040 array('foreignName'=>$this->foreignName, 'belongsTo'=>$table->_belongsTo, 'hasMany'=>$table->_hasMany));
1041 return $arr;
1044 // CFR: In introduced this method to ensure that inner workings are not disturbed by
1045 // subclasses...for instance when GetActiveRecordsClass invokes Find()
1046 // Why am I not invoking parent::Find?
1047 // Shockingly because I want to preserve PHP4 compatibility.
1048 function packageFind($whereOrderBy,$bindarr=false,$pkeysArr=false,$extra=array())
1050 $db = $this->DB();
1051 if (!$db || empty($this->_table)) {
1052 return false;
1054 $table =& $this->TableInfo();
1055 $arr = $db->GetActiveRecordsClass(get_class($this),$this, $whereOrderBy,$bindarr,$pkeysArr,$extra,
1056 array('foreignName'=>$this->foreignName, 'belongsTo'=>$table->_belongsTo, 'hasMany'=>$table->_hasMany));
1057 return $arr;
1060 // returns 0 on error, 1 on update, 2 on insert
1061 function Replace()
1063 global $ADODB_ASSOC_CASE;
1065 $db = $this->DB();
1066 if (!$db) {
1067 return false;
1069 $table = $this->TableInfo();
1071 $pkey = $table->keys;
1073 foreach($table->flds as $name=>$fld) {
1074 $val = $this->$name;
1076 if (is_null($val)) {
1077 if (isset($fld->not_null) && $fld->not_null) {
1078 if (isset($fld->default_value) && strlen($fld->default_value)) {
1079 continue;
1081 else {
1082 $this->Error("Cannot update null into $name","Replace");
1083 return false;
1087 if (is_null($val) && !empty($fld->auto_increment)) {
1088 continue;
1090 $t = $db->MetaType($fld->type);
1091 $arr[$name] = $this->doquote($db,$val,$t);
1092 $valarr[] = $val;
1095 if (!is_array($pkey)) {
1096 $pkey = array($pkey);
1100 switch ($ADODB_ASSOC_CASE == 0) {
1101 case ADODB_ASSOC_CASE_LOWER:
1102 foreach($pkey as $k => $v) {
1103 $pkey[$k] = strtolower($v);
1105 break;
1106 case ADODB_ASSOC_CASE_UPPER:
1107 foreach($pkey as $k => $v) {
1108 $pkey[$k] = strtoupper($v);
1110 break;
1113 $ok = $db->Replace($this->_table,$arr,$pkey);
1114 if ($ok) {
1115 $this->_saved = true; // 1= update 2=insert
1116 if ($ok == 2) {
1117 $autoinc = false;
1118 foreach($table->keys as $k) {
1119 if (is_null($this->$k)) {
1120 $autoinc = true;
1121 break;
1124 if ($autoinc && sizeof($table->keys) == 1) {
1125 $k = reset($table->keys);
1126 $this->$k = $this->LastInsertID($db,$k);
1130 $this->_original = $valarr;
1132 return $ok;
1135 // returns 0 on error, 1 on update, -1 if no change in data (no update)
1136 function Update()
1138 $db = $this->DB();
1139 if (!$db) {
1140 return false;
1142 $table = $this->TableInfo();
1144 $where = $this->GenWhere($db, $table);
1146 if (!$where) {
1147 $this->error("Where missing for table $table", "Update");
1148 return false;
1150 $valarr = array();
1151 $neworig = array();
1152 $pairs = array();
1153 $i = -1;
1154 $cnt = 0;
1155 foreach($table->flds as $name=>$fld) {
1156 $i += 1;
1157 $val = $this->$name;
1158 $neworig[] = $val;
1160 if (isset($table->keys[$name])) {
1161 continue;
1164 if (is_null($val)) {
1165 if (isset($fld->not_null) && $fld->not_null) {
1166 if (isset($fld->default_value) && strlen($fld->default_value)) {
1167 continue;
1169 else {
1170 $this->Error("Cannot set field $name to NULL","Update");
1171 return false;
1176 if (isset($this->_original[$i]) && $val === $this->_original[$i]) {
1177 continue;
1179 $valarr[] = $val;
1180 $pairs[] = $name.'='.$db->Param($cnt);
1181 $cnt += 1;
1185 if (!$cnt) {
1186 return -1;
1188 $sql = 'UPDATE '.$this->_table." SET ".implode(",",$pairs)." WHERE ".$where;
1189 $ok = $db->Execute($sql,$valarr);
1190 if ($ok) {
1191 $this->_original = $neworig;
1192 return 1;
1194 return 0;
1197 function GetAttributeNames()
1199 $table = $this->TableInfo();
1200 if (!$table) {
1201 return false;
1203 return array_keys($table->flds);
1208 function adodb_GetActiveRecordsClass(&$db, $class, $tableObj,$whereOrderBy,$bindarr, $primkeyArr,
1209 $extra, $relations)
1211 global $_ADODB_ACTIVE_DBS;
1213 if (empty($extra['loading'])) {
1214 $extra['loading'] = ADODB_LAZY_AR;
1216 $save = $db->SetFetchMode(ADODB_FETCH_NUM);
1217 $table = &$tableObj->_table;
1218 $tableInfo =& $tableObj->TableInfo();
1219 if(($k = reset($tableInfo->keys))) {
1220 $myId = $k;
1222 else {
1223 $myId = 'id';
1225 $index = 0; $found = false;
1226 /** @todo Improve by storing once and for all in table metadata */
1227 /** @todo Also re-use info for hasManyId */
1228 foreach($tableInfo->flds as $fld)
1230 if($fld->name == $myId) {
1231 $found = true;
1232 break;
1234 $index++;
1236 if(!$found) {
1237 $db->outp_throw("Unable to locate key $myId for $class in GetActiveRecordsClass()",'GetActiveRecordsClass');
1240 $qry = "select * from ".$table;
1241 if(ADODB_JOIN_AR == $extra['loading']) {
1242 if(!empty($relations['belongsTo'])) {
1243 foreach($relations['belongsTo'] as $foreignTable) {
1244 if(($k = reset($foreignTable->TableInfo()->keys))) {
1245 $belongsToId = $k;
1247 else {
1248 $belongsToId = 'id';
1251 $qry .= ' LEFT JOIN '.$foreignTable->_table.' ON '.
1252 $table.'.'.$foreignTable->foreignKey.'='.
1253 $foreignTable->_table.'.'.$belongsToId;
1256 if(!empty($relations['hasMany'])) {
1257 if(empty($relations['foreignName'])) {
1258 $db->outp_throw("Missing foreignName is relation specification in GetActiveRecordsClass()",'GetActiveRecordsClass');
1260 if(($k = reset($tableInfo->keys))) {
1261 $hasManyId = $k;
1263 else {
1264 $hasManyId = 'id';
1267 foreach($relations['hasMany'] as $foreignTable) {
1268 $qry .= ' LEFT JOIN '.$foreignTable->_table.' ON '.
1269 $table.'.'.$hasManyId.'='.
1270 $foreignTable->_table.'.'.$foreignTable->foreignKey;
1274 if (!empty($whereOrderBy)) {
1275 $qry .= ' WHERE '.$whereOrderBy;
1277 if(isset($extra['limit'])) {
1278 $rows = false;
1279 if(isset($extra['offset'])) {
1280 $rs = $db->SelectLimit($qry, $extra['limit'], $extra['offset']);
1281 } else {
1282 $rs = $db->SelectLimit($qry, $extra['limit']);
1284 if ($rs) {
1285 while (!$rs->EOF) {
1286 $rows[] = $rs->fields;
1287 $rs->MoveNext();
1290 } else
1291 $rows = $db->GetAll($qry,$bindarr);
1293 $db->SetFetchMode($save);
1295 $false = false;
1297 if ($rows === false) {
1298 return $false;
1302 if (!isset($_ADODB_ACTIVE_DBS)) {
1303 include(ADODB_DIR.'/adodb-active-record.inc.php');
1305 if (!class_exists($class)) {
1306 $db->outp_throw("Unknown class $class in GetActiveRecordsClass()",'GetActiveRecordsClass');
1307 return $false;
1309 $uniqArr = array(); // CFR Keep track of records for relations
1310 $arr = array();
1311 // arrRef will be the structure that knows about our objects.
1312 // It is an associative array.
1313 // We will, however, return arr, preserving regular 0.. order so that
1314 // obj[0] can be used by app developpers.
1315 $arrRef = array();
1316 $bTos = array(); // Will store belongTo's indices if any
1317 foreach($rows as $row) {
1319 $obj = new $class($table,$primkeyArr,$db);
1320 if ($obj->ErrorNo()){
1321 $db->_errorMsg = $obj->ErrorMsg();
1322 return $false;
1324 $obj->Set($row);
1325 // CFR: FIXME: Insane assumption here:
1326 // If the first column returned is an integer, then it's a 'id' field
1327 // And to make things a bit worse, I use intval() rather than is_int() because, in fact,
1328 // $row[0] is not an integer.
1330 // So, what does this whole block do?
1331 // When relationships are found, we perform JOINs. This is fast. But not accurate:
1332 // instead of returning n objects with their n' associated cousins,
1333 // we get n*n' objects. This code fixes this.
1334 // Note: to-many relationships mess around with the 'limit' parameter
1335 $rowId = intval($row[$index]);
1337 if(ADODB_WORK_AR == $extra['loading']) {
1338 $arrRef[$rowId] = $obj;
1339 $arr[] = &$arrRef[$rowId];
1340 if(!isset($indices)) {
1341 $indices = $rowId;
1343 else {
1344 $indices .= ','.$rowId;
1346 if(!empty($relations['belongsTo'])) {
1347 foreach($relations['belongsTo'] as $foreignTable) {
1348 $foreignTableRef = $foreignTable->foreignKey;
1349 // First array: list of foreign ids we are looking for
1350 if(empty($bTos[$foreignTableRef])) {
1351 $bTos[$foreignTableRef] = array();
1353 // Second array: list of ids found
1354 if(empty($obj->$foreignTableRef)) {
1355 continue;
1357 if(empty($bTos[$foreignTableRef][$obj->$foreignTableRef])) {
1358 $bTos[$foreignTableRef][$obj->$foreignTableRef] = array();
1360 $bTos[$foreignTableRef][$obj->$foreignTableRef][] = $obj;
1363 continue;
1366 if($rowId>0) {
1367 if(ADODB_JOIN_AR == $extra['loading']) {
1368 $isNewObj = !isset($uniqArr['_'.$row[0]]);
1369 if($isNewObj) {
1370 $uniqArr['_'.$row[0]] = $obj;
1373 // TODO Copy/paste code below: bad!
1374 if(!empty($relations['hasMany'])) {
1375 foreach($relations['hasMany'] as $foreignTable) {
1376 $foreignName = $foreignTable->foreignName;
1377 if(!empty($obj->$foreignName)) {
1378 $masterObj = &$uniqArr['_'.$row[0]];
1379 // Assumption: this property exists in every object since they are instances of the same class
1380 if(!is_array($masterObj->$foreignName)) {
1381 // Pluck!
1382 $foreignObj = $masterObj->$foreignName;
1383 $masterObj->$foreignName = array(clone($foreignObj));
1385 else {
1386 // Pluck pluck!
1387 $foreignObj = $obj->$foreignName;
1388 array_push($masterObj->$foreignName, clone($foreignObj));
1393 if(!empty($relations['belongsTo'])) {
1394 foreach($relations['belongsTo'] as $foreignTable) {
1395 $foreignName = $foreignTable->foreignName;
1396 if(!empty($obj->$foreignName)) {
1397 $masterObj = &$uniqArr['_'.$row[0]];
1398 // Assumption: this property exists in every object since they are instances of the same class
1399 if(!is_array($masterObj->$foreignName)) {
1400 // Pluck!
1401 $foreignObj = $masterObj->$foreignName;
1402 $masterObj->$foreignName = array(clone($foreignObj));
1404 else {
1405 // Pluck pluck!
1406 $foreignObj = $obj->$foreignName;
1407 array_push($masterObj->$foreignName, clone($foreignObj));
1412 if(!$isNewObj) {
1413 unset($obj); // We do not need this object itself anymore and do not want it re-added to the main array
1416 else if(ADODB_LAZY_AR == $extra['loading']) {
1417 // Lazy loading: we need to give AdoDb a hint that we have not really loaded
1418 // anything, all the while keeping enough information on what we wish to load.
1419 // Let's do this by keeping the relevant info in our relationship arrays
1420 // but get rid of the actual properties.
1421 // We will then use PHP's __get to load these properties on-demand.
1422 if(!empty($relations['hasMany'])) {
1423 foreach($relations['hasMany'] as $foreignTable) {
1424 $foreignName = $foreignTable->foreignName;
1425 if(!empty($obj->$foreignName)) {
1426 unset($obj->$foreignName);
1430 if(!empty($relations['belongsTo'])) {
1431 foreach($relations['belongsTo'] as $foreignTable) {
1432 $foreignName = $foreignTable->foreignName;
1433 if(!empty($obj->$foreignName)) {
1434 unset($obj->$foreignName);
1441 if(isset($obj)) {
1442 $arr[] = $obj;
1446 if(ADODB_WORK_AR == $extra['loading']) {
1447 // The best of both worlds?
1448 // Here, the number of queries is constant: 1 + n*relationship.
1449 // The second query will allow us to perform a good join
1450 // while preserving LIMIT etc.
1451 if(!empty($relations['hasMany'])) {
1452 foreach($relations['hasMany'] as $foreignTable) {
1453 $foreignName = $foreignTable->foreignName;
1454 $className = ucfirst($foreignTable->_singularize($foreignName));
1455 $obj = new $className();
1456 $dbClassRef = $foreignTable->foreignKey;
1457 $objs = $obj->packageFind($dbClassRef.' IN ('.$indices.')');
1458 foreach($objs as $obj) {
1459 if(!is_array($arrRef[$obj->$dbClassRef]->$foreignName)) {
1460 $arrRef[$obj->$dbClassRef]->$foreignName = array();
1462 array_push($arrRef[$obj->$dbClassRef]->$foreignName, $obj);
1467 if(!empty($relations['belongsTo'])) {
1468 foreach($relations['belongsTo'] as $foreignTable) {
1469 $foreignTableRef = $foreignTable->foreignKey;
1470 if(empty($bTos[$foreignTableRef])) {
1471 continue;
1473 if(($k = reset($foreignTable->TableInfo()->keys))) {
1474 $belongsToId = $k;
1476 else {
1477 $belongsToId = 'id';
1479 $origObjsArr = $bTos[$foreignTableRef];
1480 $bTosString = implode(',', array_keys($bTos[$foreignTableRef]));
1481 $foreignName = $foreignTable->foreignName;
1482 $className = ucfirst($foreignTable->_singularize($foreignName));
1483 $obj = new $className();
1484 $objs = $obj->packageFind($belongsToId.' IN ('.$bTosString.')');
1485 foreach($objs as $obj)
1487 foreach($origObjsArr[$obj->$belongsToId] as $idx=>$origObj)
1489 $origObj->$foreignName = $obj;
1496 return $arr;