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