qcubed/framework

View on GitHub
includes/framework/QModelTrait.trait.php

Summary

Maintainability
F
4 days
Test Coverage
<?php

/**
 * Class QModelTrait
 *
 * This trait class is a mixin helper for all the generated Model classes. It works together with the code generator
 * to create particular functions that are common to all the classes. For historical reasons, and to prevent problems
 * with polymorhpism, this is a trait and not a base class.
 */
trait QModelTrait {

    /*** Requirements of Model classes ***/

    /*
     * The generated model classes must implement the following functions and members.
     */

    /**
     * Returns the value of the primary key for this object. If a composite primary key, this should return a string representation
     * of the combined keys such that the combination will be unique.
     * @return integer|string
     */
    // protected function PrimaryKey();

    /**
     * A helper function to get the primary key associated with this object type from a query result row.
     *
     * @param QDatabaseRowBase $objDbRow
     * @param string $strAliasPrefix    Prefix to use if this is a row expansion (as in, a join)
     * @param string[] $strColumnAliasArray Array of column aliases associateing our column names with the minimized names in the query.
     * @return mixed The primary key found in the row
     */
    // protected static function GetRowPrimaryKey($objDbRow, $strAliasPrefix, $strColumnAliasArray){}

    /**
     * Return the database object associated with this object.
     *
     * @return QDatabaseBase
     */
    // public static function GetDatabase(){}

    /**
     * Return the name of the database table associated with this object.
     *
     * @return string
     */
    // public static function GetTableName(){}

    /**
     * Add select fields to the query as part of the query building process. The superclass should override this to add the necessary fields
     * to the query builder object. The default is to add all the fields in the object.
     *
     * @param QQueryBuilder $objBuilder
     * @param string|null $strPrefix    optional prefix to be used if this is an extended query (as in, a join)
     * @param QQSelect|null $objSelect optional QQSelect clause to select specific fields, rather than the entire set of fields in the object
     */
    //public static function GetSelectFields(QQueryBuilder $objBuilder, $strPrefix = null, QQSelect $objSelect = null){}


    /***  Implementation ***/

