Location: PHPKode > projects > ZenTrack - project/bug tracking software > zentrack_2.6.4/includes/ZenRecordBase.php
<?
/** Holds the ZenRecordBase abstract */

/**
 * A common base class for implementations of ZenRecord, providing common
 * methods and utils. In addition to declaring the abstract method getColumnList(),
 * all implementations of this abstract will have to declare the interface methods:
 * <pre>
 * $this->_dataType; (class name for elements of this list)
 * static function getSourceTable(); - (string)table name in db
 * static function getIdCol();    - (string)field name containing primary key
 * static function getLabelCol(); - (string)field name or array of (string)field names for label of this data type
 * static function validFields() - array of (string)field names
 * function fieldProps($fieldName); array((string)type,(int)maxlength,(bool)required)
 * </pre>
 *
 * Most everything else should be covered by this abstract.
 */
class ZenRecordBase {
  
  /**
   * The column list is an array that defines all the properties of fields
   * in this record type, allowing this base class to perform most of the work
   * in a generic way...
   *
   * <p>column_name should exactly match the name of the column in the database.
   *
   * <p>The valid types for columns are:
   * <ul>
   *   <li>date - a valid date object (converted to an integer)
   *   <li>id - a numeric id value
   *   <li>email - a valid email address
   *   <li>alphanumeric - a string suitable for variable/file names
   *   <li>string - anything, format unchecked
   *   <li>int - an integer value
   *   <li>float - a decimal value
   *   <li>url - a valid http address
   *   <li>boolean - true or false (1/0 also accepted)
   * </ul>
   *
   * <p>The required parameter will prevent the field from being set to an empty
   * value. Note that for numbers this means that the value must be non-zero! Do
   * not ever mark the id field as required or it will be impossible to insert records.
   *
   * <p>Declare $this->_dataType before calling this
   *
   * @param int $id the id of this record or null for a new record
   * @param array $data assumed to be valid... if not provided, and $id is set, db is queried (could be expensive!)
   */
  function ZenRecordBase($id = null, $data = null ) {
    $this->_id = $id;
    $this->_columnList = call_user_func(array($this->_dataType, "validFields"));
    $this->_sourceTable = call_user_func(array($this->_dataType, "getSourceTable"));
    $this->_idCol = call_user_func(array($this->_dataType, "getIdCol"));
    $this->_changed = false;
    $this->_validationErrors = array();
    $this->_data = array();
    if( $data ) {
      foreach($data as $k=>$v) { $this->setField($k, $v); }
    }
    else if( $id ) {
      // we need to load our own data
      $this->load($id);
    }
    else {
      // this is a new object, no data to load
      foreach($this->_columnList as $c) { 
        $this->setEmptyValue($c); 
      }
    }
  }
  
  /** @return string representing class name of this instance */
  function getDataType() { return $this->_dataType; }

  /** @return int the id of this record */
  function getId() { return $this->getField($this->_idCol); }
  
  /** @return string a descriptive label, such as a ticket title, user name, or bin name */
  function getLabel() {
    $labelCol = call_user_func(array($this->_dataType, "getLabelCol"));
    if( is_array($labelCol) ) {
      $v = '';
      foreach($labelCol as $key) {
        $val = $this->getField($key);
        if( $v && strlen($val) ) { $v .= ", $val"; }
        else if( strlen($val) ) { $v .= $val; }
      }
      return $v;
    }
    else {
      return $this->getField($labelCol);
    }
  }
  
  /**
   * Set the value of a field. This will only cause the isChanged() method to
   * return true if the value actually changed. Note that the value is only set
   * if a valid value is provided. Otherwise it is ignored.
   *
   * @param string $fieldName must exactly match the database column name
   * @param mixed $fieldValue must be the appropriate value for the column
   * @return true if field set to valid value, false if invalid or field not found
   */
  function setField( $fieldName, $fieldValue ) {
    // test the value
    if( ($err = self::invalidValue($fieldName, $fieldValue, $this->fieldProps($fieldName)))
        !== false) {
      Zen::addDebug("ZenRecordBase::setField", $err, 1);
      return false;
    }
    
    // store the value
    $this->_data[$fieldName] = $this->formatValue($fieldName, $fieldValue);
    
    // set the changed flag
    $this->_isChanged = true;
    
    // clear validation errors on field if they exist
    if( array_key_exists($fieldName, $this->_validationErrors) ) {
      unset($this->_validationErrors[$fieldName]);
    }
  }
  
