1<?php 2namespace ZM; 3require_once('database.php'); 4 5$object_cache = array(); 6 7class ZM_Object { 8 protected $_last_error; 9 10 public function __construct($IdOrRow = NULL) { 11 $class = get_class($this); 12 13 $row = NULL; 14 if ( $IdOrRow ) { 15 16 if ( is_integer($IdOrRow) or ctype_digit($IdOrRow) ) { 17 $table = $class::$table; 18 $row = dbFetchOne("SELECT * FROM `$table` WHERE `Id`=?", NULL, array($IdOrRow)); 19 if ( !$row ) { 20 Error("Unable to load $class record for Id=$IdOrRow"); 21 } 22 } else if ( is_array($IdOrRow) ) { 23 $row = $IdOrRow; 24 } 25 26 if ( $row ) { 27 foreach ($row as $k => $v) { 28 $this->{$k} = $v; 29 } 30 global $object_cache; 31 if ( ! isset($object_cache[$class]) ) { 32 $object_cache[$class] = array(); 33 } 34 $cache = &$object_cache[$class]; 35 $cache[$row['Id']] = $this; 36 } 37 } # end if isset($IdOrRow) 38 } # end function __construct 39 40 public function __call($fn, array $args){ 41 $type = (array_key_exists($fn, $this->defaults) && is_array($this->defaults[$fn])) ? $this->defaults[$fn]['type'] : 'scalar'; 42 43 if ( count($args) ) { 44 if ( $type == 'set' and is_array($args[0]) ) { 45 $this->{$fn} = implode(',', $args[0]); 46 } else if ( array_key_exists($fn, $this->defaults) && is_array($this->defaults[$fn]) && isset($this->defaults[$fn]['filter_regexp']) ) { 47 if ( is_array($this->defaults[$fn]['filter_regexp']) ) { 48 foreach ( $this->defaults[$fn]['filter_regexp'] as $regexp ) { 49 $this->{$fn} = preg_replace($regexp, '', $args[0]); 50 } 51 } else { 52 $this->{$fn} = preg_replace($this->defaults[$fn]['filter_regexp'], '', $args[0]); 53 } 54 } else { 55 if ( $args[0] == '' and array_key_exists($fn, $this->defaults) ) { 56 if ( is_array($this->defaults[$fn]) ) { 57 $this->{$fn} = $this->defaults[$fn]['default']; 58 } else { 59 $this->{$fn} = $this->defaults[$fn]; 60 } 61 } else { 62 $this->{$fn} = $args[0]; 63 } 64 } 65 } 66 67 if ( property_exists($this, $fn) ) { 68 return $this->{$fn}; 69 } else { 70 if ( array_key_exists($fn, $this->defaults) ) { 71 if ( is_array($this->defaults[$fn]) ) { 72 return $this->defaults[$fn]['default']; 73 } 74 return $this->defaults[$fn]; 75 } else { 76 $backTrace = debug_backtrace(); 77 Warning("Unknown function call Object->$fn from ".print_r($backTrace,true)); 78 } 79 } 80 } 81 82 public static function _find($class, $parameters = null, $options = null ) { 83 $table = $class::$table; 84 $filters = array(); 85 $sql = 'SELECT * FROM `'.$table.'` '; 86 $values = array(); 87 88 if ( $parameters ) { 89 $fields = array(); 90 $sql .= 'WHERE '; 91 foreach ( $parameters as $field => $value ) { 92 if ( $value == null ) { 93 $fields[] = '`'.$field.'` IS NULL'; 94 } else if ( is_array($value) ) { 95 $func = function(){return '?';}; 96 $fields[] = '`'.$field.'` IN ('.implode(',', array_map($func, $value)). ')'; 97 $values += $value; 98 99 } else { 100 $fields[] = '`'.$field.'`=?'; 101 $values[] = $value; 102 } 103 } 104 $sql .= implode(' AND ', $fields ); 105 } 106 if ( $options ) { 107 if ( isset($options['order']) ) { 108 $sql .= ' ORDER BY ' . $options['order']; 109 } 110 if ( isset($options['limit']) ) { 111 if ( is_integer($options['limit']) or ctype_digit($options['limit']) ) { 112 $sql .= ' LIMIT ' . $options['limit']; 113 } else { 114 $backTrace = debug_backtrace(); 115 Error('Invalid value for limit('.$options['limit'].') passed to '.get_class()."::find from ".print_r($backTrace,true)); 116 return array(); 117 } 118 } 119 } 120 $rows = dbFetchAll($sql, NULL, $values); 121 $results = array(); 122 if ( $rows ) { 123 foreach ( $rows as $row ) { 124 array_push($results , new $class($row)); 125 } 126 } 127 return $results; 128 } # end public function _find() 129 130 public static function _find_one($class, $parameters = array(), $options = array() ) { 131 global $object_cache; 132 if ( ! isset($object_cache[$class]) ) { 133 $object_cache[$class] = array(); 134 } 135 $cache = &$object_cache[$class]; 136 if ( 137 ( count($parameters) == 1 ) and 138 isset($parameters['Id']) and 139 isset($cache[$parameters['Id']]) ) { 140 return $cache[$parameters['Id']]; 141 } 142 $options['limit'] = 1; 143 $results = ZM_Object::_find($class, $parameters, $options); 144 if ( ! sizeof($results) ) { 145 return; 146 } 147 return $results[0]; 148 } 149 150 public static function _clear_cache($class) { 151 global $object_cache; 152 $object_cache[$class] = array(); 153 } 154 public function _remove_from_cache($class, $object) { 155 global $object_cache; 156 unset($object_cache[$class][$object->Id()]); 157 } 158 159 public static function Objects_Indexed_By_Id($class, $params=null) { 160 $results = array(); 161 foreach ( ZM_Object::_find($class, $params, array('order'=>'lower(Name)')) as $Object ) { 162 $results[$Object->Id()] = $Object; 163 } 164 return $results; 165 } 166 167 public function to_json() { 168 $json = array(); 169 foreach ($this->defaults as $key => $value) { 170 if ( is_callable(array($this, $key), false) ) { 171 $json[$key] = $this->$key(); 172 } else if ( property_exists($this, $key) ) { 173 $json[$key] = $this->{$key}; 174 } else { 175 $json[$key] = $this->defaults[$key]; 176 } 177 } 178 return json_encode($json); 179 } 180 181 public function set($data) { 182 foreach ( $data as $field => $value ) { 183 if ( method_exists($this, $field) and is_callable(array($this, $field), false) ) { 184 $this->$field($value); 185 } else { 186 if ( is_array($value) ) { 187 # perhaps should turn into a comma-separated string 188 $this->{$field} = implode(',', $value); 189 } else if (is_string($value)) { 190 if (array_key_exists($field, $this->defaults)) { 191 # Need filtering 192 if (is_array($this->defaults[$field]) && isset($this->defaults[$field]['filter_regexp'])) { 193 if (is_array($this->defaults[$field]['filter_regexp'])) { 194 foreach ($this->defaults[$field]['filter_regexp'] as $regexp) { 195 $this->{$field} = preg_replace($regexp, '', trim($value)); 196 } 197 } else { 198 $this->{$field} = preg_replace($this->defaults[$field]['filter_regexp'], '', trim($value)); 199 } 200 } else if ($value == '') { 201 if (is_array($this->defaults[$field])) { 202 $this->{$field} = $this->defaults[$field]['default']; 203 } else if (is_string($this->defaults[$field])) { 204# if the default is a string, don't set it. Having a default for empty string is to set null for numbers. 205 $this->{$field} = $value; 206 } else { 207 $this->{$field} = $this->defaults[$field]; 208 } 209 } else { 210 $this->{$field} = $value; 211 } # need a default 212 } else { 213 $this->{$field} = $value; 214 } 215 } else if ( is_integer($value) ) { 216 $this->{$field} = $value; 217 } else if ( is_bool($value) ) { 218 $this->{$field} = $value; 219 } else if ( is_null($value) ) { 220 $this->{$field} = $value; 221 } else { 222 Error("Unknown type $field => $value of var " . gettype($value)); 223 $this->{$field} = $value; 224 } 225 } # end if method_exists 226 } # end foreach $data as $field=>$value 227 } # end function set($data) 228 229 /* types is an array of fields telling use that the input might be a checkbox so not present in the input, but therefore has a value 230 */ 231 public function changes($new_values, $defaults=null) { 232 $changes = array(); 233 234 if ($defaults) { 235 foreach ($defaults as $field => $type) { 236 if (isset($new_values[$field])) continue; 237 238 if (isset($this->defaults[$field])) { 239 if (is_array($this->defaults[$field])) { 240 $new_values[$field] = $this->defaults[$field]['default']; 241 } else { 242 $new_values[$field] = $this->defaults[$field]; 243 } 244 } 245 } # end foreach default 246 } # end if defaults 247 248 foreach ($new_values as $field => $value) { 249 if (method_exists($this, $field)) { 250 if (array_key_exists($field, $this->defaults) && is_array($this->defaults[$field]) && isset($this->defaults[$field]['filter_regexp'])) { 251 if (is_array($this->defaults[$field]['filter_regexp'])) { 252 foreach ($this->defaults[$field]['filter_regexp'] as $regexp) { 253 $value = preg_replace($regexp, '', trim($value)); 254 } 255 } else { 256 $value = preg_replace($this->defaults[$field]['filter_regexp'], '', trim($value)); 257 } 258 } 259 260 $old_value = $this->$field(); 261 if (is_array($old_value)) { 262 $diff = array_recursive_diff($old_value, $value); 263 if ( count($diff) ) { 264 $changes[$field] = $value; 265 } 266 } else if ( $this->$field() != $value ) { 267 $changes[$field] = $value; 268 } 269 } else if (property_exists($this, $field)) { 270 $type = (array_key_exists($field, $this->defaults) && is_array($this->defaults[$field])) ? $this->defaults[$field]['type'] : 'scalar'; 271 if ($type == 'set') { 272 $old_value = is_array($this->$field) ? $this->$field : ($this->$field ? explode(',', $this->$field) : array()); 273 $new_value = is_array($value) ? $value : ($value ? explode(',', $value) : array()); 274 275 $diff = array_recursive_diff($old_value, $new_value); 276 if (count($diff)) $changes[$field] = $new_value; 277 278 # Input might be a command separated string, or an array 279 280 } else { 281 if (array_key_exists($field, $this->defaults) && is_array($this->defaults[$field]) && isset($this->defaults[$field]['filter_regexp'])) { 282 if (is_array($this->defaults[$field]['filter_regexp'])) { 283 foreach ($this->defaults[$field]['filter_regexp'] as $regexp) { 284 $value = preg_replace($regexp, '', trim($value)); 285 } 286 } else { 287 $value = preg_replace($this->defaults[$field]['filter_regexp'], '', trim($value)); 288 } 289 } 290 if ($this->{$field} != $value) $changes[$field] = $value; 291 } 292 } else if (array_key_exists($field, $this->defaults)) { 293 if (is_array($this->defaults[$field]) and isset($this->defaults[$field]['default'])) { 294 $default = $this->defaults[$field]['default']; 295 } else { 296 $default = $this->defaults[$field]; 297 } 298 299 if ($default != $value) $changes[$field] = $value; 300 } 301 } # end foreach newvalue 302 303 return $changes; 304 } # end public function changes 305 306 public function save($new_values = null) { 307 $class = get_class($this); 308 $table = $class::$table; 309 310 if ( $new_values ) { 311 $this->set($new_values); 312 } 313 314 # Set defaults. Note that we only replace "" with null, not other values 315 # because for example if we want to clear TimestampFormat, we clear it, but the default is a string value 316 foreach ( $this->defaults as $field => $default ) { 317 if (!property_exists($this, $field)) { 318 if (is_array($default)) { 319 $this->{$field} = $default['default']; 320 } else { 321 $this->{$field} = $default; 322 } 323 } else if ($this->{$field} === '') { 324 if (is_array($default)) { 325 $this->{$field} = $default['default']; 326 } else if ($default == null) { 327 $this->{$field} = $default; 328 } 329 } 330 } # end foreach default 331 332 $fields = array_filter( 333 $this->defaults, 334 function($v) { 335 return !( 336 is_array($v) 337 and 338 isset($v['do_not_update']) 339 and 340 $v['do_not_update'] 341 ); 342 } 343 ); 344 $fields = array_keys($fields); 345 346 if ( $this->Id() ) { 347 $sql = 'UPDATE `'.$table.'` SET '.implode(', ', array_map(function($field) {return '`'.$field.'`=?';}, $fields)).' WHERE Id=?'; 348 $values = array_map(function($field){ return $this->{$field};}, $fields); 349 $values[] = $this->{'Id'}; 350 if (dbQuery($sql, $values)) return true; 351 } else { 352 unset($fields['Id']); 353 354 $sql = 'INSERT INTO `'.$table. 355 '` ('.implode(', ', array_map(function($field) {return '`'.$field.'`';}, $fields)). 356 ') VALUES ('. 357 implode(', ', array_map(function($field){return (($this->$field() === 'NOW()') ? 'NOW()' : '?');}, $fields)).')'; 358 359 # For some reason comparing 0 to 'NOW()' returns false; So we do this. 360 $filtered = array_filter($fields, function($field){ return ( (!$this->$field()) or ($this->$field() != 'NOW()'));}); 361 $mapped = array_map(function($field){return $this->$field();}, $filtered); 362 $values = array_values($mapped); 363 if (dbQuery($sql, $values)) { 364 $this->{'Id'} = dbInsertId(); 365 return true; 366 } 367 } 368 $this->_last_error = dbError($sql); 369 return false; 370 } // end function save 371 372 public function insert($new_values = null) { 373 $class = get_class($this); 374 $table = $class::$table; 375 376 if ( $new_values ) { 377 $this->set($new_values); 378 } 379 380 # Set defaults. Note that we only replace "" with null, not other values 381 # because for example if we want to clear TimestampFormat, we clear it, but the default is a string value 382 foreach ( $this->defaults as $field => $default ) { 383 if ( (!property_exists($this, $field)) or ($this->{$field} === '') ) { 384 if ( is_array($default) ) { 385 $this->{$field} = $default['default']; 386 } else if ( $default == null ) { 387 $this->{$field} = $default; 388 } 389 } 390 } 391 392 $fields = array_filter( 393 $this->defaults, 394 function($v) { 395 return !( 396 is_array($v) 397 and 398 isset($v['do_not_update']) 399 and 400 $v['do_not_update'] 401 ); 402 } 403 ); 404 $fields = array_keys($fields); 405 406 if ( ! $this->Id() ) 407 unset($fields['Id']); 408 409 $sql = 'INSERT INTO `'.$table. 410 '` ('.implode(', ', array_map(function($field) {return '`'.$field.'`';}, $fields)). 411 ') VALUES ('. 412 implode(', ', array_map(function($field){return '?';}, $fields)).')'; 413 414 $values = array_map(function($field){return $this->$field();}, $fields); 415 if ( dbQuery($sql, $values) ) { 416 if ( ! $this->{'Id'} ) 417 $this->{'Id'} = dbInsertId(); 418 return true; 419 } 420 return false; 421 } // end function insert 422 423 public function delete() { 424 $class = get_class($this); 425 $table = $class::$table; 426 dbQuery("DELETE FROM `$table` WHERE Id=?", array($this->{'Id'})); 427 if ( isset($object_cache[$class]) and isset($object_cache[$class][$this->{'Id'}]) ) 428 unset($object_cache[$class][$this->{'Id'}]); 429 } 430 431 public function lock() { 432 $class = get_class($this); 433 $table = $class::$table; 434 $row = dbFetchOne("SELECT * FROM `$table` WHERE `Id`=?", NULL, array($this->Id())); 435 if ( !$row ) { 436 Error("Unable to lock $class record for Id=".$this->Id()); 437 } 438 } 439 public function remove_from_cache() { 440 return ZM_Object::_remove_from_cache(get_class(), $this); 441 } 442 public function get_last_error() { 443 return $this->_last_error; 444 } 445} # end class Object 446?> 447