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