Campustream 1.0
A social network MQP for WPI
|
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 }