    /**
     * Takes a query builder object and outputs the sql query that corresponds to its structure and the given parameters.
     *
     * @param QQueryBuilder &$objQueryBuilder the QueryBuilder object that will be created
     * @param QQCondition $objConditions any conditions on the query, itself
     * @param QQClause[] $objOptionalClauses additional optional QQClause object or array of QQClause objects for this query
     * @param mixed[] $mixParameterArray a array of name-value pairs to perform PrepareStatement with (sending in null will skip the PrepareStatement step)
     * @param boolean $blnCountOnly only select a rowcount
     * @return string the query statement
     * @throws QCallerException
     * @throws Exception
     */
    protected static function BuildQueryStatement(&$objQueryBuilder, QQCondition $objConditions, $objOptionalClauses, $mixParameterArray, $blnCountOnly) {
        // Get the Database Object for this Class
        $objDatabase = static::GetDatabase();
        $strTableName = static::GetTableName();

        // Create/Build out the QueryBuilder object with class-specific SELECT and FROM fields
        $objQueryBuilder = new QQueryBuilder($objDatabase, $strTableName);

        $blnAddAllFieldsToSelect = true;
        if ($objDatabase->OnlyFullGroupBy) {
            // see if we have any group by or aggregation clauses, if yes, don't add all the fields to select clause by default
            // because these databases post an error instead of just choosing a value to return when a select item could
            // have multiple values
            if ($objOptionalClauses instanceof QQClause) {
                if ($objOptionalClauses instanceof QQAggregationClause || $objOptionalClauses instanceof QQGroupBy) {
                    $blnAddAllFieldsToSelect = false;
                }
            } else if (is_array($objOptionalClauses)) {
                foreach ($objOptionalClauses as $objClause) {
                    if ($objClause instanceof QQAggregationClause || $objClause instanceof QQGroupBy) {
                        $blnAddAllFieldsToSelect = false;
                        break;
                    }
                }
            }
        }

        $objSelectClauses = QQuery::ExtractSelectClause($objOptionalClauses);
        if ($objSelectClauses || $blnAddAllFieldsToSelect) {
            static::BaseNode()->PutSelectFields($objQueryBuilder, null, $objSelectClauses);
        }

        $objQueryBuilder->AddFromItem($strTableName);

        // Set "CountOnly" option (if applicable)
        if ($blnCountOnly)
            $objQueryBuilder->SetCountOnlyFlag();

        // Apply Any Conditions
        if ($objConditions)
            try {
                $objConditions->UpdateQueryBuilder($objQueryBuilder);
            } catch (QCallerException $objExc) {
                $objExc->IncrementOffset();
                $objExc->IncrementOffset();
                throw $objExc;
            }

        // Iterate through all the Optional Clauses (if any) and perform accordingly
        if ($objOptionalClauses) {
            if ($objOptionalClauses instanceof QQClause) {
                try {
                    $objOptionalClauses->UpdateQueryBuilder($objQueryBuilder);
                } catch (QCallerException $objExc) {
                    $objExc->IncrementOffset();
                    $objExc->IncrementOffset();
                    throw $objExc;
                }

            }
            else if (is_array($objOptionalClauses)) {
                foreach ($objOptionalClauses as $objClause) {
                    try {
                        $objClause->UpdateQueryBuilder($objQueryBuilder);
                    } catch (QCallerException $objExc) {
                        $objExc->IncrementOffset();
                        $objExc->IncrementOffset();
                        throw $objExc;
                    }

                }
            }
            else
                throw new QCallerException('Optional Clauses must be a QQClause object or an array of QQClause objects');
        }

        // Get the SQL Statement
        $strQuery = $objQueryBuilder->GetStatement();

        // Substitute the correct sql variable names for the placeholders specified in the query, if any.
        if ($mixParameterArray) {
            if (is_array($mixParameterArray)) {
                if (count($mixParameterArray))
                    $strQuery = $objDatabase->PrepareStatement($strQuery, $mixParameterArray);

                // Ensure that there are no other Unresolved Named Parameters
                if (strpos($strQuery, chr(QQNamedValue::DelimiterCode) . '{') !== false)
                    throw new QCallerException('Unresolved named parameters in the query');
            } else
                throw new QCallerException('Parameter Array must be an array of name-value parameter pairs');
        }

        // Return the Objects
        return $strQuery;
    }

    /**
     * Static Qcubed Query method to query for a single <?php echo $objTable->ClassName  ?> object.
     * Uses BuildQueryStatment to perform most of the work.
     * Is called by QuerySincle function of each object so that the correct return type will be put in the comments.
     * 
     * @param QQCondition $objConditions any conditions on the query, itself
     * @param null $objOptionalClauses
     * @param mixed[] $mixParameterArray a array of name-value pairs to perform PrepareStatement with
     * @throws Exception
     * @throws QCallerException
     * @return null|QModelBase the queried object
     */
    protected static function _QuerySingle(QQCondition $objConditions, $objOptionalClauses = null, $mixParameterArray = null) {
        // Get the Query Statement
        try {
            $strQuery = static::BuildQueryStatement($objQueryBuilder, $objConditions, $objOptionalClauses, $mixParameterArray, false);
        } catch (QCallerException $objExc) {
            $objExc->IncrementOffset();
            throw $objExc;
        }

        // Perform the Query, Get the First Row, and Instantiate a new object
        $objDbResult = $objQueryBuilder->Database->Query($strQuery);

        // Do we have to expand anything?
        if ($objQueryBuilder->ExpandAsArrayNode) {
            $objToReturn = array();
            $objPrevItemArray = array();
            while ($objDbRow = $objDbResult->GetNextRow()) {
                $objItem = static::InstantiateDbRow($objDbRow, null, $objQueryBuilder->ExpandAsArrayNode, $objPrevItemArray, $objQueryBuilder->ColumnAliasArray);
                if ($objItem) {
                    $objToReturn[] = $objItem;
                    $pk = $objItem->PrimaryKey();
                    if ($pk) {
                        $objPrevItemArray[$pk][] = $objItem;
                    } else {
                        $objPrevItemArray[] = $objItem;
                    }
                }
            }
            if (count($objToReturn)) {
                // Since we only want the object to return, lets return the object and not the array.
                return $objToReturn[0];
            } else {
                return null;
            }
        } else {
            // No expands just return the first row
            $objDbRow = $objDbResult->GetNextRow();
            if(null === $objDbRow)
                return null;
            return static::InstantiateDbRow($objDbRow, null, null, null, $objQueryBuilder->ColumnAliasArray);
        }
    }

