Source Viewer
Goto Line:
 
1 <?php
2 /**
3 * ActiveRecord Class Definition
4 *
5 * This file contains the parent class for any object that needs to access the database
6 * using the ActiveRecord design pattern.
7 * @author Ian White <ian_white@hms.harvard.edu>
8 * @copyright Copyright (c) 2008, Ian White
9 * @version 0.8
10 * @package ActiveRecordPackage
11 */
12  
13  
14 //include_once "ActiveDebugger.php";
15  
16 include_once "ActiveTable.php";
17 include_once "ActiveSet.php";
18 include_once "ActiveRelationshipSets.php";
19 include_once "ActiveExceptions.php";
20  
21 include_once "Database.php";
22
23 abstract class ActiveRecord implements Iterator, ArrayAccess, Countable {
24
25 /*
26 * record_cache manages references to all of the objects loaded by ActiveRecord or ActiveSet from
27 * the database so only one copy of a given object exists at a time.
28 *
29 * @access private
30 * @static
31 */
32 private static $record_cache = array();
33
34 /*
35 * set_cache holds references to every set in existence within this http request.
36 *
37 * @access private
38 * @static
39 */
40 private static $set_cache = array();
41
42
43 /*
44 * The singular array allows you to specify class names in this project
45 * that have a non-standard plural. It converts singular -> plural.
46 *
47 * @access public
48 * @static
49 */
50 public static $singular = array ( );
51
52
53 /*
54 * The plural array allows you to specify class names in this project
55 * that have a non-standard plural. It converts plural -> singular.
56 *
57 * @access public
58 * @static
59 */
60 public static $plural = array ( );
61
62 protected static $context = 'name';
63
64 //
65 // Instance Variables
66 //
67
68
69 // Data associated with the ActiveRecord
70 private $values;
71 private $update;
72 protected $table;
73
74 // Record Relationships
75 public static $alias = array();
76 public static $protected = array();
77 public static $private = array();
78
79 public static $has_many = array();
80 public static $has_and_belongs_to_many = array();
81 public static $has_one = array();
82
83 protected $relationships = array();
84 protected $relationships_processed;
85
86
87 protected $db;
88 private $needs_load = false;
89 private $new = true;
90 private $deleted = false;
91
92 private $iter = 0;
93 protected $attributes;
94  
95 // Cache of ActiveRelationshipSets
96 private $relationship_objects;
97  
98  
99 //
100 // Public Methods
101 //
102
103 //
104 // Active Record constructor.
105 // Has three optional arguments an
106 // $id - an integer which will cause the record to load itself
107 // from the database as the record with that id
108 // $table - either the string name of the table to use for the ActiveRecord,
109 // or a table object to use.
110 // $relationship - a boolean value that tells the ActiveRecord whether or not to
111 // process the relationships specified by the child class.
112 //
113
114 public final function __construct() {
115
116 $this->table = ActiveTable::get(get_class($this));
117
118 $this->db = new Database;
119
120 $this->relationships_processed = false;
121  
122 $this->values = array();
123 $this->update = array();
124
125 $this->init();
126  
127 }
128
129 // Called after the constructor to allow the user to perform any initialization they need.
130 public function init() { }
131
132
133 //
134 // Save Active Record to the database.
135 // If a record has been loaded from the database and changed, this can be use to write those
136 // changes back to the database. If the ActiveRecord has never been created, this will throw
137 // a NotSavedException. If the database fils to update the record it will throw a
138 // DatabaseException.
139 //
140
141 public final function save($attrs = NULL) {
142 // If this record has been deleted, it cannot be saved. If it can be saved, call
143 // the user's pre_save method.
144 if(!$this->deleted) {
145 $this->pre_save();
146 }
147 else {
148 return false;
149 }
150
151 // If this record has never been saved before, it must be inserted into the database
152 if($this->new) {
153
154 $column_names = $this->table->get_insert_columns_array($attrs);
155
156 $sql_values = "";
157 $sql_columns = "";
158
159 // For each column, find any updated values
160 foreach($column_names as $attr_name => $column_name) {
161 $value_name = $this->table->get_real_column($attr_name);
162 if( $this->update[$value_name] || $this->table->is_auto_updated($attr_name) ) {
163
164 $value = ($this->table->is_auto_updated($attr_name)) ? $this->table->{"create_{$attr_name}"}() : "'" . mysql_real_escape_string($this->values[$value_name]) . "'";
165 $sql_values .= $value . ",";
166 $sql_columns .= "$column_name,";
167 }
168
169 // If a column is required (NOT NULL) but has no value, throw an exception.
170 // This should be modified to see if the database has a default value for this column. ----
171 else if( !$this->table->is_null($attr_name) ) {
172 throw new ActiveRecordException("The column '$attr_name' in the table '{$this->table}' is required for insert.");
173 }
174
175 }
176
177 // Strip the final ',' char from each string
178 $sql_values = chop($sql_values, ",");
179 $sql_columns = chop($sql_columns, ",");
180
181 // Run the query
182 $query = "INSERT INTO `{$this->table}` ({$sql_columns}) VALUES ({$sql_values})";
183 $obj_id = $this->db->query($query);
184
185 // If the object was successfully inserted, its id will be returned. Setup the object
186 // to reflect its new saved state and add the object to the cache.
187 if($obj_id) {
188 $this->new = false;
189 $this->values['id'] = $obj_id;
190 $this->update = NULL;
191 $this->needs_load = false;
192 ActiveRecord::cacheRecord("{$this->table}", $obj_id, $this);
193 }
194 else {
195 $class = get_class($this);
196 throw new Exception("The '$class' object could not be inserted into the database. Query: {$query}");
197 }
198
199 return true;
200  
201 }
202
203 // If this record has been saved, it must be updated
204 else {
205
206 // Check to see if any updates have been made to this object. If not, return false.
207 if(count($this->update) == 0) return false;
208
209 // For each column that has been updated, add a clause to the sql query updating that
210 // field in the database
211
212
213
214 $columns = $this->table->get_columns_array($attrs);
215 $updates = "";
216 foreach ($columns as $attr => $column) {
217 $value_name = $this->table->get_real_column($attr);
218 //echo "checking {$value_name}... {$this->values[$value_name]}<br>";
219 if($this->update[$value_name]) {
220 if($this->values[$value_name] != NULL) {
221 $value = mysql_real_escape_string($this->values[$value_name]);
222 $updates .= "{$column} = '{$value}',";
223 }
224 else {
225 $updates .= "{$column} = NULL,";
226 }
227 }
228
229 // If a column is auto-updated, call its auto-update function for the proper
230 // auto-update trigger.
231 if($this->table->is_auto_updated($attr)) {
232 $updates .= $this->table->{"update_{$attr}"}();
233 }
234
235 }
236
237 // Clean the extra ',' from the end of the query.
238 $updates = chop($updates, ",");
239  
240 // Construct the query for the record with this record's id.
241
242
243 $query = "UPDATE `{$this->table}` SET $updates WHERE `{$this->table}`.`id`='{$this->values['id']}'";
244 //echo "$query <BR>";
245 // Notify any sets with references to this object that it has been altered and may no
246 // longer be a part of the set if the update was successful. Otherwise, throw an
247 // exception.
248 if($this->db->query($query)) {
249 $this->update = array();
250 }
251 else {
252 throw new DatabaseException("This ActiveRecord object where $where could not be saved in table '{$this->table}'.");
253 }
254
255 }
256
257 // Call the user's post_save method.
258 $this->post_save();
259
260 }
261
262 public final function set_save($attrs) {
263 if($this->deleted) {
264 return false;
265 }
266
267
268 $this->pre_save();
269
270 foreach($attrs as $attr) {
271
272 unset($this->update[$attr]);
273 }
274
275
276 $this->post_save();
277
278 }
279
280 public function pre_save() { }
281 public function post_save() { }
282  
283
284 [section removed]
285
286 //
287 // This is a magic method that allows access to the values of the Active Record.
288 // If the table in the database that this ActiveRecord belongs to has a column, it
289 // will be accessible as an instance variable of this class through the __get() method.
290 // This throws an ActiveRecordException if the column does not exist.
291 //
292
293 protected function attr_get($name) {
294
295 // Check to see if this record has been deleted.
296 if( $this->deleted ) {
297 throw new ActiveRecordException("This ActiveRecord has been deleted.");
298 }
299
300 $table_name = $this->table->get_real_column($name);
301 $table_name = ($table_name) ? $table_name : $name;
302
303
304 $method = method_exists($this, "_get_{$name}") ? "_get_{$name}" : NULL;
305
306 $return_value = NULL;
307
308 // Check to see if the variable requested is the name of a column in the table
309 // definition of this record.
310 $local_vars = get_object_vars($this);
311
312 if($local_vars[$name] ) {
313 $return_value = $this->$name;
314 if(!$method) return $return_value;
315 }
316 else if( $this->table->exists($name) ) {
317
318
319 // Check to see if this column was aliased to hold an object.
320 if( $type = $this->table->alias($name) ) {
321 // If so, get that object. use the column value as the object id, and the alias
322 // value as the object class.
323 if($this->values[$table_name]) {
324
325 $return_value = ActiveRecord::getRecord($type, $this->values[$table_name]);
326 }
327 else $return_value = false;
328 }
329 else {
330 // Otherwise, return the value of that attribute.
331
332 if(isset($this->values[$table_name])) {
333 $return_value = $this->values[$table_name];
334 }
335 else {
336 $return_value = NULL;
337 }
338 }
339
340 if(!$method) return $return_value;
341
342 }
343
344 // If not, check to see if it is the name of an object held as a foreign key by this
345 // object.
346 else if( $this->table->exists("{$name}_id") ) {
347 if(isset($this->values["{$table_name}_id"])) {
348 // Request it from the cache, and return it.
349 $return_value = ActiveRecord::getRecord($name, $this->values["{$table_name}_id"]);
350 }
351 else $return_value = NULL;
352
353 if(!$method) return $return_value;
354
355 }
356
357 // If not, check to see if this record has a relationship by the requested name.
358 else {
359
360 // Make sure that this record has been created in the database.
361 if(!$this->new) {
362 if(!$this->relationships_processed) $this->process_relationships();
363 if( isset($this->relationships[$name]) ) {
364 $return_value = $this->relationships[$name];
365
366 if(!$method) return $return_value;
367 }
368  
369 }
370  
371 }
372
373 if($method) return $this->$method($return_value);
374
375 throw new ActiveRecordException("Column '$name' does not exist in table '{$this->table}'");
376
377 }
378  
379 public function __get($name) {
380 if($this->table->is_private($name)) {
381 print_r(debug_backtrace(), true);
382 //throw new ActiveRecordException("Column '$name' does not exist in table '{$this->table}'");
383 }
384
385 return $this->attr_get($name);
386
387 }
388
389
390 [section removed]
391  
392
393 public final function get_type($attr) {
394 $attr = strtolower($attr);
395 if($type = $this->table->getAlias($attr)){
396 return $type;
397 }
398 else if($this->offsetExists($attr)) {
399 if($this->table->exists($attr)) {
400 return false;
401 }
402 else if($this->relationships[$attr]) {
403 return $attr;
404 }
405 else if($this->table->exists("{$attr}_id")) {
406 return $attr;
407 }
408 }
409
410 throw new Exception("The attribute '{$attr}' does not exist for the object {$this->table}.");
411
412 }
413
414
415
416
417 //
418 // Iterator Methods
419 //
420
421 public final function rewind() {
422 if(!$this->attributes) $this->attributes = ActiveRecord::attributes(get_class($this), true, true);
423
424 $this->iter = 0;
425 }
426
427 public final function current() {
428 if(!$this->attributes) $this->attributes = ActiveRecord::attributes(get_class($this), true, true);
429
430 // If we have already inspected the current element,
431 // it's id will be cached.
432 $field = $this->attributes[$this->iter];
433 return $this->$field;
434 }
435
436 public final function key() {
437 if(!$this->attributes) $this->attributes = ActiveRecord::attributes(get_class($this), true, true);
438
439 return $this->attributes[$this->iter];
440 }
441
442 public final function next() {
443 $this->iter++;
444 }
445
446 public final function valid() {
447 if(!$this->attributes) $this->attributes = ActiveRecord::attributes(get_class($this), true, true);
448
449 return ($this->attributes[$this->iter] != NULL);
450 }
451
452
453 //
454 // Countable Methods
455 //
456
457 public final function count() {
458 if(!$this->attributes) $this->attributes = ActiveRecord::attributes(get_class($this), true, true);
459
460 return count($this->attributes);
461 }
462
463
464 //
465 // ArrayAccess Methods
466 //
467
468 public final function offsetExists($index) {
469 if(!$this->attributes) $this->attributes = ActiveRecord::attributes(get_class($this), true, true);
470 return in_array(strtolower($index), $this->attributes);
471 }
472
473 public final function offsetGet($index) {
474 return $this->$index;
475 }
476
477 public final function offsetSet($index, $record) {
478 return ($this->$index = $record);
479 }
480
481 public final function offsetUnset($index) {
482 return ($this->$index = NULL);
483 }
484
485
486 //
487 // Protected Methods
488 //
489
490 //
491 // All auto-update variables need create_[varname]() and update_[varname]() methods.
492 //
493  
494
495 protected final function is_private($attr) {
496 $table_private = $this->table->get_private_array();
497 $class = get_class_vars(get_class($this));
498 if(in_array($attr, $class['private']) || $this->table->is_private($attr)) {
499 return true;
500 }
501 else {
502 return false;
503 }
504
505 }
506
507 protected final function is_protected($attr) {
508
509 $table_protected = $this->table->get_protected_array();
510 $class = get_class_vars(get_class($this));
511
512 if(in_array($attr, $class['protected']) || $this->table->is_protected($attr)) {
513 return true;
514 }
515 else {
516 return $this->is_private($attr);
517 }
518 }
519
520  
521 [section removed]
522
523  
524 //
525 // Static Methods
526 //
527
528
529 public static final function attributes($object, $include_immutable = false, $include_ids=false) {
530
531 $object = ucfirst($object);
532 $table = ActiveTable::get($object);
533
534 $reflect = new ReflectionClass($object);
535 $static_properties = $reflect->getStaticProperties();
536 $all_variables = $reflect->getProperties();
537 $all_methods = $reflect->getMethods();
538
539 $ar_reflect = new ReflectionClass("ActiveRecord");
540 $ar_variables = $ar_reflect->getProperties();
541
542
543
544 // Get public object attributes explosed as instance variables
545 $var_attrs = array();
546 foreach($all_variables as $var) {
547 if(!$var->isStatic()) {
548 $var_attrs[] = $var->getName();
549 }
550 }
551
552 $ar_vars = array();
553 foreach($ar_variables as $var) {
554 $ar_vars[] = $var->getName();
555 }
556
557 // Get Relationship attributes
558 $relationship_attrs = array_merge($static_properties['has_one'], $static_properties['has_many'], $static_properties['has_and_belongs_to_many'] );
559
560
561 $var_attrs = array_merge(array_diff($var_attrs, $ar_vars), $relationship_attrs);
562
563
564
565 // Get public object attributes exposed via _get_ and _set_ methods
566 $method_attrs = array();
567 foreach($all_methods as $method) {
568 if($method->isPublic() && !$method->isStatic() ) {
569 if($include_immutable) {
570 if(substr($method->getName(), 0, 5) == "_set_") {
571 $method_attrs[] = substr($method->getName(), 5);
572 }
573
574 }
575 if(substr($method->getName(), 0, 5) == "_get_") {
576 $method_attrs[] = substr($method->getName(), 5);
577 }
578 }
579 }
580
581 // Get attributes defined by the objects' table
582 $declared_table_attrs = array_keys($table->get_columns_array());
583
584
585
586 // Get undeclared table attributes
587 $table_attrs = array();
588 foreach($declared_table_attrs as $attr) {
589 if(substr($attr, -3) == '_id') {
590 if($include_ids) $table_attrs[] = $attr;
591 $table_attrs[] = substr($attr, 0, -3);
592 }
593 else if($attr == 'id' && !$include_ids) {
594 continue;
595 }
596 else {
597 $table_attrs[] = $attr;
598 }
599 }
600
601 $all_attrs = array_unique(array_merge($table_attrs, $method_attrs, $var_attrs));
602
603 $non_private_attrs = array_diff($all_attrs, $static_properties['private']);
604
605
606 if(!$include_immutable) {
607 $table_protected_attrs = $table->get_protected_array();
608 $class_protected_attrs = $static_properties['protected'];
609
610 $all_protected_attrs = array_unique(array_merge($table_protected_attrs, $class_protected_attrs));
611
612 $public_attrs = array_diff($non_private_attrs, $all_protected_attrs);
613
614 return $public_attrs;
615
616 }
617 else {
618 return $non_private_attrs;
619 }
620
621 }
622
623 public function getForm($include_protected=false) {
624
625
626 $attrs = ActiveRecord::attributes(get_class($this), $include_protected, false);
627 $form = array();
628
629 foreach($attrs as $attr) {
630 if(method_exists($this, "_form_{$attr}")) {
631 $method = "_form_{$attr}";
632 $form[$attr] = $this->$method();
633 }
634
635 else if($this->is_protected($attr)) {
636 $form[$attr] = array('title'=>ucwords(ActiveRecord::humanize($attr)), 'element'=>"{$this->$attr}", 'editable'=>false);
637 }
638
639 else if($this->table->exists($attr)) {
640  
641 if($alias = $this->table->getAlias($attr)) {
642 $accepts = ActiveRecord::pluralize($alias);
643 $finder = "Find_{$accepts}_by_id";
644 $set = call_user_func(array($finder, 'with'), array('value'=>0, 'bool'=>'>'));
645 $html = "";
646 if(!$this->table->exists($attr) || $this->table->is_null($attr)) {
647
648 if(!$this->$attr) {
649
650 $html .= "<option value=\"\">None</option>";
651 }
652 else {
653 $html .= "<option value=\"\" selected=\"yes\" >None</option>";
654 }
655
656 }
657 foreach($set as $record) {
658 if($this->$attr && $this->$attr->id == $record->id) {
659 $html .= "<option value=\"{$record->id}\" selected=\"yes\" >{$record}</option>";
660 }
661 else {
662 $html .= "<option value=\"{$record->id}\">{$record}</option>";
663 }
664 }
665
666 $html = "<select name=\"{$attr}\" id=\"{$attr}\">{$html}</select>";
667
668
669 $form[$attr] = array('title'=>ucwords(ActiveRecord::humanize($attr)), 'element'=>$html, 'editable'=>true);
670 }
671 else {
672 if($this->table->get_type($attr) == 'bool') {
673 $checked = ($this->$attr) ? "checked=\"checked\"" : "";
674 $form[$attr] = array('title'=>ucwords(ActiveRecord::humanize($attr)), 'element'=>"<input type=\"hidden\" name=\"{$attr}\" value=\"0\" /><input type=\"checkbox\" name=\"{$attr}\" id=\"{$attr}\" value=\"1\" {$checked} />", 'editable'=>true);
675 }
676 else {
677 $form[$attr] = array('title'=>ucwords(ActiveRecord::humanize($attr)), 'element'=>"<input type=\"text\" name=\"{$attr}\" id=\"{$attr}\" value=\"{$this->$attr}\" />", 'editable'=>true);
678 }
679 }
680
681 }
682
683
684 else if(is_a($this->$attr, 'ActiveSet')) {
685
686 $accepts = ActiveRecord::pluralize($this->$attr->accepts());
687 $finder = "Find_{$accepts}_by_id";
688 $set = call_user_func(array($finder, 'with'), array('value'=>0, 'bool'=>'>'));
689 $html = "";
690 if(!$this->table->exists($attr) || $this->table->is_null($attr)) {
691 if(count($this->$attr)) {
692 $html .= "<option value=\"\" >None</option>";
693 }
694 else {
695 $html .= "<option value=\"\" selected=\"yes\" >None</option>";
696 }
697 }
698 foreach($set as $record) {
699
700 if($this->$attr->is_member($record)) {
701 $html .= "<option value=\"{$record->id}\" selected=\"yes\" >{$record}</option>";
702 }
703 else {
704 $html .= "<option value=\"{$record->id}\">{$record}</option>";
705 }
706 }
707 if(is_a($this->$attr, 'ActiveHasOneSet')) {
708 $html = "<select name=\"{$attr}\" id=\"{$attr}\">{$html}</select>";
709 }
710 else {
711 $html = "<select name=\"{$attr}[]\" id=\"{$attr}\" multiple=\"true\" size=\"5\">{$html}</select>";
712 }
713
714 $form[$attr] = array('title'=>ucwords(ActiveRecord::humanize($attr)), 'element'=>$html, 'editable'=>true);
715 }
716
717 else if($this->table->exists("{$attr}_id")) {
718
719 $accepts = ActiveRecord::pluralize($attr);
720 $finder = "Find_{$accepts}_by_id";
721 $table_attr = "{$attr}_id";
722 $set = call_user_func(array($finder, 'with'), array('value'=>0, 'bool'=>'>'));
723 $html = "";
724 if(!$this->table->exists($table_attr) || $this->table->is_null($table_attr)) {
725 if($this->$table_attr > 0) {
726 $html .= "<option value=\"\">None</option>";
727 }
728 else {
729 $html .= "<option value=\"\" selected=\"yes\" >None</option>";
730 }
731 }
732 foreach($set as $record) {
733 if($this->$table_attr && $this->$attr->id == $record->id) {
734 $html .= "<option value=\"{$record->id}\" selected=\"yes\" >{$record}</option>";
735 }
736 else {
737 $html .= "<option value=\"{$record->id}\">{$record}</option>";
738 }
739 }
740
741 $html = "<select name=\"{$attr}_id\" id==\"{$attr}_id\">{$html}</select>";
742
743 $form[$attr] = array('title'=>ucwords(ActiveRecord::humanize($attr)), 'element'=>$html, 'editable'=>true);
744 }
745 }
746
747 return $form;
748
749 }
750
751  
752 [section removed]
753  
754
755 public function __toString() {
756 $class = ucwords(ActiveRecord::humanize("{$this->table}"));
757 return "$class #{$this->id}";
758 }
759
760 public static function pluralize($name) {
761 if(!$name) return "";
762
763 $name = strtolower($name);
764 $name_array = explode("_", $name);
765 $last_name_word = array_splice($name_array, -1, 1);
766  
767 if((substr($last_name_word[0],-1) != "s" || array_key_exists($last_name_word[0], ActiveRecord::$singular)) && !array_key_exists($last_name_word[0], ActiveRecord::$plural)) {
768 // Find all instances
769 $last_name_word = array_key_exists($last_name_word[0], ActiveRecord::$singular) ? ActiveRecord::$singular[$last_name_word[0]] : $last_name_word[0] . 's';
770
771 $name_start = (count($name_array) > 1) ? implode("_", $name_array) : $name_array[0];
772 $name_start .= ($name_start) ? '_' : '';
773 $name = $name_start . $last_name_word;
774
775  
776 }
777 // Otherwise, if not pluaral and class name starts Find_x_by_
778 else {
779 // Find only first instance
780 $name_start = implode("_", $name_array);
781 $name_start .= ($name_start) ? "_" : "";
782 $name = $name_start . $last_name_word[0];
783 }
784
785 return $name;
786 }
787
788 public static function singularize($name) {
789 if(!$name) return "";
790
791 $name = strtolower($name);
792 $name_array = explode("_", $name);
793 $last_name_word = array_splice($name_array, -1, 1);
794  
795 if((substr($last_name_word[0],-1) == "s" || array_key_exists($last_name_word[0], ActiveRecord::$plural)) && !array_key_exists($last_name_word[0], ActiveRecord::$singular)) {
796 // Find all instances
797 $last_name_word = array_key_exists($last_name_word[0], ActiveRecord::$plural) ? ActiveRecord::$plural[$last_name_word[0]] : substr($last_name_word[0], 0, -1);
798
799 $name_start = (count($name_array) > 1) ? implode("_", $name_array) : $name_array[0];
800 $name_start .= ($name_start) ? '_' : '';
801 $name = $name_start . $last_name_word;
802
803  
804 }
805 // Otherwise, if not pluaral and class name starts Find_x_by_
806 else {
807 // Find only first instance
808 $name_start = implode("_", $name_array);
809 $name_start .= ($name_start) ? "_" : "";
810 $name = $name_start . $last_name_word[0];
811 }
812
813 return $name;
814 }
815
816 public static function humanize($name) {
817 return strtr($name, "_", " ");
818 }
819
820 public static function computerize($name) {
821 return strtr($name, " ", "_");
822 }
823
824
825 public static function link_context() {
826 ActiveRecord::$context = 'link';
827 }
828
829 public static function name_context() {
830 ActiveRecord::$context = 'name';
831 }
832
833
834
835 }
836  
837  
838 [section removed]
839  
840 ?>