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.
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) {
75 $obj = new ADODB_Active_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;
104 $ADODB_ACTIVE_DEFVALS = $bool;
106 return $ADODB_ACTIVE_DEFVALS;
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;
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)) {
142 // table argument exists. It is expected to be
143 // already plural form.
144 $this->_pTable
= $table;
145 $this->_sTable
= $this->_singularize($this->_pTable
);
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)
158 $this->_dbat
= ADODB_Active_Record
::SetDatabaseAdapter($db);
160 $this->_dbat
= sizeof($_ADODB_ACTIVE_DBS)-1;
163 if ($this->_dbat
< 0) {
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();
187 $class = get_class($this);
191 // CFR: Constants found in Rails
192 static $IrregularP = array(
193 'PERSON' => 'people',
196 'CHILD' => 'children',
200 static $IrregularS = array(
201 'PEOPLE' => 'PERSON',
204 'CHILDREN' => 'child',
208 static $WeIsI = array(
210 'INFORMATION' => true,
219 function _pluralize($table)
221 if (!ADODB_Active_Record
::$_changeNames) {
224 $ut = strtoupper($table);
225 if(isset(self
::$WeIsI[$ut])) {
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);
238 return substr($table,0,$len-1).'ies';
242 if ($lastc2 == 'CH' ||
$lastc2 == 'SH') {
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) {
258 $ut = strtoupper($table);
259 if(isset(self
::$WeIsI[$ut])) {
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]) {
275 return substr($table, 0, $len-2);
277 return substr($table, 0, $len-3) . 'y';
279 if($ut[$len-4] == 'C' ||
$ut[$len-4] == 'S') {
280 return substr($table, 0, $len-2);
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)
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
342 function __get($name)
344 return $this->LoadRelations($name, '', -1. -1);
347 function LoadRelations($name, $whereOrderBy, $offset=-1, $limit=-1)
351 $extras['offset'] = $offset;
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)) {
373 if(($k = reset($obj->TableInfo()->keys
))) {
382 $belongsToId.'='.$this->$columnName.' '.$whereOrderBy, false, false, $extras);
383 $this->$name = $arrayOfOne[0];
387 if(!empty($table->_hasMany
[$name])) {
388 $obj = $table->_hasMany
[$name];
389 if(($k = reset($table->keys
))) {
398 $obj->foreignKey
.'='.$this->$hasManyId.' '.$whereOrderBy, false, false, $extras);
402 //////////////////////////////////
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
;
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));
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");
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);
459 $db->SetFetchMode($savem);
461 $ADODB_FETCH_MODE = $save;
464 $this->Error("Invalid table name: $table",'UpdateActiveTable');
469 if (isset($fld->primary_key
)) {
471 foreach($cols as $name => $fld) {
472 if (!empty($fld->primary_key
)) {
477 $pkeys = $this->GetPrimaryKeys($db, $table);
481 $this->Error("No primary key found for table $table",'UpdateActiveTable');
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
;
498 $attr[$name] = $fldobj;
500 foreach($pkeys as $k => $name) {
501 $keys[strtolower($name)] = strtolower($name);
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
;
515 $attr[$name] = $fldobj;
518 foreach($pkeys as $k => $name) {
519 $keys[strtoupper($name)] = strtoupper($name);
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
;
532 $attr[$name] = $fldobj;
534 foreach($pkeys as $k => $name) {
535 $keys[$name] = $cols[$name]->name
;
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];
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) {
580 $activedb = $_ADODB_ACTIVE_DBS[$this->_dbat
];
584 if (function_exists('adodb_throw')) {
586 adodb_throw('ADOdb_Active_Record', $fn, -1, $err, 0, 0, false);
589 adodb_throw($db->databaseType
, $fn, -1, $err, 0, 0, $db);
592 if (!$db ||
$db->debug
) {
593 ADOConnection
::outp($this->_lasterr
);
599 // return last error message
602 if (!function_exists('adodb_throw')) {
603 if ($this->_dbat
< 0) {
610 // last error could be database error too
611 if ($db && $db->ErrorMsg()) {
612 return $db->ErrorMsg();
615 return $this->_lasterr
;
620 if ($this->_dbat
< 0) {
621 return -9999; // no database connection...
625 return (int) $db->ErrorNo();
629 // retrieve ADOConnection from _ADODB_Active_DBs
632 global $_ADODB_ACTIVE_DBS;
634 if ($this->_dbat
< 0) {
636 $this->Error("No database connection set: use ADOdb_Active_Record::SetDatabaseAdaptor(\$db)", "DB");
639 $activedb = $_ADODB_ACTIVE_DBS[$this->_dbat
];
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
];
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
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
672 global $ACTIVE_RECORD_SAFETY;
677 $this->_saved
= 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) {
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
)) {
697 $this->Error("Table structure of $this->_table has changed","Load");
703 $keys = array_keys($row);
708 $this->_original
= array();
709 foreach($table->flds
as $name=>$fld) {
710 $value = $row[current($keys)];
711 $this->$name = $value;
712 $this->_original
[] = $value;
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;
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;
746 // get last inserted id for INSERT
747 function LastInsertID(&$db,$fieldname)
749 if ($db->hasInsertID
) {
750 $val = $db->Insert_ID($this->_table
,$fieldname);
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
);
763 // quote data in where clause
764 function doquote(&$db, $val,$t)
777 if (strlen($val)>0 &&
778 (strncmp($val,"'",1) != 0 ||
substr($val,strlen($val)-1,1) != "'")
780 return $db->qstr($val);
789 // generate where clause for an UPDATE/SELECT
790 function GenWhere(&$db, &$table)
792 $keys = $table->keys
;
795 foreach($keys as $k) {
796 $f = $table->flds
[$k];
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)
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
))) {
826 foreach($table->_belongsTo
as $foreignTable) {
827 if(($k = reset($foreignTable->TableInfo()->keys
))) {
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
;
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);
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);
862 $db->SetFetchMode($save);
863 if(count($rows) < 1) {
866 $class = get_class($this);
869 if(($k = reset($this->TableInfo()->keys
))) {
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) {
886 $this->outp_throw("Unable to locate key $myId for $class in Load()",'Load');
889 foreach($rows as $row) {
890 $rowId = intval($row[$index]);
894 if(!$this->Set($row)) {
898 $obj = new $class($table,false,$db);
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));
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));
925 $foreignObj = $obj->$foreignName;
926 array_push($this->$foreignName, clone($foreignObj));
940 $ok = $this->Update();
943 $ok = $this->Insert();
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())
953 $this->_saved
= false;
964 $table = $this->TableInfo();
970 foreach($table->flds
as $name=>$fld) {
972 if(!is_null($val) ||
!array_key_exists($name, $table->keys
)) {
975 $valstr[] = $db->Param($cnt);
981 foreach($table->flds
as $name=>$fld) {
984 $valstr[] = $db->Param($cnt);
988 $sql = 'INSERT INTO '.$this->_table
."(".implode(',',$names).') VALUES ('.implode(',',$valstr).')';
989 $ok = $db->Execute($sql,$valarr);
992 $this->_saved
= true;
994 foreach($table->keys
as $k) {
995 if (is_null($this->$k)) {
1000 if ($autoinc && sizeof($table->keys
) == 1) {
1001 $k = reset($table->keys
);
1002 $this->$k = $this->LastInsertID($db,$k);
1006 $this->_original
= $valarr;
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())
1029 if (!$db ||
empty($this->_table
)) {
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
));
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())
1045 if (!$db ||
empty($this->_table
)) {
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
));
1054 // returns 0 on error, 1 on update, 2 on insert
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)) {
1074 $this->Error("Cannot update null into $name","Replace");
1079 if (is_null($val) && !empty($fld->auto_increment
)) {
1082 $t = $db->MetaType($fld->type
);
1083 $arr[$name] = $this->doquote($db,$val,$t);
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);
1098 case ADODB_ASSOC_CASE_UPPER
:
1099 foreach($pkey as $k => $v) {
1100 $pkey[$k] = strtoupper($v);
1105 $ok = $db->Replace($this->_table
,$arr,$pkey);
1107 $this->_saved
= true; // 1= update 2=insert
1110 foreach($table->keys
as $k) {
1111 if (is_null($this->$k)) {
1116 if ($autoinc && sizeof($table->keys
) == 1) {
1117 $k = reset($table->keys
);
1118 $this->$k = $this->LastInsertID($db,$k);
1122 $this->_original
= $valarr;
1127 // returns 0 on error, 1 on update, -1 if no change in data (no update)
1134 $table = $this->TableInfo();
1136 $where = $this->GenWhere($db, $table);
1139 $this->error("Where missing for table $table", "Update");
1147 foreach($table->flds
as $name=>$fld) {
1149 $val = $this->$name;
1152 if (isset($table->keys
[$name])) {
1156 if (is_null($val)) {
1157 if (isset($fld->not_null
) && $fld->not_null
) {
1158 if (isset($fld->default_value
) && strlen($fld->default_value
)) {
1162 $this->Error("Cannot set field $name to NULL","Update");
1168 if (isset($this->_original
[$i]) && $val === $this->_original
[$i]) {
1172 $pairs[] = $name.'='.$db->Param($cnt);
1180 $sql = 'UPDATE '.$this->_table
." SET ".implode(",",$pairs)." WHERE ".$where;
1181 $ok = $db->Execute($sql,$valarr);
1183 $this->_original
= $neworig;
1189 function GetAttributeNames()
1191 $table = $this->TableInfo();
1195 return array_keys($table->flds
);
1200 function adodb_GetActiveRecordsClass(&$db, $class, $tableObj,$whereOrderBy,$bindarr, $primkeyArr,
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
))) {
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) {
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
))) {
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
))) {
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'])) {
1271 if(isset($extra['offset'])) {
1272 $rs = $db->SelectLimit($qry, $extra['limit'], $extra['offset']);
1274 $rs = $db->SelectLimit($qry, $extra['limit']);
1278 $rows[] = $rs->fields
;
1283 $rows = $db->GetAll($qry,$bindarr);
1285 $db->SetFetchMode($save);
1289 if ($rows === 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');
1301 $uniqArr = array(); // CFR Keep track of records for relations
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.
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();
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)) {
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)) {
1349 if(empty($bTos[$foreignTableRef][$obj->$foreignTableRef])) {
1350 $bTos[$foreignTableRef][$obj->$foreignTableRef] = array();
1352 $bTos[$foreignTableRef][$obj->$foreignTableRef][] = $obj;
1359 if(ADODB_JOIN_AR
== $extra['loading']) {
1360 $isNewObj = !isset($uniqArr['_'.$row[0]]);
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)) {
1374 $foreignObj = $masterObj->$foreignName;
1375 $masterObj->$foreignName = array(clone($foreignObj));
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)) {
1393 $foreignObj = $masterObj->$foreignName;
1394 $masterObj->$foreignName = array(clone($foreignObj));
1398 $foreignObj = $obj->$foreignName;
1399 array_push($masterObj->$foreignName, clone($foreignObj));
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);
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])) {
1465 if(($k = reset($foreignTable->TableInfo()->keys
))) {
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;