    /**
     * Static Qcubed Query method to query for an array of objects.
     * Uses BuildQueryStatment to perform most of the work.
     * Is called by QueryArray function of each object so that the correct return type will be put in the comments.
     *
     * @param QQCondition $objConditions any conditions on the query, itself
     * @param QQClause[]|null $objOptionalClauses additional optional QQClause objects for this query
     * @param mixed[]|null $mixParameterArray an array of name-value pairs to substitute in to the placeholders in the query, if needed
     * @return mixed[] an array of objects
     * @throws Exception
     * @throws QCallerException
     */
    protected static function _QueryArray(QQCondition $objConditions, $objOptionalClauses = null, $mixParameterArray = null) {
        // Get the Query Statement
        try {
            $strQuery = static::BuildQueryStatement($objQueryBuilder, $objConditions, $objOptionalClauses, $mixParameterArray, false);
        } catch (QCallerException $objExc) {
            $objExc->IncrementOffset();
            throw $objExc;
        }

        // Perform the Query and Instantiate the Array Result
        $objDbResult = $objQueryBuilder->Database->Query($strQuery);
        return static::InstantiateDbResult($objDbResult, $objQueryBuilder->ExpandAsArrayNode, $objQueryBuilder->ColumnAliasArray);
    }

    /**
     * Static Qcubed query method to issue a query and get a cursor to progressively fetch its results.
     * Uses BuildQueryStatment to perform most of the work.
     *
     * @param QQCondition $objConditions any conditions on the query, itself
     * @param QQClause[] $objOptionalClauses additional optional QQClause objects for this query
     * @param mixed[] $mixParameterArray an array of name-value pairs to substitute in to the placeholders in the query, if needed
     * @return QDatabaseResultBase the cursor resource instance
     * @throws Exception
     * @throws QCallerException
     */
    public static function QueryCursor(QQCondition $objConditions, $objOptionalClauses = null, $mixParameterArray = null) {
        // Get the query statement
        try {
            $strQuery = static::BuildQueryStatement($objQueryBuilder, $objConditions, $objOptionalClauses, $mixParameterArray, false);
        } catch (QCallerException $objExc) {
            $objExc->IncrementOffset();
            throw $objExc;
        }

        // Perform the query
        $objDbResult = $objQueryBuilder->Database->Query($strQuery);

        // Return the results cursor
        $objDbResult->QueryBuilder = $objQueryBuilder;
        return $objDbResult;
    }