  /**
   * Formats a value based on the given data type. This does not validate the data
   * only ensures that it _becomes_ valid. You must validate it yourself before
   * using this method.
   *
   * @param string $column_type
   * @param mixed $fieldValue if null passed here, returns a default value
   * @return mixed a value suitable for db insertion
   * @static
   */
  function formatValue($fieldName, $fieldValue=null) {
    //todo: fix this? get rid of it?
    list($type, $maxlen, $required) = $this->fieldProps($fieldName);
    
    // check required fields
    if( $fieldValue !== true && !strlen($fieldValue) ) {
      return self::getEmptyValue($type);
    }
      
    // check the formatting matches the type specified
    switch($type) {
      case "alphanumeric":
        $val = Zen::checkAlphaNum($fieldValue);
        break;
      case "boolean":
        $fieldValue = $fieldValue? true : false;
        break;
      case "date":
        global $zen; //cheating
        $fieldValue = $zen->parseDate($fieldValue);
        break;
      case "email":
        $fieldValue = Zen::checkEmail($fieldValue);
        break;
      case "float":
        $neg = strpos($fieldValue, '-')===0;
        $fieldValue = Zen::checkNum($fieldValue, true);
        if( $neg ) { $fieldValue = 0 - $fieldValue; }
        break;
      case "int":
      case "id":
        $neg = strpos($fieldValue, '-')===0;
        $fieldValue = Zen::checkNum($fieldValue);
        if( $neg ) { $fieldValue = 0 - $fieldValue; }
        break;
      case "url":
        $fieldValue = Zen::checkUrl($fieldValue);
        break;
      case "string":
      default:
        break;
    }
    
    // check maximum length not exceeded
    if( $maxlen > 0 && strlen($fieldValue) > $maxlen ) {
      $fieldValue = substr($fieldValue, 0, $maxlen);
    }
      
    return $fieldValue;
  }
  
  /**
   * Return a suitable empty value for use with this field type
   *
   * @param string $fieldType
   * @static
   * @return mixed
   */
  function getEmptyValue($fieldType) {
    switch($fieldType) {
      case "boolean":
        return false;
        break;
      case "id":
      case "date":
      case "int":
        return 0;
        break;
      case "float":
        return 0.0;
        break;
      default:
        return null;
    }
  }
  
  /**
   * Set the value of a field to the appropriate null value based on field type.
   *
   * <p>Note that this is not intended for general use, since this method is called before the
   * class is fully initialized.
   *
   * <p>This method will set validation errors on any fields which are required, since
   * they are being set to an empty value for now. Thus, they must be set to a valid value
   * before a save can be attempted.
   * 
   * <p>Add a getDefaultValue() if more public applications of this logic are needed.
   *
   * @param string $fieldName is the db col from getColumnList()
   * @param array $type is the field properties from getColumnList()
   */
  function setEmptyValue( $fieldName, $type ) {
    $v = self::getEmptyValue($type);
    $notValid = self::invalidValue($fieldName, $v, $this->fieldProps($fieldName));
    if( $notValid ) { $this->_validationErrors[$fieldName] = $valid; }
    $this->_data[$fieldName] = $v;
  }
  
  /**
   * @param string $fieldName must exactly match the database column name
   * @return mixed a value of the type for the specified column
   * @return null if field doesn't exist
   */
  function getField( $fieldName ) {
    if( !array_key_exists($fieldName, $this->_data) ) {
      Zen::addDebug("ZenRecordBase::getField()", "Invlaid field requested ($fieldName)", 2);
      return null;
    }
    return $this->_data[$fieldName];
  }
  
  /**
   * @return mixed true if all fields in this record contain valid values and it is
   * ready to save to the database. Otherwise, this method returns an array of
   * error messages indicating the validation problems.
   */
  function isValid() {
    if( empty($this->_validationErrors) ) { return true; }
    return $this->_validationErrors;
  }
  
  /**
   * @return true if any values have changed since the record was loaded
   */
  function isChanged() { return $this->_changed; }
  
  /**
   * @return mixed (boolean)true if the record is valid and has been saved 
   * to the database. Returns an array if error(s) occurs (containing an
   * indexed array of fields and description of each error)
   */
  function save() {
    // we're just going to cheat here and call the global $zen object, since
    // we know that it must exist in the global scope for anything to run
    global $zen;
    
    // if nothing has changed, there's nothing to insert
    if( !$this->isChanged() ) { return array(tr("No values have changed, save skipped")); }
    
    // only try to insert valid data
    $valid = $this->isValid();
    if( $valid !== true ) { return $valid; }
    
    $table = $this->_sourceTable;
    $type = $this->_dataType;
    
    if( $this->_id == null ) {
      // this is a new record, insert it and set the id accordingly
      $id = $zen->db_insert($table,$this->_data);
      if( $id ) {
        $zen->addDebug("ZenRecordBase::save()", "New record of type $type created: $id", 3);
        $this->_id = $id; 
        return;
      }
      else {
        $zen->addDebug("ZenRecordBase::save()", "Could not insert a new record of type $type($table)", 1);
        return array(tr("Could not insert a new record of type ?", array($type))); 
      }
    }
    else {
      // this is an existing record, just update the values
      $zen->db_update( $table, $this->_idCol, $this->_id, $this->_data );
      if( $id ) {
        $zen->addDebug("ZenRecordBase::save()", "Updated record: $type($id)", 3);
        $this->_id = $id;
        return;
      }
      else {
        $zen->addDebug("ZenRecordBase::save()", "Could not update record $type($id)", 1);
        return array(tr("Could not update record with id ? of type ?", array($id, $type))); 
      }
    }
  }
  
