Ferreteria/archive/data.php: Difference between revisions

From Woozle Writes Code
Jump to navigation Jump to search
(main data.php seems to have all the latest stuff)
(replace ancient version with more-or-less-current version from HostGator (being used on Issuepedia))
Line 5: Line 5:
/* ===========================
/* ===========================
  *** DATA UTILITY CLASSES ***
  *** DATA UTILITY CLASSES ***
HISTORY:
  AUTHOR: Woozle (Nick) Staddon
  2007-05-20 Wzl These classes have been designed to be db-engine agnostic, but I wasn't able
  HISTORY:
to test against anything other than MySQL nor was I able to implement the usage of
    2007-05-20 (wzl) These classes have been designed to be db-engine agnostic, but I wasn't able
the dbx_ functions, as the system that I was using didn't have them installed.
  to test against anything other than MySQL nor was I able to implement the usage of
  2007-08-30 Wzl posting this version at http://htyp.org/User:Woozle/data.php
  the dbx_ functions, as the system that I was using didn't have them installed.
  2007-12-24 Wzl Some changes seem to have been made as recently as 12/17, so posting updated version
    2007-08-30 (wzl) posting this version at http://htyp.org/User:Woozle/data.php
  2008-02-06 Wzl Modified to use either mysqli or (standard) mysql library depending on flag; the latter isn't working yet
    2007-12-24 (wzl) Some changes seem to have been made as recently as 12/17, so posting updated version
  2009-03-10 Wzl adding some static functions to gradually get rid of the need for object factories
    2008-02-06 (wzl) Modified to use either mysqli or (standard) mysql library depending on flag; the latter isn't working yet
  2009-03-18 Wzl debug constants now have defaults
    2009-03-10 (wzl) adding some static functions to gradually get rid of the need for object factories
  2009-03-26 Wzl clsDataSet.Query() no longer fetches first row; this will require some rewriting
    2009-03-18 (wzl) debug constants now have defaults
    NextRow() now returns TRUE if data was fetched; use if (data->NextRow()) {..} to loop through data.
    2009-03-26 (wzl) clsDataSet.Query() no longer fetches first row; this will require some rewriting
  2009-05-02 Wzl undocumented changes -- looks like:
      NextRow() now returns TRUE if data was fetched; use if (data->NextRow()) {..} to loop through data.
    assert-checks return ID of an insertion
    2009-05-02 (wzl) undocumented changes -- looks like:
    function ExecUpdate($iSet,$iWhere)
      assert-checks return ID of an insertion
    function SQLValue($iVal)
      function ExecUpdate($iSet,$iWhere)
  2009-05-03 Wzl more undocumented changes -- looks like mainly $iWhere is now optional in GetData()
      function SQLValue($iVal)
  2009-07-05 Wzl DataSet->__get now returns NULL if no field found; DataSet->HasField()
    2009-05-03 (wzl) more undocumented changes -- looks like mainly $iWhere is now optional in GetData()
  2009-07-18 Wzl clsTable::ExecUpdate() -> Update(); clsTable::Insert()
    2009-07-05 (wzl) DataSet->__get now returns NULL if no field found; DataSet->HasField()
  2009-08-02 Wzl clsDatabase::RowsAffected()
    2009-07-18 (wzl) clsTable::ExecUpdate() -> Update(); clsTable::Insert()
  2009-10-07 Wzl minor: $dbg global added to clsTable Update() and Insert() methods
    2009-08-02 (wzl) clsDatabase::RowsAffected()
  2009-10-27 Wzl clsTableCache
    2009-10-07 (wzl) minor: $dbg global added to clsTable Update() and Insert() methods
  2009-11-23 Wzl clsDatabase.LogSQL(); some format-tidying
    2009-10-27 (wzl) clsTableCache
  2009-12-29 Wzl clsDataSet_bare
    2009-11-23 (wzl) clsDatabase.LogSQL(); some format-tidying
  2010-01-02 Wzl clsTable::DataSet()
    2009-12-29 (wzl) clsDataSet_bare
  2010-01-08 Wzl ifEmpty()
    2010-01-02 (wzl) clsTable::DataSet()
  2010-01-09 Wzl fixed bug in clsDataSet_bare::Reload()
    2010-01-08 (wzl) ifEmpty()
  2010-02-07 Wzl clsTable::LastID();
    2010-01-09 (wzl) fixed bug in clsDataSet_bare::Reload()
    2010-02-07 (wzl) clsTable::LastID()
    2010-04-11 (wzl) clsTable::KeyValue()
    2010-05-28 (wzl) split() is now deprecated -- replacing it with preg_split()
    2010-06-14 (wzl) added $iClass=NULL parameter to clsTable::SpawnItem
    2010-06-16 (wzl) nzApp()
    2010-07-19 (wzl) clsDatabase::Make()
    2010-10-04 (wzl) clsTable::ActionKey()
    2010-10-05 (wzl) removed reloading code from clsDataSet::Update()
    2010-10-16 (wzl) added clsTable::NameSQL(), clsTable::DataSetGroup()
    2010-10-19 (wzl) clsTable::DataSQL()
    2010-11-01 (wzl) clsTable::GetItem iID=NULL now means create new/blank object, i.e. SpawnItem()
    2010-11-14 (wzl) clsDataSet_bare::SameAs()
    2010-11-21 (wzl) caching helper class
    2011-02-07 (wzl) SQLValue() now handles arrays too
  FUTURE:
    API FIXES:
      GetData() should not have an $iClass parameter, or it should be the last parameter.
*/
*/
// Select which DB library to use --
// Select which DB library to use --
Line 56: Line 73:
   // status
   // status
     private $strErr; // latest error message
     private $strErr; // latest error message
     public $sql; // last SQL executed (or attempted)
     public $sql; // last SQL executed (or attempted)
     public $arSQL; // array of all SQL statements attempted
     public $arSQL; // array of all SQL statements attempted
     public $doAllowWrite; // FALSE = don't execute UPDATE or INSERT commands, just log them
     public $doAllowWrite; // FALSE = don't execute UPDATE or INSERT commands, just log them
Line 70: Line 87:
       $this->doAllowWrite = TRUE; // default
       $this->doAllowWrite = TRUE; // default
       $this->cntOpen = 0;
       $this->cntOpen = 0;
      list($part1,$part2) = split('@',$iConn);
//      list($part1,$part2) = split('@',$iConn);
       list($this->strType,$this->strUser,$this->strPass) = split(':',$part1);
       $ar = preg_split('/@/',$iConn);
      if (array_key_exists(1,$ar)) {
  list($part1,$part2) = preg_split('/@/',$iConn);
      } else {
  throw new exception('Connection string not formatted right: ['.$iConn.']');
      }
//      list($this->strType,$this->strUser,$this->strPass) = split(':',$part1);
      list($this->strType,$this->strUser,$this->strPass) = preg_split('/:/',$part1);
       list($this->strHost,$this->strName) = explode('/',$part2);
       list($this->strHost,$this->strName) = explode('/',$part2);
       $this->strType = strtolower($this->strType); // make sure it is lowercased, for comparison
       $this->strType = strtolower($this->strType); // make sure it is lowercased, for comparison
