[geeklog-cvs] MVCnPHP PersistenceManager.class.php,NONE,1.1
tony at geeklog.net
tony at geeklog.net
Mon Dec 8 22:03:44 EST 2003
Update of /usr/cvs/geeklog/MVCnPHP
In directory geeklog_prod:/tmp/cvs-serv23524
Added Files:
PersistenceManager.class.php
Log Message:
Initial Import
--- NEW FILE: PersistenceManager.class.php ---
<?php
/**
* MVCnPHP - PersistenceManager.class.php
*
* This source file is subject to version 2.02 of the PHP license,
* that is bundled with this package in the file LICENSE, and is
* available at through the world-wide-web at
* http://www.php.net/license/2_02.txt.
* If you did not receive a copy of the PHP license and are unable to
* obtain it through the world-wide-web, please send a note to
* license at php.net so we can mail you a copy immediately.
*
* @author Jason Jones <jason at javacrawl.com>
* @author Tony Bibbs <tony at geeklog.net>
* @copyright Jason Jones 2003
* @package net.geeklog.mvc
* @version $Id: PersistenceManager.class.php,v 1.1 2003/12/09 03:03:41 tony Exp $
*
*/
/**
* A PHP Object Persistence Class
*
* This class allows seemless integration between your application models
* from a PHP MVC framework and the relational database it will hit. Use of
* this in MVCnPHP is optional on an application-by-application basis and
* by a model-to-model basis. Simply put, use it if it saves you time
* which it should. Apologies to those who may find learning this difficult
* but should you take the time to learn it you should see the benefits of
* using it and MVC in general in your PHP applications.
*
* In addition to mapping your PHP objects to underlying data structures this
* package has the added benefit of handling parent-child relationships between
* models. Additionally, child models are loaded lazily (as needed) from the
* database or immediately if explicitly asked to.
*
* For you Java nuts out there who happen to be programming PHP, this is loosely
* based on Hibernate.
*
* @author Jason Jones <jason at javacrawl.com>
* @author Tony Bibbs <tony at geeklog.net>
* @package net.geeklog.mvc
*
*/
class PersistenceManager {
/**
* Allows you to define DBMS table aliases
*
* @access private
* @var array
*
*/
var $_tableAliases = array();
/**
* Maps model classes to database tables
*
* This will map your PHP classes to their corresponding
* database table. This should hold mapped entries for
* all of the models you plan on using in your application
*
* @access private
* @var array
* @see setClass2TableMapping()
*
*/
var $_classTableMap = array('Comment' => 'COMMENT',
'Location' => 'LOCATION',
'Machine' => 'MACHINE',
'Organization' => 'ORGANIZATION',
'Role' => 'GMMS_USER_ROLE',
'User' => 'GMMS_USER');
/**
* Defines foreign key relationships between various models
*
* @access private
* @var array
* @see setForeignKeys()
*/
var $_foreignKeys = array('LOCATION.ADDRESS_ID' => 'ADDRESS.ADDRESS_ID',
'ORGANIZATION.ADDRESS_ID' => 'ADDRESS.ADDRESS_ID',
'ORGANIZATION.RECORDS_ADDRESS_ID' => 'ADDRESS.ADDRESS_ID');
/**
* Constructor
*
* This doesn't currently do anything...it's just a stub
*
* @author Jason Jones <jason at javacrawl.com>
* @access public
*
*/
function PersistenceManager()
{
}
/**
* Sets the class-to-table mapping for an application
*
* @author Tony Bibbs <tony at geeklog.net>
* @access public
* @param array $mappingArray Maps model class names to DB table names
*
*/
function setClass2TableMapping($mappingArray)
{
$this->_classTableMap = $mappingArray;
}
/**
* Establishes the foreign key relationships between application
* models
*
* @author Tony Bibbs <tony at geeklog.net>
* @access public
* @param array $foreignKeyArray Array of foreign key mappings of
* the form array('<tableA>.<fieldA>' = > <tableB>.<fieldB>', etc.)
*
*/
function setForeignKeys($foreignKeyArray)
{
$this->_foreignKeys = $foreignKeyArray;
}
/**
* Begins a SQL transaction using PEAR::DB
*
* @author Tony Bibbs <tony.bibbs at iowa.gov>
* @access public
*
*/
function beginTransaction()
{
global $_DB;
$_DB->autoCommit(false);
}
/**
* Ends a SQL Transaction using PEAR::DB
*
* @author Tony Bibbs <tony.bibbs at iowa.gov>
* @access public
* @return boolean True if transaction worked, otherwise false
*
*/
function endTransaction()
{
global $_DB;
$retval = $_DB->commit();
if (DB::isError($retval)) {
$retval = $_DB->rollback();
if (DB::isError($retval)) {
// Couldn't even rollback, should handle the error
trigger_error('An error was encoungered in PersistenceManager::endTransaction(). We
were unable to roll the transaction back. NOTE: this means you had two
problems, 1) something prevented the commit() to work and 2) the rollbock()
did not work');
}
$_DB->autoCommit(true);
return false;
}
$_DB->autoCommit(true);
return true;
}
/**
* Inserts a new record for the model
*
* NOTE: dataObject is pass-by-reference in this method
*
* Inserts the object into the database. If the uniquely identifying field
* is *not* set in the object then this method will assume that it is autogenerated
* and it will populate the object with the new value
*
* @author Jason Jones <jason at javacrawl.com>
* @access public
*
*/
function insert(&$dataObject)
{
global $_DB;
$table = $this->_classTableMap[ucfirst(get_class($dataObject))];
$attribColumnMap = $dataObject->getAttributeColumnMap();
$attribValuesMap = get_object_vars($dataObject);
$uniqueAttributes = $dataObject->getUniqueAttributes();
$uniqueAutoGenAttribute = NULL;
$columnValueMap = array();
foreach ($attribColumnMap as $attribute => $column) {
$objVal = $attribValuesMap['_' . $attribute];
if (in_array($attribute, $uniqueAttributes) && $objVal === NULL) {
// unique attributes not contained in object -- need to return the later
$uniqueAutoGenAttribute = $attribute;
}
if ($objVal !== NULL) {
$columnValueMap[$column] = $objVal;
}
}
// now we need to check for "dotted" column names and handle any inserts that need
// to occur before the "main" insert
$innerInsertTables = array();
$innerInsertFKeys = array();
$innerInsertMaps = array(); // array of arrays
foreach($attribColumnMap as $attribute => $column) {
if ($dotpos = strpos($column, '.')) {
// if column is dotted (meaning it's in a PK table)
$fKeyCol = substr($column, 0, $dotpos);
$pKeyEntry = $this->_foreignKeys[$table . '.' . $fKeyCol];
$pKeyTable = substr($pKeyEntry, 0, strpos($pKeyEntry, '.'));
$pKeyCol = substr($pKeyEntry, strpos($pKeyEntry, '.')+1);
$pKeyTargetCol = substr($column, $dotpos+1);
// setup arrays for inner insert later
if (!array_key_exists($fKeyCol, $innerInsertTables)) {
$innerInsertTables[$fKeyCol] = $pKeyTable;
$innerInsertMaps[$fKeyCol] = array();
}
$innerInsertMap = &$innerInsertMaps[$fKeyCol];
$innerInsertFKeys[$fKeyCol] = $pKeyTable;
$innerInsertMap[$pKeyTargetCol] = $columnValueMap[$column];
unset($columnValueMap[$column]);
}
}
foreach($innerInsertTables as $column => $innerTable) {
$idResult = $this->_performInsert($innerTable, $innerInsertMaps[$column], FALSE);
//echo '$idResult=' . $idResult . ' for ' . $column . '<br>';
if ($idResult !== NULL) {
$columnValueMap[$column] = $idResult;
//echo 'setting new value for ' . array_search($column, $attribColumnMap) . ' to ' . $idResult . '<br>';
$this->_setValueOnObject($dataObject, array_search($column, $attribColumnMap), $idResult);
}
}
$idResult = $this->_performInsert($table, $columnValueMap, $uniqueAutoGenAttribute === NULL);
// NOTE: This can only handle one autogenerated id column at this time
if ($uniqueAutoGenAttribute !== NULL AND $idResult !== NULL) {
$this->_setValueOnObject($dataObject, $uniqueAutoGenAttribute, $idResult);
}
}
/**
* Loads data into a model given a set of criteria
*
* Returns one or more instaces of $class for all rows in the database that
* are true for the where clause
*
* @author Jason Jones <jason at javacrawl.com>
* @access public
* @param string $class Name of the model
* @param string $whereClause Criteria to get records against
* @param string $orderByClause How to order the resulting data
* @param string $limitAmount Allows us to limit the number of records retrieved
* @param string $limitOffset Used to start the retrieval of records at a predefined position
*
*/
function load($class, $whereClause='1 = 1', $orderByClause='', $limitAmount='', $limitOffset=0) {
global $_DB;
$result = null;
$attribMap = call_user_func(array($class,'getAttributeColumnMap'));
$sqlCols = array();
$fTableAlias = $this->_getTableAlias($this->_classTableMap[$class], 'NONE');
if (!$fTableAlias) {
$fTableAlias = $this->_getNewTableAlias($this->_classTableMap[$class], 'NONE');
}
$sqlTableDef = $this->_classTableMap[$class] . ' ' . $fTableAlias;
// need to check for possible joins
foreach ($attribMap as $attrib => $column) {
$fKeyTable = $this->_classTableMap[$class];
if ($dotpos = strpos($column, '.')) {
// if column is dotted (meaning it's in a PK table)
$fKeyCol = substr($column, 0, $dotpos);
$pKeyEntry = $this->_foreignKeys[$fKeyTable . '.' . $fKeyCol];
$pKeyTable = substr($pKeyEntry, 0, strpos($pKeyEntry, '.'));
$pKeyCol = substr($pKeyEntry, strpos($pKeyEntry, '.')+1);
$pKeyTargetCol = substr($column, $dotpos+1);
$pTableAlias = $this->_getTableAlias( $fKeyTable, $fKeyCol);
if (!$pTableAlias) {
// need to add join to sqlTableDef
$pTableAlias = $this->_getNewTableAlias($fKeyTable, $fKeyCol);
}
if (strpos($sqlTableDef, $pTableAlias) === false) {
$sqlTableDef .= " LEFT OUTER JOIN {$pKeyTable} {$pTableAlias} ON {$fTableAlias}.{$fKeyCol} = {$pTableAlias}.{$pKeyCol}";
}
$sqlCols[] = $pTableAlias . '.' . $pKeyTargetCol;
} else {
$sqlCols[] = $fTableAlias . '.' . $column;
}
}
// limit clause (called TOP in SQLServer)
$orderBy = ($orderByClause ? ' ORDER BY ' . $orderByClause : '');
$sql = 'SELECT ' . $topClause . ' ' . implode(', ',$sqlCols) . ' FROM ' . $sqlTableDef .
' WHERE ' . $whereClause . $orderBy;
if ($limitAmount) {
$dbresult = $_DB->limitQuery($sql, $limitOffset, $limitAmount);
} else {
$dbresult = $_DB->query($sql);
}
if (DB::isError($dbresult)) {
trigger_error($dbresult->toString(), E_USER_ERROR);
return;
}
while ($row = $dbresult->fetchRow(DB_FETCHMODE_ORDERED)) {
$instance = new $class();
foreach ($attribMap as $attrib => $column) {
// check for foreign key
if ($dotpos = strpos($column, '.')) {
$fKeyCol = substr($column, 0, $dotpos);
$primaryColumn = substr($column, $dotpos+1);
$column = $this->_getPrimaryColDef($class, $fKeyCol, $primaryColumn);
} else {
$column = $fTableAlias . '.' . $column;
}
$colNum = array_search($column, $sqlCols);
if ($colNum !== false) {
$dbValue = $row[$colNum];
if ($dbValue) {
// if a value exists for the column that maps to the attribute, call
// the setter on the current instance
$this->_setValueOnObject($instance, $attrib, $dbValue);
}
}
}
if ($result == null) {
$result = $instance;
} else if (is_object($result)) {
// if theres an object then this is the second time around and
// we shoudl conver $result into an array
$result = array($result);
$result[] = $instance;
} else {
$result[] = $instance;
}
}
$dbresult->free();
return $result;
}
/**
* Updates data from a given model
*
* Updates the data in the object to the database. Note that this method does so indiscriminantly.
* It does *not* know which fields have changed. DO NOT TRY TO UPDATE THE ID FIELDS. Or any fields
* that make up the uniquely identifiable array. This will yield unpredicatble results.
*
* @author Jason Jones <jason at javacrawl.com>
* @access public
* @param object $dataObject Model object to get data from
*
*/
function update($dataObject) {
global $_DB;
$table = $this->_classTableMap[ucfirst(get_class($dataObject))];
$attribColumnMap = $dataObject->getAttributeColumnMap();
$attribValuesMap = get_object_vars($dataObject);
$uniqueAttributes = $dataObject->getUniqueAttributes();
// create map for autoExecute()
$columnValueMap = array();
foreach ($attribColumnMap as $attribute => $column) {
// need to skip unique attributes...we can't change these
$objVal = $attribValuesMap['_' . $attribute];
if ($objVal !== NULL && !in_array($attribute, $uniqueAttributes)) {
$columnValueMap[$column] = $attribValuesMap['_' . $attribute];
}
}
// now we need to check for "dotted" column names and handle any updates that need
// to occur before the "main" update
$innerUpdateTables = array();
$innerUpdateFKeys = array();
$innerUpdateMaps = array(); // array of arrays
foreach($attribColumnMap as $attribute => $column) {
if ($dotpos = strpos($column, '.')) {
// if column is dotted (meaning it's in a PK table)
$fKeyCol = substr($column, 0, $dotpos);
$pKeyEntry = $this->_foreignKeys[$table . '.' . $fKeyCol];
$pKeyTable = substr($pKeyEntry, 0, strpos($pKeyEntry, '.'));
$pKeyCol = substr($pKeyEntry, strpos($pKeyEntry, '.')+1);
$pKeyTargetCol = substr($column, $dotpos+1);
// setup arrays for inner Update later
if (!array_key_exists($fKeyCol, $innerUpdateTables)) {
$innerUpdateTables[$fKeyCol] = $pKeyTable;
$innerUpdateMaps[$fKeyCol] = array();
}
$innerUpdateMap = &$innerUpdateMaps[$fKeyCol];
$innerUpdateFKeys[$fKeyCol] = $pKeyTable;
$innerUpdateMap[$pKeyTargetCol] = $columnValueMap[$column];
unset($columnValueMap[$column]);
}
}
foreach($innerUpdateTables as $column => $innerTable) {
$innerUpdateMap = &$innerUpdateMaps[$column];
$whereValue = $columnValueMap[$column];
$pKeyEntry = $this->_foreignKeys[$table . '.' . $column];
$pKeyTable = substr($pKeyEntry, 0, strpos($pKeyEntry, '.'));
$pKeyCol = substr($pKeyEntry, strpos($pKeyEntry, '.')+1);
if (is_string($whereValue)) {
$result = '\'' . $whereValue . '\'';
}
$innerWhereClause = $pKeyCol . ' = ?';
$innerWhereValue = array($whereValue);
unset($innerUpdateMap[$column]);
$innerResult = $this->_performUpdate($innerTable, $innerUpdateMap, $innerWhereClause, $innerWhereValue);
if (DB::isError($innerResult)) {
trigger_error($innerResult->toString(), E_USER_ERROR);
}
}
$whereClause = $this->_generateWhereClause($dataObject->getUniqueAttributes(), $dataObject, TRUE);
$valuesArray = $this->_generateValuesArray($dataObject->getUniqueAttributes(), $dataObject);
$result = $this->_performUpdate($table, $columnValueMap, $whereClause, $valuesArray);
if (PEAR::isError($result)) {
trigger_error($result->toString(), E_USER_ERROR);
return PEAR::raiseError('problem updating record');
}
}
/**
* Deletes a record corresponding to a model
*
* Deletes the record that corresponds to this object. More accurately it deletes the record in the
* database that has the same values for the uniquely identifying attributes.
*
* @author Jason Jones <jason at javacrawl.com>
* @access public
* @param object $dataObject Model to delete data for
*
*/
function delete($dataObject)
{
global $_DB;
$whereClause = $this->_generateWhereClause($dataObject->getUniqueAttributes(), $dataObject);
$valueArray = $this->_generateValuesArray($dataObject->getUniqueAttributes(), $dataObject);
$sql = "DELETE FROM {$this->_classTableMap[ucfirst(get_class($dataObject))]} WHERE {$whereClause}";
$preparedStmt = $_DB->prepare($sql);
$result = $_DB->execute($preparedStmt, $valueArray);
if (DB::isError($result)) {
trigger_error($result->toString(), E_USER_ERROR);
}
}
/**
* Generates the right 'alias.column_name' for a column in a table that is linked to the main class
* as specified in $foreignClass. Assumes that the alias already exists and it is safe to call
* _getTableAlias without checking
*
* @author Jason Jones <jason at javacrawl.com>
* @access private
* @param string $foreignClass Name of the foreign class
* @param string $foreignKey Name of foreign key
* @param string $primaryColumn Primary column name
* @return string table.fieldname result
*
*/
function _getPrimaryColDef($foreignClass, $foreignKey, $primaryColumn)
{
$foreignTable = $this->_classTableMap[$foreignClass];
$primaryCombo = $this->_foreignKeys[$foreignTable . '.' . $foreignKey];
$dotpos = strpos($primaryCombo, '.');
$primaryTable = substr($primaryCombo, 0, $dotpos);
$primaryAlias = $this->_getTableAlias($foreignTable, $foreignKey);
return $primaryAlias . '.' . $primaryColumn;
}
/**
* Generates a new table alias for the foreignKey table and column passed in. Note, this does not
* check to see if the alias it's creating already exists, nor is it able to handle a _tableAliases
* array that contains gaps.
*
* @author Jason Jones <jason at javacrawl.com>
* @access private
* @param string $foreignKeyTable Name of foreign table
* @param string $foreignKeyCol Name of foreign key in foreign table
* @return string Corresponding table alias
*
*/
function _getNewTableAlias($foreignKeyTable, $foreignKeyCol) {
$result = 'table' . (sizeof($this->_tableAliases)+1);
$this->_tableAliases[$foreignKeyTable . '.' . $foreignKeyCol] = $result;
return $result;
}
/**
* Simple getter method that retrieves the alias for a table/field pair
*
* @author Jason Jones <jason at javacrawl.com>
* @access private
* @param string $foreignKeyTable Name of table
* @param string $foreignKeyCol Name of field
* @return string table alias
*
*/
function _getTableAlias($foreignKeyTable, $foreignKeyCol)
{
$result = false;
if (array_key_exists($foreignKeyTable . '.' . $foreignKeyCol, $this->_tableAliases)) {
$result = $this->_tableAliases[$foreignKeyTable . '.' . $foreignKeyCol];
}
return $result;
}
/**
* Returns a where clause with the given attributes set equal to the values of object
*
* This function can generate raw WHERE clauses or even parameterized for use
* in a prepare statement
*
* @author Jason Jones <jason at javacrawl.com>
* @access private
* @param array $attributes Array of field names to use in where clause
* @param object $dataObject Model holding data to use in where clause
* @param boolean $parameterized Indicates if we should build standard WHERE or one for DB::prepare()
* @return string Generated where clause
*
*/
function _generateWhereClause($attributes, $dataObject, $parameterized = TRUE)
{
$attribMap = $dataObject->getAttributeColumnMap();
$whereClause = "";
foreach($attributes as $attribute) {
$column = $attribMap[$attribute];
if ($whereClause != "") {
$whereClause .= ' AND ';
}
$whereClause .= "{$column} = ";
if ($parameterized) {
$whereClause .= '?';
} else {
$methodName = 'get' . ucfirst($attribute);
$result = call_user_func(array(&$dataObject, $methodName));
if (is_string($result)) {
$result = '\'' . $result . '\'';
}
$whereClause .= $result;
}
}
return $whereClause;
}
/**
* Returns a sequential array of the values from $dataObject for $attributes
*
* @author Jason Jones <jason at javacrawl.com>
* @access private
* @param array $attributes Array of field names to use in where clause
* @param object $dataObject Model holding data to use in where clause
* @return array Array ofvalues
*
*/
function _generateValuesArray($attributes, $dataObject) {
$result = array();
foreach($attributes as $attribute) {
$methodName = 'get' . ucfirst($attribute);
$result[] = call_user_func(array(&$dataObject, $methodName));
}
return $result;
}
/**
* Does insert and possibly returns autogenerated ID value
*
* @author Jason Jones <jason at javacrawl.com>
* @access private
* @param string $table Table to insert into
* @param array $dataArray Array of values to insert
* @param boolean $containsUnique Inidicates that table uses an autongenerated ID
* @return int|null Will return autogenerated ID of table uses one
*
*/
function _performInsert($table, $dataArray, $containsUnique) {
global $_DB;
$result = $_DB->autoExecute($table, $dataArray, DB_AUTOQUERY_INSERT);
if (DB::isError($result)) {
trigger_error($result->toString(), E_USER_ERROR);
} else {
// this part is not very generic
$idResult = NULL;
if (!($containsUnique)) {
$idResult = $_DB->getOne("SELECT IDENT_CURRENT('{$table}') AS IDENT");
}
if (DB::isError($idResult)) {
trigger_error($idResult->toString(), E_USER_ERROR);
} else {
return $idResult;
}
}
}
/**
* Does insert and possibly returns autogenerated ID value
*
* @author Jason Jones <jason at javacrawl.com>
* @access private
* @param string $table Table to update
* @param array $columnValueArray Array of key/value pairs
* @param string $whereClause Any additional where clause settings
* @param array $whereValues Array of values to use in where clause
*
*/
function _performUpdate($table, $columnValueArray, $whereClause, $whereValues) {
global $_DB;
$sql = 'UPDATE ' . $table . ' SET ';
$valuesArray = array();
foreach($columnValueArray as $column => $value) {
if (sizeof($valuesArray) > 0) {
$sql .= ', ';
}
$sql .= $column . ' = ?';
$valuesArray[] = $value;
}
$sql .= ' WHERE ' . $whereClause;
$valuesArray = array_merge($valuesArray, $whereValues);
$pStmt = $_DB->prepare($sql);
$result = $_DB->execute($pStmt, $valuesArray);
if (DB::isError($result)) {
trigger_error($result->toString(), E_USER_ERROR);
return PEAR::raiseError($result->getMessage());
}
}
/**
* Calls the setter on $object for $attribute with $value
*
* NOTE: assumes all object setters are of the form setSomeattr. In
* otherwords, if you have an attribute called fname then the setter
* must be called setFname()
*
* @author Jason Jones <jason at javacrawl.com>
* @access private
* @param object $object Instance of object to call setter on
* @param string $attribute Attribute to set
* @param string $value Value to set attribute to
*
*/
function _setValueOnObject(&$object, $attribute, $value) {
$methodName = 'set' . ucfirst($attribute);
call_user_func(array(&$object, $methodName), $value);
}
}
?>
More information about the geeklog-cvs
mailing list