  /**
   * Load a record from the database, reset the changed flag. Data
   * is assumed to be valid. This does not set the id field, it assumes
   * that is done already (you can use this to copy records)
   *
   * @param int $id must be a valid record id
   * @return boolean true if query succeeded
   * @protected don't call this directly, call the extending class!
   */
  function load($id) {
    $this->_isChanged = false;
    $id = Zen::checkNum($id);
    $query = "SELECT * FROM {$this->_sourceTable} WHERE {$this->_idCol} = $id";
    $vals = $this->db_quickIndexed($query);
    if( !empty($vals) ) { 
      $this->_data = $vals;
      return true;
    }
    else {
      $this->_data = array();
      return false;
    }
  }

  
  /**
   * @param string $fieldName
   * @param mixed $fieldValue
   * @param array $fieldProps pass props for column here, see $columnList in the constructor for format
   * @return false if the value provided is valid and can be saved into the field
   * specified. If not, provides a string with the error message.
   * @static
   */
  function invalidValue($fieldName, $fieldValue, $fieldProps) {
    //todo: fix this? get rid of it?
    list($type, $maxlen, $required) = $fieldProps;

    // check required fields
    if( $required && !strlen($fieldValue) ) { return tr("Required field was empty"); }
    
    // check maximum length not exceeded
    if( $maxlen > 0 && strlen($fieldValue) > $maxlen ) { return tr("Value cannot be longer than ? characters", array($maxlen)); }
      
    // don't bother validating empty values if not required
    if( !strlen($fieldValue) && $fieldValue !== true ) { 
      return $required? tr("Required field, cannot be empty") : false;
    }
      
    // check the formatting matches the type specified
    switch($type) {
    case "alphanumeric":
      $val = Zen::checkAlphaNum($fieldValue);
      if( $val === false || $val !== $fieldValue ) {
        return tr("Value may only contain these characters: 0-9, a-z, A-Z, or \"_\"");
      }
      break;
    case "boolean":
      if( !is_bool($fieldValue) && $field !== 0 && $field !== 1 ) { 
        return tr("Not a valid boolean value"); 
      }
    case "date":
      global $zen; //cheating
      if( preg_match("@^[0-9]+$@", $fieldValue) ) { return false; }
      else if( $zen->parseDate($fieldValue) <= 0 ) { return tr("Not a valid date"); }
    case "email":
      if( Zen::checkEmail($fieldValue)===false ) {
        return tr("Value is not a valid email address");
      }
      break;
    case "float":
      if( !preg_match("@^-?[0-9.]+$@", $fieldValue) ) { return tr("Only digits 0-9, decimal, and "-" allowed"); }
      break;
    case "int":
    case "id":
      if( $required && SfieldValue < 1 ) { return tr("Value is required; must be greater than zero"); }
      if( !preg_match("@^-?[0-9]+$@", $fieldValue) ) { return tr("Only digits 0-9 allowed"); }
      break;
    case "url":
      if( Zen::checkUrl($fieldValue) === false ) {
        return tr("Value is not a proper url");
      }
      break;
    case "string":
      return false;
    default:
      return tr("Invalid data type specified");
    }
    return false;
  }
  
  var $_columnList;
  var $_dataType;
  var $_sourceTable;
  var $_idCol;
  
  /** @var int the id of this record, null for new records (until they are saved) */
  var $_id;
  
  /** 
   * @var array the values for each field, in a keyed array: (string)column_name => (mixed)value
   * Generally, one wouldn't want to modify this directly, since the isChanged()
   * might become out of sync. Generally one would call setField() instead.
   */ 
  var $_data;
  
  /** @var boolean true if any value has changed since the initial load */
  var $_changed;
  
  /** 
   * @var array when empty, this class is in a valid state, when not empty, this is
   * a list of the error conditions. These are set and cleared each time setField()
   * is called, rather than waiting until a save or called to isValid() is requested.
   */
  var $_validationErrors;
  
}

?>
Return current item: ZenTrack - project/bug tracking software