Campustream 1.0
A social network MQP for WPI
core/lib/activerecord.php
Go to the documentation of this file.
00001 <?php
00002 
00003 class ActiveRecord {
00004         
00005         public static $NOW = null;
00006         public static $NULL = null;
00007         
00008         // member variables are prefixed in order to prevent polluting the
00009         // class space and conflicting w/ column names
00010         protected $_data          = null;
00011         protected $_relationships = array();
00012         
00013         protected $_dirty         = false;
00014         protected $_new           = true;
00015         protected $_orig_pk       = null;
00016         
00017         protected $_subscribers   = array();
00018   
00019         // these should all get set by the child class.
00020         // also, these would ideally be static variables but php does
00021         // not support late-static binding til 5.3, so it would not work as expected
00022         public $table_name     = null;
00023         public $columns        = null;
00024         public $primary_key    = 'id';
00025         public $primary_key_value  = null;
00026         public $auto_increment = true;
00027         public $has_many       = array();
00028         public $has_one        = array();
00029         public $belongs_to     = array();
00030         
00031         const _UPDATE = 'UPDATE `%s` SET %s WHERE %s;';
00032         const _INSERT = 'INSERT INTO `%s` (%s) VALUES (%s);';
00033         const _SELECT = 'SELECT * FROM `%s` WHERE %s LIMIT 1';
00034         const _DELETE = 'DELETE FROM `%s` WHERE %s LIMIT 1';
00035         
00036         // model_to_table_name( 'User_Model' ) -> users
00037         public static function model_to_table_name( $class ) {
00038                 return strtolower( str_replace( "_Model", "", $class ) ) . 's';
00039         }
00040         
00041         // table_to_model_name( 'users' ) -> User_Model
00042         public static function table_to_model_name( $class ) {
00043                 $class = self::depluralize($class);
00044                 return ucfirst( $class ) . "_Model";
00045         }
00046         
00047         public static function depluralize($word){
00048             // Here is the list of rules. To add a scenario,
00049             // Add the plural ending as the key and the singular
00050             // ending as the value for that key. This could be
00051             // turned into a preg_replace and probably will be
00052             // eventually, but for now, this is what it is.
00053             //
00054             // Note: The first rule has a value of false since
00055             // we don't want to mess with words that end with
00056             // double 's'. We normally wouldn't have to create
00057             // rules for words we don't want to mess with, but
00058             // the last rule (s) would catch double (ss) words
00059             // if we didn't stop before it got to that rule. 
00060             $rules = array( 
00061                 'ss' => false, 
00062                 'os' => 'o', 
00063                 'ies' => 'y', 
00064                 'xes' => 'x', 
00065                 'oes' => 'o', 
00066                 'ies' => 'y', 
00067                 'ves' => 'f',
00068                 'nses' => 'nse',
00069                 'ses' => 's', 
00070                 's' => '');
00071             // Loop through all the rules and do the replacement. 
00072             foreach(array_keys($rules) as $key){
00073                 // If the end of the word doesn't match the key,
00074                 // it's not a candidate for replacement. Move on
00075                 // to the next plural ending. 
00076                 if(substr($word, (strlen($key) * -1)) != $key) 
00077                     continue;
00078                 // If the value of the key is false, stop looping
00079                 // and return the original version of the word. 
00080                 if($key === false) 
00081                     return $word;
00082                 // We've made it this far, so we can do the
00083                 // replacement. 
00084                 return substr($word, 0, strlen($word) - strlen($key)) . $rules[$key]; 
00085             }
00086             return $word;
00087         }
00088         
00089         // return a unique token that will be replaced at query time with the NOW() function.
00090         public static function NOW() {
00091                 if ( self::$NOW === null ) {
00092                         self::$NOW = uniqid( "RESERVED_", true );
00093                 }
00094                 return self::$NOW;
00095         }
00096 
00097         public static function NULL( $test=null ) {
00098                 
00099                 if ( $test !== null ) {
00100                         return $test;
00101                 }
00102                 
00103                 if ( self::$NULL === null ) {
00104                         self::$NULL = uniqid( "RESERVED_", true );
00105                 }
00106                 return self::$NULL;
00107         }
00108         
00109         // passthrough to mysqli->prepare, allows for using prepared statements
00110         // with the find method. safer and better than manually escaping data.
00111         // EXAMPLE:
00112         // $stmt = ActiveRecord::prepare( "SELECT * FROM users where username=?" );
00113         // $stmt->bindParam( "s", $value );
00114         // ActiveRecord::find( 'User', $stmt );
00115         public static function prepare( $clause ) {  
00116           return DatabaseManager::readConnection()->prepare( $clause );
00117         }
00118         
00119         public static function find_all( $class, $clause, $pk_hint='id' ) {
00120                 
00121                 $result = self::find( $class, $clause, $pk_hint );
00122 
00123                 if ( ! is_array( $result ) && $result->is_loaded() === true ) {
00124                         $result = array( $result );
00125                 } else if ( ! is_array( $result ) && $result->is_loaded() === false ) {
00126                         // this is needed to clear out the array if find() returns a conveninent empty
00127                         // object.
00128                         $result = array();
00129                 }
00130                 
00131                 return $result;
00132                 
00133         }
00134         
00135         // $class   = the ActiveRecord childclass that you want returned
00136         // $clause  = the SQL for the statement or primary key clause
00137         // $pk_hint = the primary key OR array or primary keys
00138         // returns the object or an array of objects  
00139         public static function find( $class, $clause, $pk_hint='id' ) {
00140 
00141                 if ( empty( $clause ) ) {
00142                         return new $class();
00143                 }
00144 
00145                 $connection = DatabaseManager::readConnection();
00146                 Logger::debug( "activerecord find $clause" );
00147                 // figure out if we are doing a lookup by primary key, or
00148                 // raw sql. assumes a primary key will always be numeric
00149                 if ( is_numeric( $clause ) ) {
00150                         $primary_key_clause = "`$pk_hint` = '$clause'";
00151                         $sql = sprintf( self::_SELECT, ActiveRecord::model_to_table_name( $class ), $primary_key_clause );
00152                         $stmt = $connection->query( $sql );
00153                 } else if ( $clause instanceof MySQLi_STMT ) {
00154                         $clause->execute();
00155                         $stmt = $clause;
00156                 } else {
00157                         $stmt = $connection->query( $clause );
00158                 }
00159                 
00160                 if ( $connection->errno !== 0 ) {
00161                         throw new Exception( 'an error occured: ' . $connection->error );
00162                 } 
00163                 
00164                 // return an empty object if the result set is empty
00165                 if ( $stmt->num_rows === 0 ) return new $class();
00166                 
00167                 $fields = $stmt->fetch_fields();
00168                 
00169                 $table_map = array();
00170                 foreach( $fields as $field ) {
00171                         isset( $table_map[$field->table] ) ? null : $table_map[$field->table] = array();
00172                         $table_map[$field->table][] = $field->name;
00173                 }
00174                 
00175                 $result_set = array();
00176                 $seen_pks   = array_fill_keys( array_keys( $table_map ), array() ); // prime the array to have table names set
00177                                 
00178                 // loop over each row. we have to use fetch_row because
00179                 // fetch_assoc has overlap problems when a join has the
00180                 // same column name.
00181                 while ( $row = $stmt->fetch_row() ) {
00182                         $table_offset = 0; // this is how deep we can go into the $row array before we hit the next table
00183                         $parent_pk  = null;
00184 
00185                         foreach( $table_map as $table_name => $column_list ) {
00186                                 $column_count = count( $column_list );
00187                                 // join the column name with the data, basically we are taking fetch_row and fetch_fields and making our own fetch_assoc
00188                                 $column_data  = array_combine( $column_list, array_slice( $row, $table_offset, $column_count ) );
00189                                 
00190                                 if ( is_array( $pk_hint ) ) {
00191                                         $pk_name = $pk_hint[ $model_name ];
00192                                 } else {
00193                                         $pk_name = $pk_hint;
00194                                 }
00195                                 
00196                                 $pk_value = $column_data[ $pk_name ];
00197                                 
00198                                 // protects against left joins- we want to ignore any joins that have a null primary key
00199                                 // and avoid creating an empty object.
00200                                 if ( $pk_value === null ) continue;
00201                                 
00202                                 // get the model name of the table we are working on
00203                                 $model_name = ActiveRecord::table_to_model_name( $fields[ $table_offset ]->table );
00204                                 
00205                                 $table_offset += $column_count;
00206                                                                 
00207                                 if ( $model_name === $class ) {
00208                                         
00209                                         // we have seen this item before, skip it
00210                                         if ( isset( $seen_pks[ $table_name ][ $pk_value ] ) ) {
00211                                                 $parent_pk = $column_data[ $pk_name ];
00212                                                 continue;
00213                                         }
00214                         
00215                                         $object = new $model_name( $column_data );
00216                                         $seen_pks[ $table_name ][ $pk_value ] = true;
00217                                         $result_set[ $pk_value ] = $object;
00218                                         
00219                                         // parent_pk is used to track the primary key of the 'main' object
00220                                         // for the join parts of this sql row
00221                                         $parent_pk = $pk_value;
00222                                         
00223                                 } else {
00224                                         if ( isset( $seen_pks[ $table_name ][ $parent_pk ][ $pk_value ] ) ) {
00225                                                 continue;
00226                                         }
00227 
00228                                         $object = new $model_name( $column_data );
00229                                         
00230                                         $seen_pks[$table_name][$parent_pk][ $pk_value ] = true;
00231                                         
00232                                         $result_set[$parent_pk]->add_relationship( $model_name, $object );
00233                                         
00234                                 }       
00235                         }
00236                 }
00237                 
00238                 // if there is only one item in the result set, just return the front item. array_values is
00239                 // used to re-order the array, since $result_set uses the value of the primary key as the array key.
00240                 return count($result_set) == 1 ? reset($result_set) : array_values($result_set);
00241         }
00242         
00243         // $data = an array of initial data to load the object with.
00244         // if $data is null, it assumes you want to create a new/blank object
00245         public function __construct( $data=null ) {
00246                 
00247                 // we can figure out the table name, based on the child class.
00248                 // we assume tables are lower case plural versions of the class name i.e, User -> users
00249                 if ( $this->table_name === null ) {
00250                         $this->table_name = ActiveRecord::model_to_table_name( get_class( $this ) );
00251                 }
00252 
00253                 $s_relation = array_merge( $this->belongs_to, $this->has_one );
00254                 foreach( $s_relation as &$value ) {
00255                         $value = strtolower( $value );
00256                 }
00257                 
00258                 
00259                 $s_relation = array_fill_keys( $s_relation, null );
00260                 $p_relation = array_fill_keys( $this->has_many, array() );
00261                                 
00262                 $this->_relationships = array_merge( $s_relation, $p_relation );
00263                 
00264                 if ( $data !== null ) { 
00265                         $this->_new  = false;
00266                         $this->_data = $data; // we trust that this will be correct
00267 
00268                         // make a copy of the primary key, incase it ever gets changed
00269                         if ( isset( $this->_data[ $this->primary_key ] ) ) {
00270                                 $this->_orig_pk = $this->_data[ $this->primary_key ];
00271                                 $this->primary_key_value = $this->_data[ $this->primary_key ];
00272                         }
00273                 } else { 
00274                         $this->_data = array_fill_keys( $this->columns, null );
00275                 }
00276                 
00277         }
00278         
00279         // delete this object from the database
00280         public function delete( $skip_delete=false) {
00281                 
00282                 if ( $skip_delete === false ) {
00283                         $where_clause = "`{$this->primary_key}`='{$this->_orig_pk}'";
00284                         $delete_sql = sprintf( self::_DELETE, $this->table_name, $where_clause );
00285                         // uncommented line below to re-enable deletions
00286                         DatabaseManager::writeConnection()->query( $delete_sql );
00287                 }
00288                 ActiveCache::invalidate_keys_for( $this );
00289                 $this->notify_subscribers();
00290                 
00291         }
00292         
00293         // return true if this object has been saved
00294         public function is_saved() { return (! (bool)$this->_dirty); }
00295         // return true if this object loaded from the database
00296         public function is_loaded() { return (! $this->_new); }
00297         
00298   // used in validation. we define it here because if no validation method is
00299   // present, validation should always succeed 
00300         public function validate() { return true; }
00301         
00302   // used to embed objects which have a relationship to this class- i.e, joins
00303   // $class  = the class name of the object
00304   // $object = the object to embed
00305         public function add_relationship( $class, $object ) {
00306 
00307                 $plural = ActiveRecord::model_to_table_name( $class );
00308                                 
00309                 if ( in_array( $plural, $this->has_many ) ) {
00310                         $this->_relationships[$plural][] = $object;
00311                 } else {
00312                         $singular = substr( ActiveRecord::model_to_table_name( $class ), 0, -1 );
00313                         $this->_relationships[$singular] = $object;
00314                 }
00315         }
00316         
00317   // used to update or insert the data, depending on if this is a new object or
00318   // not
00319         public function save() {
00320                 // only execute sql if the state has changed
00321                 if ( $this->_new === true ) {
00322                         $this->_insert();
00323                 } else if ( $this->_dirty !== false ) {
00324                         $this->_update();
00325                 }
00326                 
00327                 $this->notify_subscribers();
00328         }
00329         
00330         public function subscribe( $subscriber ) {
00331                 $this->_subscribers[] = $subscriber;
00332         }
00333         
00334         public function notify_subscribers() {
00335                 foreach( $this->_subscribers as $subscriber ) {
00336                         $subscriber->notify( $this );
00337                 }
00338         }
00339         
00340         // internal method, it runs an update on the dirty columns
00341         private function _update() {
00342                 
00343                 $connection = DatabaseManager::writeConnection();
00344                 
00345                 // generate the where clause, WHERE primary_key = primary_key_value
00346                 $where_clause = "`{$this->primary_key}`='{$this->_orig_pk}'";
00347                 
00348                 // generate the set list
00349                 $update_cols = array();
00350 
00351                 foreach( $this->_dirty as $column => $value ) {
00352                         if ( $value === ActiveRecord::NOW() ) {
00353                                 $update_cols[] = "`$column`=NOW()";
00354                                 $this->_data[$column] = date ("Y-m-d H:i:s"); // approximate the actual value, so it's in the obj w/out re-reading from mysql
00355                         } elseif ( $value === ActiveRecord::NULL() ) {
00356                                 $update_cols[] = "`$column`=NULL";
00357                                 $this->_data[$column] = null;
00358                                 continue;
00359                         } else {
00360                                 $value = $connection->real_escape_string( $value );
00361                                 $update_cols[] = "`$column`='{$value}'";
00362                         }
00363                 }
00364                 
00365                 // convert the array to a comma delimited list
00366                 $update_cols = join( ', ', $update_cols );
00367                 
00368                 // glue the sql together
00369                 $update_sql = sprintf( self::_UPDATE, $this->table_name, $update_cols, $where_clause );
00370                 Logger::debug( "updating $update_sql");
00371 
00372                 $connection->query( $update_sql );
00373                 
00374                 if ( $connection->errno !== 0 ) {
00375                         throw new Exception( 'an error occured: ' . $connection->error . ' ' . $update_sql );
00376                 }
00377                 
00378                 $this->_dirty = false;
00379                 
00380         }
00381         
00382         // internal method, it inserts any added data into the database as a new row
00383         private function _insert() {
00384 
00385                 $connection = DatabaseManager::writeConnection();
00386                 
00387                 // lets make a list of all the columns, excluding primary_key
00388                 // make a copy of the data, we need to wrap it in backticks
00389                     $data = $this->_data;
00390                                 $data = array();
00391                                 foreach ( $this->_data as $key => $value) {
00392                                         if ( $key == $this->primary_key && $this->auto_increment === true ) continue;
00393                                         
00394                                         if ( $value === ActiveRecord::NULL() ) {
00395                                                 $data[$key] = "NULL";
00396                                                 $this->_data[$key] = null;
00397                                                 continue;
00398                                         }
00399                                         
00400                                         if ( $value === ActiveRecord::NOW() ){ 
00401                                                 $data[$key] = "NOW()"; 
00402                                                 $this->_data[$key] = date ("Y-m-d H:i:s"); // approximate NOW() for this object. will use real NOW() in the database.
00403                                                 continue;
00404                                         }
00405                                         $value = $connection->real_escape_string( $value );
00406                                         $data[$key] = "'$value'";
00407                                 }
00408 
00409                     // join( ',', array_map( function($i){ return "'$i'";}, $data ) );
00410                     $insert_data = join( ',', $data );
00411 
00412                     // now lets make a list of all the columns, excluding primary_key
00413                     $column_list = array();
00414 
00415                     foreach( $this->columns as $column ) {
00416                       // skip the primary key row if it's auto increment
00417                       if ( $column == $this->primary_key && $this->auto_increment === true ) continue;
00418                       $column_list[] = "`$column`";
00419                     }
00420 
00421                     $column_list = join( ',', $column_list );
00422 
00423                     $insert_sql = sprintf( self::_INSERT, $this->table_name, $column_list, $insert_data );
00424                                 Logger::debug( "inserting $insert_sql");
00425                         
00426                                 $stmt = $connection->query($insert_sql);
00427                         
00428                     // if there is an autoincrement pk, lets grab it and set the value
00429                     if ( $this->primary_key !== null && $this->auto_increment === true ) {
00430                                         $pk_value = $connection->insert_id;
00431                                         $this->__set( $this->primary_key, $pk_value);
00432                                         $this->_orig_pk = $pk_value;
00433                                         $this->primary_key_value = $pk_value;
00434                     }
00435 
00436                     $this->_dirty = false;
00437                     $this->_new   = false;
00438 
00439                   }     
00440 // php magic method that is run when you call isset on any orm-property
00441         public function __isset( $key ) {
00442                 return isset( $this->_data[$key] );
00443         }
00444         
00445         // php magic method that is run when you retrieve any orm-property or relationship
00446         public function __get( $key ) {
00447                 if ( isset( $this->_data[$key] ) ) {
00448                         return $this->_data[$key];
00449                 } else if ( isset( $this->_relationships[$key] ) ) {
00450                         return $this->_relationships[$key];
00451                 }
00452                 return null;
00453         }
00454         
00455         // php magic method that is used for setting an orm-property
00456         public function __set( $key, $value ) {
00457                 if ( array_key_exists( $key, $this->_data ) && $this->_data[$key] !== $value ) {
00458                         $this->_data[$key] = $value;
00459 
00460                         // mark the key as dirty
00461                         $this->_dirty = ( $this->_dirty === false ? array() : $this->_dirty );
00462                         $this->_dirty[$key] = $value;
00463                 }
00464         }
00465         
00466         // php magic method that is called when the class is serialized- i.e, before
00467         // being put in memcache
00468         public function __sleep() {
00469                 if ( $this->is_loaded ) {
00470                         $this->save();
00471                 }
00472                 return array( '_data', '_relationships', '_orig_pk', 'table_name', 'columns',
00473                 'primary_key', 'primary_key_value', 'auto_increment', 'has_many', 'has_one', 'belongs_to', '_new' );
00474         }
00475         
00476         // php magic method that is called when the class is unserialized- i.e, after
00477         // being retrieved from memcache
00478         public function __wakeup() {
00479         }
00480         
00481         // this is a mess. TODO: FIX IT!
00482         // returns a 'limited' version of the current AR object. this means that columns we want hidden
00483         // are not visible. condensed flag sets whether we name array elements or not. (xml needs all keys to
00484         // be set while json does not)
00485         public function limited_object( $condensed=true) {
00486         
00487         foreach( $this->public_columns as $column ) {
00488                         if ( isset( $this->_relationships[$column] ) ) continue;
00489                 $limited_obj->{$column} = $this->$column;
00490         }
00491         
00492         foreach( $this->_relationships as $class_name => $object ) {
00493                                 
00494                         if ( ! in_array($class_name, $this->public_columns) ) continue;
00495                                         
00496                         if ( empty($object) ) { $limited_obj->{$class_name} = null; continue; }
00497                         
00498                         if ( is_array($object) ) {
00499                                                         
00500                                 if ( $condensed === true ) {
00501                                 $limited_obj->{"{$class_name}"} = array();
00502                                 } else {
00503                                         $limited_obj->{"{$class_name}"}->{substr($class_name, 0, -1)} = array();
00504                                 }
00505                                                         
00506                                 foreach( $object as $ob ) {
00507                                         if ( empty( $ob ) ) continue;
00508                                         if ( $condensed === true ) {
00509                                                 $limited_obj->{"{$class_name}"}[] = $ob->limited_object();
00510                                         } else {
00511                                                 // with xml, we can't just put things into a nameless array. need to name every key.
00512                                                 $limited_obj->{$class_name}->{substr($class_name, 0, -1)}[] = $ob->limited_object(false);
00513                                         }
00514                                 }
00515                         } else {
00516             
00517                                 if ( ! $object->is_loaded() ) { $limited_obj->{$class_name} = null; continue; }
00518                                 $limited_obj->{$class_name} = $object->limited_object();
00519                         }
00520                 }
00521         
00522                 return $limited_obj;
00523         }
00524   
00525         // convert this object to a public json object
00526   public function to_json() {
00527     
00528     $limited_object = $this->limited_object();
00529     
00530     return json_encode($limited_object);
00531   }
00532   
00533   public function to_jsonp() {
00534         
00535         $limited_object = $this->limited_object();
00536         
00537         if(isset($_REQUEST['callback'])) {
00538                 $callback = $_REQUEST['callback']; 
00539         } else {
00540                 $callback = 'TwitpicLoad';
00541         }
00542         
00543         return $callback.'('.json_encode($limited_object).');';
00544         
00545   }
00546   
00547         // convert this object to a public xml object
00548   public function to_xml() {
00549     
00550     require_once 'XML/Serializer.php';
00551     
00552     $limited_object = $this->limited_object(false);
00553     
00554     if ( substr( $this->table_name, -2 ) === 'es' ) {
00555                 $class = substr( $this->table_name, 0, -2 );
00556         } elseif ( substr( $this->table_name, -1 ) === "s" ) {
00557                 $class = substr( $this->table_name, 0, -1 );
00558         } else {
00559                 $class = $this->table_name;
00560         }
00561     
00562     $options = array( "indent"    => "    ",
00563                       "linebreak" => "\n",
00564                                                                                         "mode" => "simplexml",
00565                       "typeHints" => false,
00566                       "addDecl"   => true,
00567                       "encoding"  => "UTF-8",
00568                       "rootName"   => $class,
00569                       "defaultTagName" => "images" );
00570 
00571     $serializer = new XML_Serializer( $options );
00572     $serializer->serialize( $limited_object );
00573     return $serializer->getSerializedData();
00574   }
00575 
00576         public static function convert_rs($format, $array, $root, $tag) {
00577                 if ( $format === 'json' ) {
00578                         return self::rs_to_json($array,$root);
00579                 } else {
00580                         return self::rs_to_xml($array,$root,$tag);
00581                 }
00582         }
00583 
00584         public static function rs_to_json($array, $root) {
00585                 
00586                 $rs = $array['rs'];
00587                 unset($array['rs']);
00588                 
00589                 
00590                 foreach($rs as $object) {
00591                         $array[$root][] = $object->limited_object();
00592                 }
00593                 
00594                 echo json_encode($array);
00595         }
00596         
00597         public static function rs_to_xml($array, $root, $tag) {
00598                 
00599                 $rs = $array['rs'];
00600                 unset($array['rs']);
00601                 
00602                 
00603                 foreach($rs as $object) {
00604                         $array[] = $object->limited_object();
00605                 }
00606     
00607         $options = array( "indent"    => "    ",
00608                           "linebreak" => "\n",
00609                           "typeHints" => false,
00610                           "addDecl"   => true,
00611                           "encoding"  => "UTF-8",
00612                           "rootName"   => $root,
00613                           "defaultTagName" => $tag );
00614                 
00615         $serializer = new XML_Serializer( $options );
00616         $serializer->serialize( $array );
00617         return $serializer->getSerializedData();
00618         }
00619         
00620                 
00621 }