MDL-71669 editor_atto: Fire custom event when toggling button highlight
[moodle.git] / lib / adodb / adodb-active-recordx.inc.php
blob7598f1d6dbcd2488c098ebc4123e40835470dc09
1 <?php
2 /*
4 @version v5.20.16 12-Jan-2020
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.org/
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_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_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 ADODB_ASSOC_CASE_LOWER:
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 ADODB_ASSOC_CASE_UPPER:
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 $db = $this->DB();
1064 if (!$db) {
1065 return false;
1067 $table = $this->TableInfo();
1069 $pkey = $table->keys;
1071 foreach($table->flds as $name=>$fld) {
1072 $val = $this->$name;
1074 if (is_null($val)) {
1075 if (isset($fld->not_null) && $fld->not_null) {
1076 if (isset($fld->default_value) && strlen($fld->default_value)) {
1077 continue;
1079 else {
1080 $this->Error("Cannot update null into $name","Replace");
1081 return false;
1085 if (is_null($val) && !empty($fld->auto_increment)) {
1086 continue;
1088 $t = $db->MetaType($fld->type);
1089 $arr[$name] = $this->doquote($db,$val,$t);
1090 $valarr[] = $val;
1093 if (!is_array($pkey)) {
1094 $pkey = array($pkey);
1098 switch (ADODB_ASSOC_CASE) {
1099 case ADODB_ASSOC_CASE_LOWER:
1100 foreach($pkey as $k => $v) {
1101 $pkey[$k] = strtolower($v);
1103 break;
1104 case ADODB_ASSOC_CASE_UPPER:
1105 foreach($pkey as $k => $v) {
1106 $pkey[$k] = strtoupper($v);
1108 break;
1111 $ok = $db->Replace($this->_table,$arr,$pkey);
1112 if ($ok) {
1113 $this->_saved = true; // 1= update 2=insert
1114 if ($ok == 2) {
1115 $autoinc = false;
1116 foreach($table->keys as $k) {
1117 if (is_null($this->$k)) {
1118 $autoinc = true;
1119 break;
1122 if ($autoinc && sizeof($table->keys) == 1) {
1123 $k = reset($table->keys);
1124 $this->$k = $this->LastInsertID($db,$k);
1128 $this->_original = $valarr;
1130 return $ok;
1133 // returns 0 on error, 1 on update, -1 if no change in data (no update)
1134 function Update()
1136 $db = $this->DB();
1137 if (!$db) {
1138 return false;
1140 $table = $this->TableInfo();
1142 $where = $this->GenWhere($db, $table);
1144 if (!$where) {
1145 $this->error("Where missing for table $table", "Update");
1146 return false;
1148 $valarr = array();
1149 $neworig = array();
1150 $pairs = array();
1151 $i = -1;
1152 $cnt = 0;
1153 foreach($table->flds as $name=>$fld) {
1154 $i += 1;
1155 $val = $this->$name;
1156 $neworig[] = $val;
1158 if (isset($table->keys[$name])) {
1159 continue;
1162 if (is_null($val)) {
1163 if (isset($fld->not_null) && $fld->not_null) {
1164 if (isset($fld->default_value) && strlen($fld->default_value)) {
1165 continue;
1167 else {
1168 $this->Error("Cannot set field $name to NULL","Update");
1169 return false;
1174 if (isset($this->_original[$i]) && $val === $this->_original[$i]) {
1175 continue;
1177 $valarr[] = $val;
1178 $pairs[] = $name.'='.$db->Param($cnt);
1179 $cnt += 1;
1183 if (!$cnt) {
1184 return -1;
1186 $sql = 'UPDATE '.$this->_table." SET ".implode(",",$pairs)." WHERE ".$where;
1187 $ok = $db->Execute($sql,$valarr);
1188 if ($ok) {
1189 $this->_original = $neworig;
1190 return 1;
1192 return 0;
1195 function GetAttributeNames()
1197 $table = $this->TableInfo();
1198 if (!$table) {
1199 return false;
1201 return array_keys($table->flds);
1206 function adodb_GetActiveRecordsClass(&$db, $class, $tableObj,$whereOrderBy,$bindarr, $primkeyArr,
1207 $extra, $relations)
1209 global $_ADODB_ACTIVE_DBS;
1211 if (empty($extra['loading'])) {
1212 $extra['loading'] = ADODB_LAZY_AR;
1214 $save = $db->SetFetchMode(ADODB_FETCH_NUM);
1215 $table = &$tableObj->_table;
1216 $tableInfo =& $tableObj->TableInfo();
1217 if(($k = reset($tableInfo->keys))) {
1218 $myId = $k;
1220 else {
1221 $myId = 'id';
1223 $index = 0; $found = false;
1224 /** @todo Improve by storing once and for all in table metadata */
1225 /** @todo Also re-use info for hasManyId */
1226 foreach($tableInfo->flds as $fld)
1228 if($fld->name == $myId) {
1229 $found = true;
1230 break;
1232 $index++;
1234 if(!$found) {
1235 $db->outp_throw("Unable to locate key $myId for $class in GetActiveRecordsClass()",'GetActiveRecordsClass');
1238 $qry = "select * from ".$table;
1239 if(ADODB_JOIN_AR == $extra['loading']) {
1240 if(!empty($relations['belongsTo'])) {
1241 foreach($relations['belongsTo'] as $foreignTable) {
1242 if(($k = reset($foreignTable->TableInfo()->keys))) {
1243 $belongsToId = $k;
1245 else {
1246 $belongsToId = 'id';
1249 $qry .= ' LEFT JOIN '.$foreignTable->_table.' ON '.
1250 $table.'.'.$foreignTable->foreignKey.'='.
1251 $foreignTable->_table.'.'.$belongsToId;
1254 if(!empty($relations['hasMany'])) {
1255 if(empty($relations['foreignName'])) {
1256 $db->outp_throw("Missing foreignName is relation specification in GetActiveRecordsClass()",'GetActiveRecordsClass');
1258 if(($k = reset($tableInfo->keys))) {
1259 $hasManyId = $k;
1261 else {
1262 $hasManyId = 'id';
1265 foreach($relations['hasMany'] as $foreignTable) {
1266 $qry .= ' LEFT JOIN '.$foreignTable->_table.' ON '.
1267 $table.'.'.$hasManyId.'='.
1268 $foreignTable->_table.'.'.$foreignTable->foreignKey;
1272 if (!empty($whereOrderBy)) {
1273 $qry .= ' WHERE '.$whereOrderBy;
1275 if(isset($extra['limit'])) {
1276 $rows = false;
1277 if(isset($extra['offset'])) {
1278 $rs = $db->SelectLimit($qry, $extra['limit'], $extra['offset']);
1279 } else {
1280 $rs = $db->SelectLimit($qry, $extra['limit']);
1282 if ($rs) {
1283 while (!$rs->EOF) {
1284 $rows[] = $rs->fields;
1285 $rs->MoveNext();
1288 } else
1289 $rows = $db->GetAll($qry,$bindarr);
1291 $db->SetFetchMode($save);
1293 $false = false;
1295 if ($rows === false) {
1296 return $false;
1300 if (!isset($_ADODB_ACTIVE_DBS)) {
1301 include(ADODB_DIR.'/adodb-active-record.inc.php');
1303 if (!class_exists($class)) {
1304 $db->outp_throw("Unknown class $class in GetActiveRecordsClass()",'GetActiveRecordsClass');
1305 return $false;
1307 $uniqArr = array(); // CFR Keep track of records for relations
1308 $arr = array();
1309 // arrRef will be the structure that knows about our objects.
1310 // It is an associative array.
1311 // We will, however, return arr, preserving regular 0.. order so that
1312 // obj[0] can be used by app developpers.
1313 $arrRef = array();
1314 $bTos = array(); // Will store belongTo's indices if any
1315 foreach($rows as $row) {
1317 $obj = new $class($table,$primkeyArr,$db);
1318 if ($obj->ErrorNo()){
1319 $db->_errorMsg = $obj->ErrorMsg();
1320 return $false;
1322 $obj->Set($row);
1323 // CFR: FIXME: Insane assumption here:
1324 // If the first column returned is an integer, then it's a 'id' field
1325 // And to make things a bit worse, I use intval() rather than is_int() because, in fact,
1326 // $row[0] is not an integer.
1328 // So, what does this whole block do?
1329 // When relationships are found, we perform JOINs. This is fast. But not accurate:
1330 // instead of returning n objects with their n' associated cousins,
1331 // we get n*n' objects. This code fixes this.
1332 // Note: to-many relationships mess around with the 'limit' parameter
1333 $rowId = intval($row[$index]);
1335 if(ADODB_WORK_AR == $extra['loading']) {
1336 $arrRef[$rowId] = $obj;
1337 $arr[] = &$arrRef[$rowId];
1338 if(!isset($indices)) {
1339 $indices = $rowId;
1341 else {
1342 $indices .= ','.$rowId;
1344 if(!empty($relations['belongsTo'])) {
1345 foreach($relations['belongsTo'] as $foreignTable) {
1346 $foreignTableRef = $foreignTable->foreignKey;
1347 // First array: list of foreign ids we are looking for
1348 if(empty($bTos[$foreignTableRef])) {
1349 $bTos[$foreignTableRef] = array();
1351 // Second array: list of ids found
1352 if(empty($obj->$foreignTableRef)) {
1353 continue;
1355 if(empty($bTos[$foreignTableRef][$obj->$foreignTableRef])) {
1356 $bTos[$foreignTableRef][$obj->$foreignTableRef] = array();
1358 $bTos[$foreignTableRef][$obj->$foreignTableRef][] = $obj;
1361 continue;
1364 if($rowId>0) {
1365 if(ADODB_JOIN_AR == $extra['loading']) {
1366 $isNewObj = !isset($uniqArr['_'.$row[0]]);
1367 if($isNewObj) {
1368 $uniqArr['_'.$row[0]] = $obj;
1371 // TODO Copy/paste code below: bad!
1372 if(!empty($relations['hasMany'])) {
1373 foreach($relations['hasMany'] as $foreignTable) {
1374 $foreignName = $foreignTable->foreignName;
1375 if(!empty($obj->$foreignName)) {
1376 $masterObj = &$uniqArr['_'.$row[0]];
1377 // Assumption: this property exists in every object since they are instances of the same class
1378 if(!is_array($masterObj->$foreignName)) {
1379 // Pluck!
1380 $foreignObj = $masterObj->$foreignName;
1381 $masterObj->$foreignName = array(clone($foreignObj));
1383 else {
1384 // Pluck pluck!
1385 $foreignObj = $obj->$foreignName;
1386 array_push($masterObj->$foreignName, clone($foreignObj));
1391 if(!empty($relations['belongsTo'])) {
1392 foreach($relations['belongsTo'] as $foreignTable) {
1393 $foreignName = $foreignTable->foreignName;
1394 if(!empty($obj->$foreignName)) {
1395 $masterObj = &$uniqArr['_'.$row[0]];
1396 // Assumption: this property exists in every object since they are instances of the same class
1397 if(!is_array($masterObj->$foreignName)) {
1398 // Pluck!
1399 $foreignObj = $masterObj->$foreignName;
1400 $masterObj->$foreignName = array(clone($foreignObj));
1402 else {
1403 // Pluck pluck!
1404 $foreignObj = $obj->$foreignName;
1405 array_push($masterObj->$foreignName, clone($foreignObj));
1410 if(!$isNewObj) {
1411 unset($obj); // We do not need this object itself anymore and do not want it re-added to the main array
1414 else if(ADODB_LAZY_AR == $extra['loading']) {
1415 // Lazy loading: we need to give AdoDb a hint that we have not really loaded
1416 // anything, all the while keeping enough information on what we wish to load.
1417 // Let's do this by keeping the relevant info in our relationship arrays
1418 // but get rid of the actual properties.
1419 // We will then use PHP's __get to load these properties on-demand.
1420 if(!empty($relations['hasMany'])) {
1421 foreach($relations['hasMany'] as $foreignTable) {
1422 $foreignName = $foreignTable->foreignName;
1423 if(!empty($obj->$foreignName)) {
1424 unset($obj->$foreignName);
1428 if(!empty($relations['belongsTo'])) {
1429 foreach($relations['belongsTo'] as $foreignTable) {
1430 $foreignName = $foreignTable->foreignName;
1431 if(!empty($obj->$foreignName)) {
1432 unset($obj->$foreignName);
1439 if(isset($obj)) {
1440 $arr[] = $obj;
1444 if(ADODB_WORK_AR == $extra['loading']) {
1445 // The best of both worlds?
1446 // Here, the number of queries is constant: 1 + n*relationship.
1447 // The second query will allow us to perform a good join
1448 // while preserving LIMIT etc.
1449 if(!empty($relations['hasMany'])) {
1450 foreach($relations['hasMany'] as $foreignTable) {
1451 $foreignName = $foreignTable->foreignName;
1452 $className = ucfirst($foreignTable->_singularize($foreignName));
1453 $obj = new $className();
1454 $dbClassRef = $foreignTable->foreignKey;
1455 $objs = $obj->packageFind($dbClassRef.' IN ('.$indices.')');
1456 foreach($objs as $obj) {
1457 if(!is_array($arrRef[$obj->$dbClassRef]->$foreignName)) {
1458 $arrRef[$obj->$dbClassRef]->$foreignName = array();
1460 array_push($arrRef[$obj->$dbClassRef]->$foreignName, $obj);
1465 if(!empty($relations['belongsTo'])) {
1466 foreach($relations['belongsTo'] as $foreignTable) {
1467 $foreignTableRef = $foreignTable->foreignKey;
1468 if(empty($bTos[$foreignTableRef])) {
1469 continue;
1471 if(($k = reset($foreignTable->TableInfo()->keys))) {
1472 $belongsToId = $k;
1474 else {
1475 $belongsToId = 'id';
1477 $origObjsArr = $bTos[$foreignTableRef];
1478 $bTosString = implode(',', array_keys($bTos[$foreignTableRef]));
1479 $foreignName = $foreignTable->foreignName;
1480 $className = ucfirst($foreignTable->_singularize($foreignName));
1481 $obj = new $className();
1482 $objs = $obj->packageFind($belongsToId.' IN ('.$bTosString.')');
1483 foreach($objs as $obj)
1485 foreach($origObjsArr[$obj->$belongsToId] as $idx=>$origObj)
1487 $origObj->$foreignName = $obj;
1494 return $arr;