| alpha--model

# Source for file AlphaDAO.inc Documentation is available at AlphaDAO.inc <?php 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 * * @package alpha::model * @since 1.0 * @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). * All rights reserved. * * <pre> * 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 * following disclaimer. * * 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. * </pre> * */ abstract class AlphaDAO { /** * The object ID * * @var integer * @since 1.0 */ protected $OID; /** * The last database query run by this object. Useful for tracing an error. * * @var string * @since 1.0 */ protected $lastQuery; /** * The version number of the object, used for locking mechanism * * @var Integer * @since 1.0 */ protected $version_num; /** * The timestamp of creation * * @var Timestamp * @since 1.0 */ protected $created_ts; /** * The OID of the person who created this BO * * @var Integer * @since 1.0 */ protected $created_by; /** * The timestamp of the last update * * @var Timestamp * @since 1.0 */ protected $updated_ts; /** * The OID of the person who last updated this BO * * @var Integer * @since 1.0 */ protected $updated_by; /** * An array of the names of all of the default attributes of a persistent BO defined in this class * * @var array * @since 1.0 */ 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 * * @var array * @since 1.0 */ protected $transientAttributes = array("lastQuery", "dataLabels", "defaultAttributes", "transientAttributes", "uniqueAttributes", "TABLE_NAME", "logger"); /** * An array of the uniquely-constained attributes of this persistent BO * * @var array * @since 1.0 */ protected $uniqueAttributes = array(); /** * An array of the data labels used for displaying class attributes * * @var array * @since 1.0 */ protected $dataLabels = array(); /** * Trace logger * * @var Logger * @since 1.0 */ private static $logger = null; /** * Datebase connection * * @var mysqli * @since 1.0 */ private static $connection; /** * The constructor which sets up some housekeeping attributes * * @since 1.0 */ public function __construct() { self::$logger = new Logger('AlphaDAO'); self::$logger->debug('>>__construct()'); $this->version_num = new Integer(0); $this->created_ts = new Timestamp(date("Y-m-d H:i:s")); $person_ID = (isset($_SESSION['currentUser'])? $_SESSION['currentUser']->getOID(): 0); $this->created_by = new Integer($person_ID); $this->updated_ts = new Timestamp(date("Y-m-d H:i:s")); $this->updated_by = new Integer($person_ID); self::$logger->debug('<<__construct'); } /** * Gets the current connection singleton, or creates a new one if none exists * * @return mysqli * @since 1.0 */ public static function getConnection() { global $config; 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) * * @since 1.0 */ public static function disconnect() { 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. * @since 1.0 * @throws BONotFoundException */ public function load($OID) { self::$logger->debug('>>load(OID=['.$OID.'])'); if(method_exists($this, 'before_load_callback')) $this->before_load_callback(); $this->OID = $OID; $attributes = $this->getPersistentAttributes(); $fields = ''; foreach($attributes as $att) $fields .= $att.','; $fields = substr($fields, 0, -1); $sqlQuery = 'SELECT '.$fields.' FROM '.$this->getTableName().' WHERE OID = ? LIMIT 1;'; $this->lastQuery = $sqlQuery; $stmt = AlphaDAO::getConnection()->stmt_init(); $row = array(); if($stmt->prepare($sqlQuery)) { $stmt->bind_param('i', $OID); $stmt->execute(); $result = $this->bindResult($stmt); if(isset($result[0])) $row = $result[0]; $stmt->close(); }else{ self::$logger->warn('The following query caused an unexpected result ['.$sqlQuery.']'); if(!$this->checkTableExists()) { $this->makeTable(); throw new BONotFoundException('Failed to load object of OID ['.$OID.'], table ['.$this->getTableName().'] did not exist so had to create!'); } return; } 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'); return; } // get the class attributes $reflection = new ReflectionClass(get_class($this)); $properties = $reflection->getProperties(); try { foreach($properties as $propObj) { $propName = $propObj->name; // filter transient attributes if(!in_array($propName, $this->transientAttributes)) { $this->set($propName, $row[$propName]); }elseif(!$propObj->isPrivate() && isset($this->$propName) && $this->$propName instanceof Relation) { $prop = $this->getPropObject($propName); // handle the setting of ONE-TO-MANY relation values if($prop->getRelationType() == 'ONE-TO-MANY') { $this->set($propObj->name, $this->getOID()); } } } }catch (IllegalArguementException $e) { 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 if($this->checkTableNeedsUpdate()) { $missingFields = $this->findMissingFields(); $count = count($missingFields); for($i = 0; $i < $count; $i++) $this->addProperty($missingFields[$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'); return; } } $this->setEnumOptions(); 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) * @since 1.0 * @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).'])'); if(method_exists($this, 'before_loadByAttribute_callback')) $this->before_loadByAttribute_callback(); if(count($attributes) == 0) $attributes = $this->getPersistentAttributes(); $fields = ''; foreach($attributes as $att) $fields .= $att.','; $fields = substr($fields, 0, -1); if(!$ignoreClassType && $this->isTableOverloaded()) $sqlQuery = 'SELECT '.$fields.' FROM '.$this->getTableName().' WHERE '.$attribute.' = ? AND classname = ? LIMIT 1;'; else $sqlQuery = 'SELECT '.$fields.' FROM '.$this->getTableName().' WHERE '.$attribute.' = ? LIMIT 1;'; self::$logger->debug('Query=['.$sqlQuery.']'); $this->lastQuery = $sqlQuery; $stmt = AlphaDAO::getConnection()->stmt_init(); $row = array(); if($stmt->prepare($sqlQuery)) { if($this->$attribute instanceof Integer) { if(!$ignoreClassType && $this->isTableOverloaded()) { $stmt->bind_param('is', $value, get_class($this)); }else{ $stmt->bind_param('i', $value); } }else{ if(!$ignoreClassType && $this->isTableOverloaded()) { $stmt->bind_param('ss', $value, get_class($this)); }else{ $stmt->bind_param('s', $value); } } $stmt->execute(); $result = $this->bindResult($stmt); if(isset($result[0])) $row = $result[0]; $stmt->close(); }else{ self::$logger->warn('The following query caused an unexpected result ['.$sqlQuery.']'); if(!$this->checkTableExists()) { $this->makeTable(); throw new BONotFoundException('Failed to load object by attribute ['.$attribute.'] and value ['.$value.'], table did not exist so had to create!'); } return; } 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'); return; } $this->OID = $row['OID']; // get the class attributes $reflection = new ReflectionClass(get_class($this)); $properties = $reflection->getProperties(); try { foreach($properties as $propObj) { $propName = $propObj->name; if(isset($row[$propName])) { // filter transient attributes if(!in_array($propName, $this->transientAttributes)) { $this->set($propName, $row[$propName]); }elseif(!$propObj->isPrivate() && isset($this->$propName) && $this->$propName instanceof Relation) { $prop = $this->getPropObject($propName); // handle the setting of ONE-TO-MANY relation values if($prop->getRelationType() == 'ONE-TO-MANY') { $this->set($propObj->name, $this->getOID()); } } } } }catch (IllegalArguementException $e) { 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 if($this->checkTableNeedsUpdate()) { $missingFields = $this->findMissingFields(); $count = count($missingFields); for($i = 0; $i < $count; $i++) $this->addProperty($missingFields[$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'); return; } } $this->setEnumOptions(); 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. * @since 1.0 * @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(); global $config; // ensure that the field name provided in the orderBy param is legit try { $field = $this->get($orderBy); }catch(AlphaException $e) { throw new AlphaException('The field name ['.$orderBy.'] provided in the param orderBy does not exist on the class ['.get_class($this).']'); } if(!$ignoreClassType && $this->isTableOverloaded()) { if($limit == 0) { $sqlQuery = 'SELECT OID FROM '.$this->getTableName().' WHERE classname=\''.get_class($this).'\' ORDER BY '.$orderBy.' '.$order.';'; }else{ $sqlQuery = 'SELECT OID FROM '.$this->getTableName().' WHERE classname=\''.get_class($this).'\' ORDER BY '.$orderBy.' '.$order.' LIMIT '. $start.', '.$limit.';'; } }else{ if($limit == 0) $sqlQuery = 'SELECT OID FROM '.$this->getTableName().' ORDER BY '.$orderBy.' '.$order.';'; else $sqlQuery = 'SELECT OID FROM '.$this->getTableName().' ORDER BY '.$orderBy.' '.$order.' LIMIT '.$start.', '.$limit.';'; } $this->lastQuery = $sqlQuery; if(!$result = AlphaDAO::getConnection()->query($sqlQuery)) { throw new BONotFoundException('Failed to load object OIDs, MySql error is ['.AlphaDAO::getConnection()->error.'], query ['.$this->lastQuery.']'); self::$logger->debug('<<loadAll [0]'); return array(); } // now build an array of objects to be returned $objects = array(); $count = 0; $BO_Class = get_class($this); while($row = $result->fetch_array(MYSQLI_ASSOC)) { try { $obj = new $BO_Class(); $obj->load($row['OID']); $objects[$count] = $obj; $count++; }catch(ResourceNotAllowedException $e) { // the resource not allowed will be absent from the list } } if(method_exists($this, 'after_loadAll_callback')) $this->after_loadAll_callback(); self::$logger->debug('<<loadAll ['.count($objects).']'); return $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. * @since 1.0 * @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(); global $config; if ($start != 0 && $limit != 0) $limit = ' LIMIT '.$start.', '.$limit.';'; else $limit = ';'; if(!$ignoreClassType && $this->isTableOverloaded()) $sqlQuery = "SELECT OID FROM ".$this->getTableName()." WHERE $attribute = ? AND classname = ? ORDER BY ".$orderBy." ".$order.$limit; else $sqlQuery = "SELECT OID FROM ".$this->getTableName()." WHERE $attribute = ? ORDER BY ".$orderBy." ".$order.$limit; $this->lastQuery = $sqlQuery; self::$logger->debug($sqlQuery); $stmt = AlphaDAO::getConnection()->stmt_init(); $row = array(); if($stmt->prepare($sqlQuery)) { if($this->$attribute instanceof Integer) { if($this->isTableOverloaded()) { $stmt->bind_param('is', $value, get_class($this)); }else{ $stmt->bind_param('i', $value); } }else{ if($this->isTableOverloaded()) { $stmt->bind_param('ss', $value, get_class($this)); }else{ $stmt->bind_param('s', $value); } } $stmt->execute(); $result = $this->bindResult($stmt); $stmt->close(); }else{ self::$logger->warn('The following query caused an unexpected result ['.$sqlQuery.']'); if(!$this->checkTableExists()) { $this->makeTable(); 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 []'); return array(); } // now build an array of objects to be returned $objects = array(); $count = 0; $BO_Class = get_class($this); foreach($result as $row) { try { $obj = new $BO_Class(); $obj->load($row['OID']); $objects[$count] = $obj; $count++; }catch(ResourceNotAllowedException $e) { // the resource not allowed will be absent from the list } } if(method_exists($this, 'after_loadAllByAttribute_callback')) $this->after_loadAllByAttribute_callback(); self::$logger->debug('<<loadAllByAttribute ['.count($objects).']'); return $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. * @since 1.0 * @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.']'); if(method_exists($this, 'before_loadAllByAttributes_callback')) $this->before_loadAllByAttributes_callback(); global $config; if(!is_array($attributes) || !is_array($values)) { throw new IllegalArguementException('Illegal arrays attributes=['.var_export($attributes, true).'] and values=['.var_export($values, true). '] provided to loadAllByAttributes'); } $whereClause = ' WHERE'; $count = count($attributes); for($i = 0; $i < $count; $i++) { $whereClause .= ' '.$attributes[$i].' = ? AND'; self::$logger->debug($whereClause); } if(!$ignoreClassType && $this->isTableOverloaded()) $whereClause .= ' classname = ? AND'; // remove the last " AND" $whereClause = substr($whereClause, 0, -4); if ($limit != 0) $limit = ' LIMIT '.$start.', '.$limit.';'; else $limit = ';'; $sqlQuery = "SELECT OID FROM ".$this->getTableName().$whereClause." ORDER BY ".$orderBy." ".$order.$limit; $this->lastQuery = $sqlQuery; self::$logger->debug($sqlQuery); $stmt = AlphaDAO::getConnection()->stmt_init(); if($stmt->prepare($sqlQuery)) { // bind params where required attributes are provided if(count($attributes) > 0 && count($attributes) == count($values)) { $stmt = $this->bindParams($stmt, $attributes, $values); }else{ // we'll still need to bind the "classname" for overloaded BOs... if($this->isTableOverloaded()) $stmt->bind_param('s', get_class($this)); } $stmt->execute(); $result = $this->bindResult($stmt); $stmt->close(); }else{ self::$logger->warn('The following query caused an unexpected result ['.$sqlQuery.']'); if(!$this->checkTableExists()) { $this->makeTable(); 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 []'); return array(); } // now build an array of objects to be returned $objects = array(); $count = 0; $BO_Class = get_class($this); foreach($result as $row) { try { $obj = new $BO_Class(); $obj->load($row['OID']); $objects[$count] = $obj; $count++; }catch(ResourceNotAllowedException $e) { // the resource not allowed will be absent from the list } } if(method_exists($this, 'after_loadAllByAttributes_callback')) $this->after_loadAllByAttributes_callback(); self::$logger->debug('<<loadAllByAttributes ['.count($objects).']'); return $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. * @since 1.0 * @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(); global $config; if ($start != 0 && $limit != 0) $limit = ' LIMIT '.$start.', '.$limit.';'; else $limit = ';'; if(!$ignoreClassType && $this->isTableOverloaded()) $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; else $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; $this->lastQuery = $sqlQuery; if(!$result = AlphaDAO::getConnection()->query($sqlQuery)) { throw new BONotFoundException('Failed to load object OIDs, MySql error is ['.AlphaDAO::getConnection()->error.'], query ['.$this->lastQuery.']'); self::$logger->debug('<<loadAllByDayUpdated []'); return array(); } // now build an array of objects to be returned $objects = array(); $count = 0; $BO_Class = get_class($this); while($row = $result->fetch_array(MYSQLI_ASSOC)) { $obj = new $BO_Class(); $obj->load($row['OID']); $objects[$count] = $obj; $count++; } if(method_exists($this, 'after_loadAllByDayUpdated_callback')) $this->after_loadAllByDayUpdated_callback(); self::$logger->debug('<<loadAllByDayUpdated ['.count($objects).']'); return $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. * @since 1.0 * @throws BONotFoundException */ public function loadAllFieldValuesByAttribute($attribute, $value, $returnAttribute, $order='ASC', $ignoreClassType=false) { self::$logger->debug('>>loadAllFieldValuesByAttribute(attribute=['.$attribute.'], value=['.$value.'], returnAttribute=['.$returnAttribute.'], order=['.$order.'], ignoreClassType=['.$ignoreClassType.']'); global $config; if(!$ignoreClassType && $this->isTableOverloaded()) $sqlQuery = "SELECT ".$returnAttribute." FROM ".$this->getTableName()." WHERE $attribute = '$value' AND classname='".get_class($this)."' ORDER BY OID ".$order.";"; else $sqlQuery = "SELECT ".$returnAttribute." FROM ".$this->getTableName()." WHERE $attribute = '$value' ORDER BY OID ".$order.";"; $this->lastQuery = $sqlQuery; self::$logger->debug('lastQuery ['.$sqlQuery.']'); if(!$result = AlphaDAO::getConnection()->query($sqlQuery)) { throw new BONotFoundException('Failed to load field ['.$returnAttribute.'] values, MySql error is ['.AlphaDAO::getConnection()->error.'], query ['.$this->lastQuery.']'); self::$logger->debug('<<loadAllFieldValuesByAttribute []'); return array(); } // now build an array of attribute values to be returned $values = array(); $count = 0; $BO_Class = get_class($this); while($row = $result->fetch_array(MYSQLI_ASSOC)) { $values[$count] = $row[$returnAttribute]; $count++; } self::$logger->debug('<<loadAllFieldValuesByAttribute ['.count($values).']'); return $values; } /** * Saves the object. If $this->OID is empty or null it will INSERT, otherwise UPDATE. * * @since 1.0 * @throws FailedSaveException * @throws LockingException * @throws ValidationException */ public function save() { 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 if(!$this->validate()) { throw new FailedSaveException('Could not save due to a validation error.'); return; }else{ // get the class attributes $reflection = new ReflectionClass(get_class($this)); $properties = $reflection->getProperties(); $sqlQuery = ''; $stmt = null; if($this->getVersion() != $this->version_num->getValue()){ throw new LockingException('Could not save the object as it has been updated by another user. Please try saving again.'); return; } // 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()); $this->updated_ts = new Timestamp(date("Y-m-d H:i:s")); // check to see if it is a transient object that needs to be inserted if($this->isTransient()) { $savedFieldsCount = 0; $sqlQuery = 'INSERT INTO '.$this->getTableName().' ('; foreach($properties as $propObj) { $propName = $propObj->name; if (!in_array($propName, $this->transientAttributes)) { // Skip the OID, database auto number takes care of this. if($propName != 'OID' && $propName != 'version_num') { $sqlQuery .= "$propName,"; $savedFieldsCount++; } if($propName == 'version_num') { $sqlQuery .= 'version_num,'; $savedFieldsCount++; } } } if($this->isTableOverloaded()) $sqlQuery .= 'classname,'; $sqlQuery = rtrim($sqlQuery, ","); $sqlQuery .= ') VALUES ('; for($i = 0; $i < $savedFieldsCount; $i++) $sqlQuery.= '?,'; if($this->isTableOverloaded()) $sqlQuery.= '?,'; $sqlQuery = rtrim($sqlQuery, ',').')'; $this->lastQuery = $sqlQuery; self::$logger->debug('Query ['.$sqlQuery.']'); $stmt = AlphaDAO::getConnection()->stmt_init(); if($stmt->prepare($sqlQuery)) { $stmt = $this->bindParams($stmt); $stmt->execute(); }else{ throw new FailedSaveException('Failed to save object, error is ['.$stmt->error.'], query ['.$this->lastQuery.']'); } }else{ // assume that it is a persistent object that needs to be updated $savedFieldsCount = 0; $sqlQuery = 'UPDATE '.$this->getTableName().' SET '; foreach($properties as $propObj) { $propName = $propObj->name; if (!in_array($propName, $this->transientAttributes)) { // Skip the OID, database auto number takes care of this. if($propName != 'OID' && $propName != 'version_num') { $sqlQuery .= "$propName = ?,"; $savedFieldsCount++; } if($propName == 'version_num') { $sqlQuery .= 'version_num = ?,'; $savedFieldsCount++; } } } if($this->isTableOverloaded()) $sqlQuery .= 'classname = ?,'; $sqlQuery = rtrim($sqlQuery, ","); $sqlQuery .= " WHERE OID=?;"; $this->lastQuery = $sqlQuery; $stmt = AlphaDAO::getConnection()->stmt_init(); if($stmt->prepare($sqlQuery)) { $this->bindParams($stmt); $stmt->execute(); }else{ throw new FailedSaveException('Failed to save object, error is ['.$stmt->error.'], query ['.$this->lastQuery.']'); } } if ($stmt != null && $stmt->error == '') { // populate the updated OID in case we just done an insert if($this->isTransient()) $this->setOID(AlphaDAO::getConnection()->insert_id); try { foreach($properties as $propObj) { $propName = $propObj->name; if(!$propObj->isPrivate() && isset($this->$propName) && $this->$propName instanceof Relation) { $prop = $this->getPropObject($propName); // handle the saving of MANY-TO-MANY relation values if($prop->getRelationType() == 'MANY-TO-MANY') { try { try{ // check to see if the rel is on this class $side = $prop->getSide(get_class($this)); }catch (IllegalArguementException $iae) { $side = $prop->getSide(ucfirst($this->getTableName()).'Object'); } $lookUp = $prop->getLookup(); // first delete all of the old RelationLookup objects for this rel try { if($side == 'left') $lookUp->deleteAllByAttribute('leftID', $this->getOID()); else $lookUp->deleteAllByAttribute('rightID', $this->getOID()); }catch (Exception $e) { 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')); if($side == 'left') { $newLookUp->set('leftID', $this->getOID()); $newLookUp->set('rightID', $oid); }else{ $newLookUp->set('rightID', $this->getOID()); $newLookUp->set('leftID', $oid); } $newLookUp->save(); } } }catch (Exception $e) { throw new FailedSaveException('Failed to update a MANY-TO-MANY relation on the object, error is ['.$e->getMessage().']'); return; } } // handle the saving of ONE-TO-MANY relation values if($prop->getRelationType() == 'ONE-TO-MANY') { $prop->setValue($this->getOID()); } } } }catch (Exception $e) { throw new FailedSaveException('Failed to save object, error is ['.$e->getMessage().']'); return; } $stmt->close(); if(method_exists($this, 'after_save_callback')) $this->after_save_callback(); }else{ // there has been an error, so decrement the version number back $temp = $this->version_num->getValue(); $this->version_num->setValue($temp-1); // check for unique violations if(AlphaDAO::getConnection()->errno == '1062') { throw new ValidationException('Failed to save, the value '.$this->findOffendingValue(AlphaDAO::getConnection()->error).' is already in use!'); return; }else{ throw new FailedSaveException('Failed to save object, MySql error is ['.AlphaDAO::getConnection()->error.'], query ['.$this->lastQuery.']'); } } } } /** * 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. * @since 1.0 * @throws IllegalArguementException * @throws FailedSaveException */ public function saveAttribute($attribute, $value) { self::$logger->debug('>>saveAttribute(attribute=['.$attribute.'], value=['.$value.'])'); if(method_exists($this, 'before_saveAttribute_callback')) $this->before_saveAttribute_callback(); if(!isset($this->$attribute)) throw new IllegalArguementException('Could not perform save, as the attribute ['.$attribute.'] is not present on the class['.get_class($this).']'); if($this->isTransient()) throw new FailedSaveException('Cannot perform saveAttribute method on transient BO!'); // assume that it is a persistent object that needs to be updated $sqlQuery = 'UPDATE '.$this->getTableName().' SET '.$attribute.'=? WHERE OID=?;'; $this->lastQuery = $sqlQuery; $stmt = AlphaDAO::getConnection()->stmt_init(); if($stmt->prepare($sqlQuery)) { if($this->$attribute instanceof Integer) $bindingsType = 'i'; else $bindingsType = 's'; $stmt->bind_param($bindingsType.'i', $value, $this->getOID()); self::$logger->debug('Binding params ['.$bindingsType.'i, '.$value.', '.$this->getOID().']'); $stmt->execute(); }else{ throw new FailedSaveException('Failed to save attribute, error is ['.$stmt->error.'], query ['.$this->lastQuery.']'); } $stmt->close(); $this->set($attribute, $value); if(method_exists($this, 'after_saveAttribute_callback')) $this->after_saveAttribute_callback(); self::$logger->debug('<<saveAttribute'); } /** * Validates the object to be saved. * * @return boolean * @since 1.0 * @throws ValidationException */ protected function validate() { self::$logger->debug('>>validate()'); if(method_exists($this, 'before_validate_callback')) $this->before_validate_callback(); $valid = true; // get the class attributes $reflection = new ReflectionClass(get_class($this)); $properties = $reflection->getProperties(); foreach($properties as $propObj) { $propName = $propObj->name; if(!in_array($propName, $this->defaultAttributes) && !in_array($propName, $this->transientAttributes)) { $propClass = get_class($this->getPropObject($propName)); if (strtoupper($propClass) != "ENUM" && strtoupper($propClass) != "DENUM" && strtoupper($propClass) != "DENUMITEM" && strtoupper($propClass) != "BOOLEAN") { if ($this->getPropObject($propName) != false && !preg_match($this->getPropObject($propName)->getRule(), $this->getPropObject($propName)->getValue())) { throw new ValidationException('Failed to save, validation error is: '.$this->getPropObject($propName)->getHelper()); $valid = false; } } } } if(method_exists($this, 'after_validate_callback')) $this->after_validate_callback(); self::$logger->debug('<<validate ['.$valid.']'); return $valid; } /** * Deletes the current object from the database. * * @since 1.0 * @throws FailedDeleteException */ public function delete() { 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) { $prop = $this->getPropObject($propName); // Handle MANY-TO-MANY rels if($prop->getRelationType() == 'MANY-TO-MANY') { self::$logger->debug('Deleting MANY-TO-MANY lookup objects...'); try{ // check to see if the rel is on this class $side = $prop->getSide(get_class($this)); }catch (IllegalArguementException $iae) { $side = $prop->getSide(ucfirst($this->getTableName()).'Object'); } 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 if($side == 'left') $lookUp->deleteAllByAttribute('leftID', $this->getOID()); else $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); $object->save(); } } // 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) { $object->delete(); } } } } $sqlQuery = "DELETE FROM ".$this->getTableName()." WHERE OID = ?;"; $this->lastQuery = $sqlQuery; $stmt = AlphaDAO::getConnection()->stmt_init(); if($stmt->prepare($sqlQuery)) { $stmt->bind_param('i', $this->getOID()); $stmt->execute(); self::$logger->debug('Deleted the object ['.$this->getOID().'] of class ['.get_class($this).']'); }else{ throw new FailedDeleteException('Failed to delete object ['.$this->getOID().'], error is ['.$stmt->error.'], query ['.$this->lastQuery.']'); } $stmt->close(); if(method_exists($this, 'after_delete_callback')) $this->after_delete_callback(); $this->clear(); 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. * @since 1.0 * @throws FailedDeleteException */ public function deleteAllByAttribute($attribute, $value) { self::$logger->debug('>>deleteAllByAttribute(attribute=['.$attribute.'], value=['.$value.'])'); if(method_exists($this, 'before_deleteAllByAttribute_callback')) $this->before_deleteAllByAttribute_callback(); try { $doomedObjects = $this->loadAllByAttribute($attribute, $value); $deletedRowCount = 0; foreach ($doomedObjects as $object) { $object->delete(); $deletedRowCount++; } }catch (BONotFoundException $bonf) { // nothing found to delete self::$logger->warn($bonf->getMessage()); return 0; }catch (AlphaException $e) { throw new FailedDeleteException('Failed to delete objects, error is ['.$e->getMessage().']'); self::$logger->debug('<<deleteAllByAttribute [0]'); return 0; } if(method_exists($this, 'after_deleteAllByAttribute_callback')) $this->after_deleteAllByAttribute_callback(); self::$logger->debug('<<deleteAllByAttribute ['.$deletedRowCount.']'); return $deletedRowCount; } /** * Gets the version_num of the object from the database (returns 0 if the BO is not saved yet). * * @return integer * @since 1.0 * @throws BONotFoundException */ public function getVersion() { self::$logger->debug('>>getVersion()'); if(method_exists($this, 'before_getVersion_callback')) $this->before_getVersion_callback(); $sqlQuery = 'SELECT version_num FROM '.$this->getTableName().' WHERE OID = ?;'; $this->lastQuery = $sqlQuery; $stmt = AlphaDAO::getConnection()->stmt_init(); if($stmt->prepare($sqlQuery)) { $stmt->bind_param('i', $this->OID); $stmt->execute(); $result = $this->bindResult($stmt); if(isset($result[0])) $row = $result[0]; $stmt->close(); }else{ self::$logger->warn('The following query caused an unexpected result ['.$sqlQuery.']'); if(!$this->checkTableExists()) { $this->makeTable(); throw new BONotFoundException('Failed to get the version number, table did not exist so had to create!'); } return; } if(!isset($row['version_num']) || $row['version_num'] < 1) { if(method_exists($this, 'after_getVersion_callback')) $this->after_getVersion_callback(); self::$logger->debug('<<getVersion [0]'); return 0; }else{ if(method_exists($this, 'after_getVersion_callback')) $this->after_getVersion_callback(); $version_num = $row['version_num']; self::$logger->debug('<<getVersion ['.$version_num.']'); return $version_num; } } /** * Builds a new database table for the BO class. * * @since 1.0 * @throws AlphaException */ public function makeTable() { 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; if(!in_array($propName, $this->transientAttributes) && $propName != "OID") { $propClass = get_class($this->getPropObject($propName)); switch (strtoupper($propClass)) { case "INTEGER": // special properties for RelationLookup OIDs if($this instanceof RelationLookup && ($propName == 'leftID' || $propName == 'rightID')) $sqlQuery .= "$propName INT(".$this->getPropObject($propName)->getSize().") ZEROFILL NOT NULL,"; else $sqlQuery .= "$propName INT(".$this->getPropObject($propName)->getSize()."),"; break; case "DOUBLE": $sqlQuery .= "$propName DOUBLE(".$this->getPropObject($propName)->getSize(true)."),"; break; case "STRING": $sqlQuery .= "$propName VARCHAR(".$this->getPropObject($propName)->getSize()."),"; break; case "TEXT": $sqlQuery .= "$propName TEXT,"; break; case "BOOLEAN": $sqlQuery .= "$propName CHAR(1) DEFAULT '0',"; break; case "DATE": $sqlQuery .= "$propName DATE,"; break; case "TIMESTAMP": $sqlQuery .= "$propName DATETIME,"; break; case "ENUM": $sqlQuery .= "$propName ENUM("; $enumVals = $this->getPropObject($propName)->getOptions(); foreach($enumVals as $val) { $sqlQuery .= "'".$val."',"; } $sqlQuery = rtrim($sqlQuery, ","); $sqlQuery .= "),"; break; case "DENUM": $tmp = new DEnum(get_class($this).'::'.$propName); $tmp->save(); $sqlQuery .= "$propName INT(11) ZEROFILL,"; break; case "RELATION": $sqlQuery .= "$propName INT(11) ZEROFILL UNSIGNED,"; break; default: $sqlQuery .= ""; break; } } } if($this->isTableOverloaded()) $sqlQuery .= "classname VARCHAR(100),"; $sqlQuery .= "PRIMARY KEY (OID)) TYPE=InnoDB;"; $this->lastQuery = $sqlQuery; if(!$result = AlphaDAO::getConnection()->query($sqlQuery)) { throw new AlphaException('Failed to create the table ['.$this->getTableName().'] for the class ['.get_class($this).'], query is ['.$this->lastQuery.']'); self::$logger->debug('<<makeTable'); } // check the table indexes if any additional ones required $this->checkIndexes(); 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! * * @since 1.0 * @throws AlphaException */ public function rebuildTable() { self::$logger->debug('>>rebuildTable()'); if(method_exists($this, 'before_rebuildTable_callback')) $this->before_rebuildTable_callback(); $sqlQuery = 'DROP TABLE IF EXISTS '.$this->getTableName().';'; $this->lastQuery = $sqlQuery; if(!$result = AlphaDAO::getConnection()->query($sqlQuery)) { throw new AlphaException('Failed to drop the table ['.$this->getTableName().'] for the class ['.get_class($this).'], query is ['.$this->lastQuery.']'); self::$logger->debug('<<rebuildTable'); } $this->makeTable(); 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! * * @since 1.0 * @param string $tableName Optional table name, leave blank for the defined table for this class to be dropped * @throws AlphaException */ public function dropTable($tableName=null) { self::$logger->debug('>>dropTable()'); if(method_exists($this, 'before_dropTable_callback')) $this->before_dropTable_callback(); if($tableName == null) $tableName = $this->getTableName(); $sqlQuery = 'DROP TABLE IF EXISTS '.$tableName.';'; $this->lastQuery = $sqlQuery; if(!$result = AlphaDAO::getConnection()->query($sqlQuery)) { throw new AlphaException('Failed to drop the table ['.$tableName.'] for the class ['.get_class($this).'], query is ['.$this->lastQuery.']'); 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 * database). * * @param string $propName The name of the new field to add to the database table. * @since 1.0 * @throws AlphaException */ public function addProperty($propName) { self::$logger->debug('>>addProperty(propName=['.$propName.'])'); if(method_exists($this, 'before_addProperty_callback')) $this->before_addProperty_callback(); $sqlQuery = 'ALTER TABLE '.$this->getTableName().' ADD '; if($this->isTableOverloaded() && $propName == 'classname') { $sqlQuery .= 'classname VARCHAR(100)'; }else{ if(!in_array($propName, $this->defaultAttributes) && !in_array($propName, $this->transientAttributes)) { $propClass = get_class($this->getPropObject($propName)); switch (strtoupper($propClass)) { case 'INTEGER': $sqlQuery .= "$propName INT(".$this->getPropObject($propName)->getSize().")"; break; case 'DOUBLE': $sqlQuery .= "$propName DOUBLE(".$this->getPropObject($propName)->getSize(true).")"; break; case 'STRING': $sqlQuery .= "$propName VARCHAR(".$this->getPropObject($propName)->getSize().")"; break; case 'SEQUENCE': $sqlQuery .= "$propName VARCHAR(".$this->getPropObject($propName)->getSize().")"; break; case 'TEXT': $sqlQuery .= "$propName TEXT"; break; case 'BOOLEAN': $sqlQuery .= "$propName CHAR(1) DEFAULT '0'"; break; case 'DATE': $sqlQuery .= "$propName DATE"; break; case 'TIMESTAMP': $sqlQuery .= "$propName DATETIME"; break; case 'ENUM': $sqlQuery .= "$propName ENUM("; $enumVals = $this->getPropObject($propName)->getOptions(); foreach($enumVals as $val) { $sqlQuery .= "'".$val."',"; } $sqlQuery = rtrim($sqlQuery, ","); $sqlQuery .= ')'; break; case 'DENUM': $tmp = new DEnum(get_class($this).'::'.$propName); $tmp->save(); $sqlQuery .= "$propName INT(11) ZEROFILL"; break; case 'RELATION': $sqlQuery .= "$propName INT(11) ZEROFILL UNSIGNED"; break; default: $sqlQuery .= ''; break; } } } $this->lastQuery = $sqlQuery; if(!$result = AlphaDAO::getConnection()->query($sqlQuery)) { throw new AlphaException('Failed to add the new attribute ['.$propName.'] to the table ['.$this->getTableName().'], query is ['.$this->lastQuery.']'); 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. * * @since 1.0 */ public function populateFromPost() { 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(!in_array($propName, $this->defaultAttributes) && !in_array($propName, $this->transientAttributes)) { $propClass = get_class($this->getPropObject($propName)); if(isset($_POST[$propName])) { if (strtoupper($propClass) != 'DATE' && strtoupper($propClass) != 'TIMESTAMP') { if(strtoupper($propClass) == 'TEXT' && !$this->getPropObject($propName)->getAllowHTML()) $this->getPropObject($propName)->setValue(InputFilter::encode($_POST[$propName], false)); else $this->getPropObject($propName)->setValue(InputFilter::encode($_POST[$propName], true)); }else{ $this->getPropObject($propName)->populateFromString(InputFilter::encode($_POST[$propName], true)); } } } if ($propName == 'version_num' && isset($_POST['version_num'])) $this->version_num->setValue($_POST['version_num']); } if(method_exists($this, 'after_populateFromPost_callback')) $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. * @since 1.0 * @throws AlphaException */ public function getMAX() { 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(); $this->lastQuery = $sqlQuery; $result = AlphaDAO::getConnection()->query($sqlQuery); $row = $result->fetch_array(MYSQLI_ASSOC); if (isset($row['max_OID'])) { if(method_exists($this, 'after_getMAX_callback')) $this->after_getMAX_callback(); self::$logger->debug('<<getMAX ['.$row['max_OID'].']'); return $row['max_OID']; }else{ 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]'); return 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). * @return integer * @since 1.0 * @throws AlphaException */ public function getCount($attributes=array(), $values=array()) { self::$logger->debug('>>getCount(attributes=['.var_export($attributes, true).'], values=['.var_export($values, true).'])'); if(method_exists($this, 'before_getCount_callback')) $this->before_getCount_callback(); if(!is_array($attributes) || !is_array($values)) { throw new IllegalArguementException('Illegal arrays attributes=['.var_export($attributes, true).'] and values=['.var_export($values, true).'] provided to loadAllByAttributes'); } if($this->isTableOverloaded()) $whereClause = ' WHERE classname = \''.get_class($this).'\' AND'; else $whereClause = ' WHERE'; $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; else $sqlQuery = 'SELECT COUNT(OID) AS class_count FROM '.$this->getTableName(); $this->lastQuery = $sqlQuery; $result = AlphaDAO::getConnection()->query($sqlQuery); if ($result) { if(method_exists($this, 'after_getCount_callback')) $this->after_getCount_callback(); $row = $result->fetch_array(MYSQLI_ASSOC); self::$logger->debug('<<getCount ['.$row['class_count'].']'); return $row['class_count']; }else{ 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]'); return 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. * @since 1.0 */ public function getID() { self::$logger->debug('>>getID()'); $oid = str_pad($this->OID, 11, '0', STR_PAD_LEFT); self::$logger->debug('<<getID ['.$oid.']'); return $oid; } /** * Gets the OID for the object in zero-padded format (same as getID()). This version is final so cannot * be overridden. * * @return integer 11 digit zero-padded OID value. * @since 1.0 */ public final function getOID() { self::$logger->debug('>>getOID()'); $oid = str_pad($this->OID, 11, '0', STR_PAD_LEFT); self::$logger->debug('<<getOID ['.$oid.']'); return $oid; } /** * Method for getting version number of the object. * * @return Integer The object version number. * @since 1.0 */ public function getVersionNumber() { self::$logger->debug('>>getVersionNumber()'); self::$logger->debug('<<getVersionNumber ['.$this->version_num.']'); return $this->version_num; } /** * Populate all of the enum options for this object from the database. * * @since 1.0 * @throws AlphaException */ protected function setEnumOptions() { 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 $dbError = false; foreach($properties as $propObj) { $propName = $propObj->name; if(!in_array($propName, $this->defaultAttributes) && !in_array($propName, $this->transientAttributes)) { $propClass = get_class($this->getPropObject($propName)); if ($propClass == 'Enum') { $sqlQuery = "SHOW COLUMNS FROM ".$this->getTableName()." LIKE '$propName'"; $this->lastQuery = $sqlQuery; $result = AlphaDAO::getConnection()->query($sqlQuery); if ($result) { $row = $result->fetch_array(MYSQLI_NUM); $options = explode("','",preg_replace("/(enum|set)\('(.+?)'\)/","\\2",$row[1])); $this->getPropObject($propName)->setOptions($options); }else{ $dbError = true; break; } } } } if (!$dbError) { if(method_exists($this, 'after_setEnumOptions_callback')) $this->after_setEnumOptions_callback(); }else{ 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 * to false. * @return mixed The property value. * @since 1.0 * @throws IllegalArguementException * @throws AlphaException */ 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(); if(empty($prop)) throw new IllegalArguementException('Cannot call get with empty $prop arguement!'); // handle attributes with a get.ucfirst($prop) method if(!$noChildMethods && method_exists($this, 'get'.ucfirst($prop))) { if(method_exists($this, 'after_get_callback')) $this->after_get_callback(); self::$logger->debug('<<get ['.eval('return $this->get'.ucfirst($prop).'();').'])'); return eval('return $this->get'.ucfirst($prop).'();'); }else{ // handle attributes with no dedicated child get.ucfirst($prop) method if(isset($this->$prop) && method_exists($this->$prop, 'getValue')) { if(method_exists($this, 'after_get_callback')) $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.'])'); return $this->$prop; }else{ throw new AlphaException('Could not access the property ['.$prop.'] on the object of class ['.get_class($this).']'); self::$logger->debug('<<get [false])'); return 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 * to false. * @since 1.0 * @throws AlphaException */ 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 if(!$noChildMethods && method_exists($this, 'set'.ucfirst($prop))) { if(method_exists($this, 'after_set_callback')) $this->after_set_callback(); eval('$this->set'.ucfirst($prop)."('$value');"); }else{ // handle attributes with no dedicated child set.ucfirst($prop) method if(isset($this->$prop)) { if(method_exists($this, 'after_set_callback')) $this->after_set_callback(); // complex types will have a setValue() method to call if (get_class($this->$prop) != false) { if (strtoupper(get_class($this->$prop)) != 'DATE' && strtoupper(get_class($this->$prop)) != 'TIMESTAMP') { $this->$prop->setValue($value); }else{ // Date and Timestamp objects have a special setter accepting a string $this->$prop->populateFromString($value); } }else{ // simple types set directly $this->$prop = $value; } }else{ 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. * @since 1.0 * @throws IllegalArguementException */ public function getPropObject($prop) { 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()) { if(method_exists($this, 'after_getPropObject_callback')) $this->after_getPropObject_callback(); self::$logger->debug('<<getPropObject [false]'); return false; } foreach($properties as $propObj) { $propName = $propObj->name; if($prop == $propName) { if(method_exists($this, 'after_getPropObject_callback')) $this->after_getPropObject_callback(); self::$logger->debug('<<getPropObject ['.var_export($this->$prop, true).']'); return $this->$prop; } } throw new IllegalArguementException('Could not access the property object ['.$prop.'] on the object of class ['.get_class($this).']'); self::$logger->debug('<<getPropObject [false]'); return false; } /** * Checks to see if the table exists in the database for the current business class. * * @return boolean * @since 1.0 * @throws AlphaException */ public function checkTableExists() { self::$logger->debug('>>checkTableExists()'); if(method_exists($this, 'before_checkTableExists_callback')) $this->before_checkTableExists_callback(); global $config; $tableExists = false; $sqlQuery = 'SHOW TABLES;'; $this->lastQuery = $sqlQuery; $result = AlphaDAO::getConnection()->query($sqlQuery); while ($row = $result->fetch_array(MYSQLI_NUM)) { if (strtolower($row[0]) == strtolower($this->getTableName())) $tableExists = true; } if ($result) { if(method_exists($this, 'after_checkTableExists_callback')) $this->after_checkTableExists_callback(); self::$logger->debug('<<checkTableExists ['.$tableExists.']'); return $tableExists; }else{ throw new AlphaException('Failed to access the system database correctly, error is ['.AlphaDAO::getConnection()->error.']'); self::$logger->debug('<<checkTableExists [false]'); return 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. * @return boolean * @since 1.0 * @throws AlphaException */ public static function checkBOTableExists($BOClassName) { if(self::$logger == null) self::$logger = new Logger('AlphaDAO'); self::$logger->debug('>>checkBOTableExists(BOClassName=['.$BOClassName.'])'); global $config; eval('$tableName = '.$BOClassName.'::TABLE_NAME;'); if(empty($tableName)) $tableName = substr($BOClassName, 0, strpos($BOClassName, '_')); $tableExists = false; $sqlQuery = 'SHOW TABLES;'; $result = AlphaDAO::getConnection()->query($sqlQuery); while ($row = $result->fetch_array(MYSQLI_NUM)) { if ($row[0] == $tableName) $tableExists = true; } if ($result) { self::$logger->debug('<<checkBOTableExists ['.($tableExists ? 'true' : 'false').']'); return $tableExists; }else{ throw new AlphaException('Failed to access the system database correctly, error is ['.AlphaDAO::getConnection()->error.']'); self::$logger->debug('<<checkBOTableExists [false]'); return 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. * * @return boolean * @since 1.0 * @throws AlphaException */ public function checkTableNeedsUpdate() { self::$logger->debug('>>checkTableNeedsUpdate()'); if(method_exists($this, 'before_checkTableNeedsUpdate_callback')) $this->before_checkTableNeedsUpdate_callback(); $tableExists = $this->checkTableExists(); if (!$tableExists) { self::$logger->debug('<<checkTableNeedsUpdate [true]'); return true; }else{ $updateRequired = false; $matchCount = 0; $query = 'SHOW COLUMNS FROM '.$this->getTableName(); $result = AlphaDAO::getConnection()->query($query); $this->lastQuery = $query; // get the class attributes $reflection = new ReflectionClass(get_class($this)); $properties = $reflection->getProperties(); foreach($properties as $propObj) { $propName = $propObj->name; if (!in_array($propName, $this->transientAttributes)) { $foundMatch = false; while ($row = $result->fetch_array(MYSQLI_ASSOC)) { if ($propName == $row['Field']) { $foundMatch = true; break; } } if(!$foundMatch) $matchCount--; $result->data_seek(0); } } // check for the "classname" field in overloaded tables if($this->isTableOverloaded()) { $foundMatch = false; while ($row = $result->fetch_array(MYSQLI_ASSOC)) { if ('classname' == $row['Field']) { $foundMatch = true; break; } } if(!$foundMatch) $matchCount--; } if ($matchCount != 0) $updateRequired = true; if ($result) { if(method_exists($this, 'before_checkTableNeedsUpdate_callback')) $this->before_checkTableNeedsUpdate_callback(); // check the table indexes try { $this->checkIndexes(); }catch (AlphaException $ae) { self::$logger->warn("Error while checking database indexes:\n\n".$ae->getMessage()); } self::$logger->debug('<<checkTableNeedsUpdate ['.$updateRequired.']'); return $updateRequired; }else{ throw new AlphaException('Failed to access the system database correctly, error is ['.AlphaDAO::getConnection()->error.']'); self::$logger->debug('<<checkTableNeedsUpdate [false]'); return false; } } } /** * Returns an array containing any properties on the class which have not been created on the database * table yet. * * @return array An array of missing fields in the database table. * @since 1.0 * @throws AlphaException */ public function findMissingFields() { self::$logger->debug('>>findMissingFields()'); if(method_exists($this, 'before_findMissingFields_callback')) $this->before_findMissingFields_callback(); $missingFields = array(); $matchCount = 0; $sqlQuery = 'SHOW COLUMNS FROM '.$this->getTableName(); $result = AlphaDAO::getConnection()->query($sqlQuery); $this->lastQuery = $sqlQuery; // get the class attributes $reflection = new ReflectionClass(get_class($this)); $properties = $reflection->getProperties(); foreach($properties as $propObj) { $propName = $propObj->name; if (!in_array($propName, $this->transientAttributes)) { while ($row = $result->fetch_array(MYSQLI_ASSOC)) { if ($propName == $row['Field']) { $matchCount++; break; } } $result->data_seek(0); }else{ $matchCount++; } if($matchCount==0) { array_push($missingFields, $propName); }else{ $matchCount = 0; } } // check for the "classname" field in overloaded tables if($this->isTableOverloaded()) { $foundMatch = false; while ($row = $result->fetch_array(MYSQLI_ASSOC)) { if ('classname' == $row['Field']) { $foundMatch = true; break; } } if(!$foundMatch) array_push($missingFields, 'classname'); } if (!$result) { throw new AlphaException('Failed to access the system database correctly, error is ['.AlphaDAO::getConnection()->error.']'); } if(method_exists($this, 'after_findMissingFields_callback')) $this->after_findMissingFields_callback(); self::$logger->debug('<<findMissingFields ['.var_export($missingFields, true).']'); return $missingFields; } /** * Getter for the TABLE_NAME, which should be set by a child of this class. * * @return string The table name in the database. * @since 1.0 * @throws AlphaException */ public function getTableName() { self::$logger->debug('>>getTableName()'); eval('$TABLE_NAME = '.get_class($this).'::TABLE_NAME;'); if(!empty($TABLE_NAME)) { self::$logger->debug('<<getTableName ['.$TABLE_NAME.']'); return $TABLE_NAME; }else{ throw new AlphaException('Error: no TABLE_NAME constant set for the class '.get_class($this)); self::$logger->debug('<<getTableName []'); return ''; } } /** * Method for getting the OID of the person who created this BO. * * @return Integer The OID of the creator. * @since 1.0 */ public function getCreatorId() { self::$logger->debug('>>getCreatorId()'); self::$logger->debug('<<getCreatorId ['.$this->created_by.']'); return $this->created_by; } /** * Method for getting the OID of the person who updated this BO. * * @return Integer The OID of the updator. * @since 1.0 */ public function getUpdatorId() { self::$logger->debug('>>getUpdatorId()'); self::$logger->debug('<<getUpdatorId ['.$this->updated_by.']'); return $this->updated_by; } /** * Method for getting the date/time of when the BO was created. * * @return Timestamp * @since 1.0 */ public function getCreateTS() { self::$logger->debug('>>getCreateTS()'); self::$logger->debug('<<getCreateTS ['.$this->created_ts.']'); return $this->created_ts; } /** * Method for getting the date/time of when the BO was last updated. * * @return Timestamp * @since 1.0 */ public function getUpdateTS() { self::$logger->debug('>>getUpdateTS()'); self::$logger->debug('<<getUpdateTS ['.$this->updated_ts.']'); return $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. * @since 1.0 */ public function markTransient($attributeName) { self::$logger->debug('>>markTransient(attributeName=['.$attributeName.'])'); self::$logger->debug('<<markTransient'); array_push($this->transientAttributes, $attributeName); } /** * 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. * @since 1.0 */ public function markPersistent($attributeName) { self::$logger->debug('>>markPersistent(attributeName=['.$attributeName.'])'); self::$logger->debug('<<markPersistent'); $this->transientAttributes = array_diff($this->transientAttributes, array($attributeName)); } /** * 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). * @since 1.0 */ protected function markUnique($attribute1Name, $attribute2Name='') { self::$logger->debug('>>markUnique(attribute1Name=['.$attribute1Name.'], attribute2Name=['.$attribute2Name.'])'); if(empty($attribute2Name)) { array_push($this->uniqueAttributes, $attribute1Name); }else{ // Process composite unique keys: add them seperated by a + sign $attributes = $attribute1Name.'+'.$attribute2Name; array_push($this->uniqueAttributes, $attributes); } 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. * @since 1.0 * @throws AlphaException */ protected function getIndexes() { self::$logger->debug('>>getIndexes()'); $query = 'SHOW INDEX FROM '.$this->getTableName(); $result = AlphaDAO::getConnection()->query($query); $this->lastQuery = $query; $indexNames = array(); if (!$result) { throw new AlphaException('Failed to access the system database correctly, error is ['.AlphaDAO::getConnection()->error.']'); }else{ while ($row = $result->fetch_array(MYSQLI_ASSOC)) { array_push($indexNames, $row['Key_name']); } } self::$logger->debug('<<getIndexes'); return $indexNames; } /** * Checks to see if all of the indexes are in place for the BO's table, creates those that are missing. * * @since 1.0 */ protected function checkIndexes() { self::$logger->debug('>>checkIndexes()'); if(method_exists($this, 'before_checkIndexes_callback')) $this->before_checkIndexes_callback(); $indexNames = $this->getIndexes(); // process unique keys foreach($this->uniqueAttributes as $prop) { // check for composite indexes if(strpos($prop, '+')) { $attributes = explode('+', $prop); $index_exists = false; foreach ($indexNames as $index) { if ($attributes[0].'_'.$attributes[1].'_unq_idx' == $index) { $index_exists = true; } } if(!$index_exists) $this->createUniqueIndex($attributes[0], $attributes[1]); }else{ $index_exists = false; foreach ($indexNames as $index) { if ($prop.'_unq_idx' == $index) { $index_exists = true; } } if(!$index_exists) $this->createUniqueIndex($prop); } } // process foreign-key indexes // get the class attributes $reflection = new ReflectionClass(get_class($this)); $properties = $reflection->getProperties(); foreach($properties as $propObj) { $propName = $propObj->name; $prop = $this->getPropObject($propName); if(get_class($prop) == 'Relation') { if($prop->getRelationType() == 'MANY-TO-ONE') { $indexExists = false; foreach ($indexNames as $index) { if ($propName.'_fk_idx' == $index) { $indexExists = true; } } if(!$indexExists) { $this->createForeignIndex($propName, $prop->getRelatedClass(), $prop->getRelatedClassField()); } } if($prop->getRelationType() == 'MANY-TO-MANY') { $lookup = $prop->getLookup(); if($lookup != null) { try { $lookupIndexNames = $lookup->getIndexes(); // handle index check/creation on left side of Relation $indexExists = false; foreach ($lookupIndexNames as $index) { if ('leftID_fk_idx' == $index) { $indexExists = true; } } if(!$indexExists) { $lookup->createForeignIndex('leftID', $prop->getRelatedClass('left'), 'OID'); } // handle index check/creation on right side of Relation $indexExists = false; foreach ($lookupIndexNames as $index) { if ('rightID_fk_idx' == $index) { $indexExists = true; } } if(!$indexExists) { $lookup->createForeignIndex('rightID', $prop->getRelatedClass('right'), 'OID'); } }catch(AlphaException $e) { 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). * @since 1.0 * @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(); AlphaDAO::loadClassDef($relatedClass); $relatedBO = new $relatedClass; $tableName = $relatedBO->getTableName(); // if the relation is on itself (table-wise), exist without attempting to create the foreign keys if($this->getTableName() == $tableName) { self::$logger->debug('<<createForeignIndex'); return; } $result = false; if(AlphaDAO::checkBOTableExists(ucfirst($tableName).'Object')) { $sqlQuery = ''; 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);'; if(!empty($sqlQuery)) { $this->lastQuery = $sqlQuery; $result = AlphaDAO::getConnection()->query($sqlQuery); if (!$result) { throw new AlphaException('Failed to create an index on ['.$this->getTableName().'], error is ['.AlphaDAO::getConnection()->error.'], query ['.$this->lastQuery.']'); } } $sqlQuery = 'ALTER TABLE '.$this->getTableName().' ADD FOREIGN KEY '.$attributeName.'_fk_idx ('.$attributeName.') REFERENCES '.$tableName.' ('.$relatedClassAttribute.') ON DELETE SET NULL;'; $this->lastQuery = $sqlQuery; $result = AlphaDAO::getConnection()->query($sqlQuery); } if ($result) { if(method_exists($this, 'after_createForeignIndex_callback')) $this->after_createForeignIndex_callback(); self::$logger->debug('Successfully created the foreign key index ['.$attributeName.'_fk_idx]'); }else{ throw new FailedIndexCreateException('Failed to create the index ['.$attributeName.'_fk_idx] on ['.$this->getTableName().'], error is ['.AlphaDAO::getConnection()->error.'], query ['.$this->lastQuery.']'); } 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). * @since 1.0 * @throws FailedIndexCreateException */ protected function createUniqueIndex($attribute1Name, $attribute2Name = '') { 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->lastQuery = $sqlQuery; $result = AlphaDAO::getConnection()->query($sqlQuery); if ($result) { if(method_exists($this, 'after_createUniqueIndex_callback')) $this->after_createUniqueIndex_callback(); }else{ throw new FailedIndexCreateException('Failed to create the index ['.$attribute1Name.'_unq_idx] on ['.$this->getTableName().'], error is ['.AlphaDAO::getConnection()->error.']'); } }else{ // process composite unique keys $sqlQuery = 'CREATE UNIQUE INDEX '.$attribute1Name.'_'.$attribute2Name.'_unq_idx ON '.$this->getTableName().' ('.$attribute1Name.','.$attribute2Name.');'; $this->lastQuery = $sqlQuery; $result = AlphaDAO::getConnection()->query($sqlQuery); if ($result) { if(method_exists($this, 'after_create_unique_index_callback')) $this->after_createUniqueIndex_callback(); }else{ throw new FailedIndexCreateException('Failed to create the index ['.$attribute1Name.'_'.$attribute2Name.'_unq_idx] on ['.$this->getTableName().'], error is ['.AlphaDAO::getConnection()->error.']'); } } self::$logger->debug('<<createUniqueIndex'); } /** * Parses a MySQL error for the value that violated a unique constraint. * * @param string $error The MySQL error string. * @since 1.0 */ protected function findOffendingValue($error) { self::$logger->debug('>>findOffendingValue(error=['.$error.'])'); $singleQuote1 = strpos($error,"'"); $singleQuote2 = strrpos($error,"'"); $value = substr($error, $singleQuote1, ($singleQuote2-$singleQuote1)+1); self::$logger->debug('<<findOffendingValue ['.$value.'])'); return $value; } /** * Gets the data labels array. * * @return array An array of attribute labels. * @since 1.0 */ public function getDataLabels() { self::$logger->debug('>>getDataLabels()'); self::$logger->debug('<<getDataLabels() ['.var_export($this->dataLabels, true).'])'); return $this->dataLabels; } /** * Gets the data label for the given attribute name. * * @param $att The attribute name to get the label for. * @return string * @since 1.0 * @throws IllegalArguementException */ public function getDataLabel($att) { self::$logger->debug('>>getDataLabel(att=['.$att.'])'); if(in_array($att, array_keys($this->dataLabels))) { self::$logger->debug('<<getDataLabel ['.$this->dataLabels[$att].'])'); return $this->dataLabels[$att]; }else{ throw new IllegalArguementException('No data label found on the class ['.get_class($this).'] for the attribute ['.$att.']'); self::$logger->debug('<<getDataLabel [])'); return ''; } } /** * 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. * @since 1.0 */ public static function getBOClassNames() { if(self::$logger == null) self::$logger = new Logger('AlphaDAO'); self::$logger->debug('>>getBOClassNames()'); global $config; $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))) { if (preg_match("/Object.inc/", $file)) { $classname = substr($file, 0, -4); array_push($classNameArray, $classname); } } } // 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))) { if (preg_match("/Object.inc/", $file)) { $classname = substr($file, 0, -4); array_push($classNameArray, $classname); } } asort($classNameArray); self::$logger->debug('<<getBOClassNames ['.var_export($classNameArray, true).']'); return $classNameArray; } /** * Get the array of default attribute names. * * @return array An array of attribute names. * @since 1.0 */ public function getDefaultAttributes() { self::$logger->debug('>>getDefaultAttributes()'); self::$logger->debug('<<getDefaultAttributes ['.var_export($this->defaultAttributes, true).']'); return $this->defaultAttributes; } /** * Get the array of transient attribute names. * * @return array An array of attribute names. * @since 1.0 */ public function getTransientAttributes() { self::$logger->debug('>>getTransientAttributes()'); self::$logger->debug('<<getTransientAttributes ['.var_export($this->transientAttributes, true).']'); return $this->transientAttributes; } /** * Get the array of persistent attribute names, i.e. those that are saved in the database. * * @return array An array of attribute names. * @since 1.0 */ public function getPersistentAttributes() { self::$logger->debug('>>getPersistentAttributes()'); $attributes = array(); // get the class attributes $reflection = new ReflectionClass(get_class($this)); $properties = $reflection->getProperties(); foreach($properties as $propObj) { $propName = $propObj->name; // filter transient attributes if(!in_array($propName, $this->transientAttributes)) { array_push($attributes, $propName); } } self::$logger->debug('<<getPersistentAttributes ['.var_export($attributes, true).']'); return $attributes; } /** * Private setter for the object ID, used from load methods. * * @param integer $OID The Object ID. * @since 1.0 */ private function setOID($OID) { self::$logger->debug('>>setOID(OID=['.$OID.'])'); self::$logger->debug('<<setOID'); $this->OID = $OID; } /** * Inspector to see if the business object is transient (not presently stored in the database). * * @return boolean * @since 1.0 */ public function isTransient() { self::$logger->debug('>>isTransient()'); if(empty($this->OID) || !isset($this->OID) || $this->OID == '00000000000') { self::$logger->debug('<<isTransient [true]'); return true; }else{ self::$logger->debug('<<isTransient [false]'); return false; } } /** * Get the last database query run on this object. * * @return string An SQL query string. * @since 1.0 */ public function getLastQuery() { self::$logger->debug('>>getLastQuery()'); self::$logger->debug('<<getLastQuery ['.$this->lastQuery.']'); return $this->lastQuery; } /** * Unsets all of the attributes of this object to null. * * @since 1.0 */ 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()) unset($this->$propName); } self::$logger->debug('<<clear'); } /** * Reloads the object from the database, overwritting any attribute values in memory. * * @since 1.0 * @throws AlphaException */ public function reload() { self::$logger->debug('>>reload()'); if(!$this->isTransient()) { $this->load($this->getOID()); }else{ 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. * @since 1.0 * @throws IllegalArguementException */ public static function loadClassDef($classname) { if(self::$logger == null) self::$logger = new Logger('AlphaDAO'); self::$logger->debug('>>loadClassDef(classname=['.$classname.'])'); global $config; 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'; else throw new IllegalArguementException('The class ['.$classname.'] is not defined anywhere!'); 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. * @return boolean * @since 1.0 */ public static function checkClassDefExists($classname) { if(self::$logger == null) self::$logger = new Logger('AlphaDAO'); self::$logger->debug('>>checkClassDefExists(classname=['.$classname.'])'); global $config; $exists = false; if(file_exists($config->get('sysRoot').'model/'.$classname.'.inc')) $exists = true; if(file_exists($config->get('sysRoot').'alpha/model/'.$classname.'.inc')) $exists = true; if(file_exists($config->get('sysRoot').'alpha/model/types/'.$classname.'.inc')) $exists = true; self::$logger->debug('<<checkClassDefExists ['.$exists.']'); return $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. * @return boolean * @since 1.0 * @throws AlphaException */ public function checkRecordExists($OID) { 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 = ?;'; $this->lastQuery = $sqlQuery; $stmt = AlphaDAO::getConnection()->stmt_init(); if($stmt->prepare($sqlQuery)) { $stmt->bind_param('i', $OID); $stmt->execute(); $result = $this->bindResult($stmt); $stmt->close(); if ($result) { if(method_exists($this, 'after_checkRecordExists_callback')) $this->after_checkRecordExists_callback(); if(count($result) > 0) { self::$logger->debug('<<checkRecordExists [true]'); return true; }else{ self::$logger->debug('<<checkRecordExists [false]'); return false; } }else{ throw new AlphaException('Failed to check for the record ['.$OID.'] on the class ['.get_class($this).'] from the table ['.$this->getTableName().'], query is ['.$this->lastQuery.']'); self::$logger->debug('<<checkRecordExists [false]'); return false; } }else{ throw new AlphaException('Failed to check for the record ['.$OID.'] on the class ['.get_class($this).'] from the table ['.$this->getTableName().'], query is ['.$this->lastQuery.']'); self::$logger->debug('<<checkRecordExists [false]'); return 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 * multiple types of BOs. * * @return bool * @since 1.0 * @throws BadBOTableNameException */ public function isTableOverloaded() { self::$logger->debug('>>isTableOverloaded()'); $classname = get_class($this); $tablename = ucfirst($this->getTableName()).'Object'; // 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]'); return 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]'); return 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]'); return false; }else{ // check to see if there is already a "classname" column in the database for this BO $query = 'SHOW COLUMNS FROM '.$this->getTableName(); $result = AlphaDAO::getConnection()->query($query); if($result) { while ($row = $result->fetch_array(MYSQLI_ASSOC)) { if ('classname' == $row['Field']) { self::$logger->debug('<<isTableOverloaded [true]'); return true; } } }else{ self::$logger->warn('Error during show columns ['.AlphaDAO::getConnection()->error.']'); } self::$logger->debug('<<isTableOverloaded [false]'); return false; } } /** * Starts a new database transaction. * * @since 1.0 * @throws AlphaException */ public static function begin() { if(self::$logger == null) self::$logger = new Logger('AlphaDAO'); self::$logger->debug('>>begin()'); if (!AlphaDAO::getConnection()->autocommit(false)) throw new AlphaException('Error beginning a new transaction, error is ['.AlphaDAO::getConnection()->error.']'); self::$logger->debug('<<begin'); } /** * Commits the current database transaction. * * @since 1.0 * @throws FailedSaveException */ public static function commit() { if(self::$logger == null) self::$logger = new Logger('AlphaDAO'); self::$logger->debug('>>commit()'); if (!AlphaDAO::getConnection()->commit()) throw new FailedSaveException('Error commiting a transaction, error is ['.AlphaDAO::getConnection()->error.']'); self::$logger->debug('<<commit'); } /** * Aborts the current database transaction. * * @since 1.0 * @throws AlphaException */ public static function rollback() { if(self::$logger == null) self::$logger = new Logger('AlphaDAO'); self::$logger->debug('>>rollback()'); if (!AlphaDAO::getConnection()->rollback()) throw new AlphaException('Error aborting a transaction, error is ['.AlphaDAO::getConnection()->error.']'); self::$logger->debug('<<rollback'); } /** * Static method that tries to determine if the system database has been installed or not. * * @return boolean * @since 1.0 */ public static function isInstalled() { if(self::$logger == null) self::$logger = new Logger('AlphaDAO'); self::$logger->debug('>>isInstalled()'); global $config; /* * Install conditions are: * * 1. person table exists * 2. rights table exists */ if(AlphaDAO::checkBOTableExists('PersonObject') && AlphaDAO::checkBOTableExists('RightsObject')) { self::$logger->debug('<<isInstalled [true]'); return true; }else{ self::$logger->debug('<<isInstalled [false]'); return false; } } /** * Returns true if the BO has a Relation property called tags, false otherwise. * * @return boolean * @since 1.0 */ public function isTagged() { if(isset($this->taggedAttributes) && isset($this->tags) && $this->tags instanceof Relation) return true; else return false; } /** * Setter for the BO version number. * * @param integer $versionNumber The version number. * @since 1.0 */ private function setVersion($versionNumber) { $this->version_num->setValue($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. * @since 1.0 */ public function cast($targetClassName, $originalBO) { AlphaDAO::loadClassDef($targetClassName); $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; if(!in_array($propName, $this->transientAttributes)) $BO->set($propName, $originalBO->get($propName)); } }else{ // the new BO is smaller, so loop over its properties foreach($newBOproperties as $propObj) { $propName = $propObj->name; if(!in_array($propName, $this->transientAttributes)) $BO->set($propName, $originalBO->get($propName)); } } return $BO; } /** * Converts "BusinessObject" to "Business" and returns. * * @return string * @since 1.0 */ public function getFriendlyClassName() { $name = substr(get_class($this), 0, -6); return $name; } /** * 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. * @return mysqli_stmt * @since 1.0 */ protected function bindParams($stmt, $attributes=array(), $values=array()) { self::$logger->debug('>>bindParams(stmt=['.var_export($stmt, true).'])'); $bindingsTypes = ''; $params = array(); // here we are only binding the supplied attributes if(count($attributes) > 0 && count($attributes) == count($values)) { $count = count($values); for($i = 0; $i < $count; $i++) { if (AlphaValidator::isInteger($values[$i])) $bindingsTypes .= 'i'; else $bindingsTypes .= 's'; array_push($params, $values[$i]); } if($this->isTableOverloaded()) { if(isset($this->classname)) { $bindingsTypes .= 's'; array_push($params, $this->classname); }else{ $bindingsTypes .= 's'; array_push($params, get_class($this)); } } }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; if (!in_array($propName, $this->transientAttributes)) { // Skip the OID, database auto number takes care of this. if($propName != 'OID' && $propName != 'version_num') { if($this->$propName instanceof Integer) $bindingsTypes .= 'i'; else $bindingsTypes .= 's'; array_push($params, $this->get($propName)); } if($propName == 'version_num') { $temp = $this->version_num->getValue(); $this->version_num->setValue($temp+1); $bindingsTypes .= 'i'; array_push($params, $this->version_num->getValue()); } } } if($this->isTableOverloaded()) { if(isset($this->classname)) { $bindingsTypes .= 's'; array_push($params, $this->classname); }else{ $bindingsTypes .= 's'; array_push($params, get_class($this)); } } // the OID may be on the WHERE clause for UPDATEs and DELETEs if(!$this->isTransient()) { $bindingsTypes .= 'i'; array_push($params, $this->OID); } } self::$logger->debug('bindingsTypes=['.$bindingsTypes.'], count: ['.strlen($bindingsTypes).']'); self::$logger->debug('params ['.var_export($params, true).']'); if ($params != null) { $bind_names[] = $bindingsTypes; $count = count($params); for ($i = 0; $i < $count; $i++) { $bind_name = 'bind'.$i; $$bind_name = $params[$i]; $bind_names[] = &$$bind_name; } call_user_func_array(array($stmt,'bind_param'), $bind_names); } self::$logger->debug('<<bindParams ['.var_export($stmt, true).']'); return $stmt; } /** * 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. * @since 1.0 */ protected function bindResult($stmt) { $result = array(); $metadata = $stmt->result_metadata(); $fields = $metadata->fetch_fields(); while(true) { $pointers = array(); $row = array(); $pointers[] = $stmt; foreach ($fields as $field) { $fieldname = $field->name; $pointers[] = &$row[$fieldname]; } call_user_func_array('mysqli_stmt_bind_result', $pointers); if (!$stmt->fetch()) break; $result[] = $row; } $metadata->free(); return $result; } /** * Check to see if an attribute exists on the BO. * * @param $attribute The attribute name. * @return boolean * @since 1.0 */ public function hasAttribute($attribute) { try{ $exists = $this->$attribute; return true; }catch(Exception $e) { return false; } } } ?> Documentation generated on Thu, 17 Mar 2011 16:43:23 +0000 by phpDocumentor 1.4.3