    /**
     * Static Qcubed Query method to query for a count of objects.
     * Uses BuildQueryStatment to perform most of the work.
     *
     * @param QQCondition $objConditions any conditions on the query, itself
     * @param QQClause[] $objOptionalClauses additional optional QQClause objects for this query
     * @param mixed[] $mixParameterArray a array of name-value pairs to perform PrepareStatement with
     * @return integer the count of queried objects as an integer
     */
    public static function QueryCount(QQCondition $objConditions, $objOptionalClauses = null, $mixParameterArray = null) {
        // Get the Query Statement
        try {
            $strQuery = static::BuildQueryStatement($objQueryBuilder, $objConditions, $objOptionalClauses, $mixParameterArray, true);
        } catch (QCallerException $objExc) {
            $objExc->IncrementOffset();
            throw $objExc;
        }

        // Perform the Query and return the row_count
        $objDbResult = $objQueryBuilder->Database->Query($strQuery);

        // Figure out if the query is using GroupBy
        $blnGrouped = false;

        if ($objOptionalClauses) {
            if ($objOptionalClauses instanceof QQClause) {
                if ($objOptionalClauses instanceof QQGroupBy) {
                    $blnGrouped = true;
                }
            } else if (is_array($objOptionalClauses)) {
                foreach ($objOptionalClauses as $objClause) {
                    if ($objClause instanceof QQGroupBy) {
                        $blnGrouped = true;
                        break;
                    }
                }
            } else {
                throw new QCallerException('Optional Clauses must be a QQClause object or an array of QQClause objects');
            }
        }

        if ($blnGrouped)
            // Groups in this query - return the count of Groups (which is the count of all rows)
            return $objDbResult->CountRows();
        else {
            // No Groups - return the sql-calculated count(*) value
            $strDbRow = $objDbResult->FetchRow();
            return (integer)$strDbRow[0];
        }
    }

    public static function QueryArrayCached(QQCondition $objConditions, $objOptionalClauses = null, $mixParameterArray = null, $blnForceUpdate = false) {
        $strQuery = static::BuildQueryStatement($objQueryBuilder, $objConditions, $objOptionalClauses, $mixParameterArray, false);

        $strTableName = static::GetTableName();
        $objCache = new QCache(sprintf('qquery/%s', $strTableName), $strQuery);
        $cacheData = $objCache->GetData();

        if (!$cacheData || $blnForceUpdate) {
            $objDbResult = $objQueryBuilder->Database->Query($strQuery);
            $arrResult = static::InstantiateDbResult($objDbResult, $objQueryBuilder->ExpandAsArrayNode, $objQueryBuilder->ColumnAliasArray);
            $objCache->SaveData(serialize($arrResult));
        } else {
            $arrResult = unserialize($cacheData);
        }

        return $arrResult;
    }

    /**
     * Do a possible array expansion on the given node. If the node is an ExpandAsArray node,
     * it will add to the corresponding array in the object. Otherwise, it will follow the node
     * so that any leaf expansions can be handled.
     *
     * @param QDatabaseRowBase $objDbRow
     * @param string $strAliasPrefix
     * @param QQNode $objNode
     * @param QModelBase[] $objPreviousItemArray
     * @param string[] $strColumnAliasArray
     * @return boolean|null Returns true if the we used the row for an expansion, false if we already expanded this node in a previous row, or null if no expansion data was found
     */
    public static function ExpandArray ($objDbRow, $strAliasPrefix, $objNode, $objPreviousItemArray, $strColumnAliasArray) {
        if (!$objNode->ChildNodeArray) {
            return null;
        }
        $blnExpanded = null;

        $pk = static::GetRowPrimaryKey ($objDbRow, $strAliasPrefix, $strColumnAliasArray);

        foreach ($objPreviousItemArray as $objPreviousItem) {
            if ($pk != $objPreviousItem->PrimaryKey()) {
                continue;
            }

            foreach ($objNode->ChildNodeArray as $objChildNode) {
                $strPropName = $objChildNode->_PropertyName;
                $strClassName = $objChildNode->_ClassName;
                $strLongAlias = $objChildNode->FullAlias();
                $blnExpandAsArray = false;

                if ($objChildNode->ExpandAsArray) {
                    $strPostfix = 'Array';
                    $blnExpandAsArray = true;
                } else {
                    $strPostfix = '';
                }
                $nodeType = $objChildNode->_Type;
                if ($nodeType == 'reverse_reference') {
                    $strPrefix = '_obj';
                } elseif ($nodeType == 'association') {
                    $objChildNode = $objChildNode->FirstChild();
                    if ($objChildNode->IsType) {
                        $strPrefix = '_int';
                    } else {
                        $strPrefix = '_obj';
                    }
                } else {
                    $strPrefix = 'obj';
                }

                $strVarName = $strPrefix . $strPropName . $strPostfix;

                if ($blnExpandAsArray) {
                    if (null === $objPreviousItem->$strVarName) {
                        $objPreviousItem->$strVarName = array();
                    }
                    if (count($objPreviousItem->$strVarName)) {
                        $objPreviousChildItems = $objPreviousItem->$strVarName;
                        $nextAlias = $objChildNode->FullAlias() . '__';

                        $objChildItem = $strClassName::InstantiateDbRow ($objDbRow, $nextAlias, $objChildNode, $objPreviousChildItems, $strColumnAliasArray, true);

                        if ($objChildItem) {
                            $objPreviousItem->{$strVarName}[] = $objChildItem;
                            $blnExpanded = true;
                        } elseif ($objChildItem === false) {
                            $blnExpanded = true;
                        }
                    }
                } elseif (!$objChildNode->IsType) {

                    // Follow single node if keys match
                    if (null === $objPreviousItem->$strVarName) {
                        return false;
                    }
                    $objPreviousChildItems = array($objPreviousItem->$strVarName);
                    $blnResult = $strClassName::ExpandArray ($objDbRow, $strLongAlias . '__', $objChildNode, $objPreviousChildItems, $strColumnAliasArray);

                    if ($blnResult) {
                        $blnExpanded = true;
                    }
                }
            }
        }
        return $blnExpanded;
    }