Line 122: Line 146:
     public function GetUser() {
     public function GetUser() {
return $this->strUser;
return $this->strUser;
    }
    /*-----
      PURPOSE: generic table-creation function
      HISTORY:
2010-12-01 Added iID parameter to get singular item
2011-02-23 Changed from protected to public, to support class registration
    */
    public function Make($iName,$iID=NULL) {
if (!isset($this->$iName)) {
    $this->$iName = new $iName($this);
}
if (!is_null($iID)) {
    return $this->$iName->GetItem($iID);
} else {
    return $this->$iName;
}
     }
     }
     /*=====
     /*=====
Line 169: Line 209:
return $out;
return $out;
     }
     }
    /*----
      HISTORY:
2011-03-04 added DELETE to list of write commands; rewrote to be more robust
    */
     protected function OkToExecSQL($iSQL) {
     protected function OkToExecSQL($iSQL) {
if ($this->doAllowWrite) {
if ($this->doAllowWrite) {
Line 174: Line 218:
} else {
} else {
    // this is a bit of a kluge... need to strip out comments and whitespace
    // this is a bit of a kluge... need to strip out comments and whitespace
    // but basically, if the SQL starts with UPDATE or INSERT, then it's a write command so forbid it
    // but basically, if the SQL starts with UPDATE, INSERT, or DELETE, then it's a write command so forbid it
    if (strpos($iSQL,'UPDATE') === 0) return FALSE;
    $sql = strtoupper(trim($iSQL));
    if (strpos($iSQL,'INSERT') === 0) return FALSE;
    $cmd = preg_split (' ',$sql,1); // get just the first word
    return TRUE;
    switch ($cmd) {
      case 'UPDATE':
      case 'INSERT':
      case 'DELETE':
return FALSE;
      default:
return TRUE;
    }
}
}
     }
     }
     /*=====
     /*=====
       FUTURE: Exec() and _api_query() perform almost identical functions. Do we really need them both?
       FUTURE: Apparently _api_query() has very similar code. Eventually _api_query() should be a method in
an SQL engine class.
OLD NOTE: Exec() and _api_query() perform almost identical functions. Do we really need them both?
When we rewrite these as a single function, perhaps include a $iIsWrite flag parameter so we can
When we rewrite these as a single function, perhaps include a $iIsWrite flag parameter so we can
eliminate OkToExecSQL().
eliminate OkToExecSQL().
      HISTORY:
2011-02-24 Now passing $this->Conn to mysql_query() because somehow the connection was getting set
  to the wiki database instead of the original.
     */
     */
     public function Exec($iSQL) {
     public function Exec($iSQL) {
Line 190: Line 246:
if ($this->OkToExecSQL($iSQL)) {
if ($this->OkToExecSQL($iSQL)) {
    if (KF_USE_MYSQL) {
    if (KF_USE_MYSQL) {
$ok = mysql_query($iSQL);
$ok = mysql_query($iSQL,$this->Conn);
if (is_resource($ok)) { // this should never happen here
    $ok = TRUE;
}
    }
    }
    if (KF_USE_MYSQLI) {
    if (KF_USE_MYSQLI) {
Line 267: Line 326:
if ($this->OkToExecSQL($iSQL)) {
if ($this->OkToExecSQL($iSQL)) {
    if (KF_USE_MYSQL) {
    if (KF_USE_MYSQL) {
return mysql_query($iSQL,$this->Conn);
if (is_resource($this->Conn)) {
    return mysql_query($iSQL,$this->Conn);
} else {
    throw new Exception('Database Connection object is not a resource');
}
    }
    }
    if (KF_USE_MYSQLI) {
    if (KF_USE_MYSQLI) {
Line 324: Line 387:
  SECTION: OBJECT FACTORY
  SECTION: OBJECT FACTORY
*/
*/
     public function DataSet($iSQL = NULL,$iClass = NULL) {
     public function DataSet($iSQL=NULL,$iClass=NULL) {
CallEnter($this,__LINE__,__CLASS__.'.DataSet("'.$iSQL.'","'.$iClass.'")');
if (is_string($iClass)) {
if (is_string($iClass)) {
    $objData = new $iClass($this);
    $objData = new $iClass($this);
Line 336: Line 398:
    assert('is_object($objData)');
    assert('is_object($objData)');
}
}
assert('is_object($objData->Engine())');
if (!is_null($iSQL)) {
if (!is_null($iSQL)) {
    if (is_object($objData)) {
    if (is_object($objData)) {
Line 341: Line 404:
    }
    }
}
}
CallExit(__CLASS__.'.DataSet()');
return $objData;
return $objData;
     }
     }
}
}
/*=============
/*=============
| NAME: clsTable
  NAME: clsTable_abstract
| PURPOSE: objects for operating on particular tables
  PURPOSE: objects for operating on particular tables
    Does not attempt to deal with keys.
*/
*/
class clsTable {
abstract class clsTable_abstract {
     protected $objDB;
     protected $objDB;
     protected $vTblName;
     protected $vTblName;
    protected $vKeyName;
     protected $vSngClass; // name of singular class
     protected $vSngClass; // name of singular class
    public $sqlExec; // last SQL executed on this table


     public function __construct($iDB) {
     public function __construct(clsDatabase $iDB) {
$this->objDB = $iDB;
$this->objDB = $iDB;
     }
     }
     public function DB() {
     public function DB() { // DEPRECATED - use Engine()
return $this->objDB;
    }
    public function Engine() {
return $this->objDB;
return $this->objDB;
     }
     }
Line 367: Line 432:
}
}
return $this->vTblName;
return $this->vTblName;
    }
    public function NameSQL() {
assert('is_string($this->vTblName); /* '.print_r($this->vTblName,TRUE).' */');
return '`'.$this->vTblName.'`';
     }
     }
     public function ClassSng($iName=NULL) {
     public function ClassSng($iName=NULL) {
Line 374: Line 443:
return $this->vSngClass;
return $this->vSngClass;
     }
     }
     public function KeyName($iName=NULL) {
    /*----
if (!is_null($iName)) {
      ACTION: Make sure the item is ready to be released in the wild
    $this->vKeyName = $iName;
    */
    protected function ReleaseItem(clsRecs_abstract $iItem) {
$iItem->Table = $this;
$iItem->objDB = $this->objDB;
    }
    /*----
      ACTION: creates a new uninitialized singular object but sets the Table pointer back to self
      RETURNS: created object
    */
     public function SpawnItem($iClass=NULL) {
if (is_null($iClass)) {
    $strCls = $this->ClassSng();
} else {
    $strCls = $iClass;
}
}
return $this->vKeyName;
assert('!empty($strCls);');
$objItem = new $strCls;
$this->ReleaseItem($objItem);
return $objItem;
     }
     }
     // creates a new uninitialized singular object but sets the Table pointer back to self
     /*----
     public function SpawnItem() {
      RETURNS: dataset defined by the given SQL, wrapped in an object of the current class
      USAGE: primarily for joins where you want only records where there is no matching record
in the joined table. (If other examples come up, maybe a DataNoJoin() method would
be appropriate.)
    */
     public function DataSQL($iSQL) {
$strCls = $this->ClassSng();
$strCls = $this->ClassSng();
$objItem = new $strCls;
$obj = $this->Engine()->DataSet($iSQL,$strCls);
$objItem->Table = $this;
$this->ReleaseItem($obj);
$objItem->objDB = $this->objDB;
$this->sqlExec = $iSQL;
return $objItem;
return $obj;
     }
     }
     public function GetItem($iID,$iClass=NULL) {
    /*----
$objItem = $this->GetData($this->vKeyName.'='.SQLValue($iID),$iClass);
      RETURNS: dataset containing all fields from the current table,
$objItem->NextRow();
with additional options (everything after the table name) being
return $objItem;
defined by $iSQL, wrapped in the current object class.
    */
     public function DataSet($iSQL=NULL,$iClass=NULL) {
global $sql; // for debugging
 
$sql = 'SELECT * FROM '.$this->NameSQL();
if (!is_null($iSQL)) {
    $sql .= ' '.$iSQL;
}
return $this->DataSQL($sql);
/*
$strCls = $this->vSngClass;
$obj = $this->objDB->DataSet($sql,$strCls);
$obj->Table = $this;
return $obj;
*/
     }
     }
     public function GetData($iWhere=NULL,$iClass=NULL,$iSort=NULL) {
     public function GetData($iWhere=NULL,$iClass=NULL,$iSort=NULL) {
global $sql; // for debugging
global $sql; // for debugging


CallEnter($this,__LINE__,__CLASS__.'.'.__METHOD__.'("'.$iWhere.'","'.$iClass.'")');
$sql = 'SELECT * FROM '.$this->NameSQL();
$sql = 'SELECT * FROM `'.$this->vTblName.'`';
if (!is_null($iWhere)) {
if (!is_null($iWhere)) {
    $sql .= ' WHERE '.$iWhere;
    $sql .= ' WHERE '.$iWhere;
Line 404: Line 508:
    $sql .= ' ORDER BY '.$iSort;
    $sql .= ' ORDER BY '.$iSort;
}
}
/*
if (is_null($iClass)) {
if (is_null($iClass)) {
    $strCls = $this->vSngClass;
    $strCls = $this->vSngClass;
Line 409: Line 514:
    $strCls = $iClass;
    $strCls = $iClass;
}
}
CallExit('GetData() - SQL: '.$sql);
*/
$obj = $this->objDB->DataSet($sql,$strCls);
 
$obj->Table = $this;
//$obj = $this->objDB->DataSet($sql,$strCls);
//$res = $this->DB()->Exec($sql);
$obj = $this->SpawnItem($iClass);
assert('is_object($obj->Table);');
$obj->Query($sql);
 
$this->sqlExec = $sql;
if (!is_null($obj)) {
//     $obj->Table = $this; // 2011-01-20 this should be redundant now
    $obj->sqlMake = $sql;
}
return $obj;
return $obj;
     }
     }
     public function DataSet($iSQL=NULL) {
    /*----
$sql = 'SELECT * FROM `'.$this->vTblName.'`';
      RETURNS: SQL for creating a new record for the given data
if (!is_null($iSQL)) {
      HISTORY:
    $sql .= ' '.$iSQL;
2010-11-20 Created.
    */
     public function SQL_forInsert(array $iData) {
$sqlNames = '';
$sqlVals = '';
foreach($iData as $key=>$val) {
    if ($sqlNames != '') {
$sqlNames .= ',';
$sqlVals .= ',';
    }
    $sqlNames .= $key;
    $sqlVals .= $val;
}
}
$strCls = $this->vSngClass;
return 'INSERT INTO `'.$this->Name().'` ('.$sqlNames.') VALUES('.$sqlVals.');';
$obj = $this->objDB->DataSet($sql,$strCls);
$obj->Table = $this;
return $obj;
     }
     }
     public function Update($iSet,$iWhere) {
    /*----
      HISTORY:
2010-11-16 Added "array" requirement for iData
2010-11-20 Calculation now takes place in SQL_forInsert()
    */
     public function Insert(array $iData) {
global $sql;
global $sql;


if (is_array($iSet)) {
$sql = $this->SQL_forInsert($iData);
    $sqlSet = '';
$this->sqlExec = $sql;
    foreach($iSet as $key=>$val) {
return $this->objDB->Exec($sql);
if ($sqlSet != '') {
    }
    $sqlSet .= ',';
    /*----
}
      HISTORY:
$sqlSet .= ' '.$key.'='.$val;
2011-02-02 created for deleting topic-title pairs
    }
    */
    public function Delete($iFilt) {
$sql = 'DELETE FROM `'.$this->Name().'` WHERE '.$iFilt;
$this->sqlExec = $sql;
return $this->Engine()->Exec($sql);
    }
}
/*=============
  NAME: clsTable_keyed_abstract
  PURPOSE: adds abstract methods for dealing with keys
*/
abstract class clsTable_keyed_abstract extends clsTable_abstract {
 
    abstract public function GetItem();
    /*----
      PURPOSE: method for setting a key which uniquely refers to this table
Useful for logging, menus, and other text-driven contexts.
    */
    public function ActionKey($iName=NULL) {
if (!is_null($iName)) {
    $this->ActionKey = $iName;
}
return $this->ActionKey;
    }
    /*----
      INPUT:
$iData: array of data necessary to create a new record
  or update an existing one, if found
$iFilt: SQL defining what constitutes an existing record
  If NULL, MakeFilt() will be called to build this from $iData.
      HISTORY:
2011-02-22 created
2011-03-23 added madeNew and dataOld fields
  Nothing is actually using these yet, but that will probably change.
  For example, we might want to log when an existing record gets modified.
2011-03-31 why is this protected? Leaving it that way for now, but consider making it public.
    */
    public $madeNew,$dataOld; // additional status output
    protected function Make(array $iData,$iFilt=NULL) {
if (is_null($iFilt)) {
    $sqlFilt = $this->MakeFilt($iData);
} else {
    $sqlFilt = $iFilt;
}
$rs = $this->GetData($sqlFilt);
if ($rs->HasRows()) {
    assert('$rs->RowCount() == 1');
    $rs->NextRow();
 
    $this->madeNew = FALSE;
    $this->dataOld = $this->Values();
 
    $rs->Update($iData);
    $id = $rs->KeyValue();
} else {
} else {
    $sqlSet = ' '.$iSet;
    $this->Insert($iData);
    $id = $this->Engine()->NewID();
    $this->madeNew = TRUE;
}
}
return $id;
    }
// LATER:
//    abstract protected function MakeFilt(array $iData);
}
/*=============
  NAME: clsTable_key_single
  PURPOSE: table with a single key field
*/
class clsTable_key_single extends clsTable_keyed_abstract {
    protected $vKeyName;
    public function __construct(clsDatabase $iDB) {
parent::__construct($iDB);
$this->ClassSng('clsDataSet');
    }


$sql = 'UPDATE `'.$this->Name().'` SET'.$sqlSet.' WHERE '.$iWhere;
    public function KeyName($iName=NULL) {
$ok = $this->objDB->Exec($sql);
if (!is_null($iName)) {
    $this->vKeyName = $iName;
}
return $this->vKeyName;
    }
    /*----
      HISTORY:
2010-11-01 iID=NULL now means create new/blank object, i.e. SpawnItem()
    */
    public function GetItem($iID=NULL,$iClass=NULL) {
if (is_null($iID)) {
    $objItem = $this->SpawnItem($iClass);
    $objItem->KeyValue(NULL);
} else {
    $objItem = $this->GetData($this->vKeyName.'='.SQLValue($iID),$iClass);
    $objItem->NextRow();
}
return $objItem;
    }
    /*----
      INPUT:
iFields: array of source fields and their output names - specified as iFields[output]=input, because you can
  have a single input used for multiple outputs, but not vice-versa. Yes, this is confusing but that's how
  arrays are indexed.
      HISTORY:
2010-10-16 Created for VbzAdminCartLog::AdminPage()
    */
    public function DataSetGroup(array $iFields, $iGroupBy, $iSort=NULL) {
global $sql; // for debugging


if ($ok) {
foreach ($iFields AS $fDest => $fSrce) {
    // update this object's fields to the new values
    if(isset($sqlFlds)) {
    foreach ($iSet as $name=>$val) {
$sqlFlds .= ', ';
$this->row[$name] = $val;
    } else {
$sqlFlds = '';
    }
    }
    $sqlFlds .= $fSrce.' AS '.$fDest;
}
$sql = 'SELECT '.$sqlFlds.' FROM '.$this->NameSQL().' GROUP BY '.$iGroupBy;
if (!is_null($iSort)) {
    $sql .= ' ORDER BY '.$iSort;
}
$obj = $this->objDB->DataSet($sql);
return $obj;
    }
    /*----
      HISTORY:
2010-11-20 Created
    */
    public function SQL_forUpdate(array $iSet,$iWhere) {
$sqlSet = '';
foreach($iSet as $key=>$val) {
    if ($sqlSet != '') {
$sqlSet .= ',';
    }
    $sqlSet .= ' `'.$key.'`='.$val;
}
}


return $ok;
return 'UPDATE `'.$this->Name().'` SET'.$sqlSet.' WHERE '.$iWhere;
     }
     }
     public function Insert($iData) {
    /*----
      HISTORY:
2010-10-05 Commented out code which updated the row[] array from iSet's values.
  * It doesn't work if the input is a string instead of an array.
  * Also, it seems like a better idea to actually re-read the data if
    we really need to update the object.
2010-11-16 Added "array" requirement for iSet; removed code for handling
  iSet as a string. If we want to support single-field updates, make a
  new method: UpdateField($iField,$iVal,$iWhere). This makes it easier
  to support automatic updates of certain fields in descendent classes
  (e.g. updating a WhenEdited timestamp).
2010-11-20 Calculation now takes place in SQL_forUpdate()
    */
     public function Update(array $iSet,$iWhere) {
global $sql;
global $sql;


$sqlNames = '';
$sql = $this->SQL_forUpdate($iSet,$iWhere);
$sqlVals = '';
$this->sqlExec = $sql;
foreach($iData as $key=>$val) {
$ok = $this->objDB->Exec($sql);
    if ($sqlNames != '') {
$sqlNames .= ',';
$sqlVals .= ',';
    }
    $sqlNames .= $key;
    $sqlVals .= $val;
}


$sql = 'INSERT INTO `'.$this->Name().'` ('.$sqlNames.') VALUES('.$sqlVals.');';
return $ok;
return $this->objDB->Exec($sql);
     }
     }
     public function LastID() {
     public function LastID() {
$strKey = $this->vKeyName;
$strKey = $this->vKeyName;
$sql = 'SELECT '.$strKey.' FROM `'.$this->Name().'` ORDER BY '.$strKey.' DESC LIMIT 1;';
$sql = 'SELECT '.$strKey.' FROM `'.$this->Name().'` ORDER BY '.$strKey.' DESC LIMIT 1;';
$objRows = $this->objDB->DataSet($sql);
$objRows = $this->objDB->DataSet($sql);
if ($objRows->HasRows()) {
if ($objRows->HasRows()) {
    $objRows->NextRow();
    $objRows->NextRow();
Line 480: Line 735:
}
}
     }
     }
    /*----
      HISTORY:
2011-02-22 created
      IMPLEMENTATION:
KeyName must equal KeyValue
    */
    protected function MakeFilt(array $iData) {
return $this->KeyName().'='.SQLValue($this->KeyValue());
    }
}
// alias -- sort of a default table type
class clsTable extends clsTable_key_single {
}
}
// DEPRECATED -- use clsCache_Table helper class
class clsTableCache extends clsTable {
class clsTableCache extends clsTable {
     private $arCache;
     private $arCache;


     public function GetItem($iID,$iClass=NULL) {
     public function GetItem($iID=NULL,$iClass=NULL) {
if (!isset($this->arCache[$iID])) {
if (!isset($this->arCache[$iID])) {
    $objItem = $this->GetData($this->vKeyName.'='.SQLValue($iID),$iClass);
    $objItem = $this->GetData($this->vKeyName.'='.SQLValue($iID),$iClass);
Line 493: Line 762:
     }
     }
}
}
/*====
  CLASS: cache for Tables
  ACTION: provides a cached GetItem()
  USAGE: clsTable descendants should NOT override GetItem() or GetData() to use this class,
    as the class needs those methods to load data into the cache.
  BOILERPLATE:
    protected $objCache;
    protected function Cache() {
if (!isset($this->objCache)) {
    $this->objCache = new clsCache_Table($this);
}
return $this->objCache;
    }
    public function GetItem_Cached($iID=NULL,$iClass=NULL) {
return $this->Cache()->GetItem($iID,$iClass);
    }
    public function GetData_Cached($iWhere=NULL,$iClass=NULL,$iSort=NULL) {
return $this->Cache()->GetItem($iWhere,$iClass,$iSort);
    }
*/
/*----
*/
class clsCache_Table {
    protected $objTbl;
    protected $arRows; // arRows[id] = rows[]
    protected $arSets; // caches entire datasets
    public function __construct(clsTable $iTable) {
$this->objTbl = $iTable;
    }
    public function GetItem($iID=NULL,$iClass=NULL) {
$objTbl = $this->objTbl;
if (isset($this->arRows[$iID])) {
    $objItem = $objTbl->SpawnItem($iClass);
    $objItem->Row = $this->arCache[$iID];
} else {
    $objItem = $objTbl->GetItem($iID,$iClass);
    $this->arCache[$iID] = $objItem->Row;
}
return $objItem;
    }
    /*----
      HISTORY:
2011-02-11 Renamed GetData_Cached() to GetData()
  This was probably a leftover from before multiple inheritance
  Fixed some bugs. Renamed from GetData() to GetData_array()
    because caching the resource blob didn't seem to work very well.
  Now returns an array instead of an object.
      FUTURE: Possibly we should be reading all rows into memory, instead of just saving the Res.
That way, Res could be protected again instead of public.
    */
    public function GetData_array($iWhere=NULL,$iClass=NULL,$iSort=NULL) {
$objTbl = $this->objTbl;
$strKeyFilt = "$iWhere\t$iSort";
$isCached = FALSE;
if (is_array($this->arSets)) {
    if (array_key_exists($strKeyFilt,$this->arSets)) {
$isCached = TRUE;
    }
}
if ($isCached) {
    //$objSet = $objTbl->SpawnItem($iClass);
    //$objSet->Res = $this->arSets[$strKey];
    //assert('is_resource($objSet->Res); /* KEY='.$strKey.'*/');


class clsDataSet_bare {
    // 2011-02-11 this code has not been tested yet
     public $objDB; // ugh, I hate this; it should be just "DB".
//echo '<pre>'.print_r($this->arSets,TRUE).'</pre>';
     public $Res; // native result set
    foreach ($this->arSets[$strKeyFilt] as $key) {
     public $Row; // data from the active row
$arOut[$key] = $this->arRows[$key];
    public $Table; // optional: table object
    }
} else {
    $objSet = $objTbl->GetData($iWhere,$iClass,$iSort);
    while ($objSet->NextRow()) {
$strKeyRow = $objSet->KeyString();
$arOut[$strKeyRow] = $objSet->Values();
$this->arSets[$strKeyFilt][] = $strKeyRow;
    }
    if (is_array($this->arRows)) {
$this->arRows = array_merge($this->arRows,$arOut); // add to cached rows
    } else {
$this->arRows = $arOut; // start row cache
    }
}
return $arOut;
    }
}
/*=============
  NAME: clsRecs_abstract -- abstract recordset
    Does not deal with keys.
*/
abstract class clsRecs_abstract {
     public $objDB; // deprecated; use Engine()
    public $sqlMake; // optional: SQL used to create the dataset -- used for reloading
    public $sqlExec; // last SQL executed on this dataset
    public $Table; // public access deprecated; use Table()
     protected $Res; // native result set
     public $Row; // public access deprecated; use Values()/Value() (data from the active row)


     public function __construct(clsDatabase $iDB=NULL, $iRes=NULL, array $iRow=NULL) {
     public function __construct(clsDatabase $iDB=NULL, $iRes=NULL, array $iRow=NULL) {
CallEnter($this,__LINE__,__CLASS__.'.'.__FUNCTION__.'(['.get_class($iDB).'])');
$this->objDB = $iDB;
$this->objDB = $iDB;
$this->Res = $iRes;
$this->Res = $iRes;
$this->Row = $iRow;
$this->Row = $iRow;
CallExit(__CLASS__.'.'.__FUNCTION__.'()');
$this->InitVars();
    }
    protected function InitVars() {
    }
    protected function Table(clsTable_abstract $iTable=NULL) {
if (!is_null($iTable)) {
    $this->Table = $iTable;
}
return $this->Table;
     }
     }
     /*-----
     public function Engine() {
       FUNCTION: KeyValue()
if (is_null($this->objDB)) {
    assert('!is_null($this->Table()); /* SQL: '.$this->sqlMake.' */');
    return $this->Table()->Engine();
} else {
    return $this->objDB;
}
    }
    /*----
       RETURNS: associative array of fields/values for the current row
      HISTORY:
2011-01-08 created
2011-01-09 actually working; added option to write values
     */
     */
     public function KeyValue() {
     public function Values(array $iRow=NULL) {
$strKeyName = $this->Table->KeyName();
if (is_array($iRow)) {
return $this->Row[$strKeyName];
    $this->Row = $iRow;
}
return $this->Row;
     }
     }
     /*-----
     /*----
       FUNCTION: SelfFilter()
       FUNCTION: Value(name)
       RETURNS: SQL for WHERE clause which will select only the current row, based on KeyValue()
       RETURNS: Value of named field
       USED BY: Update(), Reload()
       HISTORY:
2010-11-19 Created to help with data-form processing.
2010-11-26 Added value-setting, so we can set defaults for new records
2011-02-09 replaced direct call to array_key_exists() with call to new function HasValue()
     */
     */
     public function SelfFilter() {
     public function Value($iName,$iVal=NULL) {
$strKeyName = $this->Table->KeyName();
if (is_null($iVal)) {
//$sqlWhere = $strKeyName.'='.$this->$strKeyName;
    if (!$this->HasValue($iName)) {
//$sqlWhere = $strKeyName.'='.$this->Row[$strKeyName];
echo '<pre>'.print_r($this->Row,TRUE).'</pre>';
$sqlWhere = $strKeyName.'='.$this->KeyValue();
throw new Exception('Attempted to read nonexistent field "'.$iName.'" in class '.get_class($this));
return $sqlWhere;
    }
} else {
    $this->Row[$iName] = $iVal;
}
return $this->Row[$iName];
     }
     }
 
    /*----
     public function Query($iSQL) {
      PURPOSE: Like Value() but handles new records gracefully, and is read-only
CallEnter($this,__LINE__,__CLASS__.'.'.__FUNCTION__.'('.$iSQL.')');
      HISTORY:
$this->Res = $this->objDB->_api_query($iSQL);
2011-02-12 written
assert('is_resource($this->Res) /* SQL='.$iSQL.' */');
    */
CallExit(__CLASS__.'.'.__FUNCTION__.'()');
     public function ValueNz($iName,$iDefault=NULL) {
if ($this->HasValue($iName)) {
    return $this->Value($iName);
} else {
    return $iDefault;
}
     }
     }
     /*-----
     /*----
       ACTION: Reloads only the current row unless $iFilt is set
       HISTORY:
2011-02-09 created so we can test for field existence before trying to access
     */
     */
     public function Reload($iFilt=NULL) {
     public function HasValue($iName) {
$sql = 'SELECT * FROM `'.$this->Table->Name().'` WHERE ';
if (is_array($this->Row)) {
if (is_null($iFilt)) {
    return array_key_exists($iName,$this->Row);
    $sql .= $this->SelfFilter();
} else {
} else {
    $sql .= $iFilt;
    return FALSE;
}
}
$this->Query($sql);
$this->NextRow();
     }
     }
     /*-----
     /*----
       ACTION: Saves the data in $iSet to the current record (or records filtered by $iWhere)
      FUNCTION: Clear();
       ACTION: Clears Row[] of any leftover data
     */
     */
     public function Update($iarSet,$iWhere=NULL) {
     public function Clear() {
$doIns = FALSE;
$this->Row = NULL;
if (is_null($iWhere)) {
    }
// default: modify the current record
    public function Query($iSQL) {
// build SQL filter for just the current record
$this->Res = $this->Engine()->_api_query($iSQL);
    $sqlWhere = $this->SelfFilter();
$this->sqlMake = $iSQL;
} else {
if (!is_resource($this->Res)) {
    $sqlWhere = $iWhere;
    throw new exception ('SQL='.$iSQL);
}
}
return $this->Table->Update($iarSet,$sqlWhere);
     }
     }
     /*-----
     /*----
       ACTION: Saves to the current record; creates a new record if ID is 0 or NULL
       ACTION: Checks given values for any differences from current values
      RETURNS: TRUE if all values are same
     */
     */
     public function Make(array $iarSet) {
     public function SameAs(array $iValues) {
$strKeyName = $this->Table->KeyName();
$isSame = TRUE;
if (empty($this->Row[$strKeyName])) {
foreach($iValues as $name => $value) {
    return $this->Table->Insert($iarSet);
    $oldVal = $this->Row[$name];
} else {
    if ($oldVal != $value) {
    return $this->Update($iarSet);
$isSame = FALSE;
    }
 
}
}
return $isSame;
     }
     }
     /*-----
     /*-----
Line 615: Line 1,009:
return $this->hasRow();
return $this->hasRow();
     }
     }
}
/*=============
  NAME: clsRecs_keyed_abstract -- abstract recordset for keyed data
    Adds abstract and concrete methods for dealing with keys.
*/
abstract class clsRecs_keyed_abstract extends clsRecs_abstract {
    // ABSTRACT methods
    abstract public function SelfFilter();
    abstract public function KeyString();
    abstract public function SQL_forUpdate(array $iSet);
    abstract public function SQL_forMake(array $iSet);


    /*-----
      ACTION: Saves the data in $iSet to the current record (or records filtered by $iWhere)
      HISTORY:
2010-11-20 Calculation now takes place in SQL_forUpdate()
2010-11-28 SQL saved to table as well, for when we might be doing Insert() or Update()
  and need a single place to look up the SQL for whatever happened.
    */
    public function Update(array $iSet,$iWhere=NULL) {
//$ok = $this->Table->Update($iSet,$sqlWhere);
$sql = $this->SQL_forUpdate($iSet,$iWhere);
//$this->sqlExec = $this->Table->sqlExec;
$this->sqlExec = $sql;
$this->Table->sql = $sql;
$ok = $this->objDB->Exec($sql);
return $ok;
    }
}
/*=============
  NAME: clsDataSet_bare
  DEPRECATED - USE clsRecs_key_single INSTEAD
  PURPOSE: base class for datasets, with single key
    Does not add field overloading. Field overloading seems to have been a bad idea anyway;
      Use Value() instead.
*/
class clsRecs_key_single extends clsRecs_keyed_abstract {
    /*----
      HISTORY:
2010-11-01 iID=NULL now means object does not have data from an existing record
    */
    public function IsNew() {
return is_null($this->KeyValue());
    }
    /*-----
      FUNCTION: KeyValue()
    */
    public function KeyValue($iVal=NULL) {
if (!is_object($this->Table)) {
    throw new Exception('Recordset needs a Table object to retrieve key value.');
}
$strKeyName = $this->Table->KeyName();
assert('!empty($strKeyName); /* TABLE: '.$this->Table->Name().' */');
assert('is_string($strKeyName); /* TABLE: '.$this->Table->Name().' */');
if (is_null($iVal)) {
    if (!isset($this->Row[$strKeyName])) {
$this->Row[$strKeyName] = NULL;
    }
} else {
    $this->Row[$strKeyName] = $iVal;
}
return $this->Row[$strKeyName];
    }
    public function KeyString() {
return (string)$this->KeyValue();
    }
    /*----
      FUNCTION: Load_fromKey()
      ACTION: Load a row of data whose key matches the given value
      HISTORY:
2010-11-19 Created for form processing.
    */
    public function Load_fromKey($iKeyValue) {
$this->sqlMake = NULL;
$this->KeyValue($iKeyValue);
$this->Reload();
    }
    /*-----
      FUNCTION: SelfFilter()
      RETURNS: SQL for WHERE clause which will select only the current row, based on KeyValue()
      USED BY: Update(), Reload()
    */
    public function SelfFilter() {
if (!is_object($this->Table)) {
    throw new exception('Table not set in class '.get_class($this));
}
$strKeyName = $this->Table->KeyName();
//$sqlWhere = $strKeyName.'='.$this->$strKeyName;
//$sqlWhere = $strKeyName.'='.$this->Row[$strKeyName];
$sqlWhere = '`'.$strKeyName.'`='.$this->KeyValue();
return $sqlWhere;
    }
    /*-----
      ACTION: Reloads only the current row unless $iFilt is set
      TO DO: iFilt should probably be removed, now that we save
the creation SQL in $this->sql.
    */
    public function Reload($iFilt=NULL) {
if (is_string($this->sqlMake)) {
    $sql = $this->sqlMake;
} else {
    $sql = 'SELECT * FROM `'.$this->Table->Name().'` WHERE ';
    if (is_null($iFilt)) {
$sql .= $this->SelfFilter();
    } else {
$sql .= $iFilt;
    }
}
$this->Query($sql);
$this->NextRow();
    }
    /*----
      HISTORY:
2010-11-20 Created
    */
    public function SQL_forUpdate(array $iSet,$iWhere=NULL) {
$doIns = FALSE;
if (is_null($iWhere)) {
// default: modify the current record
// build SQL filter for just the current record
    $sqlWhere = $this->SelfFilter();
} else {
    $sqlWhere = $iWhere;
}
return $this->Table->SQL_forUpdate($iSet,$sqlWhere);
    }
    /*----
      HISTORY:
2010-11-23 Created
    */
    public function SQL_forMake(array $iarSet) {
$strKeyName = $this->Table->KeyName();
if ($this->IsNew()) {
    $sql = $this->Table->SQL_forInsert($iarSet);
} else {
    $sql = $this->SQL_forUpdate($iarSet);
}
return $sql;
    }
    /*-----
      ACTION: Saves to the current record; creates a new record if ID is 0 or NULL
      HISTORY:
2010-11-03
  Now uses this->IsNew() to determine whether to use Insert() or Update()
  Loads new ID into KeyValue
    */
    public function Make(array $iarSet) {
if ($this->IsNew()) {
    $ok = $this->Table->Insert($iarSet);
    $this->KeyValue($this->objDB->NewID());
    return $ok;
} else {
    return $this->Update($iarSet);
}
    }
    // DEPRECATED -- should be a function of Table type
     public function HasField($iName) {
     public function HasField($iName) {
return isset($this->Row[$iName]);
return isset($this->Row[$iName]);
     }
     }
    // DEPRECATED - use Values()
     public function RowCopy() {
     public function RowCopy() {
$strClass = get_class($this);
$strClass = get_class($this);
Line 636: Line 1,186:
}
}
     }
     }
}
// alias -- a sort of default dataset type
class clsDataSet_bare extends clsRecs_key_single {
}
}
/*
/*
PURPOSE: clsDataSet with overloaded field access methods
  PURPOSE: clsDataSet with overloaded field access methods
   This has turned out to be more problematic than useful, so I'm deprecating it.
   DEPRECATED -- This has turned out to be more problematic than useful.
  Retained only for compatibility with existing code; hope to eliminate eventually.
    Retained only for compatibility with existing code; hope to eliminate eventually.
*/
*/
class clsDataSet extends clsDataSet_bare {
class clsDataSet extends clsRecs_key_single {
   // -- accessing individual fields
   // -- accessing individual fields
     public function __set($iName, $iValue) {
     public function __set($iName, $iValue) {
Line 656: Line 1,209:
     /*-----
     /*-----
       FUNCTION: KeyValue()
       FUNCTION: KeyValue()
Really, this should be redundant. Replace usages of __set() and __get with something like Value(),
  which can then be overridden if needed.
     */
     */
/*
     public function KeyValue() {
     public function KeyValue() {
$strKeyName = $this->Table->KeyName();
$strKeyName = $this->Table->KeyName();
return $this->$strKeyName;
return $this->$strKeyName;
     }
     }
*/
}
// HELPER CLASSES
/*====
  CLASS: Table Indexer
*/
class clsIndexer_Table {
    private $objTbl; // clsTable object
    public function __construct(clsTable_abstract $iObj) {
$this->objTbl = $iObj;
    }
    /*----
      RETURNS: newly-created clsIndexer_Recs-descended object
Override this method to change how indexing works
    */
    protected function NewRecsIdxer(clsRecs_indexed $iRecs) {
return new clsIndexer_Recs($iRecs,$this);
    }
    public function TableObj() {
return $this->objTbl;
    }
    public function InitRecs(clsRecs_indexed $iRecs) {
$objIdx = $this->NewRecsIdxer($iRecs);
$iRecs->Indexer($objIdx);
return $objIdx;
    }
}
/*====
  HISTORY:
    2011-01-19 Started; not ready yet -- just saving bits of code I know I will need
*/
class clsIndexer_Table_single_key extends clsIndexer_Table {
    protected function NewRecsIdxer(clsRecs_indexed $iRecs) {
return new clsIndexer_Recs_single_key($iRecs,$this);
    }
    public function KeyName($iName=NULL) {
if (!is_null($iName)) {
    $this->vKeyName = $iName;
}
return $this->vKeyName;
    }
    public function GetItem($iID=NULL,$iClass=NULL) {
if (is_null($iID)) {
    $objItem = $this->SpawnItem($iClass);
    $objItem->KeyValue(NULL);
} else {
    assert('!is_array($iID); /* TABLE='.$this->TableObj()->Name().' */');
    $objItem = $this->TableObj()->GetData($this->vKeyName.'='.SQLValue($iID),$iClass);
    $objItem->NextRow();
}
return $objItem;
    }
}
class clsIndexer_Table_multi_key extends clsIndexer_Table {
    private $arKeys;
    /*----
      RETURNS: newly-created clsIndexer_Recs-descended object
Override this method to change how indexing works
    */
    protected function NewRecsIdxer(clsRecs_indexed $iRecs) {
return new clsIndexer_Recs_multi_key($iRecs,$this);
    }
    public function KeyNames(array $iNames=NULL) {
if (!is_null($iNames)) {
    $this->arKeys = $iNames;
}
return $this->arKeys;
    }
    /*----
      HISTORY:
2011-01-08 written
    */
    public function GetItem(array $iVals=NULL) {
if (is_null($iVals)) {
    $objItem = $this->TableObj()->SpawnItem();
} else {
    $sqlFilt = $this->SQL_for_filter($iVals);
    $objItem = $this->TableObj()->GetData($sqlFilt);
    $objItem->NextRow();
}
return $objItem;
    }
    /*----
      RETURNS: SQL to filter for the current record by key value(s)
'(name=value) AND (name=value) AND...'
      INPUT: array of keynames and values
array[name] = value
      USED BY: GetItem()
      HISTORY:
2011-01-08 written
2011-01-19 replaced with boilerplate call to indexer in clsIndexer_Table
    */
/*
    protected function SQL_for_filter(array $iVals) {
$arVals = $iVals;
$arKeys = $this->KeyNames();
$sql = NULL;
foreach ($arKeys as $name) {
    $val = $arVals[$name];
    if (!is_null($sql)) {
$sql .= ' AND ';
    }
    $sql .= '('.$name.'='.SQLValue($val).')';
}
return $sql;
    }
*/
    /*----
      RETURNS: SQL for creating a new record for the given data
      HISTORY:
2010-11-20 Created.
2011-01-08 adapted from clsTable::Insert()
    */
    public function SQL_forInsert(array $iData) {
$sqlNames = '';
$sqlVals = '';
foreach($iData as $key=>$val) {
    if ($sqlNames != '') {
$sqlNames .= ',';
$sqlVals .= ',';
    }
    $sqlNames .= '`'.$key.'`';
    $sqlVals .= $val;
}
return 'INSERT INTO `'.$this->Name().'` ('.$sqlNames.') VALUES('.$sqlVals.');';
    }
}
}
/*====
  CLASS: clsIndexer_Recs -- record set indexer
  PURPOSE: Handles record sets for tables with multiple keys
  HISTORY:
    2010-?? written for clsCacheFlows/clsCacheFlow in cache.php
    2011-01-08 renamed, revised and clarified
*/
abstract class clsIndexer_Recs {
    private $objData;
    private $objTbl; // table indexer


    // ABSTRACT functions
    abstract public function IndexIsSet();
    abstract public function KeyString();
    abstract public function SQL_forWhere();
    /*----
      INPUT:
iObj = DataSet
iKeys = array of field names
    */
    public function __construct(clsRecs_keyed_abstract $iData,clsIndexer_Table $iTable) {
$this->objData = $iData;
$this->objTbl = $iTable;
    }
    public function DataObj() {
return $this->objData;
    }
    public function TblIdxObj() {
assert('is_a($this->objTbl,"clsIndexer_Table"); /* CLASS='.get_class($this->objTbl).' */');
return $this->objTbl;
    }
    public function Engine() {
return $this->DataObj()->Engine();
    }
    public function TableObj() {
return $this->TblIdxObj()->TableObj();
    }
    public function TableName() {
return $this->TableObj()->Name();
    }
/*
    public function Keys() {
$arKeys = $this->objTbl->Keys();
reset($arKeys);
return $arKeys;
    }
*/
    /*-----
      FUNCTION: KeyValue()
      IN/OUT: array of key values
array[key name] = value
      USED BY: clsCacheFlow::KeyValue()
    */
/*
    public function KeyValue(array $iVals=NULL) {
$arKeys = $this->KeyNames();
$arRow = $this->DataObj()->Row;
if (is_array($iVals)) {
    foreach ($iVals as $val) {
list($key) = each($arKeys);
$arRow[$key] = $val;
    }
    $this->DataObj()->Row = $arRow;
}
foreach ($arKeys as $key) {
    $arOut[$key] = $arRow[$key];
}
return $arOut;
    }
*/
    /*----
      FUNCTION: KeyString()
      IN/OUT: prefix-delimited string of all key values
      QUERY: What uses this?
    */
/*
    public function KeyString($iVals=NULL) {
if (is_string($iVals)) {
    $xts = new xtString($iVals);
    $arVals = $xts->Xplode();
    $arKeys = $this->Keys();
    foreach ($arVals as $val) {
list($key) = each($arKeys);
$this->Row[$key] = $val;
    }
}
$out = '';
foreach ($this->arKeys as $key) {
    $val = $this->Row[$key];
    $out .= '.'.$val;
}
return $out;
    }
*/
/*
    public function KeyValue(array $iVals=NULL) {
$arKeys = $this->KeyNames();
$arRow = $this->DataObj()->Row;
if (is_array($iVals)) {
    foreach ($iVals as $val) {
list($key) = each($arKeys);
$arRow[$key] = $val;
    }
    $this->DataObj()->Row = $arRow;
}
foreach ($arKeys as $key) {
    $arOut[$key] = $arRow[$key];
}
return $arOut;
    }
*/
/* There's no need for this here; it doesn't require indexing
    public function SQL_forInsert() {
return $this->TblIdxObj()->SQL_forInsert($this->KeyValues());
    }
*/
    /*----
      INPUT:
iSet: array specifying fields to update and the values to update them to
  iSet[field name] = value
      HISTORY:
2010-11-20 Created
2011-01-09 Adapted from clsDataSet_bare
    */
    public function SQL_forUpdate(array $iSet) {
$sqlSet = '';
foreach($iSet as $key=>$val) {
    if ($sqlSet != '') {
$sqlSet .= ',';
    }
    $sqlSet .= ' `'.$key.'`='.$val;
}
$sqlWhere = $this->SQL_forWhere();
return 'UPDATE `'.$this->TableName().'` SET'.$sqlSet.' WHERE '.$sqlWhere;
    }
    /*----
      HISTORY:
2010-11-16 Added "array" requirement for iData
2010-11-20 Calculation now takes place in SQL_forInsert()
2011-01-08 adapted from clsTable::Insert()
    */
/* There's no need for this here; it doesn't require indexing
    public function Insert(array $iData) {
global $sql;
$sql = $this->SQL_forInsert($iData);
$this->sql = $sql;
return $this->Engine()->Exec($sql);
    }
*/
}
class clsIndexer_Recs_single_key extends clsIndexer_Recs {
    private $vKeyName;
    public function KeyName() {
return $this->TblIdxObj()->KeyName();
    }
    public function KeyValue() {
return $this->DataObj()->Value($this->KeyName());
    }
    public function KeyString() {
return (string)$this->KeyValue();
    }
    public function IndexIsSet() {
return !is_null($this->KeyValue());
    }
    public function SQL_forWhere() {
$sql = $this->KeyName().'='.SQLValue($this->KeyValue());
return $sql;
    }
}
class clsIndexer_Recs_multi_key extends clsIndexer_Recs {
    /*----
      RETURNS: Array of values which constitute this row's key
array[key name] = key value
    */
    public function KeyArray() {
$arKeys = $this->TblIdxObj()->KeyNames();
$arRow = $this->DataObj()->Row;
foreach ($arKeys as $key) {
    $arOut[$key] = $arRow[$key];
}
return $arOut;
    }
    /*----
      ASSUMES: keys will always be returned in the same order
If this changes, add field names.
      POTENTIAL BUG: Non-numeric keys might contain the separator character
that we are currently using ('.'). Some characters may not be appropriate
for some contexts. The caller should be able to specify what separator it wants.
    */
    public function KeyString() {
$arKeys = $this->KeyArray();
$out = NULL;
foreach ($arKeys as $name=>$val) {
    $out .= '.'.$val;
}
return $out;
    }
  /*----
      RETURNS: TRUE if any index fields are NULL
      ASSUMES: An index may not contain any NULL fields. Perhaps this is untrue, and it should
only return TRUE if *all* index fields are NULL.
    */
    public function IndexIsSet() {
$arKeys = $this->KeyArray();
$isset = TRUE;
foreach ($arKeys as $key=>$val) {
    if (is_null($val)) { $isset = FALSE; }
}
return $isset;
    }
    /*----
      RETURNS: SQL to filter for the current record by key value(s)
      HISTORY:
2011-01-08 written for Insert()
2011-01-19 moved from clsIndexer_Recs to clsIndexer_Recs_multi_key
    */
    public function SQL_forWhere() {
$arVals = $this->TblIdxObj()->KeyNames();
return SQL_for_filter($arVals);
    }
}
/*=============
  NAME: clsTable_indexed
  PURPOSE: handles indexes via a helper object
*/
class clsTable_indexed extends clsTable_keyed_abstract {
    protected $objIdx;
    /*----
      NOTE: In practice, how would you ever have the Indexer object created before the Table object,
since the Indexer object requires a Table object in its constructor? Possibly descendent classes
can create the Indexer in their constructors and then pass it back to the parent constructor,
which lets you have a default Indexer that you can override if you need, but how useful is this?
    */
    public function __construct(clsDatabase $iDB, clsIndexer_Table $iIndexer=NULL) {
parent::__construct($iDB);
$this->Indexer($iIndexer);
    }
    // BOILERPLATE BEGINS
    protected function Indexer(clsIndexer_Table $iObj=NULL) {
if (!is_null($iObj)) {
    $this->objIdx = $iObj;
}
return $this->objIdx;
    }
    public function GetItem(array $iVals=NULL) {
return $this->Indexer()->GetItem($iVals);
    }
    // BOILERPLATE ENDS
    // OVERRIDES
    /*----
      ADDS: spawns an indexer and attaches it to the item
    */
    protected function ReleaseItem(clsRecs_abstract $iItem) {
parent::ReleaseItem($iItem);
$this->Indexer()->InitRecs($iItem);
    }
    /*----
      ADDS: spawns an indexer and attaches it to the item
    */
/*
    public function SpawnItem($iClass=NULL) {
$obj = parent::SpawnItem($iClass);
return $obj;
    }
*/
}
/*=============
  NAME: clsRecs_indexed
*/
class clsRecs_indexed extends clsRecs_keyed_abstract {
    protected $objIdx;
/* This is never used
    public function __construct(clsIndexer_Recs $iIndexer=NULL) {
$this->Indexer($iIndexer);
    }
*/
    // BOILERPLATE BEGINS
    public function Indexer(clsIndexer_Recs $iObj=NULL) {
if (!is_null($iObj)) {
    $this->objIdx = $iObj;
}
assert('is_object($this->objIdx);');
return $this->objIdx;
    }
    public function IsNew() {
return !$this->Indexer()->IndexIsSet();
    }
    /*----
      USED BY: Administrative UI classes which need a string for referring to a particular record
    */
    public function KeyString() {
return $this->Indexer()->KeyString();
    }
    public function SelfFilter() {
return $this->Indexer()->SQL_forWhere();
    }
    public function SQL_forUpdate(array $iSet) {
return $this->Indexer()->SQL_forUpdate($iSet);
    }
    // BOILERPLATE ENDS
    public function SQL_forMake(array $iarSet) { die('Not yet written.'); }
}
/*%%%%
  PURPOSE: for tracking whether a cached object has the expected data or not
  HISTORY:
    2011-03-30 written
*/
class clsObjectCache {
    private $vKey;
    private $vObj;
    public function __construct() {
$this->vKey = NULL;
$this->vObj = NULL;
    }
    public function IsCached($iKey) {
if (is_object($this->vObj)) {
    return ($this->vKey == $iKey);
} else {
    return FALSE;
}
    }
    public function Object($iObj=NULL,$iKey=NULL) {
if (!is_null($iObj)) {
    $this->vKey = $iKey;
    $this->vObj = $iObj;
}
return $this->vObj;
    }
    public function Clear() {
$this->vObj = NULL;
    }
}
class clsSQLFilt {
class clsSQLFilt {
     private $arFilt;
     private $arFilt;
Line 690: Line 1,717:
  *** UTILITY FUNCTIONS ***
  *** UTILITY FUNCTIONS ***
*/
*/
/*----
  PURPOSE: This gets around PHP's apparent lack of built-in object type-conversion.
  ACTION: Copies all public fields from iSrce to iDest
*/
function CopyObj(object $iSrce, object $iDest) {
    foreach($iSrce as $key => $val) {
$iDest->$key = $val;
    }
}
if (!function_exists('Pluralize')) {
if (!function_exists('Pluralize')) {
     function Pluralize($iQty,$iSingular='',$iPlural='s') {
     function Pluralize($iQty,$iSingular='',$iPlural='s') {
Line 701: Line 1,737:


function SQLValue($iVal) {
function SQLValue($iVal) {
     if (is_null($iVal)) {
     if (is_array($iVal)) {
return 'NULL';
foreach ($iVal as $key => $val) {
    } else if (is_bool($iVal)) {
    $arOut[$key] = SQLValue($val);
return $iVal?'TRUE':'FALSE';
}
    } else if (is_string($iVal)) {
return $arOut;
$oVal = '"'.mysql_real_escape_string($iVal).'"';
    } else {
return $oVal;
if (is_null($iVal)) {
    return 'NULL';
} else if (is_bool($iVal)) {
    return $iVal?'TRUE':'FALSE';
} else if (is_string($iVal)) {
    $oVal = '"'.mysql_real_escape_string($iVal).'"';
    return $oVal;
} else {
    // numeric can be raw
    // all others, we don't know how to handle, so return raw as well
    return $iVal;
}
    }
}
function SQL_for_filter(array $iVals) {
    $sql = NULL;
    foreach ($arVals as $name => $val) {
if (!is_null($sql)) {
    $sql .= ' AND ';
}
$sql .= '('.$name.'='.SQLValue($val).')';
    }
    return $sql;
}
function NoYes($iBool,$iNo='no',$iYes='yes') {
    if ($iBool) {
return $iYes;
     } else {
     } else {
// numeric can be raw
return $iNo;
// all others, we don't know how to handle, so return raw as well
return $iVal;
     }
     }
}
}
Line 718: Line 1,778:
     return empty($iVal)?$default:$iVal;
     return empty($iVal)?$default:$iVal;
}
}
/*-----
  FUNCTION: nzAdd -- NZ Add
  RETURNS: ioVal += iAmt, but assumes ioVal is zero if not set (prevents runtime error)
  NOTE: iAmt is a reference so that we can pass variables which might not be set.
    Need to document why this is better than being able to pass constants.
*/
function nzAdd(&$ioVal,&$iAmt=NULL) {
function nzAdd(&$ioVal,&$iAmt=NULL) {
     $intAmt = empty($iAmt)?0:$iAmt;
     $intAmt = empty($iAmt)?0:$iAmt;
Line 726: Line 1,792:
     }
     }
     return $ioVal;
     return $ioVal;
}
/*-----
  FUNCTION: nzApp -- NZ Append
  PURPOSE: Like nzAdd(), but appends strings instead of adding numbers
*/
function nzApp(&$ioVal,$iTxt=NULL) {
    if (empty($ioVal)) {
$ioVal = $iTxt;
    } else {
$ioVal .= $iTxt;
    }
    return $ioVal;
}
function nzArray(array $iArr=NULL,$iKey,$iDefault=NULL) {
    $out = $iDefault;
    if (is_array($iArr)) {
if (array_key_exists($iKey,$iArr)) {
    $out = $iArr[$iKey];
}
    }
    return $out;
}
}
function ifEmpty(&$iVal,$iDefault) {
function ifEmpty(&$iVal,$iDefault) {
Line 740: Line 1,827:
}
}
     }
     }
}
/*----
  ACTION: Takes a two-dimensional array and returns it flipped diagonally,
    i.e. each element out[x][y] is element in[y][x].
  EXAMPLE:
    INPUT      OUTPUT
    +---+---+  +---+---+---+
    | A | 1 |  | A | B | C |
    +---+---+  +---+---+---+
    | B | 2 |  | 1 | 2 | 3 |
    +---+---+  +---+---+---+
    | C | 3 |
    +---+---+
*/
function ArrayPivot($iArray) {
    foreach ($iArray as $row => $col) {
if (is_array($col)) {
    foreach ($col as $key => $val) {
$arOut[$key][$row] = $val;
    }
}
    }
    return $arOut;
}
}
/* ========================
/* ========================

Revision as of 17:49, 24 January 2012

About

Database abstraction classes; used by VbzCart, SpamFerret, AudioFerret, WorkFerret

Code

<php><?php /* ===========================

*** DATA UTILITY CLASSES ***
 AUTHOR: Woozle (Nick) Staddon
 HISTORY:
   2007-05-20 (wzl) These classes have been designed to be db-engine agnostic, but I wasn't able

to test against anything other than MySQL nor was I able to implement the usage of the dbx_ functions, as the system that I was using didn't have them installed.

   2007-08-30 (wzl) posting this version at http://htyp.org/User:Woozle/data.php
   2007-12-24 (wzl) Some changes seem to have been made as recently as 12/17, so posting updated version
   2008-02-06 (wzl) Modified to use either mysqli or (standard) mysql library depending on flag; the latter isn't working yet
   2009-03-10 (wzl) adding some static functions to gradually get rid of the need for object factories
   2009-03-18 (wzl) debug constants now have defaults
   2009-03-26 (wzl) clsDataSet.Query() no longer fetches first row; this will require some rewriting
     NextRow() now returns TRUE if data was fetched; use if (data->NextRow()) {..} to loop through data.
   2009-05-02 (wzl) undocumented changes -- looks like:
     assert-checks return ID of an insertion
     function ExecUpdate($iSet,$iWhere)
     function SQLValue($iVal)
   2009-05-03 (wzl) more undocumented changes -- looks like mainly $iWhere is now optional in GetData()
   2009-07-05 (wzl) DataSet->__get now returns NULL if no field found; DataSet->HasField()
   2009-07-18 (wzl) clsTable::ExecUpdate() -> Update(); clsTable::Insert()
   2009-08-02 (wzl) clsDatabase::RowsAffected()
   2009-10-07 (wzl) minor: $dbg global added to clsTable Update() and Insert() methods
   2009-10-27 (wzl) clsTableCache
   2009-11-23 (wzl) clsDatabase.LogSQL(); some format-tidying
   2009-12-29 (wzl) clsDataSet_bare
   2010-01-02 (wzl) clsTable::DataSet()
   2010-01-08 (wzl) ifEmpty()
   2010-01-09 (wzl) fixed bug in clsDataSet_bare::Reload()
   2010-02-07 (wzl) clsTable::LastID()
   2010-04-11 (wzl) clsTable::KeyValue()
   2010-05-28 (wzl) split() is now deprecated -- replacing it with preg_split()
   2010-06-14 (wzl) added $iClass=NULL parameter to clsTable::SpawnItem
   2010-06-16 (wzl) nzApp()
   2010-07-19 (wzl) clsDatabase::Make()
   2010-10-04 (wzl) clsTable::ActionKey()
   2010-10-05 (wzl) removed reloading code from clsDataSet::Update()
   2010-10-16 (wzl) added clsTable::NameSQL(), clsTable::DataSetGroup()
   2010-10-19 (wzl) clsTable::DataSQL()
   2010-11-01 (wzl) clsTable::GetItem iID=NULL now means create new/blank object, i.e. SpawnItem()
   2010-11-14 (wzl) clsDataSet_bare::SameAs()
   2010-11-21 (wzl) caching helper class
   2011-02-07 (wzl) SQLValue() now handles arrays too
 FUTURE:
   API FIXES:
     GetData() should not have an $iClass parameter, or it should be the last parameter.
  • /

// Select which DB library to use -- // exactly one of the following must be true: define('KF_USE_MYSQL',TRUE); // in progress define('KF_USE_MYSQLI',FALSE); // complete & tested define('KF_USE_DBX',false); // not completely written; stalled

if (!defined('KDO_DEBUG')) { define('KDO_DEBUG',FALSE); } if (!defined('KDO_DEBUG_STACK')) { define('KDO_DEBUG_STACK',FALSE); } if (!defined('KDO_DEBUG_IMMED')) { define('KDO_DEBUG_IMMED',FALSE); } if (!defined('KS_DEBUG_HTML')) { define('KS_DEBUG_HTML',FALSE); } if (!defined('KDO_DEBUG_DARK')) { define('KDO_DEBUG_DARK',FALSE); }

class clsDatabase {

   private $cntOpen;	// count of requests to keep db open
   private $strType;	// type of db (MySQL etc.)
   private $strUser;	// database user
   private $strPass;	// password
   private $strHost;	// host (database server domain-name or IP address)
   private $strName;	// database (schema) name
   private $Conn;	// connection object
 // status
   private $strErr;	// latest error message
   public $sql;	// last SQL executed (or attempted)
   public $arSQL;	// array of all SQL statements attempted
   public $doAllowWrite;	// FALSE = don't execute UPDATE or INSERT commands, just log them
   public function __construct($iConn) {
     $this->Init($iConn);
   }
   /*=====
     INPUT: 

$iConn: type:user:pass@server/dbname

   */
   public function Init($iConn) {
     $this->doAllowWrite = TRUE;	// default
     $this->cntOpen = 0;

// list($part1,$part2) = split('@',$iConn);

     $ar = preg_split('/@/',$iConn);
     if (array_key_exists(1,$ar)) {

list($part1,$part2) = preg_split('/@/',$iConn);

     } else {

throw new exception('Connection string not formatted right: ['.$iConn.']');

     }

// list($this->strType,$this->strUser,$this->strPass) = split(':',$part1);

     list($this->strType,$this->strUser,$this->strPass) = preg_split('/:/',$part1);
     list($this->strHost,$this->strName) = explode('/',$part2);
     $this->strType = strtolower($this->strType);	// make sure it is lowercased, for comparison
     $this->strErr = NULL;
   }
   public function Open() {
     CallEnter($this,__LINE__,'clsDatabase.Open()');
     if ($this->cntOpen == 0) {
 // then actually open the db
     if (KF_USE_MYSQL) {

$this->Conn = mysql_connect( $this->strHost, $this->strUser, $this->strPass, false ); assert('is_resource($this->Conn)'); $ok = mysql_select_db($this->strName, $this->Conn); if (!$ok) { $this->getError(); }

     }
     if (KF_USE_MYSQLI) {

$this->Conn = new mysqli($this->strHost,$this->strUser,$this->strPass,$this->strName);

     }
     if (KF_USE_DBX) {

$this->Conn = dbx_connect($this->strType,$this->strHost,$this->strName,$this->strUser,$this->strPass);

     }
     }
     if (!$this->isOk()) {

$this->getError();

     }
     $this->cntOpen++;
     CallExit('clsDatabase.Open() - '.$this->cntOpen.' lock'.Pluralize($this->cntOpen));
   }
   public function Shut() {
     CallEnter($this,__LINE__,'clsDatabase.Shut()');
     $this->cntOpen--;
     if ($this->cntOpen == 0) {
     if (KF_USE_MYSQL) {

mysql_close($this->Conn);

     }
     if (KF_USE_MYSQLI) {

$this->Conn->close();

     }
     if (KF_USE_DBX) {

dbx_close($this->Conn);

     }
     }
     CallExit('clsDatabase.Shut() - '.$this->cntOpen.' lock'.Pluralize($this->cntOpen));
   }
   public function GetHost() {

return $this->strHost;

   }
   public function GetUser() {

return $this->strUser;

   }
   /*-----
     PURPOSE: generic table-creation function
     HISTORY:

2010-12-01 Added iID parameter to get singular item 2011-02-23 Changed from protected to public, to support class registration

   */
   public function Make($iName,$iID=NULL) {

if (!isset($this->$iName)) { $this->$iName = new $iName($this); } if (!is_null($iID)) { return $this->$iName->GetItem($iID); } else { return $this->$iName; }

   }
   /*=====
     PURPOSE: For debugging, mainly
     RETURNS: TRUE if database connection is supposed to be open
   */
   public function isOpened() {

return ($this->cntOpen > 0);

   }
   /*=====
     PURPOSE: Checking status of a db operation
     RETURNS: TRUE if last operation was successful
   */
   public function isOk() {

if (empty($this->strErr)) { return TRUE; } elseif ($this->Conn == FALSE) { return FALSE; } else { return FALSE; }

   }
   public function getError() {
     if (is_null($this->strErr)) {
     // avoid having an ok status overwrite an actual error

if (KF_USE_MYSQL) { $this->strErr = mysql_error(); } if (KF_USE_MYSQLI) { $this->strErr = $this->Conn->error; }

     }
     return $this->strErr;
   }
   public function ClearError() {

$this->strErr = NULL;

   }
   protected function LogSQL($iSQL) {

$this->sql = $iSQL; $this->arSQL[] = $iSQL;

   }
   public function ListSQL($iPfx=NULL) {

$out = ; foreach ($this->arSQL as $sql) { $out .= $iPfx.$sql; } return $out;

   }
   /*----
     HISTORY:

2011-03-04 added DELETE to list of write commands; rewrote to be more robust

   */
   protected function OkToExecSQL($iSQL) {

if ($this->doAllowWrite) { return TRUE; } else { // this is a bit of a kluge... need to strip out comments and whitespace // but basically, if the SQL starts with UPDATE, INSERT, or DELETE, then it's a write command so forbid it $sql = strtoupper(trim($iSQL)); $cmd = preg_split (' ',$sql,1); // get just the first word switch ($cmd) { case 'UPDATE': case 'INSERT': case 'DELETE': return FALSE; default: return TRUE; } }

   }
   /*=====
     FUTURE: Apparently _api_query() has very similar code. Eventually _api_query() should be a method in

an SQL engine class. OLD NOTE: Exec() and _api_query() perform almost identical functions. Do we really need them both? When we rewrite these as a single function, perhaps include a $iIsWrite flag parameter so we can eliminate OkToExecSQL().

     HISTORY:

2011-02-24 Now passing $this->Conn to mysql_query() because somehow the connection was getting set to the wiki database instead of the original.

   */
   public function Exec($iSQL) {

CallEnter($this,__LINE__,__CLASS__.'.'.__METHOD__.'('.$iSQL.')'); $this->LogSQL($iSQL); if ($this->OkToExecSQL($iSQL)) { if (KF_USE_MYSQL) { $ok = mysql_query($iSQL,$this->Conn); if (is_resource($ok)) { // this should never happen here $ok = TRUE; } } if (KF_USE_MYSQLI) { $objQry = $this->Conn->prepare($iSQL); if (is_object($objQry)) { $ok = $objQry->execute(); } else { $ok = false; //echo '
SQL error: '.$iSQL.'
'; } }

if (!$ok) { $this->getError(); }

if (KF_USE_MYSQL) { // no need to do anything; no resource allocated as long as query SQL was non-data-fetching } if (KF_USE_MYSQLI) { $objQry->close(); } } else { $ok = TRUE; } CallExit(__CLASS__.'.'.__METHOD__.'()'); return $ok;

   }
   public function RowsAffected() {

if (KF_USE_MYSQL) { return mysql_affected_rows($this->Conn); }

   }
   public function NewID($iDbg=NULL) {

if (KF_USE_MYSQL) { $id = mysql_insert_id($this->Conn); } if (KF_USE_MYSQLI) { $id = $this->Conn->insert_id; } if ($this->doAllowWrite) { assert('$id!=0 /*'.$iDbg.'// SQL was: [ '.$this->sql.' ] */'); } return $id;

   }
   public function SafeParam($iString) {

CallEnter($this,__LINE__,__CLASS__.'.SafeParam("'.$iString.'")'); if (KF_USE_MYSQL) { if (is_resource($this->Conn)) { $out = mysql_real_escape_string($iString,$this->Conn); } else { $out = '
'.get_class($this).'.SafeParam("'.$iString.'") has no connection.'; } } if (KF_USE_MYSQLI) { $out = $this->Conn->escape_string($iString); } CallExit('SafeParam("'.$iString.'")'); return $out;

   }
   public function ErrorText() {

if ($this->strErr == ) { $this->_api_getError(); } return $this->strErr;

   }

/******

SECTION: API WRAPPER FUNCTIONS
 FUTURE: Create cls_db_api, which should encapsulate the different APIs of the different db libraries.
   On initialization, the clsDatabase can detect which one to use. This will eliminate the need for
   "if-then" statements based on pre-set constants.
  • /
   public function _api_query($iSQL) {

$this->LogSQL($iSQL); if ($this->OkToExecSQL($iSQL)) { if (KF_USE_MYSQL) { if (is_resource($this->Conn)) { return mysql_query($iSQL,$this->Conn); } else { throw new Exception('Database Connection object is not a resource'); } } if (KF_USE_MYSQLI) { $this->Conn->real_query($iSQL); return $this->Conn->store_result(); } if (KF_USE_DBX) { return dbx_query($this->Conn,$iSQL,DBX_RESULT_ASSOC); } }

   }
   public function _api_rows_rewind($iRes) {

if (KF_USE_MYSQL) { mysql_data_seek($iRes, 0); }

   }
   public function _api_fetch_row($iRes) {
   // ACTION: Fetch the first/next row of data from a result set

if (KF_USE_MYSQL) { if (is_resource($iRes)) { return mysql_fetch_assoc($iRes); } else { return NULL; } } if (KF_USE_MYSQLI) { return $iRes->fetch_assoc(); }

   }
   /*=====
     ACTION: Return the number of rows in the result set
   */
   public function _api_count_rows($iRes) {

if (KF_USE_MYSQL) { if ($iRes === FALSE) { return NULL; } else { if (is_resource($iRes)) { return mysql_num_rows($iRes); } else { return NULL; } } } if (KF_USE_MYSQLI) { return $iRes->num_rows; }

   }
   public function _api_row_filled($iRow) {

if (KF_USE_MYSQL) { return ($iRow !== FALSE) ; }

   }

/******

SECTION: OBJECT FACTORY
  • /
   public function DataSet($iSQL=NULL,$iClass=NULL) {

if (is_string($iClass)) { $objData = new $iClass($this); assert('is_object($objData)'); if (!($objData instanceof clsDataSet)) { LogError($iClass.' is not a clsDataSet subclass.'); } } else { $objData = new clsDataSet($this); assert('is_object($objData)'); } assert('is_object($objData->Engine())'); if (!is_null($iSQL)) { if (is_object($objData)) { $objData->Query($iSQL); } } return $objData;

   }

} /*=============

 NAME: clsTable_abstract
 PURPOSE: objects for operating on particular tables
   Does not attempt to deal with keys.
  • /

abstract class clsTable_abstract {

   protected $objDB;
   protected $vTblName;
   protected $vSngClass;	// name of singular class
   public $sqlExec;		// last SQL executed on this table
   public function __construct(clsDatabase $iDB) {

$this->objDB = $iDB;

   }
   public function DB() {	// DEPRECATED - use Engine()

return $this->objDB;

   }
   public function Engine() {

return $this->objDB;

   }
   public function Name($iName=NULL) {

if (!is_null($iName)) { $this->vTblName = $iName; } return $this->vTblName;

   }
   public function NameSQL() {

assert('is_string($this->vTblName); /* '.print_r($this->vTblName,TRUE).' */'); return '`'.$this->vTblName.'`';

   }
   public function ClassSng($iName=NULL) {

if (!is_null($iName)) { $this->vSngClass = $iName; } return $this->vSngClass;

   }
   /*----
     ACTION: Make sure the item is ready to be released in the wild
   */
   protected function ReleaseItem(clsRecs_abstract $iItem) {

$iItem->Table = $this; $iItem->objDB = $this->objDB;

   }
   /*----
     ACTION: creates a new uninitialized singular object but sets the Table pointer back to self
     RETURNS: created object
   */
   public function SpawnItem($iClass=NULL) {

if (is_null($iClass)) { $strCls = $this->ClassSng(); } else { $strCls = $iClass; } assert('!empty($strCls);'); $objItem = new $strCls; $this->ReleaseItem($objItem); return $objItem;

   }
   /*----
     RETURNS: dataset defined by the given SQL, wrapped in an object of the current class
     USAGE: primarily for joins where you want only records where there is no matching record

in the joined table. (If other examples come up, maybe a DataNoJoin() method would be appropriate.)

   */
   public function DataSQL($iSQL) {

$strCls = $this->ClassSng(); $obj = $this->Engine()->DataSet($iSQL,$strCls); $this->ReleaseItem($obj); $this->sqlExec = $iSQL; return $obj;

   }
   /*----
     RETURNS: dataset containing all fields from the current table,

with additional options (everything after the table name) being defined by $iSQL, wrapped in the current object class.

   */
   public function DataSet($iSQL=NULL,$iClass=NULL) {

global $sql; // for debugging

$sql = 'SELECT * FROM '.$this->NameSQL(); if (!is_null($iSQL)) { $sql .= ' '.$iSQL; } return $this->DataSQL($sql); /* $strCls = $this->vSngClass; $obj = $this->objDB->DataSet($sql,$strCls); $obj->Table = $this; return $obj;

  • /
   }
   public function GetData($iWhere=NULL,$iClass=NULL,$iSort=NULL) {

global $sql; // for debugging

$sql = 'SELECT * FROM '.$this->NameSQL(); if (!is_null($iWhere)) { $sql .= ' WHERE '.$iWhere; } if (!is_null($iSort)) { $sql .= ' ORDER BY '.$iSort; } /* if (is_null($iClass)) { $strCls = $this->vSngClass; } else { $strCls = $iClass; }

  • /

//$obj = $this->objDB->DataSet($sql,$strCls); //$res = $this->DB()->Exec($sql); $obj = $this->SpawnItem($iClass); assert('is_object($obj->Table);'); $obj->Query($sql);

$this->sqlExec = $sql; if (!is_null($obj)) { // $obj->Table = $this; // 2011-01-20 this should be redundant now $obj->sqlMake = $sql; } return $obj;

   }
   /*----
     RETURNS: SQL for creating a new record for the given data
     HISTORY:

2010-11-20 Created.

   */
   public function SQL_forInsert(array $iData) {

$sqlNames = ; $sqlVals = ; foreach($iData as $key=>$val) { if ($sqlNames != ) { $sqlNames .= ','; $sqlVals .= ','; } $sqlNames .= $key; $sqlVals .= $val; } return 'INSERT INTO `'.$this->Name().'` ('.$sqlNames.') VALUES('.$sqlVals.');';

   }
   /*----
     HISTORY:

2010-11-16 Added "array" requirement for iData 2010-11-20 Calculation now takes place in SQL_forInsert()

   */
   public function Insert(array $iData) {

global $sql;

$sql = $this->SQL_forInsert($iData); $this->sqlExec = $sql; return $this->objDB->Exec($sql);

   }
   /*----
     HISTORY:

2011-02-02 created for deleting topic-title pairs

   */
   public function Delete($iFilt) {

$sql = 'DELETE FROM `'.$this->Name().'` WHERE '.$iFilt; $this->sqlExec = $sql; return $this->Engine()->Exec($sql);

   }

} /*=============

 NAME: clsTable_keyed_abstract
 PURPOSE: adds abstract methods for dealing with keys
  • /

abstract class clsTable_keyed_abstract extends clsTable_abstract {

   abstract public function GetItem();
   /*----
     PURPOSE: method for setting a key which uniquely refers to this table

Useful for logging, menus, and other text-driven contexts.

   */
   public function ActionKey($iName=NULL) {

if (!is_null($iName)) { $this->ActionKey = $iName; } return $this->ActionKey;

   }
   /*----
     INPUT:

$iData: array of data necessary to create a new record or update an existing one, if found $iFilt: SQL defining what constitutes an existing record If NULL, MakeFilt() will be called to build this from $iData.

     HISTORY:

2011-02-22 created 2011-03-23 added madeNew and dataOld fields Nothing is actually using these yet, but that will probably change. For example, we might want to log when an existing record gets modified. 2011-03-31 why is this protected? Leaving it that way for now, but consider making it public.

   */
   public $madeNew,$dataOld;	// additional status output
   protected function Make(array $iData,$iFilt=NULL) {

if (is_null($iFilt)) { $sqlFilt = $this->MakeFilt($iData); } else { $sqlFilt = $iFilt; } $rs = $this->GetData($sqlFilt); if ($rs->HasRows()) { assert('$rs->RowCount() == 1'); $rs->NextRow();

$this->madeNew = FALSE; $this->dataOld = $this->Values();

$rs->Update($iData); $id = $rs->KeyValue(); } else { $this->Insert($iData); $id = $this->Engine()->NewID(); $this->madeNew = TRUE; } return $id;

   }

// LATER: // abstract protected function MakeFilt(array $iData); } /*=============

 NAME: clsTable_key_single
 PURPOSE: table with a single key field
  • /

class clsTable_key_single extends clsTable_keyed_abstract {

   protected $vKeyName;
   public function __construct(clsDatabase $iDB) {

parent::__construct($iDB); $this->ClassSng('clsDataSet');

   }
   public function KeyName($iName=NULL) {

if (!is_null($iName)) { $this->vKeyName = $iName; } return $this->vKeyName;

   }
   /*----
     HISTORY:

2010-11-01 iID=NULL now means create new/blank object, i.e. SpawnItem()

   */
   public function GetItem($iID=NULL,$iClass=NULL) {

if (is_null($iID)) { $objItem = $this->SpawnItem($iClass); $objItem->KeyValue(NULL); } else { $objItem = $this->GetData($this->vKeyName.'='.SQLValue($iID),$iClass); $objItem->NextRow(); } return $objItem;

   }
   /*----
     INPUT:

iFields: array of source fields and their output names - specified as iFields[output]=input, because you can have a single input used for multiple outputs, but not vice-versa. Yes, this is confusing but that's how arrays are indexed.

     HISTORY:

2010-10-16 Created for VbzAdminCartLog::AdminPage()

   */
   public function DataSetGroup(array $iFields, $iGroupBy, $iSort=NULL) {

global $sql; // for debugging

foreach ($iFields AS $fDest => $fSrce) { if(isset($sqlFlds)) { $sqlFlds .= ', '; } else { $sqlFlds = ; } $sqlFlds .= $fSrce.' AS '.$fDest; } $sql = 'SELECT '.$sqlFlds.' FROM '.$this->NameSQL().' GROUP BY '.$iGroupBy; if (!is_null($iSort)) { $sql .= ' ORDER BY '.$iSort; } $obj = $this->objDB->DataSet($sql); return $obj;

   }
   /*----
     HISTORY:

2010-11-20 Created

   */
   public function SQL_forUpdate(array $iSet,$iWhere) {

$sqlSet = ; foreach($iSet as $key=>$val) { if ($sqlSet != ) { $sqlSet .= ','; } $sqlSet .= ' `'.$key.'`='.$val; }

return 'UPDATE `'.$this->Name().'` SET'.$sqlSet.' WHERE '.$iWhere;

   }
   /*----
     HISTORY:

2010-10-05 Commented out code which updated the row[] array from iSet's values. * It doesn't work if the input is a string instead of an array. * Also, it seems like a better idea to actually re-read the data if we really need to update the object. 2010-11-16 Added "array" requirement for iSet; removed code for handling iSet as a string. If we want to support single-field updates, make a new method: UpdateField($iField,$iVal,$iWhere). This makes it easier to support automatic updates of certain fields in descendent classes (e.g. updating a WhenEdited timestamp). 2010-11-20 Calculation now takes place in SQL_forUpdate()

   */
   public function Update(array $iSet,$iWhere) {

global $sql;

$sql = $this->SQL_forUpdate($iSet,$iWhere); $this->sqlExec = $sql; $ok = $this->objDB->Exec($sql);

return $ok;

   }
   public function LastID() {

$strKey = $this->vKeyName; $sql = 'SELECT '.$strKey.' FROM `'.$this->Name().'` ORDER BY '.$strKey.' DESC LIMIT 1;';

$objRows = $this->objDB->DataSet($sql);

if ($objRows->HasRows()) { $objRows->NextRow(); $intID = $objRows->$strKey; return $intID; } else { return 0; }

   }
   /*----
     HISTORY:

2011-02-22 created

     IMPLEMENTATION:

KeyName must equal KeyValue

   */
   protected function MakeFilt(array $iData) {

return $this->KeyName().'='.SQLValue($this->KeyValue());

   }

} // alias -- sort of a default table type class clsTable extends clsTable_key_single { }

// DEPRECATED -- use clsCache_Table helper class class clsTableCache extends clsTable {

   private $arCache;
   public function GetItem($iID=NULL,$iClass=NULL) {

if (!isset($this->arCache[$iID])) { $objItem = $this->GetData($this->vKeyName.'='.SQLValue($iID),$iClass); $objItem->NextRow(); $this->arCache[$iID] = $objItem->RowCopy(); } return $this->arCache[$iID];

   }

} /*====

 CLASS: cache for Tables
 ACTION: provides a cached GetItem()
 USAGE: clsTable descendants should NOT override GetItem() or GetData() to use this class,
   as the class needs those methods to load data into the cache.
 BOILERPLATE:
   protected $objCache;
   protected function Cache() {

if (!isset($this->objCache)) { $this->objCache = new clsCache_Table($this); } return $this->objCache;

   }
   public function GetItem_Cached($iID=NULL,$iClass=NULL) {

return $this->Cache()->GetItem($iID,$iClass);

   }
   public function GetData_Cached($iWhere=NULL,$iClass=NULL,$iSort=NULL) {

return $this->Cache()->GetItem($iWhere,$iClass,$iSort);

   }
  • /

/*----

  • /

class clsCache_Table {

   protected $objTbl;
   protected $arRows;	// arRows[id] = rows[]
   protected $arSets;	// caches entire datasets
   public function __construct(clsTable $iTable) {

$this->objTbl = $iTable;

   }
   public function GetItem($iID=NULL,$iClass=NULL) {

$objTbl = $this->objTbl; if (isset($this->arRows[$iID])) { $objItem = $objTbl->SpawnItem($iClass); $objItem->Row = $this->arCache[$iID]; } else { $objItem = $objTbl->GetItem($iID,$iClass); $this->arCache[$iID] = $objItem->Row; } return $objItem;

   }
   /*----
     HISTORY:

2011-02-11 Renamed GetData_Cached() to GetData() This was probably a leftover from before multiple inheritance Fixed some bugs. Renamed from GetData() to GetData_array() because caching the resource blob didn't seem to work very well. Now returns an array instead of an object.

     FUTURE: Possibly we should be reading all rows into memory, instead of just saving the Res.

That way, Res could be protected again instead of public.

   */
   public function GetData_array($iWhere=NULL,$iClass=NULL,$iSort=NULL) {

$objTbl = $this->objTbl; $strKeyFilt = "$iWhere\t$iSort"; $isCached = FALSE; if (is_array($this->arSets)) { if (array_key_exists($strKeyFilt,$this->arSets)) { $isCached = TRUE; } } if ($isCached) { //$objSet = $objTbl->SpawnItem($iClass); //$objSet->Res = $this->arSets[$strKey]; //assert('is_resource($objSet->Res); /* KEY='.$strKey.'*/');

// 2011-02-11 this code has not been tested yet

//echo '

'.print_r($this->arSets,TRUE).'

';

foreach ($this->arSets[$strKeyFilt] as $key) { $arOut[$key] = $this->arRows[$key]; } } else { $objSet = $objTbl->GetData($iWhere,$iClass,$iSort); while ($objSet->NextRow()) { $strKeyRow = $objSet->KeyString(); $arOut[$strKeyRow] = $objSet->Values(); $this->arSets[$strKeyFilt][] = $strKeyRow; } if (is_array($this->arRows)) { $this->arRows = array_merge($this->arRows,$arOut); // add to cached rows } else { $this->arRows = $arOut; // start row cache } } return $arOut;

   }

} /*=============

 NAME: clsRecs_abstract -- abstract recordset
   Does not deal with keys.
  • /

abstract class clsRecs_abstract {

   public $objDB;	// deprecated; use Engine()
   public $sqlMake;	// optional: SQL used to create the dataset -- used for reloading
   public $sqlExec;	// last SQL executed on this dataset
   public $Table;	// public access deprecated; use Table()
   protected $Res;	// native result set
   public $Row;	// public access deprecated; use Values()/Value() (data from the active row)
   public function __construct(clsDatabase $iDB=NULL, $iRes=NULL, array $iRow=NULL) {

$this->objDB = $iDB; $this->Res = $iRes; $this->Row = $iRow; $this->InitVars();

   }
   protected function InitVars() {
   }
   protected function Table(clsTable_abstract $iTable=NULL) {

if (!is_null($iTable)) { $this->Table = $iTable; } return $this->Table;

   }
   public function Engine() {

if (is_null($this->objDB)) { assert('!is_null($this->Table()); /* SQL: '.$this->sqlMake.' */'); return $this->Table()->Engine(); } else { return $this->objDB; }

   }
   /*----
     RETURNS: associative array of fields/values for the current row
     HISTORY:

2011-01-08 created 2011-01-09 actually working; added option to write values

   */
   public function Values(array $iRow=NULL) {

if (is_array($iRow)) { $this->Row = $iRow; } return $this->Row;

   }
   /*----
     FUNCTION: Value(name)
     RETURNS: Value of named field
     HISTORY:

2010-11-19 Created to help with data-form processing. 2010-11-26 Added value-setting, so we can set defaults for new records 2011-02-09 replaced direct call to array_key_exists() with call to new function HasValue()

   */
   public function Value($iName,$iVal=NULL) {

if (is_null($iVal)) { if (!$this->HasValue($iName)) {

echo '

'.print_r($this->Row,TRUE).'

';

throw new Exception('Attempted to read nonexistent field "'.$iName.'" in class '.get_class($this)); } } else { $this->Row[$iName] = $iVal; } return $this->Row[$iName];

   }
   /*----
     PURPOSE: Like Value() but handles new records gracefully, and is read-only
     HISTORY:

2011-02-12 written

   */
   public function ValueNz($iName,$iDefault=NULL) {

if ($this->HasValue($iName)) { return $this->Value($iName); } else { return $iDefault; }

   }
   /*----
     HISTORY:

2011-02-09 created so we can test for field existence before trying to access

   */
   public function HasValue($iName) {

if (is_array($this->Row)) { return array_key_exists($iName,$this->Row); } else { return FALSE; }

   }
   /*----
     FUNCTION: Clear();
     ACTION: Clears Row[] of any leftover data
   */
   public function Clear() {

$this->Row = NULL;

   }
   public function Query($iSQL) {

$this->Res = $this->Engine()->_api_query($iSQL); $this->sqlMake = $iSQL; if (!is_resource($this->Res)) { throw new exception ('SQL='.$iSQL); }

   }
   /*----
     ACTION: Checks given values for any differences from current values
     RETURNS: TRUE if all values are same
   */
   public function SameAs(array $iValues) {

$isSame = TRUE; foreach($iValues as $name => $value) { $oldVal = $this->Row[$name]; if ($oldVal != $value) { $isSame = FALSE; }

} return $isSame;

   }
   /*-----
     RETURNS: # of rows iff result has rows, otherwise FALSE
   */
   public function hasRows() {

$rows = $this->objDB->_api_count_rows($this->Res); if ($rows === FALSE) { return FALSE; } elseif ($rows == 0) { return FALSE; } else { return $rows; }

   }
   public function hasRow() {

return $this->objDB->_api_row_filled($this->Row);

   }
   public function RowCount() {

return $this->objDB->_api_count_rows($this->Res);

   }
   public function StartRows() {

if ($this->hasRows()) { $this->objDB->_api_rows_rewind($this->Res); return TRUE; } else { return FALSE; }

   }
   public function FirstRow() {

if ($this->StartRows()) { return $this->NextRow(); // get the first row of data } else { return FALSE; }

   }
   /*=====
     ACTION: Fetch the next row of data into $this->Row.

If no data has been fetched yet, then fetch the first row.

     RETURN: TRUE if row was fetched; FALSE if there were no more rows

or the row could not be fetched.

   */
   public function NextRow() {

$this->Row = $this->objDB->_api_fetch_row($this->Res); return $this->hasRow();

   }

} /*=============

 NAME: clsRecs_keyed_abstract -- abstract recordset for keyed data
   Adds abstract and concrete methods for dealing with keys.
  • /

abstract class clsRecs_keyed_abstract extends clsRecs_abstract {

   // ABSTRACT methods
   abstract public function SelfFilter();
   abstract public function KeyString();
   abstract public function SQL_forUpdate(array $iSet);
   abstract public function SQL_forMake(array $iSet);
   /*-----
     ACTION: Saves the data in $iSet to the current record (or records filtered by $iWhere)
     HISTORY:

2010-11-20 Calculation now takes place in SQL_forUpdate() 2010-11-28 SQL saved to table as well, for when we might be doing Insert() or Update() and need a single place to look up the SQL for whatever happened.

   */
   public function Update(array $iSet,$iWhere=NULL) {

//$ok = $this->Table->Update($iSet,$sqlWhere); $sql = $this->SQL_forUpdate($iSet,$iWhere); //$this->sqlExec = $this->Table->sqlExec; $this->sqlExec = $sql; $this->Table->sql = $sql; $ok = $this->objDB->Exec($sql); return $ok;

   }

} /*=============

 NAME: clsDataSet_bare
 DEPRECATED - USE clsRecs_key_single INSTEAD
 PURPOSE: base class for datasets, with single key
   Does not add field overloading. Field overloading seems to have been a bad idea anyway;
     Use Value() instead.
  • /

class clsRecs_key_single extends clsRecs_keyed_abstract {

   /*----
     HISTORY:

2010-11-01 iID=NULL now means object does not have data from an existing record

   */
   public function IsNew() {

return is_null($this->KeyValue());

   }
   /*-----
     FUNCTION: KeyValue()
   */
   public function KeyValue($iVal=NULL) {

if (!is_object($this->Table)) { throw new Exception('Recordset needs a Table object to retrieve key value.'); } $strKeyName = $this->Table->KeyName(); assert('!empty($strKeyName); /* TABLE: '.$this->Table->Name().' */'); assert('is_string($strKeyName); /* TABLE: '.$this->Table->Name().' */'); if (is_null($iVal)) { if (!isset($this->Row[$strKeyName])) { $this->Row[$strKeyName] = NULL; } } else { $this->Row[$strKeyName] = $iVal; } return $this->Row[$strKeyName];

   }
   public function KeyString() {

return (string)$this->KeyValue();

   }
   /*----
     FUNCTION: Load_fromKey()
     ACTION: Load a row of data whose key matches the given value
     HISTORY:

2010-11-19 Created for form processing.

   */
   public function Load_fromKey($iKeyValue) {

$this->sqlMake = NULL; $this->KeyValue($iKeyValue); $this->Reload();

   }
   /*-----
     FUNCTION: SelfFilter()
     RETURNS: SQL for WHERE clause which will select only the current row, based on KeyValue()
     USED BY: Update(), Reload()
   */
   public function SelfFilter() {

if (!is_object($this->Table)) { throw new exception('Table not set in class '.get_class($this)); } $strKeyName = $this->Table->KeyName(); //$sqlWhere = $strKeyName.'='.$this->$strKeyName; //$sqlWhere = $strKeyName.'='.$this->Row[$strKeyName]; $sqlWhere = '`'.$strKeyName.'`='.$this->KeyValue(); return $sqlWhere;

   }
   /*-----
     ACTION: Reloads only the current row unless $iFilt is set
     TO DO: iFilt should probably be removed, now that we save

the creation SQL in $this->sql.

   */
   public function Reload($iFilt=NULL) {

if (is_string($this->sqlMake)) { $sql = $this->sqlMake; } else { $sql = 'SELECT * FROM `'.$this->Table->Name().'` WHERE '; if (is_null($iFilt)) { $sql .= $this->SelfFilter(); } else { $sql .= $iFilt; } } $this->Query($sql); $this->NextRow();

   }
   /*----
     HISTORY:

2010-11-20 Created

   */
   public function SQL_forUpdate(array $iSet,$iWhere=NULL) {

$doIns = FALSE; if (is_null($iWhere)) { // default: modify the current record // build SQL filter for just the current record $sqlWhere = $this->SelfFilter(); } else { $sqlWhere = $iWhere; } return $this->Table->SQL_forUpdate($iSet,$sqlWhere);

   }
   /*----
     HISTORY:

2010-11-23 Created

   */
   public function SQL_forMake(array $iarSet) {

$strKeyName = $this->Table->KeyName(); if ($this->IsNew()) { $sql = $this->Table->SQL_forInsert($iarSet); } else { $sql = $this->SQL_forUpdate($iarSet); } return $sql;

   }
   /*-----
     ACTION: Saves to the current record; creates a new record if ID is 0 or NULL
     HISTORY:

2010-11-03 Now uses this->IsNew() to determine whether to use Insert() or Update() Loads new ID into KeyValue

   */
   public function Make(array $iarSet) {

if ($this->IsNew()) { $ok = $this->Table->Insert($iarSet); $this->KeyValue($this->objDB->NewID()); return $ok; } else { return $this->Update($iarSet); }

   }
   // DEPRECATED -- should be a function of Table type
   public function HasField($iName) {

return isset($this->Row[$iName]);

   }
   // DEPRECATED - use Values()
   public function RowCopy() {

$strClass = get_class($this); if (is_array($this->Row)) { $objNew = new $strClass; // copy critical object fields so methods will work: $objNew->objDB = $this->objDB; $objNew->Table = $this->Table; // copy data fields: foreach ($this->Row AS $key=>$val) { $objNew->Row[$key] = $val; } return $objNew; } else { //echo 'RowCopy(): No data to copy in class '.$strClass; return NULL; }

   }

} // alias -- a sort of default dataset type class clsDataSet_bare extends clsRecs_key_single { } /*

 PURPOSE: clsDataSet with overloaded field access methods
 DEPRECATED -- This has turned out to be more problematic than useful.
   Retained only for compatibility with existing code; hope to eliminate eventually.
  • /

class clsDataSet extends clsRecs_key_single {

 // -- accessing individual fields
   public function __set($iName, $iValue) {

$this->Row[$iName] = $iValue;

   }
   public function __get($iName) {

if (isset($this->Row[$iName])) { return $this->Row[$iName]; } else { return NULL; }

   }
   /*-----
     FUNCTION: KeyValue()

Really, this should be redundant. Replace usages of __set() and __get with something like Value(),

 which can then be overridden if needed.
   */

/*

   public function KeyValue() {

$strKeyName = $this->Table->KeyName(); return $this->$strKeyName;

   }
  • /

} // HELPER CLASSES

/*====

 CLASS: Table Indexer
  • /

class clsIndexer_Table {

   private $objTbl;	// clsTable object
   public function __construct(clsTable_abstract $iObj) {

$this->objTbl = $iObj;

   }
   /*----
     RETURNS: newly-created clsIndexer_Recs-descended object

Override this method to change how indexing works

   */
   protected function NewRecsIdxer(clsRecs_indexed $iRecs) {

return new clsIndexer_Recs($iRecs,$this);

   }
   public function TableObj() {

return $this->objTbl;

   }
   public function InitRecs(clsRecs_indexed $iRecs) {

$objIdx = $this->NewRecsIdxer($iRecs); $iRecs->Indexer($objIdx); return $objIdx;

   }

} /*====

 HISTORY:
   2011-01-19 Started; not ready yet -- just saving bits of code I know I will need
  • /

class clsIndexer_Table_single_key extends clsIndexer_Table {

   protected function NewRecsIdxer(clsRecs_indexed $iRecs) {

return new clsIndexer_Recs_single_key($iRecs,$this);

   }
   public function KeyName($iName=NULL) {

if (!is_null($iName)) { $this->vKeyName = $iName; } return $this->vKeyName;

   }
   public function GetItem($iID=NULL,$iClass=NULL) {

if (is_null($iID)) { $objItem = $this->SpawnItem($iClass); $objItem->KeyValue(NULL); } else { assert('!is_array($iID); /* TABLE='.$this->TableObj()->Name().' */'); $objItem = $this->TableObj()->GetData($this->vKeyName.'='.SQLValue($iID),$iClass); $objItem->NextRow(); } return $objItem;

   }

}

class clsIndexer_Table_multi_key extends clsIndexer_Table {

   private $arKeys;
   /*----
     RETURNS: newly-created clsIndexer_Recs-descended object

Override this method to change how indexing works

   */
   protected function NewRecsIdxer(clsRecs_indexed $iRecs) {

return new clsIndexer_Recs_multi_key($iRecs,$this);

   }
   public function KeyNames(array $iNames=NULL) {

if (!is_null($iNames)) { $this->arKeys = $iNames; } return $this->arKeys;

   }
   /*----
     HISTORY:

2011-01-08 written

   */
   public function GetItem(array $iVals=NULL) {

if (is_null($iVals)) { $objItem = $this->TableObj()->SpawnItem(); } else { $sqlFilt = $this->SQL_for_filter($iVals); $objItem = $this->TableObj()->GetData($sqlFilt); $objItem->NextRow(); } return $objItem;

   }
   /*----
     RETURNS: SQL to filter for the current record by key value(s)

'(name=value) AND (name=value) AND...'

     INPUT: array of keynames and values

array[name] = value

     USED BY: GetItem()
     HISTORY:

2011-01-08 written 2011-01-19 replaced with boilerplate call to indexer in clsIndexer_Table

   */

/*

   protected function SQL_for_filter(array $iVals) {

$arVals = $iVals; $arKeys = $this->KeyNames(); $sql = NULL; foreach ($arKeys as $name) { $val = $arVals[$name]; if (!is_null($sql)) { $sql .= ' AND '; } $sql .= '('.$name.'='.SQLValue($val).')'; } return $sql;

   }
  • /
   /*----
     RETURNS: SQL for creating a new record for the given data
     HISTORY:

2010-11-20 Created. 2011-01-08 adapted from clsTable::Insert()

   */
   public function SQL_forInsert(array $iData) {

$sqlNames = ; $sqlVals = ; foreach($iData as $key=>$val) { if ($sqlNames != ) { $sqlNames .= ','; $sqlVals .= ','; } $sqlNames .= '`'.$key.'`'; $sqlVals .= $val; } return 'INSERT INTO `'.$this->Name().'` ('.$sqlNames.') VALUES('.$sqlVals.');';

   }

} /*====

 CLASS: clsIndexer_Recs -- record set indexer
 PURPOSE: Handles record sets for tables with multiple keys
 HISTORY:
   2010-?? written for clsCacheFlows/clsCacheFlow in cache.php
   2011-01-08 renamed, revised and clarified
  • /

abstract class clsIndexer_Recs {

   private $objData;
   private $objTbl;	// table indexer
   // ABSTRACT functions
   abstract public function IndexIsSet();
   abstract public function KeyString();
   abstract public function SQL_forWhere();
   /*----
     INPUT:

iObj = DataSet iKeys = array of field names

   */
   public function __construct(clsRecs_keyed_abstract $iData,clsIndexer_Table $iTable) {

$this->objData = $iData; $this->objTbl = $iTable;

   }
   public function DataObj() {

return $this->objData;

   }
   public function TblIdxObj() {

assert('is_a($this->objTbl,"clsIndexer_Table"); /* CLASS='.get_class($this->objTbl).' */'); return $this->objTbl;

   }
   public function Engine() {

return $this->DataObj()->Engine();

   }
   public function TableObj() {

return $this->TblIdxObj()->TableObj();

   }
   public function TableName() {

return $this->TableObj()->Name();

   }

/*

   public function Keys() {

$arKeys = $this->objTbl->Keys(); reset($arKeys); return $arKeys;

   }
  • /
   /*-----
     FUNCTION: KeyValue()
     IN/OUT: array of key values

array[key name] = value

     USED BY: clsCacheFlow::KeyValue()
   */

/*

   public function KeyValue(array $iVals=NULL) {

$arKeys = $this->KeyNames(); $arRow = $this->DataObj()->Row; if (is_array($iVals)) { foreach ($iVals as $val) { list($key) = each($arKeys); $arRow[$key] = $val; } $this->DataObj()->Row = $arRow; } foreach ($arKeys as $key) { $arOut[$key] = $arRow[$key]; } return $arOut;

   }
  • /
   /*----
     FUNCTION: KeyString()
     IN/OUT: prefix-delimited string of all key values
     QUERY: What uses this?
   */

/*

   public function KeyString($iVals=NULL) {

if (is_string($iVals)) { $xts = new xtString($iVals); $arVals = $xts->Xplode(); $arKeys = $this->Keys(); foreach ($arVals as $val) { list($key) = each($arKeys); $this->Row[$key] = $val; } } $out = ; foreach ($this->arKeys as $key) { $val = $this->Row[$key]; $out .= '.'.$val; } return $out;

   }
  • /

/*

   public function KeyValue(array $iVals=NULL) {

$arKeys = $this->KeyNames(); $arRow = $this->DataObj()->Row; if (is_array($iVals)) { foreach ($iVals as $val) { list($key) = each($arKeys); $arRow[$key] = $val; } $this->DataObj()->Row = $arRow; } foreach ($arKeys as $key) { $arOut[$key] = $arRow[$key]; } return $arOut;

   }
  • /

/* There's no need for this here; it doesn't require indexing

   public function SQL_forInsert() {

return $this->TblIdxObj()->SQL_forInsert($this->KeyValues());

   }
  • /
   /*----
     INPUT:

iSet: array specifying fields to update and the values to update them to iSet[field name] = value

     HISTORY:

2010-11-20 Created 2011-01-09 Adapted from clsDataSet_bare

   */
   public function SQL_forUpdate(array $iSet) {

$sqlSet = ; foreach($iSet as $key=>$val) { if ($sqlSet != ) { $sqlSet .= ','; } $sqlSet .= ' `'.$key.'`='.$val; } $sqlWhere = $this->SQL_forWhere();

return 'UPDATE `'.$this->TableName().'` SET'.$sqlSet.' WHERE '.$sqlWhere;

   }
   /*----
     HISTORY:

2010-11-16 Added "array" requirement for iData 2010-11-20 Calculation now takes place in SQL_forInsert() 2011-01-08 adapted from clsTable::Insert()

   */

/* There's no need for this here; it doesn't require indexing

   public function Insert(array $iData) {

global $sql;

$sql = $this->SQL_forInsert($iData); $this->sql = $sql; return $this->Engine()->Exec($sql);

   }
  • /

} class clsIndexer_Recs_single_key extends clsIndexer_Recs {

   private $vKeyName;
   public function KeyName() {

return $this->TblIdxObj()->KeyName();

   }
   public function KeyValue() {

return $this->DataObj()->Value($this->KeyName());

   }
   public function KeyString() {

return (string)$this->KeyValue();

   }
   public function IndexIsSet() {

return !is_null($this->KeyValue());

   }
   public function SQL_forWhere() {

$sql = $this->KeyName().'='.SQLValue($this->KeyValue()); return $sql;

   }

} class clsIndexer_Recs_multi_key extends clsIndexer_Recs {

   /*----
     RETURNS: Array of values which constitute this row's key

array[key name] = key value

   */
   public function KeyArray() {

$arKeys = $this->TblIdxObj()->KeyNames(); $arRow = $this->DataObj()->Row; foreach ($arKeys as $key) { $arOut[$key] = $arRow[$key]; } return $arOut;

   }
   /*----
     ASSUMES: keys will always be returned in the same order

If this changes, add field names.

     POTENTIAL BUG: Non-numeric keys might contain the separator character

that we are currently using ('.'). Some characters may not be appropriate for some contexts. The caller should be able to specify what separator it wants.

   */
   public function KeyString() {

$arKeys = $this->KeyArray(); $out = NULL; foreach ($arKeys as $name=>$val) { $out .= '.'.$val; } return $out;

   }
  /*----
     RETURNS: TRUE if any index fields are NULL
     ASSUMES: An index may not contain any NULL fields. Perhaps this is untrue, and it should

only return TRUE if *all* index fields are NULL.

   */
   public function IndexIsSet() {

$arKeys = $this->KeyArray(); $isset = TRUE; foreach ($arKeys as $key=>$val) { if (is_null($val)) { $isset = FALSE; } } return $isset;

   }
   /*----
     RETURNS: SQL to filter for the current record by key value(s)
     HISTORY:

2011-01-08 written for Insert() 2011-01-19 moved from clsIndexer_Recs to clsIndexer_Recs_multi_key

   */
   public function SQL_forWhere() {

$arVals = $this->TblIdxObj()->KeyNames(); return SQL_for_filter($arVals);

   }

} /*=============

 NAME: clsTable_indexed
 PURPOSE: handles indexes via a helper object
  • /

class clsTable_indexed extends clsTable_keyed_abstract {

   protected $objIdx;
   /*----
     NOTE: In practice, how would you ever have the Indexer object created before the Table object,

since the Indexer object requires a Table object in its constructor? Possibly descendent classes can create the Indexer in their constructors and then pass it back to the parent constructor, which lets you have a default Indexer that you can override if you need, but how useful is this?

   */
   public function __construct(clsDatabase $iDB, clsIndexer_Table $iIndexer=NULL) {

parent::__construct($iDB); $this->Indexer($iIndexer);

   }
   // BOILERPLATE BEGINS
   protected function Indexer(clsIndexer_Table $iObj=NULL) {

if (!is_null($iObj)) { $this->objIdx = $iObj; } return $this->objIdx;

   }
   public function GetItem(array $iVals=NULL) {

return $this->Indexer()->GetItem($iVals);

   }
   // BOILERPLATE ENDS
   // OVERRIDES
   /*----
     ADDS: spawns an indexer and attaches it to the item
   */
   protected function ReleaseItem(clsRecs_abstract $iItem) {

parent::ReleaseItem($iItem); $this->Indexer()->InitRecs($iItem);

   }
   /*----
     ADDS: spawns an indexer and attaches it to the item
   */

/*

   public function SpawnItem($iClass=NULL) {

$obj = parent::SpawnItem($iClass); return $obj;

   }
  • /

} /*=============

 NAME: clsRecs_indexed
  • /

class clsRecs_indexed extends clsRecs_keyed_abstract {

   protected $objIdx;

/* This is never used

   public function __construct(clsIndexer_Recs $iIndexer=NULL) {

$this->Indexer($iIndexer);

   }
  • /
   // BOILERPLATE BEGINS
   public function Indexer(clsIndexer_Recs $iObj=NULL) {

if (!is_null($iObj)) { $this->objIdx = $iObj; } assert('is_object($this->objIdx);'); return $this->objIdx;

   }
   public function IsNew() {

return !$this->Indexer()->IndexIsSet();

   }
   /*----
     USED BY: Administrative UI classes which need a string for referring to a particular record
   */
   public function KeyString() {

return $this->Indexer()->KeyString();

   }
   public function SelfFilter() {

return $this->Indexer()->SQL_forWhere();

   }
   public function SQL_forUpdate(array $iSet) {

return $this->Indexer()->SQL_forUpdate($iSet);

   }
   // BOILERPLATE ENDS
   public function SQL_forMake(array $iarSet) { die('Not yet written.'); }

} /*%%%%

 PURPOSE: for tracking whether a cached object has the expected data or not
 HISTORY:
   2011-03-30 written
  • /

class clsObjectCache {

   private $vKey;
   private $vObj;
   public function __construct() {

$this->vKey = NULL; $this->vObj = NULL;

   }
   public function IsCached($iKey) {

if (is_object($this->vObj)) { return ($this->vKey == $iKey); } else { return FALSE; }

   }
   public function Object($iObj=NULL,$iKey=NULL) {

if (!is_null($iObj)) { $this->vKey = $iKey; $this->vObj = $iObj; } return $this->vObj;

   }
   public function Clear() {

$this->vObj = NULL;

   }

} class clsSQLFilt {

   private $arFilt;
   private $strConj;
   public function __construct($iConj) {

$this->strConj = $iConj;

   }
   /*-----
     ACTION: Add a condition
   */
   public function AddCond($iSQL) {

$this->arFilt[] = $iSQL;

   }
   public function RenderFilter() {

$out = ; foreach ($this->arFilt as $sql) { if ($out != ) { $out .= ' '.$this->strConj.' '; } $out .= '('.$sql.')'; } return $out;

   }

} /* ========================

*** UTILITY FUNCTIONS ***
  • /

/*----

 PURPOSE: This gets around PHP's apparent lack of built-in object type-conversion.
 ACTION: Copies all public fields from iSrce to iDest
  • /

function CopyObj(object $iSrce, object $iDest) {

   foreach($iSrce as $key => $val) {

$iDest->$key = $val;

   }

} if (!function_exists('Pluralize')) {

   function Pluralize($iQty,$iSingular=,$iPlural='s') {

if ($iQty == 1) { return $iSingular; } else { return $iPlural; }

 }

}

function SQLValue($iVal) {

   if (is_array($iVal)) {

foreach ($iVal as $key => $val) { $arOut[$key] = SQLValue($val); } return $arOut;

   } else {

if (is_null($iVal)) { return 'NULL'; } else if (is_bool($iVal)) { return $iVal?'TRUE':'FALSE'; } else if (is_string($iVal)) { $oVal = '"'.mysql_real_escape_string($iVal).'"'; return $oVal; } else {

   // numeric can be raw
   // all others, we don't know how to handle, so return raw as well

return $iVal; }

   }

} function SQL_for_filter(array $iVals) {

   $sql = NULL;
   foreach ($arVals as $name => $val) {

if (!is_null($sql)) { $sql .= ' AND '; } $sql .= '('.$name.'='.SQLValue($val).')';

   }
   return $sql;

} function NoYes($iBool,$iNo='no',$iYes='yes') {

   if ($iBool) {

return $iYes;

   } else {

return $iNo;

   }

}

function nz(&$iVal,$default=NULL) {

   return empty($iVal)?$default:$iVal;

} /*-----

 FUNCTION: nzAdd -- NZ Add
 RETURNS: ioVal += iAmt, but assumes ioVal is zero if not set (prevents runtime error)
 NOTE: iAmt is a reference so that we can pass variables which might not be set.
   Need to document why this is better than being able to pass constants.
  • /

function nzAdd(&$ioVal,&$iAmt=NULL) {

   $intAmt = empty($iAmt)?0:$iAmt;
   if (empty($ioVal)) {

$ioVal = $intAmt;

   } else {

$ioVal += $intAmt;

   }
   return $ioVal;

} /*-----

 FUNCTION: nzApp -- NZ Append
 PURPOSE: Like nzAdd(), but appends strings instead of adding numbers
  • /

function nzApp(&$ioVal,$iTxt=NULL) {

   if (empty($ioVal)) {

$ioVal = $iTxt;

   } else {

$ioVal .= $iTxt;

   }
   return $ioVal;

} function nzArray(array $iArr=NULL,$iKey,$iDefault=NULL) {

   $out = $iDefault;
   if (is_array($iArr)) {

if (array_key_exists($iKey,$iArr)) { $out = $iArr[$iKey]; }

   }
   return $out;

} function ifEmpty(&$iVal,$iDefault) {

   if (empty($iVal)) {

return $iDefault;

   } else {

return $iVal;

   }

} function FirstNonEmpty(array $iList) {

   foreach ($iList as $val) {

if (!empty($val)) { return $val; }

   }

} /*----

 ACTION: Takes a two-dimensional array and returns it flipped diagonally,
   i.e. each element out[x][y] is element in[y][x].
 EXAMPLE:
   INPUT      OUTPUT
   +---+---+  +---+---+---+
   | A | 1 |  | A | B | C |
   +---+---+  +---+---+---+
   | B | 2 |  | 1 | 2 | 3 |
   +---+---+  +---+---+---+
   | C | 3 |
   +---+---+
  • /

function ArrayPivot($iArray) {

   foreach ($iArray as $row => $col) {

if (is_array($col)) { foreach ($col as $key => $val) { $arOut[$key][$row] = $val; } }

   }
   return $arOut;

} /* ========================

*** DEBUGGING FUNCTIONS ***
  • /

// these could later be expanded to create a call-path for errors, etc.

function CallEnter($iObj,$iLine,$iName) {

 global $intCallDepth, $debug;
 if (KDO_DEBUG_STACK) {
   $strDescr =  ' line '.$iLine.' ('.get_class($iObj).')'.$iName;
   _debugLine('enter','>',$strDescr);
   $intCallDepth++;
   _debugDump();
 }

} function CallExit($iName) {

 global $intCallDepth, $debug;
 if (KDO_DEBUG_STACK) {
   $intCallDepth--;
   _debugLine('exit','<',$iName);
   _debugDump();
 }

} function CallStep($iDescr) {

 global $intCallDepth, $debug;
 if (KDO_DEBUG_STACK) {
   _debugLine('step',':',$iDescr);
   _debugDump();
 }

} function LogError($iDescr) {

 global $intCallDepth, $debug;
 if (KDO_DEBUG_STACK) {
   _debugLine('error',':',$iDescr);
   _debugDump();
 }

} function _debugLine($iType,$iSfx,$iText) {

   global $intCallDepth, $debug;
   if (KDO_DEBUG_HTML) {
     $debug .= ''.str_repeat('—',$intCallDepth).$iSfx.' '.$iText.'
'; } else { $debug .= str_repeat('*',$intCallDepth).'++ '.$iText."\n"; }

} function _debugDump() {

   global $debug;
   if (KDO_DEBUG_IMMED) {

DoDebugStyle(); echo $debug; $debug = ;

   }

} function DumpArray($iArr) {

 global $intCallDepth, $debug;
 if (KDO_DEBUG) {
   while (list($key, $val) = each($iArr)) {
     if (KS_DEBUG_HTML) {
       $debug .= '
'.str_repeat('-- ',$intCallDepth+1).''; $debug .= " $key => $val"; $debug .= ''; } else { $debug .= "/ $key => $val /"; } if (KDO_DEBUG_IMMED) { DoDebugStyle(); echo $debug; $debug = ; } } }

} function DumpValue($iName,$iVal) {

 global $intCallDepth, $debug;
 if (KDO_DEBUG) {
   if (KS_DEBUG_HTML) {
     $debug .= '
'.str_repeat('-- ',$intCallDepth+1); $debug .= " $iName: [$iVal]"; $debug .= ''; } else { $debug .= "/ $iName => $iVal /"; } if (KDO_DEBUG_IMMED) { DoDebugStyle(); echo $debug; $debug = ; } }

} function DoDebugStyle() {

 static $isStyleDone = false;
 if (!$isStyleDone) {
   echo '<style type="text/css"></style>';
   $isStyleDone = true;
 }

} </php>