Source for file AlphaDAO.inc
Documentation is available at AlphaDAO.inc
require_once $config->get('sysRoot'). 'alpha/util/AlphaErrorHandlers.inc';
require_once $config->get('sysRoot'). 'alpha/util/Logger.inc';
require_once $config->get('sysRoot'). 'alpha/util/InputFilter.inc';
require_once $config->get('sysRoot'). 'alpha/exceptions/BONotFoundException.inc';
require_once $config->get('sysRoot'). 'alpha/exceptions/FailedSaveException.inc';
require_once $config->get('sysRoot'). 'alpha/exceptions/FailedDeleteException.inc';
require_once $config->get('sysRoot'). 'alpha/exceptions/LockingException.inc';
require_once $config->get('sysRoot'). 'alpha/exceptions/ValidationException.inc';
require_once $config->get('sysRoot'). 'alpha/exceptions/IllegalArguementException.inc';
require_once $config->get('sysRoot'). 'alpha/exceptions/MailNotSentException.inc';
require_once $config->get('sysRoot'). 'alpha/exceptions/BadBOTableNameException.inc';
require_once $config->get('sysRoot'). 'alpha/exceptions/FailedIndexCreateException.inc';
require_once $config->get('sysRoot'). 'alpha/exceptions/FailedLookupCreateException.inc';
require_once $config->get('sysRoot'). 'alpha/model/types/Date.inc';
require_once $config->get('sysRoot'). 'alpha/model/types/Timestamp.inc';
require_once $config->get('sysRoot'). 'alpha/model/types/Double.inc';
require_once $config->get('sysRoot'). 'alpha/model/types/Integer.inc';
require_once $config->get('sysRoot'). 'alpha/model/types/String.inc';
require_once $config->get('sysRoot'). 'alpha/model/types/Text.inc';
require_once $config->get('sysRoot'). 'alpha/model/types/Enum.inc';
require_once $config->get('sysRoot'). 'alpha/model/types/DEnum.inc';
require_once $config->get('sysRoot'). 'alpha/model/types/DEnumItem.inc';
require_once $config->get('sysRoot'). 'alpha/model/types/Boolean.inc';
require_once $config->get('sysRoot'). 'alpha/model/types/Relation.inc';
require_once $config->get('sysRoot'). 'alpha/model/types/Sequence.inc';
* Base business object class definition
* @author John Collins <dev@alphaframework.org>
* @version $Id: AlphaDAO.inc 1342 2011-03-17 16:09:36Z johnc $
* @license http://www.opensource.org/licenses/bsd-license.php The BSD License
* @copyright Copyright (c) 2011, John Collins (founder of Alpha Framework).
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the
* following conditions are met:
* * Redistributions of source code must retain the above
* copyright notice, this list of conditions and the
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other
* materials provided with the distribution.
* * Neither the name of the Alpha Framework nor the names
* of its contributors may be used to endorse or promote
* products derived from this software without specific
* prior written permission.
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
* The last database query run by this object. Useful for tracing an error.
* The version number of the object, used for locking mechanism
* The timestamp of creation
* The OID of the person who created this BO
* The timestamp of the last update
* The OID of the person who last updated this BO
* An array of the names of all of the default attributes of a persistent BO defined in this class
protected $defaultAttributes = array("OID", "lastQuery", "version_num", "dataLabels", "created_ts", "created_by", "updated_ts", "updated_by", "defaultAttributes", "transientAttributes", "uniqueAttributes", "TABLE_NAME", "logger");
* An array of the names of all of the transient attributes of a persistent BO which are not saved to the DB
protected $transientAttributes = array("lastQuery", "dataLabels", "defaultAttributes", "transientAttributes", "uniqueAttributes", "TABLE_NAME", "logger");
* An array of the uniquely-constained attributes of this persistent BO
* An array of the data labels used for displaying class attributes
private static $logger = null;
private static $connection;
* The constructor which sets up some housekeeping attributes
self::$logger = new Logger('AlphaDAO');
self::$logger->debug('>>__construct()');
$person_ID = (isset ($_SESSION['currentUser'])? $_SESSION['currentUser']->getOID(): 0);
self::$logger->debug('<<__construct');
* Gets the current connection singleton, or creates a new one if none exists
if (!isset (self::$connection)) {
self::$connection = new mysqli($config->get('sysDBHost'), $config->get('sysDBUsername'), $config->get('sysDBPassword'), $config->get('sysDB'));
if (mysqli_connect_error()) {
self::$logger->fatal('Could not connect to database: ['. mysqli_connect_errno(). '] '. mysqli_connect_error());
return self::$connection;
* Disconnects the current database connection if one exists (self::$connection is set)
if (isset (self::$connection)) {
self::$connection->close();
self::$connection = null;
* Populates the child object with the properties retrived from the database for the object $OID.
* @param integer $OID The object ID of the business object to load.
* @throws BONotFoundException
public function load($OID) {
self::$logger->debug('>>load(OID=['. $OID. '])');
if(method_exists($this, 'before_load_callback'))
$this->before_load_callback();
foreach($attributes as $att)
$fields = substr($fields, 0, - 1);
$sqlQuery = 'SELECT '. $fields. ' FROM '. $this->getTableName(). ' WHERE OID = ? LIMIT 1;';
if($stmt->prepare($sqlQuery)) {
$stmt->bind_param('i', $OID);
self::$logger->warn('The following query caused an unexpected result ['. $sqlQuery. ']');
throw new BONotFoundException('Failed to load object of OID ['. $OID. '], table ['. $this->getTableName(). '] did not exist so had to create!');
if(!isset ($row['OID']) || $row['OID'] < 1) {
throw new BONotFoundException('Failed to load object of OID ['. $OID. '] not found in database.');
self::$logger->debug('<<load');
// get the class attributes
$reflection = new ReflectionClass(get_class($this));
$properties = $reflection->getProperties();
foreach($properties as $propObj) {
$propName = $propObj->name;
// filter transient attributes
$this->set($propName, $row[$propName]);
}elseif(!$propObj->isPrivate() && isset ($this->$propName) && $this->$propName instanceof Relation) {
// handle the setting of ONE-TO-MANY relation values
if($prop->getRelationType() == 'ONE-TO-MANY') {
self::$logger->warn('Bad data stored in the table ['. $this->getTableName(). '], field ['. $propObj->name. '] bad value['. $row[$propObj->name]. '], exception ['. $e->getMessage(). ']');
}catch (PHPException $e) {
// it is possible that the load failed due to the table not being up-to-date
$count = count($missingFields);
for($i = 0; $i < $count; $i++ )
throw new BONotFoundException('Failed to load object of OID ['. $OID. '], table ['. $this->getTableName(). '] was out of sync with the database so had to be updated!');
self::$logger->debug('<<load');
if(method_exists($this, 'after_load_callback'))
$this->after_load_callback();
self::$logger->debug('<<load');
* Populates the child object from the database table by the given attribute value.
* @param string $atribute The name of the attribute to load the object by.
* @param string $value The value of the attribute to load the object by.
* @param boolean $ignoreClassType Default is false, set to true if you want to load from overloaded tables and ignore the class type
* @param array $attributes The attributes to load from the database to this object (leave blank to load all attributes)
* @throws BONotFoundException
public function loadByAttribute($attribute, $value, $ignoreClassType= false, $attributes= array()) {
self::$logger->debug('>>loadByAttribute(attribute=['. $attribute. '], value=['. $value. '], ignoreClassType=['. $ignoreClassType. '],
attributes=['. var_export($attributes, true). '])');
$this->before_loadByAttribute_callback();
if(count($attributes) == 0)
foreach($attributes as $att)
$fields = substr($fields, 0, - 1);
$sqlQuery = 'SELECT '. $fields. ' FROM '. $this->getTableName(). ' WHERE '. $attribute. ' = ? AND classname = ? LIMIT 1;';
$sqlQuery = 'SELECT '. $fields. ' FROM '. $this->getTableName(). ' WHERE '. $attribute. ' = ? LIMIT 1;';
self::$logger->debug('Query=['. $sqlQuery. ']');
$stmt = AlphaDAO::getConnection()->stmt_init();
if($stmt->prepare($sqlQuery)) {
if($this->$attribute instanceof Integer) {
$stmt->bind_param('is', $value, get_class($this));
$stmt->bind_param('i', $value);
$stmt->bind_param('ss', $value, get_class($this));
$stmt->bind_param('s', $value);
self::$logger->warn('The following query caused an unexpected result ['. $sqlQuery. ']');
throw new BONotFoundException('Failed to load object by attribute ['. $attribute. '] and value ['. $value. '], table did not exist so had to create!');
if(!isset ($row['OID']) || $row['OID'] < 1) {
throw new BONotFoundException('Failed to load object by attribute ['. $attribute. '] and value ['. $value. '], not found in database.');
self::$logger->debug('<<loadByAttribute');
$this->OID = $row['OID'];
// get the class attributes
$reflection = new ReflectionClass(get_class($this));
$properties = $reflection->getProperties();
foreach($properties as $propObj) {
$propName = $propObj->name;
if(isset ($row[$propName])) {
// filter transient attributes
$this->set($propName, $row[$propName]);
}elseif(!$propObj->isPrivate() && isset ($this->$propName) && $this->$propName instanceof Relation) {
// handle the setting of ONE-TO-MANY relation values
if($prop->getRelationType() == 'ONE-TO-MANY') {
self::$logger->warn('Bad data stored in the table ['. $this->getTableName(). '], field ['. $propObj->name. '] bad value['. $row[$propObj->name]. '], exception ['. $e->getMessage(). ']');
}catch (PHPException $e) {
// it is possible that the load failed due to the table not being up-to-date
$count = count($missingFields);
for($i = 0; $i < $count; $i++ )
throw new BONotFoundException('Failed to load object by attribute ['. $attribute. '] and value ['. $value. '], table ['. $this->getTableName(). '] was out of sync with the database so had to be updated!');
self::$logger->debug('<<loadByAttribute');
if(method_exists($this, 'after_loadByAttribute_callback'))
$this->after_loadByAttribute_callback();
self::$logger->debug('<<loadByAttribute');
* Loads all of the objects of this class into an array which is returned.
* @param integer $start The start of the SQL LIMIT clause, useful for pagination.
* @param integer $limit The amount (limit) of objects to load, useful for pagination.
* @param string $orderBy The name of the field to sort the objects by.
* @param string $order The order to sort the objects by.
* @param boolean $ignoreClassType Default is false, set to true if you want to load from overloaded tables and ignore the class type
* @return array An array containing objects of this type of business object.
* @throws BONotFoundException
public function loadAll($start= 0, $limit= 0, $orderBy= 'OID', $order= 'ASC', $ignoreClassType= false) {
self::$logger->debug('>>loadAll(start=['. $start. '], limit=['. $limit. '], orderBy=['. $orderBy. '], order=['. $order. '], ignoreClassType=['. $ignoreClassType. ']');
if(method_exists($this, 'before_loadAll_callback'))
$this->before_loadAll_callback();
// ensure that the field name provided in the orderBy param is legit
$field = $this->get($orderBy);
throw new AlphaException('The field name ['. $orderBy. '] provided in the param orderBy does not exist on the class ['. get_class($this). ']');
$sqlQuery = 'SELECT OID FROM '. $this->getTableName(). ' WHERE classname=\''. get_class($this). '\' ORDER BY '. $orderBy. ' '. $order. ';';
$sqlQuery = 'SELECT OID FROM '. $this->getTableName(). ' WHERE classname=\''. get_class($this). '\' ORDER BY '. $orderBy. ' '. $order. ' LIMIT '.
$sqlQuery = 'SELECT OID FROM '. $this->getTableName(). ' ORDER BY '. $orderBy. ' '. $order. ';';
$sqlQuery = 'SELECT OID FROM '. $this->getTableName(). ' ORDER BY '. $orderBy. ' '. $order. ' LIMIT '. $start. ', '. $limit. ';';
self::$logger->debug('<<loadAll [0]');
// now build an array of objects to be returned
$BO_Class = get_class($this);
while($row = $result->fetch_array(MYSQLI_ASSOC)) {
// the resource not allowed will be absent from the list
$this->after_loadAll_callback();
self::$logger->debug('<<loadAll ['. count($objects). ']');
* Loads all of the objects of this class by the specified attribute into an array which is returned.
* @param string $atribute The attribute to load the objects by.
* @param string $value The value of the attribute to load the objects by.
* @param integer $start The start of the SQL LIMIT clause, useful for pagination.
* @param integer $limit The amount (limit) of objects to load, useful for pagination.
* @param string $orderBy The name of the field to sort the objects by.
* @param string $order The order to sort the objects by.
* @param boolean $ignoreClassType Default is false, set to true if you want to load from overloaded tables and ignore the class type
* @return array An array containing objects of this type of business object.
* @throws BONotFoundException
public function loadAllByAttribute($attribute, $value, $start= 0, $limit= 0, $orderBy= "OID", $order= "ASC", $ignoreClassType= false) {
self::$logger->debug('>>loadAllByAttribute(attribute=['. $attribute. '], value=['. $value. '], start=['. $start. '], limit=['. $limit. '], orderBy=['. $orderBy. '], order=['. $order. '], ignoreClassType=['. $ignoreClassType. ']');
if(method_exists($this, 'before_loadAllByAttribute_callback'))
$this->before_loadAllByAttribute_callback();
if ($start != 0 && $limit != 0)
$limit = ' LIMIT '. $start. ', '. $limit. ';';
$sqlQuery = "SELECT OID FROM ". $this->getTableName(). " WHERE $attribute = ? AND classname = ? ORDER BY ". $orderBy. " ". $order. $limit;
$sqlQuery = "SELECT OID FROM ". $this->getTableName(). " WHERE $attribute = ? ORDER BY ". $orderBy. " ". $order. $limit;
self::$logger->debug($sqlQuery);
$stmt = AlphaDAO::getConnection()->stmt_init();
if($stmt->prepare($sqlQuery)) {
if($this->$attribute instanceof Integer) {
$stmt->bind_param('is', $value, get_class($this));
$stmt->bind_param('i', $value);
$stmt->bind_param('ss', $value, get_class($this));
$stmt->bind_param('s', $value);
self::$logger->warn('The following query caused an unexpected result ['. $sqlQuery. ']');
throw new BONotFoundException('Failed to load objects by attribute ['. $attribute. '] and value ['. $value. '], table did not exist so had to create!');
self::$logger->debug('<<loadAllByAttribute []');
// now build an array of objects to be returned
$BO_Class = get_class($this);
foreach($result as $row) {
// the resource not allowed will be absent from the list
$this->after_loadAllByAttribute_callback();
self::$logger->debug('<<loadAllByAttribute ['. count($objects). ']');
* Loads all of the objects of this class by the specified attributes into an array which is returned.
* @param array $atributes The attributes to load the objects by.
* @param array $values The values of the attributes to load the objects by.
* @param integer $start The start of the SQL LIMIT clause, useful for pagination.
* @param integer $limit The amount (limit) of objects to load, useful for pagination.
* @param string $orderBy The name of the field to sort the objects by.
* @param string $order The order to sort the objects by.
* @param boolean $ignoreClassType Default is false, set to true if you want to load from overloaded tables and ignore the class type
* @return array An array containing objects of this type of business object.
* @throws BONotFoundException
* @throws IllegalArguementException
public function loadAllByAttributes($attributes= array(), $values= array(), $start= 0, $limit= 0, $orderBy= 'OID', $order= 'ASC', $ignoreClassType= false) {
self::$logger->debug('>>loadAllByAttributes(attributes=['. var_export($attributes, true). '], values=['. var_export($values, true). '], start=['.
$start. '], limit=['. $limit. '], orderBy=['. $orderBy. '], order=['. $order. '], ignoreClassType=['. $ignoreClassType. ']');
$this->before_loadAllByAttributes_callback();
'] provided to loadAllByAttributes');
$count = count($attributes);
for($i = 0; $i < $count; $i++ ) {
$whereClause .= ' '. $attributes[$i]. ' = ? AND';
self::$logger->debug($whereClause);
$whereClause .= ' classname = ? AND';
// remove the last " AND"
$whereClause = substr($whereClause, 0, - 4);
$limit = ' LIMIT '. $start. ', '. $limit. ';';
$sqlQuery = "SELECT OID FROM ". $this->getTableName(). $whereClause. " ORDER BY ". $orderBy. " ". $order. $limit;
self::$logger->debug($sqlQuery);
$stmt = AlphaDAO::getConnection()->stmt_init();
if($stmt->prepare($sqlQuery)) {
// bind params where required attributes are provided
$stmt = $this->bindParams($stmt, $attributes, $values);
// we'll still need to bind the "classname" for overloaded BOs...
self::$logger->warn('The following query caused an unexpected result ['. $sqlQuery. ']');
throw new BONotFoundException('Failed to load objects by attributes ['. var_export($attributes, true). '] and values ['.
var_export($values, true). '], table did not exist so had to create!');
self::$logger->debug('<<loadAllByAttributes []');
// now build an array of objects to be returned
$BO_Class = get_class($this);
foreach($result as $row) {
// the resource not allowed will be absent from the list
$this->after_loadAllByAttributes_callback();
self::$logger->debug('<<loadAllByAttributes ['. count($objects). ']');
* Loads all of the objects of this class that where updated (updated_ts value) on the date indicated.
* @param string $date The date for which to load the objects updated on, in the format 'YYYY-MM-DD'.
* @param integer $start The start of the SQL LIMIT clause, useful for pagination.
* @param integer $limit The amount (limit) of objects to load, useful for pagination.
* @param string $orderBy The name of the field to sort the objects by.
* @param string $order The order to sort the objects by.
* @param boolean $ignoreClassType Default is false, set to true if you want to load from overloaded tables and ignore the class type
* @return array An array containing objects of this type of business object.
* @throws BONotFoundException
public function loadAllByDayUpdated($date, $start= 0, $limit= 0, $orderBy= "OID", $order= "ASC", $ignoreClassType= false) {
self::$logger->debug('>>loadAllByDayUpdated(date=['. $date. '], start=['. $start. '], limit=['. $limit. '], orderBy=['. $orderBy. '], order=['. $order. '], ignoreClassType=['. $ignoreClassType. ']');
if(method_exists($this, 'before_loadAllByDayUpdated_callback'))
$this->before_loadAllByDayUpdated_callback();
if ($start != 0 && $limit != 0)
$limit = ' LIMIT '. $start. ', '. $limit. ';';
$sqlQuery = "SELECT OID FROM ". $this->getTableName(). " WHERE updated_ts >= '". $date. " 00:00:00' AND updated_ts <= '". $date. " 23:59:59' AND classname='". get_class($this). "' ORDER BY ". $orderBy. " ". $order. $limit;
$sqlQuery = "SELECT OID FROM ". $this->getTableName(). " WHERE updated_ts >= '". $date. " 00:00:00' AND updated_ts <= '". $date. " 23:59:59' ORDER BY ". $orderBy. " ". $order. $limit;
self::$logger->debug('<<loadAllByDayUpdated []');
// now build an array of objects to be returned
$BO_Class = get_class($this);
while($row = $result->fetch_array(MYSQLI_ASSOC)) {
$this->after_loadAllByDayUpdated_callback();
self::$logger->debug('<<loadAllByDayUpdated ['. count($objects). ']');
* Loads all of the specified attribute values of this class by the specified attribute into an
* array which is returned.
* @param string $attribute The attribute name to load the field values by.
* @param string $value The value of the attribute to load the field values by.
* @param string $returnAttribute The name of the attribute to return.
* @param string $order The order to sort the BOs by.
* @param boolean $ignoreClassType Default is false, set to true if you want to load from overloaded tables and ignore the class type.
* @return array An array of field values.
* @throws BONotFoundException
self::$logger->debug('>>loadAllFieldValuesByAttribute(attribute=['. $attribute. '], value=['. $value. '], returnAttribute=['. $returnAttribute. '], order=['. $order. '], ignoreClassType=['. $ignoreClassType. ']');
$sqlQuery = "SELECT ". $returnAttribute. " FROM ". $this->getTableName(). " WHERE $attribute = '$value' AND classname='". get_class($this). "' ORDER BY OID ". $order. ";";
$sqlQuery = "SELECT ". $returnAttribute. " FROM ". $this->getTableName(). " WHERE $attribute = '$value' ORDER BY OID ". $order. ";";
self::$logger->debug('lastQuery ['. $sqlQuery. ']');
if(!$result = AlphaDAO::getConnection()->query($sqlQuery)) {
self::$logger->debug('<<loadAllFieldValuesByAttribute []');
// now build an array of attribute values to be returned
$BO_Class = get_class($this);
while($row = $result->fetch_array(MYSQLI_ASSOC)) {
$values[$count] = $row[$returnAttribute];
self::$logger->debug('<<loadAllFieldValuesByAttribute ['. count($values). ']');
* Saves the object. If $this->OID is empty or null it will INSERT, otherwise UPDATE.
* @throws FailedSaveException
* @throws LockingException
* @throws ValidationException
self::$logger->debug('>>save()');
if(method_exists($this, 'before_save_callback'))
$this->before_save_callback();
// firstly we will validate the object before we try to save it
// get the class attributes
$reflection = new ReflectionClass(get_class($this));
$properties = $reflection->getProperties();
throw new LockingException('Could not save the object as it has been updated by another user. Please try saving again.');
// set the "updated by" fields, we can only set the user id if someone is logged in
if(isset ($_SESSION['currentUser']))
$this->updated_by->setValue($_SESSION['currentUser']->getOID());
// check to see if it is a transient object that needs to be inserted
foreach($properties as $propObj) {
$propName = $propObj->name;
// Skip the OID, database auto number takes care of this.
if($propName != 'OID' && $propName != 'version_num') {
$sqlQuery .= "$propName,";
if($propName == 'version_num') {
$sqlQuery .= 'version_num,';
$sqlQuery .= 'classname,';
$sqlQuery = rtrim($sqlQuery, ",");
$sqlQuery .= ') VALUES (';
for($i = 0; $i < $savedFieldsCount; $i++ )
$sqlQuery = rtrim($sqlQuery, ','). ')';
self::$logger->debug('Query ['. $sqlQuery. ']');
$stmt = AlphaDAO::getConnection()->stmt_init();
if($stmt->prepare($sqlQuery)) {
// assume that it is a persistent object that needs to be updated
foreach($properties as $propObj) {
$propName = $propObj->name;
// Skip the OID, database auto number takes care of this.
if($propName != 'OID' && $propName != 'version_num') {
$sqlQuery .= "$propName = ?,";
if($propName == 'version_num') {
$sqlQuery .= 'version_num = ?,';
$sqlQuery .= 'classname = ?,';
$sqlQuery = rtrim($sqlQuery, ",");
$sqlQuery .= " WHERE OID=?;";
if($stmt->prepare($sqlQuery)) {
if ($stmt != null && $stmt->error == '') {
// populate the updated OID in case we just done an insert
foreach($properties as $propObj) {
$propName = $propObj->name;
if(!$propObj->isPrivate() && isset ($this->$propName) && $this->$propName instanceof Relation) {
// handle the saving of MANY-TO-MANY relation values
if($prop->getRelationType() == 'MANY-TO-MANY') {
// check to see if the rel is on this class
$lookUp = $prop->getLookup();
// first delete all of the old RelationLookup objects for this rel
$lookUp->deleteAllByAttribute('leftID', $this->getOID());
$lookUp->deleteAllByAttribute('rightID', $this->getOID());
throw new FailedSaveException('Failed to delete old RelationLookup objects on the table ['. $prop->getLookup()->getTableName(). '], error is ['. $e->getMessage(). ']');
if(isset ($_POST[$propName]) && $_POST[$propName] != '00000000000')
$OIDs = explode(',', $_POST[$propName]);
if(isset ($OIDs) && !empty($OIDs[0])) {
// now for each posted OID, create a new RelationLookup record and save
foreach ($OIDs as $oid) {
$newLookUp = new RelationLookup($lookUp->get('leftClassName'), $lookUp->get('rightClassName'));
$newLookUp->set('leftID', $this->getOID());
$newLookUp->set('rightID', $oid);
$newLookUp->set('rightID', $this->getOID());
$newLookUp->set('leftID', $oid);
throw new FailedSaveException('Failed to update a MANY-TO-MANY relation on the object, error is ['. $e->getMessage(). ']');
// handle the saving of ONE-TO-MANY relation values
if($prop->getRelationType() == 'ONE-TO-MANY') {
$prop->setValue($this->getOID());
$this->after_save_callback();
// there has been an error, so decrement the version number back
// check for unique violations
* Saves the field specified with the value supplied. Only works for persistent BOs. Note that no Alpha type
* validation is performed with this method!
* @param string $attribute The name of the attribute to save.
* @param mixed $value The value of the attribute to save.
* @throws IllegalArguementException
* @throws FailedSaveException
self::$logger->debug('>>saveAttribute(attribute=['. $attribute. '], value=['. $value. '])');
if(method_exists($this, 'before_saveAttribute_callback'))
$this->before_saveAttribute_callback();
if(!isset ($this->$attribute))
// assume that it is a persistent object that needs to be updated
$sqlQuery = 'UPDATE '. $this->getTableName(). ' SET '. $attribute. '=? WHERE OID=?;';
if($stmt->prepare($sqlQuery)) {
if($this->$attribute instanceof Integer)
$stmt->bind_param($bindingsType. 'i', $value, $this->getOID());
self::$logger->debug('Binding params ['. $bindingsType. 'i, '. $value. ', '. $this->getOID(). ']');
throw new FailedSaveException('Failed to save attribute, error is ['. $stmt->error. '], query ['. $this->lastQuery. ']');
$this->set($attribute, $value);
$this->after_saveAttribute_callback();
self::$logger->debug('<<saveAttribute');
* Validates the object to be saved.
* @throws ValidationException
self::$logger->debug('>>validate()');
if(method_exists($this, 'before_validate_callback'))
$this->before_validate_callback();
// get the class attributes
$reflection = new ReflectionClass(get_class($this));
$properties = $reflection->getProperties();
foreach($properties as $propObj) {
$propName = $propObj->name;
$this->after_validate_callback();
self::$logger->debug('<<validate ['. $valid. ']');
* Deletes the current object from the database.
* @throws FailedDeleteException
self::$logger->debug('>>delete()');
if(method_exists($this, 'before_delete_callback'))
$this->before_delete_callback();
// get the class attributes
$reflection = new ReflectionClass(get_class($this));
$properties = $reflection->getProperties();
// check for any relations on this object, then remove them to prevent orphaned data
foreach($properties as $propObj) {
$propName = $propObj->name;
if(!$propObj->isPrivate() && isset ($this->$propName) && $this->$propName instanceof Relation) {
// Handle MANY-TO-MANY rels
if($prop->getRelationType() == 'MANY-TO-MANY') {
self::$logger->debug('Deleting MANY-TO-MANY lookup objects...');
// check to see if the rel is on this class
$side = $prop->getSide(get_class($this));
self::$logger->debug('Side is ['. $side. ']'. $this->getOID());
$lookUp = $prop->getLookup();
self::$logger->debug('Lookup object['. var_export($lookUp, true). ']');
// delete all of the old RelationLookup objects for this rel
$lookUp->deleteAllByAttribute('leftID', $this->getOID());
$lookUp->deleteAllByAttribute('rightID', $this->getOID());
self::$logger->debug('...done deleting!');
// should set related field values to null (MySQL is doing this for us as-is)
if($prop->getRelationType() == 'ONE-TO-MANY' && !$prop->getRelatedClass() == 'TagObject') {
$relatedObjects = $prop->getRelatedObjects();
foreach($relatedObjects as $object) {
$object->set($prop->getRelatedClassField(), null);
// in the case of tags, we will always remove the related tags once the BO is deleted
if($prop->getRelationType() == 'ONE-TO-MANY' && $prop->getRelatedClass() == 'TagObject') {
// just making sure that the Relation is set to current OID as its transient
$prop->setValue($this->getOID());
$relatedObjects = $prop->getRelatedObjects();
foreach($relatedObjects as $object) {
$sqlQuery = "DELETE FROM ". $this->getTableName(). " WHERE OID = ?;";
if($stmt->prepare($sqlQuery)) {
$stmt->bind_param('i', $this->getOID());
self::$logger->debug('Deleted the object ['. $this->getOID(). '] of class ['. get_class($this). ']');
$this->after_delete_callback();
self::$logger->debug('<<delete');
* Delete all object instances from the database by the specified attribute matching the value provided.
* @param string $attribute The name of the field to delete the objects by.
* @param mixed $value The value of the field to delete the objects by.
* @return integer The number of rows deleted.
* @throws FailedDeleteException
self::$logger->debug('>>deleteAllByAttribute(attribute=['. $attribute. '], value=['. $value. '])');
if(method_exists($this, 'before_deleteAllByAttribute_callback'))
$this->before_deleteAllByAttribute_callback();
foreach ($doomedObjects as $object) {
// nothing found to delete
self::$logger->warn($bonf->getMessage());
}catch (AlphaException $e) {
self::$logger->debug('<<deleteAllByAttribute [0]');
if(method_exists($this, 'after_deleteAllByAttribute_callback'))
$this->after_deleteAllByAttribute_callback();
self::$logger->debug('<<deleteAllByAttribute ['. $deletedRowCount. ']');
* Gets the version_num of the object from the database (returns 0 if the BO is not saved yet).
* @throws BONotFoundException
self::$logger->debug('>>getVersion()');
if(method_exists($this, 'before_getVersion_callback'))
$this->before_getVersion_callback();
$sqlQuery = 'SELECT version_num FROM '. $this->getTableName(). ' WHERE OID = ?;';
if($stmt->prepare($sqlQuery)) {
$stmt->bind_param('i', $this->OID);
self::$logger->warn('The following query caused an unexpected result ['. $sqlQuery. ']');
throw new BONotFoundException('Failed to get the version number, table did not exist so had to create!');
if(!isset ($row['version_num']) || $row['version_num'] < 1) {
$this->after_getVersion_callback();
self::$logger->debug('<<getVersion [0]');
if(method_exists($this, 'after_getVersion_callback'))
$this->after_getVersion_callback();
$version_num = $row['version_num'];
self::$logger->debug('<<getVersion ['. $version_num. ']');
* Builds a new database table for the BO class.
self::$logger->debug('>>makeTable()');
if(method_exists($this, 'before_makeTable_callback'))
$this->before_makeTable_callback();
$sqlQuery = "CREATE TABLE ". $this->getTableName(). " (OID INT(11) ZEROFILL NOT NULL AUTO_INCREMENT,";
// get the class attributes
$reflection = new ReflectionClass(get_class($this));
$properties = $reflection->getProperties();
foreach($properties as $propObj) {
$propName = $propObj->name;
// special properties for RelationLookup OIDs
if($this instanceof RelationLookup && ($propName == 'leftID' || $propName == 'rightID'))
$sqlQuery .= "$propName INT(". $this->getPropObject($propName)->getSize(). ") ZEROFILL NOT NULL,";
$sqlQuery .= "$propName INT(". $this->getPropObject($propName)->getSize(). "),";
$sqlQuery .= "$propName DOUBLE(". $this->getPropObject($propName)->getSize(true). "),";
$sqlQuery .= "$propName VARCHAR(". $this->getPropObject($propName)->getSize(). "),";
$sqlQuery .= "$propName TEXT,";
$sqlQuery .= "$propName CHAR(1) DEFAULT '0',";
$sqlQuery .= "$propName DATE,";
$sqlQuery .= "$propName DATETIME,";
$sqlQuery .= "$propName ENUM(";
foreach($enumVals as $val) {
$sqlQuery .= "'". $val. "',";
$sqlQuery = rtrim($sqlQuery, ",");
$sqlQuery .= "$propName INT(11) ZEROFILL,";
$sqlQuery .= "$propName INT(11) ZEROFILL UNSIGNED,";
$sqlQuery .= "classname VARCHAR(100),";
$sqlQuery .= "PRIMARY KEY (OID)) TYPE=InnoDB;";
self::$logger->debug('<<makeTable');
// check the table indexes if any additional ones required
if(method_exists($this, 'after_makeTable_callback'))
$this->after_makeTable_callback();
self::$logger->info('Successfully created the table ['. $this->getTableName(). '] for the class ['. get_class($this). ']');
self::$logger->debug('<<makeTable');
* Re-builds the table if the model requirements have changed. All data is lost!
self::$logger->debug('>>rebuildTable()');
if(method_exists($this, 'before_rebuildTable_callback'))
$this->before_rebuildTable_callback();
$sqlQuery = 'DROP TABLE IF EXISTS '. $this->getTableName(). ';';
self::$logger->debug('<<rebuildTable');
if(method_exists($this, 'after_rebuildTable_callback'))
$this->after_rebuildTable_callback();
self::$logger->debug('<<rebuildTable');
* Drops the table if the model requirements have changed. All data is lost!
* @param string $tableName Optional table name, leave blank for the defined table for this class to be dropped
self::$logger->debug('>>dropTable()');
if(method_exists($this, 'before_dropTable_callback'))
$this->before_dropTable_callback();
$sqlQuery = 'DROP TABLE IF EXISTS '. $tableName. ';';
self::$logger->debug('<<dropTable');
if(method_exists($this, 'after_dropTable_callback'))
$this->after_dropTable_callback();
self::$logger->debug('<<dropTable');
* Adds in a new class property without loosing existing data (does an ALTER TABLE query on the
* @param string $propName The name of the new field to add to the database table.
self::$logger->debug('>>addProperty(propName=['. $propName. '])');
if(method_exists($this, 'before_addProperty_callback'))
$this->before_addProperty_callback();
$sqlQuery .= 'classname VARCHAR(100)';
$sqlQuery .= "$propName INT(". $this->getPropObject($propName)->getSize(). ")";
$sqlQuery .= "$propName DOUBLE(". $this->getPropObject($propName)->getSize(true). ")";
$sqlQuery .= "$propName VARCHAR(". $this->getPropObject($propName)->getSize(). ")";
$sqlQuery .= "$propName VARCHAR(". $this->getPropObject($propName)->getSize(). ")";
$sqlQuery .= "$propName TEXT";
$sqlQuery .= "$propName CHAR(1) DEFAULT '0'";
$sqlQuery .= "$propName DATE";
$sqlQuery .= "$propName DATETIME";
$sqlQuery .= "$propName ENUM(";
foreach($enumVals as $val) {
$sqlQuery .= "'". $val. "',";
$sqlQuery = rtrim($sqlQuery, ",");
$sqlQuery .= "$propName INT(11) ZEROFILL";
$sqlQuery .= "$propName INT(11) ZEROFILL UNSIGNED";
self::$logger->debug('<<addProperty');
if(method_exists($this, 'after_addProperty_callback'))
$this->after_addProperty_callback();
self::$logger->debug('<<addProperty');
* Populates the current business object from global POST data.
self::$logger->debug('>>populateFromPost()');
if(method_exists($this, 'before_populateFromPost_callback'))
$this->before_populateFromPost_callback();
// get the class attributes
$reflection = new ReflectionClass(get_class($this));
$properties = $reflection->getProperties();
foreach($properties as $propObj) {
$propName = $propObj->name;
if(isset ($_POST[$propName])) {
if ($propName == 'version_num' && isset ($_POST['version_num']))
$this->after_populateFromPost_callback();
self::$logger->debug('<<populateFromPost');
* Gets the maximum OID value from the database for this class type.
* @return integer The maximum OID value in the class table.
self::$logger->debug('>>getMAX()');
if(method_exists($this, 'before_getMAX_callback'))
$this->before_getMAX_callback();
$sqlQuery = 'SELECT MAX(OID) AS max_OID FROM '. $this->getTableName();
$row = $result->fetch_array(MYSQLI_ASSOC);
if (isset ($row['max_OID'])) {
$this->after_getMAX_callback();
self::$logger->debug('<<getMAX ['. $row['max_OID']. ']');
throw new AlphaException('Failed to get the MAX ID for the class ['. get_class($this). '] from the table ['. $this->getTableName(). '], query is ['. $this->lastQuery. ']');
self::$logger->debug('<<getMAX [0]');
* Gets the count from the database for the amount of objects of this class.
* @param array $atributes The attributes to count the objects by (optional).
* @param array $values The values of the attributes to count the objects by (optional).
public function getCount($attributes= array(), $values= array()) {
self::$logger->debug('>>getCount(attributes=['. var_export($attributes, true). '], values=['. var_export($values, true). '])');
$this->before_getCount_callback();
$whereClause = ' WHERE classname = \''. get_class($this). '\' AND';
$count = count($attributes);
for($i = 0; $i < $count; $i++ ) {
$whereClause .= ' '. $attributes[$i]. ' = \''. $values[$i]. '\' AND';
self::$logger->debug($whereClause);
// remove the last " AND"
$whereClause = substr($whereClause, 0, - 4);
if($whereClause != ' WHERE')
$sqlQuery = 'SELECT COUNT(OID) AS class_count FROM '. $this->getTableName(). $whereClause;
$sqlQuery = 'SELECT COUNT(OID) AS class_count FROM '. $this->getTableName();
$this->after_getCount_callback();
$row = $result->fetch_array(MYSQLI_ASSOC);
self::$logger->debug('<<getCount ['. $row['class_count']. ']');
return $row['class_count'];
throw new AlphaException('Failed to get the count for the class ['. get_class($this). '] from the table ['. $this->getTableName(). '], query is ['. $this->lastQuery. ']');
self::$logger->debug('<<getCount [0]');
* Gets the OID for the object in zero-padded format (same as getOID()). This version is designed
* to be overridden in case you want to do something different with the ID of your objects outside
* of the standard 11 digit OID sequence used internally in Alpha.
* @return integer 11 digit zero-padded OID value.
public function getID() {
self::$logger->debug('>>getID()');
$oid = str_pad($this->OID, 11, '0', STR_PAD_LEFT);
self::$logger->debug('<<getID ['. $oid. ']');
* Gets the OID for the object in zero-padded format (same as getID()). This version is final so cannot
* @return integer 11 digit zero-padded OID value.
public final function getOID() {
self::$logger->debug('>>getOID()');
$oid = str_pad($this->OID, 11, '0', STR_PAD_LEFT);
self::$logger->debug('<<getOID ['. $oid. ']');
* Method for getting version number of the object.
* @return Integer The object version number.
self::$logger->debug('>>getVersionNumber()');
self::$logger->debug('<<getVersionNumber ['. $this->version_num. ']');
* Populate all of the enum options for this object from the database.
self::$logger->debug('>>setEnumOptions()');
if(method_exists($this, 'before_setEnumOptions_callback'))
$this->before_setEnumOptions_callback();
// get the class attributes
$reflection = new ReflectionClass(get_class($this));
$properties = $reflection->getProperties();
// flag for any database errors
foreach($properties as $propObj) {
$propName = $propObj->name;
if ($propClass == 'Enum') {
$sqlQuery = "SHOW COLUMNS FROM ". $this->getTableName(). " LIKE '$propName'";
$row = $result->fetch_array(MYSQLI_NUM);
$this->after_setEnumOptions_callback();
throw new AlphaException('Failed to load enum options correctly for object instance of class ['. get_class($this). ']');
self::$logger->debug('<<setEnumOptions');
* Generic getter method for accessing class properties. Will use the method get.ucfirst($prop) instead if that
* method exists at a child level (by default). Set $noChildMethods to true if you don't want to use any
* get.ucfirst($prop) method even if it exists, false otherwise (default).
* @param string $prop The name of the object property to get.
* @param boolean $noChildMethods Set to true if you do not want to use getters in the child object, defaults
* @return mixed The property value.
* @throws IllegalArguementException
public function get($prop, $noChildMethods = false) {
if(self::$logger == null)
self::$logger = new Logger('AlphaDAO');
self::$logger->debug('>>get(prop=['. $prop. '], noChildMethods=['. $noChildMethods. '])');
if(method_exists($this, 'before_get_callback'))
$this->before_get_callback();
// handle attributes with a get.ucfirst($prop) method
$this->after_get_callback();
self::$logger->debug('<<get ['.eval ('return $this->get'. ucfirst($prop). '();'). '])');
return eval ('return $this->get'. ucfirst($prop). '();');
// handle attributes with no dedicated child get.ucfirst($prop) method
if(isset ($this->$prop) && method_exists($this->$prop, 'getValue')) {
$this->after_get_callback();
// complex types will have a getValue() method, return the value of that
self::$logger->debug('<<get ['. $this->$prop->getValue(). '])');
return $this->$prop->getValue();
}elseif(isset ($this->$prop)) {
if(method_exists($this, 'after_get_callback'))
$this->after_get_callback();
// simple types returned as-is
self::$logger->debug('<<get ['. $this->$prop. '])');
throw new AlphaException('Could not access the property ['. $prop. '] on the object of class ['. get_class($this). ']');
self::$logger->debug('<<get [false])');
* Generic setter method for setting class properties. Will use the method set.ucfirst($prop) instead if that
* method exists at a child level (by default). Set $noChildMethods to true if you don't want to use
* any get.ucfirst($prop) method even if it exists, false otherwise (default).
* @param string $prop The name of the property to set.
* @param mixed $value The value of the property to set.
* @param boolean $noChildMethods Set to true if you do not want to use setters in the child object, defaults
public function set($prop, $value, $noChildMethods = false) {
self::$logger->debug('>>set(prop=['. $prop. '], $value=['. $value. '], noChildMethods=['. $noChildMethods. '])');
if(method_exists($this, 'before_set_callback'))
$this->before_set_callback();
// handle attributes with a set.ucfirst($prop) method
$this->after_set_callback();
eval ('$this->set'. ucfirst($prop). "('$value');");
// handle attributes with no dedicated child set.ucfirst($prop) method
if(isset ($this->$prop)) {
$this->after_set_callback();
// complex types will have a setValue() method to call
$this->$prop->setValue($value);
// Date and Timestamp objects have a special setter accepting a string
$this->$prop->populateFromString($value);
// simple types set directly
throw new AlphaException('Could not set the property ['. $prop. '] on the object of the class ['. get_class($this). ']. Property may not exist, or else does not have a setValue() method and is private or protected.');
self::$logger->debug('<<set');
* Gets the property object rather than the value for complex attributes. Returns false if
* the property exists but is private.
* @param string $prop The name of the property we are getting.
* @return AlphaType The complex type object found.
* @throws IllegalArguementException
self::$logger->debug('>>getPropObject(prop=['. $prop. '])');
if(method_exists($this, 'before_getPropObject_callback'))
$this->before_getPropObject_callback();
// get the class attributes
$reflection = new ReflectionClass(get_class($this));
$properties = $reflection->getProperties();
// firstly, check for private
$attribute = new ReflectionProperty(get_class($this), $prop);
if($attribute->isPrivate()) {
$this->after_getPropObject_callback();
self::$logger->debug('<<getPropObject [false]');
foreach($properties as $propObj) {
$propName = $propObj->name;
$this->after_getPropObject_callback();
self::$logger->debug('<<getPropObject ['. var_export($this->$prop, true). ']');
self::$logger->debug('<<getPropObject [false]');
* Checks to see if the table exists in the database for the current business class.
self::$logger->debug('>>checkTableExists()');
if(method_exists($this, 'before_checkTableExists_callback'))
$this->before_checkTableExists_callback();
$sqlQuery = 'SHOW TABLES;';
while ($row = $result->fetch_array(MYSQLI_NUM)) {
$this->after_checkTableExists_callback();
self::$logger->debug('<<checkTableExists ['. $tableExists. ']');
throw new AlphaException('Failed to access the system database correctly, error is ['. AlphaDAO::getConnection()->error. ']');
self::$logger->debug('<<checkTableExists [false]');
* Static method to check the database and see if the table for the indicated BO class name
* exists (assumes table name will be $BOClassName less "Object").
* @param string $BOClassName The name of the business object class we are checking.
if(self::$logger == null)
self::$logger = new Logger('AlphaDAO');
self::$logger->debug('>>checkBOTableExists(BOClassName=['. $BOClassName. '])');
eval ('$tableName = '. $BOClassName. '::TABLE_NAME;');
$tableName = substr($BOClassName, 0, strpos($BOClassName, '_'));
$sqlQuery = 'SHOW TABLES;';
while ($row = $result->fetch_array(MYSQLI_NUM)) {
if ($row[0] == $tableName)
self::$logger->debug('<<checkBOTableExists ['. ($tableExists ? 'true' : 'false'). ']');
throw new AlphaException('Failed to access the system database correctly, error is ['. AlphaDAO::getConnection()->error. ']');
self::$logger->debug('<<checkBOTableExists [false]');
* Checks to see if the table in the database matches (for fields) the business class definition, i.e. if the
* database table is in sync with the class definition.
self::$logger->debug('>>checkTableNeedsUpdate()');
if(method_exists($this, 'before_checkTableNeedsUpdate_callback'))
$this->before_checkTableNeedsUpdate_callback();
self::$logger->debug('<<checkTableNeedsUpdate [true]');
// get the class attributes
$reflection = new ReflectionClass(get_class($this));
$properties = $reflection->getProperties();
foreach($properties as $propObj) {
$propName = $propObj->name;
while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
if ($propName == $row['Field']) {
// check for the "classname" field in overloaded tables
while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
if ('classname' == $row['Field']) {
if(method_exists($this, 'before_checkTableNeedsUpdate_callback'))
$this->before_checkTableNeedsUpdate_callback();
// check the table indexes
self::$logger->warn("Error while checking database indexes:\n\n". $ae->getMessage());
self::$logger->debug('<<checkTableNeedsUpdate ['. $updateRequired. ']');
throw new AlphaException('Failed to access the system database correctly, error is ['. AlphaDAO::getConnection()->error. ']');
self::$logger->debug('<<checkTableNeedsUpdate [false]');
* Returns an array containing any properties on the class which have not been created on the database
* @return array An array of missing fields in the database table.
self::$logger->debug('>>findMissingFields()');
if(method_exists($this, 'before_findMissingFields_callback'))
$this->before_findMissingFields_callback();
$missingFields = array();
// get the class attributes
$reflection = new ReflectionClass(get_class($this));
$properties = $reflection->getProperties();
foreach($properties as $propObj) {
$propName = $propObj->name;
while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
if ($propName == $row['Field']) {
// check for the "classname" field in overloaded tables
while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
if ('classname' == $row['Field']) {
$this->after_findMissingFields_callback();
self::$logger->debug('<<findMissingFields ['. var_export($missingFields, true). ']');
* Getter for the TABLE_NAME, which should be set by a child of this class.
* @return string The table name in the database.
self::$logger->debug('>>getTableName()');
eval ('$TABLE_NAME = '. get_class($this). '::TABLE_NAME;');
if(!empty($TABLE_NAME)) {
self::$logger->debug('<<getTableName ['. $TABLE_NAME. ']');
throw new AlphaException('Error: no TABLE_NAME constant set for the class '. get_class($this));
self::$logger->debug('<<getTableName []');
* Method for getting the OID of the person who created this BO.
* @return Integer The OID of the creator.
self::$logger->debug('>>getCreatorId()');
self::$logger->debug('<<getCreatorId ['. $this->created_by. ']');
* Method for getting the OID of the person who updated this BO.
* @return Integer The OID of the updator.
self::$logger->debug('>>getUpdatorId()');
self::$logger->debug('<<getUpdatorId ['. $this->updated_by. ']');
* Method for getting the date/time of when the BO was created.
self::$logger->debug('>>getCreateTS()');
self::$logger->debug('<<getCreateTS ['. $this->created_ts. ']');
* Method for getting the date/time of when the BO was last updated.
self::$logger->debug('>>getUpdateTS()');
self::$logger->debug('<<getUpdateTS ['. $this->updated_ts. ']');
* Adds the name of the attribute provided to the list of transient (non-saved) attributes for this BO.
* @param string $attributeName The name of the attribute to not save.
self::$logger->debug('>>markTransient(attributeName=['. $attributeName. '])');
self::$logger->debug('<<markTransient');
* Removes the name of the attribute provided from the list of transient (non-saved) attributes for this BO,
* ensuring that it will be saved on the next attempt.
* @param string $attributeName The name of the attribute to save.
self::$logger->debug('>>markPersistent(attributeName=['. $attributeName. '])');
self::$logger->debug('<<markPersistent');
* Adds the name of the attribute(s) provided to the list of unique (constrained) attributes for this BO.
* @param string $attribute1Name The first attribute to mark unique in the database.
* @param string $attribute2Name The second attribute to mark unique in the databse (optional, use only for composite keys).
protected function markUnique($attribute1Name, $attribute2Name= '') {
self::$logger->debug('>>markUnique(attribute1Name=['. $attribute1Name. '], attribute2Name=['. $attribute2Name. '])');
if(empty($attribute2Name)) {
// Process composite unique keys: add them seperated by a + sign
$attributes = $attribute1Name. '+'. $attribute2Name;
self::$logger->debug('<<markUnique');
* Gets an array of all of the names of the active database indexes for this class.
* @return array An array of database indexes on this table.
self::$logger->debug('>>getIndexes()');
$result = AlphaDAO::getConnection()->query($query);
while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
self::$logger->debug('<<getIndexes');
* Checks to see if all of the indexes are in place for the BO's table, creates those that are missing.
self::$logger->debug('>>checkIndexes()');
if(method_exists($this, 'before_checkIndexes_callback'))
$this->before_checkIndexes_callback();
// check for composite indexes
foreach ($indexNames as $index) {
if ($attributes[0]. '_'. $attributes[1]. '_unq_idx' == $index) {
foreach ($indexNames as $index) {
if ($prop. '_unq_idx' == $index) {
// process foreign-key indexes
// get the class attributes
$reflection = new ReflectionClass(get_class($this));
$properties = $reflection->getProperties();
foreach($properties as $propObj) {
$propName = $propObj->name;
if($prop->getRelationType() == 'MANY-TO-ONE') {
foreach ($indexNames as $index) {
if ($propName. '_fk_idx' == $index) {
$this->createForeignIndex($propName, $prop->getRelatedClass(), $prop->getRelatedClassField());
if($prop->getRelationType() == 'MANY-TO-MANY') {
$lookup = $prop->getLookup();
$lookupIndexNames = $lookup->getIndexes();
// handle index check/creation on left side of Relation
foreach ($lookupIndexNames as $index) {
if ('leftID_fk_idx' == $index) {
$lookup->createForeignIndex('leftID', $prop->getRelatedClass('left'), 'OID');
// handle index check/creation on right side of Relation
foreach ($lookupIndexNames as $index) {
if ('rightID_fk_idx' == $index) {
$lookup->createForeignIndex('rightID', $prop->getRelatedClass('right'), 'OID');
self::$logger->error($e->getMessage());
if(method_exists($this, 'after_checkIndexes_callback'))
$this->after_checkIndexes_callback();
self::$logger->debug('<<checkIndexes');
* Creates a foreign key constraint (index) in the database on the given attribute.
* @param string $attributeName The name of the attribute to apply the index on.
* @param string $relatedClass The name of the related class in the format "NameObject".
* @param string $relatedClassAttribute The name of the field to relate to on the related class.
* @param bool $allowNullValues For foreign key indexes that don't allow null values, set this to false (default is true).
* @throws FailedIndexCreateException
protected function createForeignIndex($attributeName, $relatedClass, $relatedClassAttribute) {
self::$logger->debug('>>createForeignIndex(attributeName=['. $attributeName. '], relatedClass=['. $relatedClass. '], relatedClassAttribute=['. $relatedClassAttribute. ']');
if(method_exists($this, 'before_createForeignIndex_callback'))
$this->before_createForeignIndex_callback();
$relatedBO = new $relatedClass;
$tableName = $relatedBO->getTableName();
// if the relation is on itself (table-wise), exist without attempting to create the foreign keys
self::$logger->debug('<<createForeignIndex');
if($attributeName == 'leftID')
$sqlQuery = 'ALTER TABLE '. $this->getTableName(). ' ADD INDEX leftID_fk_idx (leftID);';
if($attributeName == 'rightID')
$sqlQuery = 'ALTER TABLE '. $this->getTableName(). ' ADD INDEX rightID_fk_idx (rightID);';
$sqlQuery = 'ALTER TABLE '. $this->getTableName(). ' ADD FOREIGN KEY '. $attributeName. '_fk_idx ('. $attributeName. ') REFERENCES '. $tableName. ' ('. $relatedClassAttribute. ') ON DELETE SET NULL;';
$this->after_createForeignIndex_callback();
self::$logger->debug('Successfully created the foreign key index ['. $attributeName. '_fk_idx]');
self::$logger->debug('<<createForeignIndex');
* Creates a unique index in the database on the given attribute(s).
* @param string $attribute1Name The first attribute to mark unique in the database.
* @param string $attribute2Name The second attribute to mark unique in the databse (optional, use only for composite keys).
* @throws FailedIndexCreateException
self::$logger->debug('>>createUniqueIndex(attribute1Name=['. $attribute1Name. '], attribute2Name=['. $attribute2Name. '])');
if(method_exists($this, 'before_createUniqueIndex_callback'))
$this->before_createUniqueIndex_callback();
if(empty($attribute2Name)) {
$sqlQuery = 'CREATE UNIQUE INDEX '. $attribute1Name. '_unq_idx ON '. $this->getTableName(). ' ('. $attribute1Name. ');';
$this->after_createUniqueIndex_callback();
// process composite unique keys
$sqlQuery = 'CREATE UNIQUE INDEX '. $attribute1Name. '_'. $attribute2Name. '_unq_idx ON '. $this->getTableName(). ' ('. $attribute1Name. ','. $attribute2Name. ');';
$this->after_createUniqueIndex_callback();
self::$logger->debug('<<createUniqueIndex');
* Parses a MySQL error for the value that violated a unique constraint.
* @param string $error The MySQL error string.
self::$logger->debug('>>findOffendingValue(error=['. $error. '])');
$singleQuote1 = strpos($error,"'");
$singleQuote2 = strrpos($error,"'");
$value = substr($error, $singleQuote1, ($singleQuote2- $singleQuote1)+ 1);
self::$logger->debug('<<findOffendingValue ['. $value. '])');
* Gets the data labels array.
* @return array An array of attribute labels.
self::$logger->debug('>>getDataLabels()');
self::$logger->debug('<<getDataLabels() ['. var_export($this->dataLabels, true). '])');
* Gets the data label for the given attribute name.
* @param $att The attribute name to get the label for.
* @throws IllegalArguementException
self::$logger->debug('>>getDataLabel(att=['. $att. '])');
self::$logger->debug('<<getDataLabel ['. $this->dataLabels[$att]. '])');
throw new IllegalArguementException('No data label found on the class ['. get_class($this). '] for the attribute ['. $att. ']');
self::$logger->debug('<<getDataLabel [])');
* Loops over the core and custom BO directories and builds an array of all of the BO class names in the system.
* @return array An array of business object class names.
if(self::$logger == null)
self::$logger = new Logger('AlphaDAO');
self::$logger->debug('>>getBOClassNames()');
$classNameArray = array();
if(file_exists($config->get('sysRoot'). 'model')) { // it is possible it has not been created yet...
// first get any custom BOs
$handle = opendir($config->get('sysRoot'). 'model');
// loop over the business object directory
while (false !== ($file = readdir($handle))) {
$classname = substr($file, 0, - 4);
// now loop over the core BOs provided with Alpha
$handle = opendir($config->get('sysRoot'). 'alpha/model');
// loop over the business object directory
while (false !== ($file = readdir($handle))) {
$classname = substr($file, 0, - 4);
self::$logger->debug('<<getBOClassNames ['. var_export($classNameArray, true). ']');
* Get the array of default attribute names.
* @return array An array of attribute names.
self::$logger->debug('>>getDefaultAttributes()');
self::$logger->debug('<<getDefaultAttributes ['. var_export($this->defaultAttributes, true). ']');
* Get the array of transient attribute names.
* @return array An array of attribute names.
self::$logger->debug('>>getTransientAttributes()');
self::$logger->debug('<<getTransientAttributes ['. var_export($this->transientAttributes, true). ']');
* Get the array of persistent attribute names, i.e. those that are saved in the database.
* @return array An array of attribute names.
self::$logger->debug('>>getPersistentAttributes()');
// get the class attributes
$reflection = new ReflectionClass(get_class($this));
$properties = $reflection->getProperties();
foreach($properties as $propObj) {
$propName = $propObj->name;
// filter transient attributes
self::$logger->debug('<<getPersistentAttributes ['. var_export($attributes, true). ']');
* Private setter for the object ID, used from load methods.
* @param integer $OID The Object ID.
private function setOID($OID) {
self::$logger->debug('>>setOID(OID=['. $OID. '])');
self::$logger->debug('<<setOID');
* Inspector to see if the business object is transient (not presently stored in the database).
self::$logger->debug('>>isTransient()');
if(empty($this->OID) || !isset ($this->OID) || $this->OID == '00000000000') {
self::$logger->debug('<<isTransient [true]');
self::$logger->debug('<<isTransient [false]');
* Get the last database query run on this object.
* @return string An SQL query string.
self::$logger->debug('>>getLastQuery()');
self::$logger->debug('<<getLastQuery ['. $this->lastQuery. ']');
* Unsets all of the attributes of this object to null.
private function clear() {
self::$logger->debug('>>clear()');
// get the class attributes
$reflection = new ReflectionClass(get_class($this));
$properties = $reflection->getProperties();
foreach($properties as $propObj) {
$propName = $propObj->name;
if(!$propObj->isPrivate())
self::$logger->debug('<<clear');
* Reloads the object from the database, overwritting any attribute values in memory.
self::$logger->debug('>>reload()');
throw new AlphaException('Cannot reload transient object from database!');
self::$logger->debug('<<reload');
* Loads the definition from the file system for the BO class name provided.
* @param string $classname The name of the business object class name.
* @throws IllegalArguementException
if(self::$logger == null)
self::$logger = new Logger('AlphaDAO');
self::$logger->debug('>>loadClassDef(classname=['. $classname. '])');
if(file_exists($config->get('sysRoot'). 'model/'. $classname. '.inc'))
require_once $config->get('sysRoot'). 'model/'. $classname. '.inc';
elseif(file_exists($config->get('sysRoot'). 'alpha/model/'. $classname. '.inc'))
require_once $config->get('sysRoot'). 'alpha/model/'. $classname. '.inc';
elseif(file_exists($config->get('sysRoot'). 'alpha/model/types/'. $classname. '.inc'))
require_once $config->get('sysRoot'). 'alpha/model/types/'. $classname. '.inc';
self::$logger->debug('<<loadClassDef');
* Checks if the definition for the BO class name provided exists on the file system.
* @param string $classname The name of the business object class name.
if(self::$logger == null)
self::$logger = new Logger('AlphaDAO');
self::$logger->debug('>>checkClassDefExists(classname=['. $classname. '])');
if(file_exists($config->get('sysRoot'). 'model/'. $classname. '.inc'))
if(file_exists($config->get('sysRoot'). 'alpha/model/'. $classname. '.inc'))
if(file_exists($config->get('sysRoot'). 'alpha/model/types/'. $classname. '.inc'))
self::$logger->debug('<<checkClassDefExists ['. $exists. ']');
* Checks that a record exists for the BO in the database.
* @param int $OID The Object ID of the object we want to see whether it exists or not.
self::$logger->debug('>>checkRecordExists(OID=['. $OID. '])');
if(method_exists($this, 'before_checkRecordExists_callback'))
$this->before_checkRecordExists_callback();
$sqlQuery = 'SELECT OID FROM '. $this->getTableName(). ' WHERE OID = ?;';
if($stmt->prepare($sqlQuery)) {
$stmt->bind_param('i', $OID);
$this->after_checkRecordExists_callback();
self::$logger->debug('<<checkRecordExists [true]');
self::$logger->debug('<<checkRecordExists [false]');
self::$logger->debug('<<checkRecordExists [false]');
self::$logger->debug('<<checkRecordExists [false]');
* Checks to see if the table name matches the classname, and if not if the table
* name matches the classname name of another BO, i.e. the table is used to store
* @throws BadBOTableNameException
self::$logger->debug('>>isTableOverloaded()');
$classname = get_class($this);
// use reflection to check to see if we are dealing with a persistent type (e.g. DEnum) which are never overloaded
$reflection = new ReflectionClass($classname);
$implementedInterfaces = $reflection->getInterfaces();
foreach ($implementedInterfaces as $interface) {
if ($interface->name == 'AlphaTypeInterface') {
self::$logger->debug('<<isTableOverloaded [false]');
if($classname != $tablename) {
// loop over all BOs to see if there is one using the same table as this BO
$BOclasses = self::getBOClassNames();
foreach($BOclasses as $BOclassName) {
if($tablename == $BOclassName) {
self::$logger->debug('<<isTableOverloaded [true]');
throw new BadBOTableNameException('The table name ['. $tablename. '] for the class ['. $classname. '] is invalid as it does not match a BO definition in the system!');
self::$logger->debug('<<isTableOverloaded [false]');
// check to see if there is already a "classname" column in the database for this BO
while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
if ('classname' == $row['Field']) {
self::$logger->debug('<<isTableOverloaded [true]');
self::$logger->warn('Error during show columns ['. AlphaDAO::getConnection()->error. ']');
self::$logger->debug('<<isTableOverloaded [false]');
* Starts a new database transaction.
public static function begin() {
if(self::$logger == null)
self::$logger = new Logger('AlphaDAO');
self::$logger->debug('>>begin()');
if (!AlphaDAO::getConnection()->autocommit(false))
self::$logger->debug('<<begin');
* Commits the current database transaction.
* @throws FailedSaveException
public static function commit() {
if(self::$logger == null)
self::$logger = new Logger('AlphaDAO');
self::$logger->debug('>>commit()');
if (!AlphaDAO::getConnection()->commit())
self::$logger->debug('<<commit');
* Aborts the current database transaction.
if(self::$logger == null)
self::$logger = new Logger('AlphaDAO');
self::$logger->debug('>>rollback()');
if (!AlphaDAO::getConnection()->rollback())
self::$logger->debug('<<rollback');
* Static method that tries to determine if the system database has been installed or not.
if(self::$logger == null)
self::$logger = new Logger('AlphaDAO');
self::$logger->debug('>>isInstalled()');
* Install conditions are:
self::$logger->debug('<<isInstalled [true]');
self::$logger->debug('<<isInstalled [false]');
* Returns true if the BO has a Relation property called tags, false otherwise.
if(isset ($this->taggedAttributes) && isset ($this->tags) && $this->tags instanceof Relation)
* Setter for the BO version number.
* @param integer $versionNumber The version number.
private function setVersion($versionNumber) {
* Cast a BO to another type of BO. A new BO will be returned with the same OID and
* version_num as the old BO, so this is NOT a true cast but is a copy. All attribute
* values will be copied accross.
* @param string $targetClassName The name of the target BO class.
* @param AlphaDAO $originalBO The original business object.
* @return AlphaDAO The new business object resulting from the cast.
public function cast($targetClassName, $originalBO) {
$BO = new $targetClassName;
$BO->setOID($originalBO->getOID());
$BO->setVersion($originalBO->getVersion());
// get the class attributes
$originalBOreflection = new ReflectionClass(get_class($originalBO));
$originalBOproperties = $originalBOreflection->getProperties();
$newBOreflection = new ReflectionClass($targetClassName);
$newBOproperties = $newBOreflection->getProperties();
// copy the property values from the old BO to the new BO
if(count($originalBOproperties) < count($newBOproperties)) {
// the original BO is smaller, so loop over its properties
foreach($originalBOproperties as $propObj) {
$propName = $propObj->name;
$BO->set($propName, $originalBO->get($propName));
// the new BO is smaller, so loop over its properties
foreach($newBOproperties as $propObj) {
$propName = $propObj->name;
$BO->set($propName, $originalBO->get($propName));
* Converts "BusinessObject" to "Business" and returns.
* Dynamically binds all of the attributes for the current BO to the supplied prepared statement
* parameters. If arrays of attribute names and values are provided, only those will be bound to
* the supplied statement.
* @param mysqli_stmt $stmt The SQL statement to bind to.
* @param array Optional array of BO attributes.
* @param array Optional array of BO values.
protected function bindParams($stmt, $attributes= array(), $values= array()) {
self::$logger->debug('>>bindParams(stmt=['. var_export($stmt, true). '])');
// here we are only binding the supplied attributes
for($i = 0; $i < $count; $i++ ) {
if(isset ($this->classname)) {
}else{ // bind all attributes on the business object
// get the class attributes
$reflection = new ReflectionClass(get_class($this));
$properties = $reflection->getProperties();
foreach($properties as $propObj) {
$propName = $propObj->name;
// Skip the OID, database auto number takes care of this.
if($propName != 'OID' && $propName != 'version_num') {
if($this->$propName instanceof Integer)
if($propName == 'version_num') {
if(isset ($this->classname)) {
// the OID may be on the WHERE clause for UPDATEs and DELETEs
self::$logger->debug('bindingsTypes=['. $bindingsTypes. '], count: ['. strlen($bindingsTypes). ']');
self::$logger->debug('params ['. var_export($params, true). ']');
$bind_names[] = $bindingsTypes;
for ($i = 0; $i < $count; $i++ ) {
$ $bind_name = $params[$i];
$bind_names[] = &$ $bind_name;
self::$logger->debug('<<bindParams ['. var_export($stmt, true). ']');
* Dynamically binds the result of the supplied prepared statement to a 2d array, where each element in the array is another array
* representing a database row.
* @param mysqli_stmt $stmt
* @return array A 2D array containing the query result.
$metadata = $stmt->result_metadata();
$fields = $metadata->fetch_fields();
foreach ($fields as $field) {
$fieldname = $field->name;
$pointers[] = &$row[$fieldname];
* Check to see if an attribute exists on the BO.
* @param $attribute The attribute name.
$exists = $this->$attribute;
|