Automatically generated installer lang files
[moodle.git] / lib / adodb / adodb-active-recordx.inc.php
blob5de5b13096fc026be96beabc6e505d95e0c1257f
1 <?php
2 /**
3 * Active Record implementation. Superset of Zend Framework's.
5 * This is "Active Record eXtended" to support JOIN, WORK and LAZY mode
7 * This file is part of ADOdb, a Database Abstraction Layer library for PHP.
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
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
20 * @copyright 2000-2013 John Lim
21 * @copyright 2014 Damien Regad, Mark Newnham and the ADOdb community
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 ($d->db === $db) {
71 return $k;
75 $obj = new ADODB_Active_DB();
76 $obj->db = $db;
77 $obj->tables = array();
79 $_ADODB_ACTIVE_DBS[] = $obj;
81 return sizeof($_ADODB_ACTIVE_DBS)-1;
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
98 var $foreignName; // CFR: class name when in a relationship
100 static function UseDefaultValues($bool=null)
102 global $ADODB_ACTIVE_DEFVALS;
103 if (isset($bool)) {
104 $ADODB_ACTIVE_DEFVALS = $bool;
106 return $ADODB_ACTIVE_DEFVALS;
109 // should be static
110 static function SetDatabaseAdapter(&$db)
112 return ADODB_SetDatabaseAdapter($db);
116 public function __set($name, $value)
118 $name = str_replace(' ', '_', $name);
119 $this->$name = $value;
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.
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())
134 global $_ADODB_ACTIVE_DBS;
136 if ($db == false && is_object($pkeyarr)) {
137 $db = $pkeyarr;
138 $pkeyarr = false;
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);
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);
153 $this->_table = &$this->_pTable;
155 $this->foreignName = $this->_sTable; // CFR: default foreign name (singular)
157 if ($db) {
158 $this->_dbat = ADODB_Active_Record::SetDatabaseAdapter($db);
159 } else
160 $this->_dbat = sizeof($_ADODB_ACTIVE_DBS)-1;
163 if ($this->_dbat < 0) {
164 $this->Error(
165 "No database connection set; use ADOdb_Active_Record::SetDatabaseAdapter(\$db)",
166 'ADODB_Active_Record::__constructor'
170 $this->_tableat = $this->_table; # reserved for setting the assoc value to a non-table name, eg. the sql string in future
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();
185 function __wakeup()
187 $class = get_class($this);
188 new $class;
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',
200 static $IrregularS = array(
201 'PEOPLE' => 'PERSON',
202 'MEN' => 'man',
203 'WOMEN' => 'woman',
204 'CHILDREN' => 'child',
205 'KINE' => 'cow',
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,
219 function _pluralize($table)
221 if (!ADODB_Active_Record::$_changeNames) {
222 return $table;
224 $ut = strtoupper($table);
225 if(isset(self::$WeIsI[$ut])) {
226 return $table;
228 if(isset(self::$IrregularP[$ut])) {
229 return self::$IrregularP[$ut];
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';
245 default:
246 return $table.'s';
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)
255 if (!ADODB_Active_Record::$_changeNames) {
256 return $table;
258 $ut = strtoupper($table);
259 if(isset(self::$WeIsI[$ut])) {
260 return $table;
262 if(isset(self::$IrregularS[$ut])) {
263 return self::$IrregularS[$ut];
265 $len = strlen($table);
266 if($ut[$len-1] != 'S') {
267 return $table; // I know...forget oxen
269 if($ut[$len-2] != 'E') {
270 return substr($table, 0, $len-1);
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);
282 default:
283 return substr($table, 0, $len-1); // ?
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)
292 * this-table.id = other-table-#1.this-table_id
293 * = other-table-#2.this-table_id
295 function hasMany($foreignRef,$foreignKey=false)
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;
302 $table =& $this->TableInfo();
303 if(!isset($table->_hasMany[$foreignRef])) {
304 $table->_hasMany[$foreignRef] = $ar;
305 $table->updateColsCount();
307 # @todo Can I make this guy be lazy?
308 $this->$foreignRef = $table->_hasMany[$foreignRef]; // WATCHME Removed assignment by ref. to please __get()
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)
316 * this-table.other-table_id = other-table.id
318 function belongsTo($foreignRef,$foreignKey=false)
320 global $inflector;
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;
327 $table =& $this->TableInfo();
328 if(!isset($table->_belongsTo[$foreignRef])) {
329 $table->_belongsTo[$foreignRef] = $ar;
330 $table->updateColsCount();
332 $this->$foreignRef = $table->_belongsTo[$foreignRef];
336 * __get Access properties - used for lazy loading
338 * @param mixed $name
339 * @access protected
340 * @return void
342 function __get($name)
344 return $this->LoadRelations($name, '', -1. -1);
347 function LoadRelations($name, $whereOrderBy, $offset=-1, $limit=-1)
349 $extras = array();
350 if($offset >= 0) {
351 $extras['offset'] = $offset;
353 if($limit >= 0) {
354 $extras['limit'] = $limit;
356 $table =& $this->TableInfo();
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;
366 if(!empty($table->_belongsTo[$name])) {
367 $obj = $table->_belongsTo[$name];
368 $columnName = $obj->foreignKey;
369 if(empty($this->$columnName)) {
370 $this->$name = null;
372 else {
373 if(($k = reset($obj->TableInfo()->keys))) {
374 $belongsToId = $k;
376 else {
377 $belongsToId = 'id';
380 $arrayOfOne =
381 $obj->Find(
382 $belongsToId.'='.$this->$columnName.' '.$whereOrderBy, false, false, $extras);
383 $this->$name = $arrayOfOne[0];
385 return $this->$name;
387 if(!empty($table->_hasMany[$name])) {
388 $obj = $table->_hasMany[$name];
389 if(($k = reset($table->keys))) {
390 $hasManyId = $k;
392 else {
393 $hasManyId = 'id';
396 $this->$name =
397 $obj->Find(
398 $obj->foreignKey.'='.$this->$hasManyId.' '.$whereOrderBy, false, false, $extras);
399 return $this->$name;
402 //////////////////////////////////
404 // update metadata
405 function UpdateActiveTable($pkeys=false,$forceUpdate=false)
407 global $_ADODB_ACTIVE_DBS , $ADODB_CACHE_DIR, $ADODB_ACTIVE_CACHESECS;
408 global $ADODB_ACTIVE_DEFVALS, $ADODB_FETCH_MODE;
410 $activedb = $_ADODB_ACTIVE_DBS[$this->_dbat];
412 $table = $this->_table;
413 $tables = $activedb->tables;
414 $tableat = $this->_tableat;
415 if (!$forceUpdate && !empty($tables[$tableat])) {
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;
422 else {
423 $this->$name = null;
426 return;
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;
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");
447 $activetab = new ADODB_Active_Table();
448 $activetab->name = $table;
450 $save = $ADODB_FETCH_MODE;
451 $ADODB_FETCH_MODE = ADODB_FETCH_ASSOC;
452 if ($db->fetchMode !== false) {
453 $savem = $db->SetFetchMode(false);
456 $cols = $db->MetaColumns($table);
458 if (isset($savem)) {
459 $db->SetFetchMode($savem);
461 $ADODB_FETCH_MODE = $save;
463 if (!$cols) {
464 $this->Error("Invalid table name: $table",'UpdateActiveTable');
465 return false;
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;
476 } else {
477 $pkeys = $this->GetPrimaryKeys($db, $table);
480 if (empty($pkeys)) {
481 $this->Error("No primary key found for table $table",'UpdateActiveTable');
482 return false;
485 $attr = array();
486 $keys = array();
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;
495 else {
496 $this->$name = null;
498 $attr[$name] = $fldobj;
500 foreach($pkeys as $k => $name) {
501 $keys[strtolower($name)] = strtolower($name);
503 break;
505 case ADODB_ASSOC_CASE_UPPER:
506 foreach($cols as $name => $fldobj) {
507 $name = strtoupper($name);
509 if ($ADODB_ACTIVE_DEFVALS && isset($fldobj->default_value)) {
510 $this->$name = $fldobj->default_value;
512 else {
513 $this->$name = null;
515 $attr[$name] = $fldobj;
518 foreach($pkeys as $k => $name) {
519 $keys[strtoupper($name)] = strtoupper($name);
521 break;
522 default:
523 foreach($cols as $name => $fldobj) {
524 $name = ($fldobj->name);
526 if ($ADODB_ACTIVE_DEFVALS && isset($fldobj->default_value)) {
527 $this->$name = $fldobj->default_value;
529 else {
530 $this->$name = null;
532 $attr[$name] = $fldobj;
534 foreach($pkeys as $k => $name) {
535 $keys[$name] = $cols[$name]->name;
537 break;
540 $activetab->keys = $keys;
541 $activetab->flds = $attr;
542 $activetab->updateColsCount();
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');
550 adodb_write_file($fname,$s);
552 if (isset($activedb->tables[$table])) {
553 $oldtab = $activedb->tables[$table];
555 if ($oldtab) {
556 $activetab->_belongsTo = $oldtab->_belongsTo;
557 $activetab->_hasMany = $oldtab->_hasMany;
560 $activedb->tables[$table] = $activetab;
563 function GetPrimaryKeys(&$db, $table)
565 return $db->MetaPrimaryKeys($table);
568 // error handler for both PHP4+5.
569 function Error($err,$fn)
571 global $_ADODB_ACTIVE_DBS;
573 $fn = get_class($this).'::'.$fn;
574 $this->_lasterr = $fn.': '.$err;
576 if ($this->_dbat < 0) {
577 $db = false;
579 else {
580 $activedb = $_ADODB_ACTIVE_DBS[$this->_dbat];
581 $db = $activedb->db;
584 if (function_exists('adodb_throw')) {
585 if (!$db) {
586 adodb_throw('ADOdb_Active_Record', $fn, -1, $err, 0, 0, false);
588 else {
589 adodb_throw($db->databaseType, $fn, -1, $err, 0, 0, $db);
591 } else {
592 if (!$db || $db->debug) {
593 ADOConnection::outp($this->_lasterr);
599 // return last error message
600 function ErrorMsg()
602 if (!function_exists('adodb_throw')) {
603 if ($this->_dbat < 0) {
604 $db = false;
606 else {
607 $db = $this->DB();
610 // last error could be database error too
611 if ($db && $db->ErrorMsg()) {
612 return $db->ErrorMsg();
615 return $this->_lasterr;
618 function ErrorNo()
620 if ($this->_dbat < 0) {
621 return -9999; // no database connection...
623 $db = $this->DB();
625 return (int) $db->ErrorNo();
629 // retrieve ADOConnection from _ADODB_Active_DBs
630 function DB()
632 global $_ADODB_ACTIVE_DBS;
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;
639 $activedb = $_ADODB_ACTIVE_DBS[$this->_dbat];
640 $db = $activedb->db;
641 return $db;
644 // retrieve ADODB_Active_Table
645 function &TableInfo()
647 global $_ADODB_ACTIVE_DBS;
649 $activedb = $_ADODB_ACTIVE_DBS[$this->_dbat];
650 $table = $activedb->tables[$this->_tableat];
651 return $table;
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()
659 $db =& $this->DB();
660 if (!$db) {
661 return false;
663 $table =& $this->TableInfo();
664 $where = $this->GenWhere($db, $table);
665 return($this->Load($where));
669 // set a numeric array (using natural table field ordering) as object properties
670 function Set(&$row)
672 global $ACTIVE_RECORD_SAFETY;
674 $db = $this->DB();
676 if (!$row) {
677 $this->_saved = false;
678 return false;
681 $this->_saved = true;
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;
696 if ($bad_size) {
697 $this->Error("Table structure of $this->_table has changed","Load");
698 return false;
700 # </AP>
702 else {
703 $keys = array_keys($row);
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;
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;
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;
741 # </AP>
743 return true;
746 // get last inserted id for INSERT
747 function LastInsertID(&$db,$fieldname)
749 if ($db->hasInsertID) {
750 $val = $db->Insert_ID($this->_table,$fieldname);
752 else {
753 $val = false;
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);
760 return $val;
763 // quote data in where clause
764 function doquote(&$db, $val,$t)
766 switch($t) {
767 case 'D':
768 case 'T':
769 if (empty($val)) {
770 return 'null';
772 case 'C':
773 case 'X':
774 if (is_null($val)) {
775 return 'null';
777 if (strlen($val)>0 &&
778 (strncmp($val,"'",1) != 0 || substr($val,strlen($val)-1,1) != "'")
780 return $db->qstr($val);
781 break;
783 default:
784 return $val;
785 break;
789 // generate where clause for an UPDATE/SELECT
790 function GenWhere(&$db, &$table)
792 $keys = $table->keys;
793 $parr = array();
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));
801 return implode(' and ', $parr);
805 //------------------------------------------------------------ Public functions below
807 function Load($where=null,$bindarr=false)
809 $db = $this->DB();
810 if (!$db) {
811 return false;
813 $this->_where = $where;
815 $save = $db->SetFetchMode(ADODB_FETCH_NUM);
816 $qry = "select * from ".$this->_table;
817 $table =& $this->TableInfo();
819 if(($k = reset($table->keys))) {
820 $hasManyId = $k;
822 else {
823 $hasManyId = 'id';
826 foreach($table->_belongsTo as $foreignTable) {
827 if(($k = reset($foreignTable->TableInfo()->keys))) {
828 $belongsToId = $k;
830 else {
831 $belongsToId = 'id';
833 $qry .= ' LEFT JOIN '.$foreignTable->_table.' ON '.
834 $this->_table.'.'.$foreignTable->foreignKey.'='.
835 $foreignTable->_table.'.'.$belongsToId;
837 foreach($table->_hasMany as $foreignTable)
839 $qry .= ' LEFT JOIN '.$foreignTable->_table.' ON '.
840 $this->_table.'.'.$hasManyId.'='.
841 $foreignTable->_table.'.'.$foreignTable->foreignKey;
843 if($where) {
844 $qry .= ' WHERE '.$where;
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;
853 $db->SetFetchMode($save);
854 return $this->Set($row);
857 // More complex case when relations have to be collated
858 $rows = $db->GetAll($qry,$bindarr);
859 if(!$rows) {
860 return false;
862 $db->SetFetchMode($save);
863 if(count($rows) < 1) {
864 return false;
866 $class = get_class($this);
867 $isFirstRow = true;
869 if(($k = reset($this->TableInfo()->keys))) {
870 $myId = $k;
872 else {
873 $myId = 'id';
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;
883 $index++;
885 if(!$found) {
886 $this->outp_throw("Unable to locate key $myId for $class in Load()",'Load');
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;
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));
909 else {
910 $foreignObj = $obj->$foreignName;
911 array_push($this->$foreignName, clone($foreignObj));
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));
924 else {
925 $foreignObj = $obj->$foreignName;
926 array_push($this->$foreignName, clone($foreignObj));
933 return true;
936 // false on error
937 function Save()
939 if ($this->_saved) {
940 $ok = $this->Update();
942 else {
943 $ok = $this->Insert();
946 return $ok;
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()
953 $this->_saved = false;
956 // false on error
957 function Insert()
959 $db = $this->DB();
960 if (!$db) {
961 return false;
963 $cnt = 0;
964 $table = $this->TableInfo();
966 $valarr = array();
967 $names = array();
968 $valstr = array();
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;
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;
988 $sql = 'INSERT INTO '.$this->_table."(".implode(',',$names).') VALUES ('.implode(',',$valstr).')';
989 $ok = $db->Execute($sql,$valarr);
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;
1000 if ($autoinc && sizeof($table->keys) == 1) {
1001 $k = reset($table->keys);
1002 $this->$k = $this->LastInsertID($db,$k);
1006 $this->_original = $valarr;
1007 return !empty($ok);
1010 function Delete()
1012 $db = $this->DB();
1013 if (!$db) {
1014 return false;
1016 $table = $this->TableInfo();
1018 $where = $this->GenWhere($db,$table);
1019 $sql = 'DELETE FROM '.$this->_table.' WHERE '.$where;
1020 $ok = $db->Execute($sql);
1022 return $ok ? true : false;
1025 // returns an array of active record objects
1026 function Find($whereOrderBy,$bindarr=false,$pkeysArr=false,$extra=array())
1028 $db = $this->DB();
1029 if (!$db || empty($this->_table)) {
1030 return false;
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;
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())
1044 $db = $this->DB();
1045 if (!$db || empty($this->_table)) {
1046 return false;
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;
1054 // returns 0 on error, 1 on update, 2 on insert
1055 function Replace()
1057 $db = $this->DB();
1058 if (!$db) {
1059 return false;
1061 $table = $this->TableInfo();
1063 $pkey = $table->keys;
1065 foreach($table->flds as $name=>$fld) {
1066 $val = $this->$name;
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;
1073 else {
1074 $this->Error("Cannot update null into $name","Replace");
1075 return false;
1079 if (is_null($val) && !empty($fld->auto_increment)) {
1080 continue;
1082 $t = $db->MetaType($fld->type);
1083 $arr[$name] = $this->doquote($db,$val,$t);
1084 $valarr[] = $val;
1087 if (!is_array($pkey)) {
1088 $pkey = array($pkey);
1092 switch (ADODB_ASSOC_CASE) {
1093 case ADODB_ASSOC_CASE_LOWER:
1094 foreach($pkey as $k => $v) {
1095 $pkey[$k] = strtolower($v);
1097 break;
1098 case ADODB_ASSOC_CASE_UPPER:
1099 foreach($pkey as $k => $v) {
1100 $pkey[$k] = strtoupper($v);
1102 break;
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;
1116 if ($autoinc && sizeof($table->keys) == 1) {
1117 $k = reset($table->keys);
1118 $this->$k = $this->LastInsertID($db,$k);
1122 $this->_original = $valarr;
1124 return $ok;
1127 // returns 0 on error, 1 on update, -1 if no change in data (no update)
1128 function Update()
1130 $db = $this->DB();
1131 if (!$db) {
1132 return false;
1134 $table = $this->TableInfo();
1136 $where = $this->GenWhere($db, $table);
1138 if (!$where) {
1139 $this->error("Where missing for table $table", "Update");
1140 return false;
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;
1152 if (isset($table->keys[$name])) {
1153 continue;
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;
1161 else {
1162 $this->Error("Cannot set field $name to NULL","Update");
1163 return false;
1168 if (isset($this->_original[$i]) && $val === $this->_original[$i]) {
1169 continue;
1171 $valarr[] = $val;
1172 $pairs[] = $name.'='.$db->Param($cnt);
1173 $cnt += 1;
1177 if (!$cnt) {
1178 return -1;
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;
1186 return 0;
1189 function GetAttributeNames()
1191 $table = $this->TableInfo();
1192 if (!$table) {
1193 return false;
1195 return array_keys($table->flds);
1200 function adodb_GetActiveRecordsClass(&$db, $class, $tableObj,$whereOrderBy,$bindarr, $primkeyArr,
1201 $extra, $relations)
1203 global $_ADODB_ACTIVE_DBS;
1205 if (empty($extra['loading'])) {
1206 $extra['loading'] = ADODB_LAZY_AR;
1208 $save = $db->SetFetchMode(ADODB_FETCH_NUM);
1209 $table = &$tableObj->_table;
1210 $tableInfo =& $tableObj->TableInfo();
1211 if(($k = reset($tableInfo->keys))) {
1212 $myId = $k;
1214 else {
1215 $myId = 'id';
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)
1222 if($fld->name == $myId) {
1223 $found = true;
1224 break;
1226 $index++;
1228 if(!$found) {
1229 $db->outp_throw("Unable to locate key $myId for $class in GetActiveRecordsClass()",'GetActiveRecordsClass');
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;
1239 else {
1240 $belongsToId = 'id';
1243 $qry .= ' LEFT JOIN '.$foreignTable->_table.' ON '.
1244 $table.'.'.$foreignTable->foreignKey.'='.
1245 $foreignTable->_table.'.'.$belongsToId;
1248 if(!empty($relations['hasMany'])) {
1249 if(empty($relations['foreignName'])) {
1250 $db->outp_throw("Missing foreignName is relation specification in GetActiveRecordsClass()",'GetActiveRecordsClass');
1252 if(($k = reset($tableInfo->keys))) {
1253 $hasManyId = $k;
1255 else {
1256 $hasManyId = 'id';
1259 foreach($relations['hasMany'] as $foreignTable) {
1260 $qry .= ' LEFT JOIN '.$foreignTable->_table.' ON '.
1261 $table.'.'.$hasManyId.'='.
1262 $foreignTable->_table.'.'.$foreignTable->foreignKey;
1266 if (!empty($whereOrderBy)) {
1267 $qry .= ' WHERE '.$whereOrderBy;
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']);
1276 if ($rs) {
1277 while (!$rs->EOF) {
1278 $rows[] = $rs->fields;
1279 $rs->MoveNext();
1282 } else
1283 $rows = $db->GetAll($qry,$bindarr);
1285 $db->SetFetchMode($save);
1287 $false = false;
1289 if ($rows === false) {
1290 return $false;
1294 if (!isset($_ADODB_ACTIVE_DBS)) {
1295 include_once(ADODB_DIR.'/adodb-active-record.inc.php');
1297 if (!class_exists($class)) {
1298 $db->outp_throw("Unknown class $class in GetActiveRecordsClass()",'GetActiveRecordsClass');
1299 return $false;
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) {
1311 $obj = new $class($table,$primkeyArr,$db);
1312 if ($obj->ErrorNo()){
1313 $db->_errorMsg = $obj->ErrorMsg();
1314 return $false;
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.
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]);
1329 if(ADODB_WORK_AR == $extra['loading']) {
1330 $arrRef[$rowId] = $obj;
1331 $arr[] = &$arrRef[$rowId];
1332 if(!isset($indices)) {
1333 $indices = $rowId;
1335 else {
1336 $indices .= ','.$rowId;
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();
1345 // Second array: list of ids found
1346 if(empty($obj->$foreignTableRef)) {
1347 continue;
1349 if(empty($bTos[$foreignTableRef][$obj->$foreignTableRef])) {
1350 $bTos[$foreignTableRef][$obj->$foreignTableRef] = array();
1352 $bTos[$foreignTableRef][$obj->$foreignTableRef][] = $obj;
1355 continue;
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;
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));
1377 else {
1378 // Pluck pluck!
1379 $foreignObj = $obj->$foreignName;
1380 array_push($masterObj->$foreignName, clone($foreignObj));
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));
1396 else {
1397 // Pluck pluck!
1398 $foreignObj = $obj->$foreignName;
1399 array_push($masterObj->$foreignName, clone($foreignObj));
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
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);
1422 if(!empty($relations['belongsTo'])) {
1423 foreach($relations['belongsTo'] as $foreignTable) {
1424 $foreignName = $foreignTable->foreignName;
1425 if(!empty($obj->$foreignName)) {
1426 unset($obj->$foreignName);
1433 if(isset($obj)) {
1434 $arr[] = $obj;
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();
1454 array_push($arrRef[$obj->$dbClassRef]->$foreignName, $obj);
1459 if(!empty($relations['belongsTo'])) {
1460 foreach($relations['belongsTo'] as $foreignTable) {
1461 $foreignTableRef = $foreignTable->foreignKey;
1462 if(empty($bTos[$foreignTableRef])) {
1463 continue;
1465 if(($k = reset($foreignTable->TableInfo()->keys))) {
1466 $belongsToId = $k;
1468 else {
1469 $belongsToId = 'id';
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)
1479 foreach($origObjsArr[$obj->$belongsToId] as $idx=>$origObj)
1481 $origObj->$foreignName = $obj;
1488 return $arr;