    /**
     * Return an object corresponding to the given key, or null.
     *
     * The key might be null if:
     *     The table has no primary key, or
     *  SetSkipPrimaryKey was used in a query with QSelect.
     *
     * Otherwise, the default here is to use the local cache.
     *
     * Note that you rarely would want to change this. Caching at this level happens
     * after a query has executed. Using a cache like APC or MemCache at this point would
     * be really expensive, and would only be worth it for a large table.
     *
     * @param $key
     * @return null|object
     */
    public static function GetFromCache($key) {
        if ($key===null) return null;
        if (QApplication::$blnLocalCache === true && !empty(static::$objCacheArray[$key])) {
            return clone(static::$objCacheArray[$key]);
        }
        elseif (QApplication::$objCacheProvider) {
            $strCacheKey = QApplication::$objCacheProvider->CreateKey(static::GetDatabase()->Database, __CLASS__, $key);
            return QApplication::$objCacheProvider->Get($strCacheKey);
        }
        return null;
    }

    /**
     * Put the current object in the cache for future reference.
     */
    public function WriteToCache() {
        $key = $this->PrimaryKey();
        if ($key === null) return;
        $obj = clone($this);
        if (QApplication::$blnLocalCache === true) static::$objCacheArray[$key] = $obj;
        if (QApplication::$objCacheProvider) {
            $strCacheKey = QApplication::$objCacheProvider->CreateKey(static::GetDatabase()->Database, __CLASS__, $key);
            QApplication::$objCacheProvider->Set($strCacheKey, $obj);
        }
    }

    /**
     * Delete this particular object from the cache
     * @return void
     */
    public function DeleteFromCache() {
        $key = $this->PrimaryKey();
        if ($key === null) return;
        unset (static::$objCacheArray[$key]);
        if (QApplication::$objCacheProvider) {
            $strCacheKey = QApplication::$objCacheProvider->CreateKey(static::GetDatabase()->Database, __CLASS__, $key);
            QApplication::$objCacheProvider->Delete($strCacheKey);
        }
    }


    /**
     * Clears the caches associated with this table.
     */
    public static function ClearCache() {
        static::$objCacheArray = array();
        if (QApplication::$objCacheProvider) {
            QApplication::$objCacheProvider->DeleteAll();
        }
    }

}