2012-12-31

PHP Object Persistence

Documentation (together with my other libs)

<?php



// --- INCLUDES --- //
        require_once dirname(__FILE__).'/'.'inc.php4';
        require_once dirname(__FILE__).'/'.'lib.cDBAccess_MySQL.php4';
        // Event Logging
        require_once dirname(__FILE__).'/'.'lib.cApp.php';
        require_once dirname(__FILE__).'/'.'lib.cLog.php';
        require_once dirname(__FILE__).'/'.'lib.cObjectPersistence_ColumnTypes.php';



// --- CONSTANTS --- //

        // Column types
        define('DWOP_TYPE_STR',  1);
        define('DWOP_TYPE_NUM',  2);
        define('DWOP_TYPE_REAL', 3);
        define('DWOP_TYPE_BOOL', 4);
        define('DWOP_TYPE_ENUM', 5);
        define('DWOP_TYPE_SET',  6);

        // Column flags
        define('DWOP_NOT_NULL', 1);
        define('DWOP_BINARY',   2);
        //define('DWOP_BINARY',   2);

        // For cDwOpClass::GetObjectMultiIdString()
        define('DWOP_VALUE_DUMP_MODE__SQL', 0);
        define('DWOP_VALUE_DUMP_MODE__SQL_SHORT', 2);
        define('DWOP_VALUE_DUMP_MODE__SQL_VALUES', 3);
        define('DWOP_VALUE_DUMP_MODE__ESCAPED_VALUES', 4);
        define('DWOP_VALUE_DUMP_MODE__COMA', 5);


/** *********************************************************************************
 <h1>  RDBMS Object Loader for PHP  </h1>

        @version 2.4.6
        @author Ondra Zizka, Dynawest;  ondra@dynawest.cz

        <h2>Plan:</h2>
        3.0.0:
                - Add:      Complete cross-class references handling
        2.x.x:
                - Class::RemProperty() - handle removal of ID and KEY properties.
                        Class::aoIdProperties - rewrite so that GetIdProperties etc will compute from properties themselves. ??? Good idea???

        <h2>History:</h2>
        2.4.7:
                - Fixed:  cDwOpProperty::FormatForSql() returns correctly for DWOP_TYPE_REAL column types.
        2.4.7:
                - Added:  DeleteObject() supports multi-property PRIMARY KEYS in it's second parameter
        2.4.6:
                - Added:  Object Pool supports multi-property PRIMARY KEYS
        2.4.5:
                - Fixed:  Fix in SaveObject() - now saves all properties that are set. Null values for NOT NULL columns converted to default.
                - Added:  function GetNonKeyProperties()
                - Added:  DeleteObject() now can take Class name and single ID as parameters.
                - Added:  cDwOpProperty now remembers it's PRIMARY KEY properties added through AddIdProperty() etc.
                - Added:  cDwOpProperty::IsKey()
                - Changed:  cDwOpColumnType::GetDefaultDefault() is not abstract returns a value according to basic column type.
                - Fixed:  DwFwIdAndProperties::HasProperty() now uses array_key_exists() instead of fucking isset().
        2.4.4:
                - Fixed:  Massive fix in SaveObject() and one in DeleteObject()
                - Added:  param $sGlue to  GetIdPropertiesSql($oObject, $sGlue=', ')
        2.4.3:
                - Added:  Support for multi-property PRIMARY KEYs  in SaveObject(), DeleteObject()
        2.4.2:
                - Added:  DeleteObject($oObject);
                - Added:  GetIdPropertiesSql()
        2.4.1:
                - Added:  param $bOverwrite to function AddObjectIntoPool( $oObject, $bOverwrite=true )
        2.4.0:
                - Added:    M : N cross-class references
                  - Added:   function SetMnRelation($oClass1, $oClassGlue, $oClass2)
                  - Added:   function LoadObjectsByMnRelation($oObject, $oClassGlue, $oClass2, $xId=null)

                - Added:   Support for multi-property PRIMARY key - but only for keeping it, not for loading or saving

                - Added:   Real column types
                              (abstract class cDwOpColumnType_Real extends cDwOpColumnType) and derived classes)
                - Added:   Ordinal column types can take possible values definiton as they go from the DB:
                              "'value', 'value', ..."
                - Added:   Column types BIGINT and MEDIUMINT

                Objects loading:
                - Added:   function LoadObjects($oClass)
                - Added:   function LoadObjectsBySql($xClass, $sSQL)
                - Added:   function LoadObjectsFromResult($xClass, $oRes)

                Class related:
          - Added:   function ConvertAndCheckClass( $oClass )
                - Added:   function CreateClassCopy( $sName, $xClass )
                - Added:   function CreateClassByTable( $sName, $sTable )
                - Added:   function CreateColumnTypeObject($sTypeString, $bNull, $sDefault)


        2.3.0:
                - Added:    CreateClassByTable()
                            Creates class definition according to a table in a database.
        2.2.1:
                - Added:    LoadObjectsFromResult().
                - Changed:  LoadObjectsByValue() now uses LoadObjectsFromResult().
        2.2.0:
                - Added:    Ordering by properties support.
                            GetOrderProperties(), SetOrderProperties()
        2.1.0:
                - Added:    Default values, better checking.
                            GetDefault(), SetDefault(), GetDefaultDefault()
        2.0.2:
                - Added:    Load* functions' first parameter can be also a class name string.
        2.0.1:
                - Added:    GetPoolCount()
                - Changed:  SaveObject() adds UPDATEd (overwrite) and INSERTed objects into pool
        2.0.0:
                - Add central object registry - object is loaded only once,
                  then return the formerly loaded
        1.1.0:
                - Added support for AUTO_INCREMENT when creating objects.
                - Added possibility to load objects by values.
 ***********************************************************************************/
class cDwObjectPersistence_DB {

        var $oDB = null;
        function &GetDB()    { return $this->oDB; }

        var $sError = null;
        function GetError()       { return $this->sError; }
        function SetError($sError){ $this->sError = $sError; }


        function cDwObjectPersistence_DB($oDB){
                $this->oDB = $oDB;
        }


        /**<***********************************************************************>
        *  Class objects dictionary                                                *
        ***************************************************************************/
        var $aoClassesDictionary = Array();
        function GetClassNames(){ return array_keys($this->aoClassesDictionary); }
        function AddClassIntoDictionary( $oClass ){
                $this->aoClassesDictionary[$oClass->GetName()] = $oClass;
        }
        function GetClassFromDictionary( $sClassName ){
                return isset($this->aoClassesDictionary[$sClassName]) ? $this->aoClassesDictionary[$sClassName] : null;
        }


        /**<***********************************************************************>
        *  Objects pool                                                            *
        ***************************************************************************/
        var $aaoObjectPool = Array();

        function AddObjectIntoPool( $oObject, $bOverwrite=true ){
                if( !is_object($oObject) ) return false;
                $sClassName = get_class($oObject);
                //App::Log("\$sClassName: ".$sClassName);///
                if( !$oClass = $this->GetClassFromDictionary($sClassName) ){ return false; }

                // Get the object unique PRIMARY KEYs string
                //App::Log("GetIdPropertiesCount(): ".$oClass->GetIdPropertiesCount());///
                switch( $oClass->GetIdPropertiesCount() ){
                        case 0: $bUsePool = false; return false; break;
                        case 1:  $xId = $oObject->GetId(); break;
                        default: $xId = $oClass->GetObjectMultiIdString( $oObject, DWOP_VALUE_DUMP_MODE__SQL_VALUES ); break;
                }
                //if( 0 == $oClass->GetIdPropertiesCount() ) return false;
                //$xId = $oClass->GetObjectMultiIdString( $oObject, DWOP_VALUE_DUMP_MODE__SQL_VALUES );

                // If we should not overwrite...
                if( !$bOverwrite ){
                        // and the object exists, don't write and return true.
                        if( isset( $this->aaoObjectPool[get_class($oObject)][$xId] ) )
                                return true;
                }

                // Save the object handle.
                $this->aaoObjectPool[get_class($oObject)][$xId] = $oObject;
                return true;
        }

        function AddObjectIntoPool_SingleId( $oObject, $bOverwrite=true ){
                if( !is_object($oObject) ) return false;

                // If we should not overwrite...
                if( !$bOverwrite ){
                        // and the object exists, don't write and return true.
                        if( isset( $this->aaoObjectPool[get_class($oObject)][$oObject->GetId()] ) )
                                return true;
                }

                // Save the object handle.
                $this->aaoObjectPool[get_class($oObject)][$oObject->GetId()] = $oObject;
                return true;
        }

        function GetObjectFromPool( $sClassName, $xId ){
                $oObject = null;
                if( isset($this->aaoObjectPool[$sClassName][$xId]) ){
                        $oObject = $this->aaoObjectPool[$sClassName][$xId];
                        //echo "<pre><strong>Pool hit!</strong></pre>";///
                }
                return $oObject;
        }


        function RemObjectFromPool( $oObject ){
                if( !is_object($oObject) ) return false;
                // Get the Class object
                $sClassName = get_class($oObject);
                if( !($oClass = $this->GetClassFromDictionary($sClassName)) ){ return false; }

                // Get the object unique PRIMARY KEYs string
                //App::Log("GetIdPropertiesCount(): ".$oClass->GetIdPropertiesCount());///
                switch( $oClass->GetIdPropertiesCount() ){
                        case 0: $bUsePool = false; return false; break;
                        case 1:  $xId = $oObject->GetId(); break;
                        default: $xId = $oClass->GetObjectMultiIdString( $oObject, DWOP_VALUE_DUMP_MODE__SQL_VALUES ); break;
                }
                // Remove the object with the acquired ID
                if( isset( $this->aaoObjectPool[$sClassName][$xId] ) )
                        unset($this->aaoObjectPool[$sClassName][$xId]);
                return true;
        }

        function RemObjectFromPool_SingleId( $oObject ){
                if( !is_object($oObject) ) return false;
                $sClassName = get_class($oObject);
                $xId = $oObject->GetId();
                if( isset( $this->aaoObjectPool[$sClassName][$xId] ) )
                        unset($this->aaoObjectPool[$sClassName][$xId]);
                return true;
        }


        function HasObjectInPool( $oObject ){
                if( !is_object($oObject) ) return false;
                // Get the Class object
                $sClassName = get_class($oObject);
                if( !($oClass = $this->GetClassFromDictionary($sClassName)) ){ return false; }

                // Get the object unique PRIMARY KEYs string
                switch( $oClass->GetIdPropertiesCount() ){
                        case 0: $bUsePool = false; break;
                        case 1:  $xId = $oObject->GetId(); break;
                        default: $xId = $oClass->GetObjectMultiIdString( $oObject, DWOP_VALUE_DUMP_MODE__SQL_VALUES ); break;
                }
                // $xId = $oObject->GetId();

                $xId = $oClass->GetObjectMultiIdString( $oObject, DWOP_VALUE_DUMP_MODE__SQL_VALUES );

                if( isset( $this->aaoObjectPool[get_class($oObject)][$xId] ) )
                        return true;
                return false;
        }

        function HasObjectInPool_SingleId( $oObject ){
                if( !is_object($oObject) ) return false;
                $xId = $oObject->GetId();
                if( isset( $this->aaoObjectPool[get_class($oObject)][$xId] ) )
                        return true;
                return false;
        }


        function GetPoolCount(){
                return array_sum(array_map('Count', $this->aaoObjectPool));
        }






        /**<***********************************************************************>
        *  Converts the class name to the DwOpClass object and checks the class.   *
        ***************************************************************************/
        function ConvertAndCheckClass( $oClass ){

                // $oClass can be either cDwOpClass object or a class name
                if( !$oClass ){ $this->SetError("Bad param (null) \$oClass for ".__METHOD__." in ".CallInfo(-2)); return false; }

                // A string - try to find the Class in the Dictionary
                if( is_string($oClass) ){
                        $sClassName = $oClass;
                        $oClass = $this->GetClassFromDictionary($sClassName);
                        if( !is_object($oClass) ){ $this->SetError("Class [$sClassName] not found in dictionary."); return false; }
                }
                // Not a string - either the Class object or an object of some Class.
                else{
                        if( !is_object($oClass) ){ $this->SetError("Bad param (not string or object) \$oClass for ".__METHOD__." in ".CallInfo(-2)); return false; }

                        if( $oClass instanceof cDwOpClass ){
                                $sClassName = $oClass->GetName();
                        }else{
                                // object of some Class - try to find the Class object.
                                $sClassName = get_class($oClass);
                                $oClass = $this->GetClassFromDictionary( $sClassName );
                                if( !is_object($oClass) ){ $this->SetError("Object's Class [$sClassName] not found in dictionary."); return false; }
                        }
                }

                // Check class existence
                if( !class_exists($sClassName) ){ $this->SetError("Non-existent PHP class [$sClassName]. in ".__METHOD__." in ".CallInfo(-2)); return false; }

                return $oClass;
        }


        /**<***********************************************************************>
        *  Deletes an object with ID $iID  of the given class $oClass              *
           Deletes the given object from the DB.
        ***************************************************************************/
        function DeleteObject($oObject, $xId=null){
                //echo "<pre>\n class object: ".AdjustedPrintR($oClass)."</pre>";///
                //if( !is_object( $oObject ) ) return false;
                $bSucc = false;
                do{

                        /*// Find the Class object in the Dictionary
                        $sClassName = get_class($oObject);
                        do{
                                $oClass = $this->GetClassFromDictionary($sClassName);
                                $sClassName = get_parent_class($sClassName);
                        }while( $sClassName && !$oClass );

                        // Check class object //
                        //if( !$oClass ){ $this->SetError("Class [$sClassName] not found in the dictionary in ".__METHOD__." @ ".__LINE__); break; }
                        /*/
                        if( !($oClass = $this->ConvertAndCheckClass($oObject)) ){ $this->SetError("!\$oClass in ".__METHOD__." @ ".__LINE__); break; }
                        $sClassName = $oClass->GetName();
                        /**/

                        // If $oObject is an instance of Class, get it's ID.

                        // If $oObject was just identification of a Class, create a temporary object with just IDs.
                        if( !($oObject instanceof $sClassName) ){
                                // Check the ID - false, null, or empty array? -> fail.
                                if( !$xId ){ $this->SetError("Unknown object ID to delete in ".__METHOD__." @ ".__LINE__); break; }

                                // If $xId is multiple ID - associative array of ids
                                if( is_array($xId) ){
                                        $oObject = new $sClassName();
                                        foreach( $xId as $k => $v )
                                                $oObject->SetProperty($k, $v);
                                }
                                // Single ID
                                else{
                                        //$oObject = $this->LoadObjectById($oClass, $xId);
                                        //if(!$oObject) { $this->SetError("!LoadObjectById(".$oClass->GetName().", $xId) in ".__METHOD__." @ ".__LINE__); break; }
                                        $oObject = new $sClassName();
                                        $oObject->SetId($xId);
                                }
                        }

                        // -- From here further, we have DwOpClass $oClass  and  it's (pseudo)instance $oObject. -- //


                        // Remove from the Object Pool
                        if( !$this->RemObjectFromPool($oObject) ){ $this->SetError("!RemObjectFromPool() in ".__METHOD__." @ ".__LINE__); }


                        $sTable  = $oClass->GetTable();

                        /*/ For one ID (PRIMARY KEY) property only...
                        $oIdProp = $oClass->GetIdProperty();
                        $sColId  = $oIdProp->GetColName();
                        $xId = $oIdProp->FormatForSql( $oObject->GetProperty($oIdProp->GetColName()) );
                        $sSQL = "DELETE FROM $sTable WHERE $sColId = $xId";
                        /*/
                        // For all PRIMARY KEYs:
                        $saxIdsCond = $oClass->GetIdPropertiesSql($oObject, ' AND ');
                        $sSQL = "DELETE FROM $sTable WHERE $saxIdsCond";
                        /**/

                        // Perform SQL query
                        $oRes = $this->GetDB()->Execute($sSQL);
                        if( !$oRes || !$oRes->IsOK() ){ $this->SetError("SQL error [".$oRes->GetError()."] SQL: [$sSQL]"); break; }

                        $bSucc = true;
                }while(false);
                return $bSucc;
        }// cDwObjectPersistence_DB::DeleteObject()


        /**<*********************************************************************************>
        *  Deletes objects of the given class $oClass
        *  with some property equal to the given value.

          @param oClass:         cDwOpClass object or a class name to find in dictionary.
          @param sPropertyName:  Name of the property to compare with $xVal.
          @param xVal:           Value to compare property with.
                @returns array of objects with value $sPropertyName equal to $xVal.
        *************************************************************************************/
        function DeleteObjectsByValueBad($oClass, $sPropertyName, $xVal){
                //echo "<pre>\n class object: ".AdjustedPrintR($oClass)."</pre>";///

                if( !( $oClass = $this->ConvertAndCheckClass($oClass) ) ) return false;
                $sClassName = $oClass->GetName();


                // Get the property by which to recognize the object to load
                $oProp = $oClass->GetProperty($sPropertyName);
                if( null == $oProp ){ $this->SetError("No such property in class [$sClassName]: [$sPropertyName]"); return false; }

                // Create SQL query
                $sTable  = $oClass->GetTable();
                $sCol    = $oProp->GetColName();
                $oIdProp = $oClass->GetIdProperty();
                $sColId  = $oIdProp->GetColName();

                $sOrderSql = $oClass->GetOrderPropertiesSql();   // ORDER BY part
                $sSQL = "DELETE FROM $sTable WHERE $sCol = ".$oProp->FormatForSql($xVal)." ".$sOrderSql;

                // Perform SQL query
                $oRes = $this->GetDB()->Execute($sSQL);
                if( !$oRes || !$oRes->IsOK() ){ $this->SetError("SQL error [".$oRes->GetError()."] SQL: [$sSQL] in ".__METHOD__.' @ '.__LINE__); return false; }

                // TODO: Remove objects from Object Pool!
                $this->RemObjectFromPool($oObject);

                return true;
        }// LoadObjectsByValue($oClass, $sPropertyName, $xVal)


        /**<***********************************************************************>
        *  Second version                                                          *
        ***************************************************************************/
        function DeleteObjectsByValue($oClass, $sPropertyName, $xVal){

                $aoObjects = $this->LoadObjectsByValue($oClass, $sPropertyName, $xVal);
                if(!is_array($aoObjects)) return false;

                foreach( $aoObjects as $oObject){
                        $this->DeleteObject($oObject);
                }

                return true;
        }// LoadObjectsByValue($oClass, $sPropertyName, $xVal)




        /**<***********************************************************************>
        *  Loads an object with ID $iID  of the given class $oClass                *
        ***************************************************************************/
        function LoadObjectById($oClass, $iID){
                //echo "<pre>\n class object: ".AdjustedPrintR($oClass)."</pre>";///


                /*/ First parameter can be either cDwOpClass object or a class name
                if( is_string($oClass) ){
                        $sClassName = $oClass;
                        $oClass = $this->GetClassFromDictionary($sClassName);
                        if( !$oClass ) return false;
                }else{
                        $sClassName = $oClass->GetName();
                }
                // Check class existence
                if( !class_exists($sClassName) )
                        return false;
                /*/
                if( !( $oClass = $this->ConvertAndCheckClass( $oClass ) ) ) return false;
                $sClassName = $oClass->GetName();/**/


                // Look for the object in the pool
                if( $oObject = $this->GetObjectFromPool($sClassName, $iID) )
                        return $oObject;



                $sTable  = $oClass->GetTable();
                $oIdProp = $oClass->GetIdProperty();
                $sColId  = $oIdProp->GetColName();

                // Create and perform SQL query
                $sSQL = "SELECT * FROM $sTable WHERE $sColId = ".$oIdProp->FormatForSql($iID);
                $oRet = $this->GetDB()->Select($sSQL);
                if( !$oRet->IsOK() ) return false;
                if( 0 == $oRet->NumRows() ) return null;

                $aData = $oRet->FetchRow(MYSQL_ASSOC);

                // Create the object
                $oObject = new $sClassName();

                // Load all declared properties
                foreach( $oClass->GetProperties() as $oProperty ){
                        //echo "<div>\$oObject->SetProperty( ".$oProperty->GetColName().", \$aData[".$oProperty->GetColName()."] );</div>";///
                        $oObject->SetProperty( $oProperty->GetColName(), $oProperty->GetColType()->ConvertFromResult( $aData[$oProperty->GetColName()] ) );
                }

                $this->AddObjectIntoPool($oObject);
                return $oObject;
        }// LoadObjectById($oClass, $iID)





        /**<***********************************************************************************>
        *  Loads all objects of the given class $oClass.

          @param oClass:         cDwOpClass object or a class name to find in dictionary.
                @returns array of objects of the specified class.
        ***************************************************************************************/
        function LoadObjects($oClass){

                /*/ First parameter can be either cDwOpClass object or a class name
                if( is_string($oClass) ){
                        $sClassName = $oClass;
                        $oClass = $this->GetClassFromDictionary($sClassName);
                        if( !is_object($oClass) ){ $this->SetError("Class [$sClassName] not found in dictionary."); return false; }
                }else{
                        $sClassName = $oClass->GetName();
                }

                // Check class existence
                if( !class_exists($sClassName) ){ $this->SetError("Non-existent class [$sClassName]."); return false; }
                /*/
                if( !( $oClass = $this->ConvertAndCheckClass( $oClass ) ) ) return false;
                $sClassName = $oClass->GetName();/**/


                // Create SQL query
                $sTable    = $oClass->GetTable();
                $sOrderSql = $oClass->GetOrderPropertiesSql();   // ORDER BY part
                $sSQL = "SELECT * FROM $sTable ".$sOrderSql;

                // Perform SQL query
                $oRes = $this->GetDB()->Select($sSQL);
                if( !$oRes || !$oRes->IsOK() ){
                        $this->SetError("SQL error [".$oRes->GetError()."] SQL: [$sSQL]");
                        return false;
                }

                // Load Objects
                $aoObjects = $this->LoadObjectsFromResult($oClass, $oRes);
                $oRes->FreeResult();

                return $aoObjects;

        }// cDwObjectPersistence_DB::LoadObjects()



        /**<*********************************************************************************>
        *  Loads an objects of the given class $oClass
        *  with some property equal to the given value.

          @param oClass:         cDwOpClass object or a class name to find in dictionary.
          @param sPropertyName:  Name of the property to compare with $xVal.
          @param xVal:           Value to compare property with.
                @returns array of objects with value $sPropertyName equal to $xVal.
        *************************************************************************************/
        function LoadObjectsByValue($oClass, $sPropertyName, $xVal){
                //echo "<pre>\n class object: ".AdjustedPrintR($oClass)."</pre>";///


                /*/ First parameter can be either cDwOpClass object or a class name
                if( is_string($oClass) ){
                        $sClassName = $oClass;
                        $oClass = $this->GetClassFromDictionary($sClassName);
                        if( !is_object($oClass) ){
                                $this->SetError("Class [$sClassName] not found in dictionary.");
                                return false;
                        }
                }else{
                        $sClassName = $oClass->GetName();
                }

                // Check class existence
                if( !class_exists($sClassName) ){ $this->SetError("Non-existent class [$sClassName]."); return false; }
                /*/
                if( !( $oClass = $this->ConvertAndCheckClass( $oClass ) ) ) return false;
                $sClassName = $oClass->GetName();/**/


                // Get the property by which to recognize the object to load
                $oProp = $oClass->GetProperty($sPropertyName);
                if( null == $oProp ){
                        $this->SetError("No such property in class [$sClassName]: [$sPropertyName]");
                        return false;
                }

                // Create SQL query
                $sTable  = $oClass->GetTable();
                $sCol    = $oProp->GetColName();
                $oIdProp = $oClass->GetIdProperty();
                $sColId  = $oIdProp->GetColName();

                $sOrderSql = $oClass->GetOrderPropertiesSql();   // ORDER BY part
                $sSQL = "SELECT * FROM $sTable WHERE $sCol = ".$oProp->FormatForSql($xVal)." ".$sOrderSql;

                /*// Perform SQL query
                $oRes = $this->GetDB()->Select($sSQL);
                if( !$oRes || !$oRes->IsOK() ){
                        $this->SetError("SQL error [".$oRes->GetError()."] SQL: [$sSQL]");
                        return false;
                }

                // Load Objects
                $aoObjects = $this->LoadObjectsFromResult($oClass, $oRes);
                $oRes->FreeResult();/*/

                $aoObjects = $this->LoadObjectsBySql($oClass, $sSQL);/**/

                return $aoObjects;
        }// LoadObjectsByValue($oClass, $sPropertyName, $xVal)





        /**<*********************************************************************************>
        *  Performs SQL query and converts the result to an array of objects.                *
        *************************************************************************************/
        function LoadObjectsBySql($xClass, $sSQL){

                // Perform SQL query
                $oRes = $this->GetDB()->Select($sSQL);
                if( !$oRes || !$oRes->IsOK() ){
                        $this->SetError("SQL error [".$oRes->GetError()."] SQL: [$sSQL]");
                        return false;
                }

                // Load Objects
                $aoObjects = $this->LoadObjectsFromResult($xClass, $oRes);
                $oRes->FreeResult();

                return $aoObjects;
        }



        /**<*********************************************************************************>
        *  Converts the result to an array of objects.                                       *
        *************************************************************************************/
        function LoadObjectsFromResult($xClass, $oRes){

                // Convert and check the class
                if( !( $oClass = $this->ConvertAndCheckClass( $xClass ) ) ){
                        $this->SetError("!ConvertAndCheckClass(".gettype($xClass)."): ".$this->GetError()); return false;
                }
                $sClassName = $oClass->GetName();


                $aoObjects = Array();
                //if( 0 == $oRes->NumRows() ) return $aoObjects;

                // Class Name
                //if( !is_object($oClass) ) echo CallInfo(-2);



                $aoPropertiesToTraverse = Array();
                // Get the list of the properties that will be set from the result.
                // Check column existence for all declared properties
                $aoFields = $oRes->GetColumns();
                $absFieldsContained = Array();
                foreach( $aoFields as $oField ){ $absFieldsContained[$oField->name] = 1; }
                //App::Log("\$absFieldsContained: ".AdjustedPrintR($absFieldsContained));///
                foreach( $oClass->GetProperties() as $oProperty ){
                        $sColName = $oProperty->GetColName();
                        if( !array_key_exists($sColName, $absFieldsContained) ){
                                $this->SetError("Warn: Column [$sColName] for property [".$oProperty->GetName()."] is not set; in ".__METHOD__." in ".CallInfo(-2));
                                App::Log("Warn: Column [$sColName] for property [".$oProperty->GetName()."] is not set; in ".__METHOD__); continue;
                        }
                        $aoPropertiesToTraverse[] = $oProperty;
                }/**/

                // Create the objects. For each row:
                while( $aData = $oRes->FetchRow(MYSQL_ASSOC) ){
                        $oObject = new $sClassName();
                        /*// Load all declared properties
                        foreach( $oClass->GetProperties() as $oProperty ){
                                //echo "<div>\$oObject->SetProperty( ".$oProperty->GetColName().", \$aData[".$oProperty->GetColName()."] );</div>";///
                                $sColName = $oProperty->GetColName();
                                //if( !isset( $aData[$sColName] ) && !is_null($aData[$sColName]) ){
                                if( !array_key_exists($sColName, $aData) ){
                                        App::Log("Warn: Column [$sColName] for property [".$oProperty->GetName()."] is not set."); continue;
                                } // DONE: Move before the while - fetch_fields()
                                $oObject->SetProperty( $oProperty->GetColName(), $aData[$sColName] );
                        }/**/
                        // Load properties that are present in the result
                        foreach( $aoPropertiesToTraverse as $oProperty ){
                                //echo "<div>\$oObject->SetProperty( ".$oProperty->GetColName().", \$aData[".$oProperty->GetColName()."] );</div>";///
                                $sColName = $oProperty->GetColName();
                                $oObject->SetProperty( $sColName, $oProperty->GetColType()->ConvertFromResult($aData[$sColName]) );
                        }

                        // Get object's unique ID, if possible
                        $bUsePool = true;
                        //App::Log("GetIdPropertiesCount(): ".$oClass->GetIdPropertiesCount());///
                        switch( $oClass->GetIdPropertiesCount() ){
                                case 0: $bUsePool = false; break;
                                case 1:  $xId = $oObject->GetId(); break;
                                default: $xId = $oClass->GetObjectMultiIdString( $oObject, DWOP_VALUE_DUMP_MODE__SQL_VALUES ); break;
                        }

                        // Look for the object in the pool
                        if(!$bUsePool)
                                $aoObjects[] = $oObject;
                        else
                        if( $oObjectFromPool = $this->GetObjectFromPool($sClassName, $xId) ){
                                //App::Log("Pool hit [$sClassName, ".$xId."]");///
                                $aoObjects[] = $oObjectFromPool;
                        }else{
                                //App::Log("Pool miss [$sClassName, ".$xId."]");///
                                $aoObjects[] = $oObject;
                                $this->AddObjectIntoPool($oObject);
                        }

                }// while( for each row in result )

                return $aoObjects;

        }// cDwObjectPersistence::LoadObjectsFromResult($oClass, $oRes)




        /**<***********************************************************************>
        *  Saves an object with ID $iID  of the given class $oClass                *
        ***************************************************************************/
        function SaveObject($oObject){
                //echo "<pre>\n class object: ".AdjustedPrintR($oClass)."</pre>";///
                //App::Log("Object: ".AdjustedPrintR($oObject));///
                if( !is_object( $oObject ) ) return false;


                // Whether to try UPDATE first before INSERT
                $bDoUpdate = true;
                $bZeroUpdatedRows = false;


                // Add this Class object into the dictionary

                // Get the Class object - for this PHP class or some ancestor class
                $sClassName = get_class($oObject);
                do{
                        $oClass = $this->GetClassFromDictionary($sClassName);
                        $sClassName = get_parent_class($sClassName);
                }while( $sClassName && !$oClass );

                // Check class object //
                if( !$oClass )
                        return false;

                $sTable  = $oClass->GetTable();



                /// --- PRIMARY KEYS --- ///


                /*/ For one ID (PRIMARY KEY) property only...
                $oIdProp = $oClass->GetIdProperty();         //x  Single-ID way
                $sColId  = $oIdProp->GetColName();           //x  Single-ID way
                $iID = $oObject->GetProperty( $oIdProp->GetColName() );
                $sIdForSql = $oIdProp->FormatForSql($iID); //x  Single-ID way
                /*/
                // For all PRIMARY KEYs:
                $saxIds = $oClass->GetIdPropertiesSql( $oObject );
                $saxIdsCond = $oClass->GetIdPropertiesSql( $oObject, ' AND ' );
                /**/





                /// --- VALUES --- ///


                // Store all declared properties - create the SQL part
                $asSqlParts = Array();
                $aoIdProps = $oClass->GetIdProperties();
                foreach( $oClass->GetProperties() as $oProperty ){
                        //echo "<div>\$oObject->GetProperty( ".$oProperty->GetColName()." );</div>";///
                        $bPropertySet = $oObject->HasProperty( $oProperty->GetName() );
                        //App::Log("HasProperty(".$oProperty->GetName().") ? : ".(int)$oObject->HasProperty($oProperty->GetName()) );///


                        // Check whether PRIMARY KEY properties are set and skip them.
                        //if( $oProperty == $oIdProp ) continue;
                        //if( $oProperty->IsIdProperty() ) continue;
                        if( in_array($oProperty, $aoIdProps) ){
                                // Multi-property PRIMARY key and some is not set -> error
                                if( !$bPropertySet  &&  1 < Count($aoIdProps) ){
                                        $this->SetError("Multi-property PRIMARY - property [".$oProperty->GetName()."] not set! in ".__METHOD__." @ ".__LINE__);
                                        //App::Log("Error!");///
                                        return false;
                                }
                                // Skip ID column(s) - we don't want to set them as other properties
                                //App::Log("Skipping ID col [".$oProperty->GetName()."]");///
                                continue;
                        }

                        // Skip undefined properties - or set them to DEFAULT ???  ->  TODO
                        if( !$bPropertySet ){
                                //$xVal = $oProperty->GetDefault();
                                continue;
                        }

                        //App::Log("Adding col [".$oProperty->GetName()."]");///
                        $sPropColName = $oProperty->GetColName();
                        $xVal = $oObject->GetProperty( $sPropColName );
                        //if( $sPropColName == 'targeting_fee' ) App::Log("FDG: (".gettype($xVal).") $xVal, ".AdjustedPrintR($oProperty));///
                        if( null === $xVal && !$oProperty->GetColType()->IsNull() )
                                $xVal = $oProperty->GetColType()->GetDefault();
                        $asSqlParts[] = $sPropColName.'='.$oProperty->FormatForSql($xVal);
                }
                $saParts = implode(', ', $asSqlParts);
                //App::Log("\$saParts: ".$saParts);///

                // Multiple KEY and nothing set - error (we do not INSERT unset multi-column rows )
                // So other way:  Don't try UPDATE if no other property set.
                if( 0 == Count($asSqlParts) &&  1 < Count($aoIdProps) ){
                        //$this->SetError("Multiple KEY and no normal properties set. in ".__METHOD__." @ ".__LINE__);); return false;
                        $bDoUpdate = false;
                        $bZeroUpdatedRows = true;
                }




                // Whether to add the object into the Object Pool.
                // Generally, add if the object was created; don't overwrite we are only saving existing object.
                $bAddObjectIntoPool = true;
                $bOverwriteObjectInPool = false;

                // If we are saving single-prop PRIMARY KEYed object and it is set to null, we should inser new.
                //App::Log("ID: (".gettype($oObject->GetId()).") ".$oObject->GetId());///
                if( 1 == $oClass->GetIdPropertiesCount() && (null === $oObject->GetId()) ){
                        $bDoUpdate = false;
                }

                // Multi-property PRIMARY key and some is not set -> error
                //if( 1 < $oClass->GetIdPropertiesCount() ){ } // Done above

                // UPDATE
                //if( null !== $oObject->GetId() )
                if( $bDoUpdate ){
                        // Create SQL query
                        //$sSQL = "UPDATE $sTable SET %s WHERE $sColId = ".$sIdForSql;  //x  Single-ID way
                        $sSQL = "UPDATE $sTable SET %s WHERE $saxIdsCond";
                        $sSQL = sprintf($sSQL, $saParts );
                        //echo "<div>$sSQL</div>";///

                        // Perform SQL query
                        $oRet = $this->GetDB()->Execute($sSQL);
                        //echo $oRet->GetError();///
                        if( !($oRet->IsOK()) ){ $this->SetError($oRet->GetError()); return false; }
                        if( 0 < $oRet->NumRows() ){
                                $this->AddObjectIntoPool( $oObject );
                                return true;
                        }
                        $bZeroUpdatedRows = true;
                        /*// If commented, tries to INSERT. If not, checks here for the row existence first.
                        // No rows affected - did we UPDATE with no changes, or the row was not found?
                        $sSQL = "SELECT COUNT(*) FROM $sTable WHERE $sColId = ".$sIdForSql;
                        //$oRet = $this->GetDB()->Execute($sSQL);
                        //if( !($oRet->IsOK()) ){ $this->SetError($oRet->GetError()); return false; }
                        //if( 0 != $oRet->GetCell(0,0) ){
                        $sVal = $oRet->SelectCell($sSQL, 0,0);
                        if( null === $sVal ){ $this->SetError("Error in SelectCell: [$sSQL]"); return false; }
                        if( 0 != $oRet->GetCell(0,0) ){
                                $this->AddObjectIntoPool( $oObject );
                                return true;
                        }/**/
                }

                // Row with ID not found  or  object ID is NULL -> INSERT
                do{
                        //$sSQL = "INSERT INTO $sTable SET $sColId = $sIdForSql, ".$saParts;
                        $sSQL = "INSERT INTO $sTable SET $saxIds ";
                        if( $saParts ) $sSQL .= ", ".$saParts;
                        //echo "<div>$sSQL</div>";///


                        // Perform SQL query
                        //echo "\nSelectMode: ".(int)$this->GetDB()->GetSelectMode();///
                        $oRet = $this->GetDB()->Execute($sSQL);

                        // If we did UPDATE with no changes,
                        if($bZeroUpdatedRows){
                                // And this insert caused error 1062, then it's OK - the row just existed before.
                                if( !$oRet || (!$oRet->IsOK() && $oRet->GetErrno() == 1062 ) ){
                                        break;
                                }
                        }
                        /*/ Other way:
                        // If INSERT caused an error:
                        if( !$oRet || !$oRet->IsOK() ){
                                // If we did not UPDATE with no changes  OR   the error is DUPLICATE ENTRY
                                if(!$bZeroUpdatedRows || ( !$oRet || $oRet->GetErrno() == 1062 ) )
                                        $this->SetError("INSERT failed. ".($oRet ? '['.$oRet->GetError().']' : '')); return false;
                        }/* Too complex */
                        if( 0 == $oRet->NumRows() ){ $this->SetError("INSERT affected no rows. SQL: [".$sSQL."] Errno: ".$oRet->GetErrno()); return false; }
                        $oObject->SetId( $this->GetDB()->GetLastInsertId() );

                        // We created a new object, thus if any of the same ID is in the pool, overwrite.
                        $bOverwriteObjectInPool = true;

                }while(false);

                // If asked, put object into the bool. If already there, it's overwritten.
                if( $bAddObjectIntoPool )
                        $this->AddObjectIntoPool( $oObject, $bOverwriteObjectInPool );
                return true;

        }// LoadObjectById($oClass, $iID)





        /**<***********************************************************************>
        *  Creates a cDwOpClass object copy.                                       *
        ***************************************************************************/
        function CreateClassCopy( $sName, $xClass ){

                // The second parameter can be either cDwOpClass object or a class name
                if( is_string($xClass) ){
                        $sClassName = $xClass;
                        $xClass = $this->GetClassFromDictionary($sClassName);
                        if( !is_object($xClass) ){
                                $this->SetError("Class [$sClassName] not found in dictionary.");
                                return false;
                        }
                }else{
                        $sClassName = $xClass->GetName();
                }

                // Make a copy
                $oClass = clone $xClass;
                $oClass->SetName( $sName );
                $this->AddClassIntoDictionary($oClass);
                return $oClass;
        }


        /**<***********************************************************************>
        *  Creates a cDwOpClass using it's definition from database.               *
        ***************************************************************************/
        function CreateClassByTable( $sName, $sTable ){

                do{
                        $oClass = new cDwOpClass( $sName, $sTable );

                        // Load columns info
                        $sSQL = "SHOW COLUMNS FROM $sTable";
                        $oRes = $this->GetDB()->Select($sSQL);
                        if( !$oRes || !$oRes->IsOK() ){ $this->SetError("SQL error [".$oRes->GetError()."] SQL: [$sSQL]"); return null; }

                        // For each row (which represent columns in the table)
                        while( $a = $oRes->FetchRow(MYSQL_ASSOC) ){
                                $oColObject = $this->CreateColumnTypeObject( $a['Type'], $a['Null']=='YES', $a['Default'] );
                                if( null === $oColObject ){
                                        $this->SetError("Table column [$sTable.".$a['Field']."]: ".$this->GetError()." in ".__METHOD__." @ ".__LINE__);
                                        //App::Log($this->GetError());///
                                        $oClass = null; break;
                                }
                                $oProp = $oClass->AddProperty($a['Field'], $oColObject );
                                $oProp->SetNull($a['Null']=='YES');

                                if( 'PRI' == $a['Key'] ){ $oClass->AddIdProperty($oProp); }

                                $oProp->SetUnique( 'PRI' == $a['Key']  ||  'UNI' == $a['Key'] );
                        }
                        $oRes->FreeResult();

                        if($oClass)
                                $this->AddClassIntoDictionary($oClass);

                }while(false);
                return $oClass;
        }


        //$oClass = $oOP->ExtendClassByTable('cAdplazeAdPage_Coupon', 'cAdplazeAdPage', 'ap_adpages_coupons');/**/
        /**<***********************************************************************>
        *  Extends an existings cDwOpClass with columns from a table.              *
        ***************************************************************************/
        function ExtendClassByTable($sClassNameNew, $sClassNameOld, $sTable){

                // Old Class object
                $oClassOld = $this->ConvertAndCheckClass($sClassNameOld);
                if(!$oClassOld){ $this->SetError("!ConvertAndCheckClass($sClassNameOld)"); return null; }
                $sClassNameOld = $oClassOld->GetName();
                $aoOldIdProps = $oClassOld->GetIdProperties();

                //$oClass = $this->CreateClassCopy($sClassNameNew, $sClassNameOld);
                //$oClass = $this->CreateClassByTable($sClassNameNew, $sTable);
                $oClass = new cDwOpClass( $sClassNameNew, $sTable );
                $oClass->_SetParentClass( $oClassOld );

                // Load columns info
                $sSQL = "SHOW COLUMNS FROM $sTable";
                $oRes = $this->GetDB()->Select($sSQL);
                if( !$oRes || !$oRes->IsOK() ){ $this->SetError("SQL error [".$oRes->GetError()."] SQL: [$sSQL]"); return null; }

                // --- Add the Properties to the Class --- //

                // Temp array to store matched id properties //
                $aoMatchedIdProps = Array();

                // For each row (which represent columns in the table)
                while( $a = $oRes->FetchRow(MYSQL_ASSOC) ){
                        $oColObject = $this->CreateColumnTypeObject( $a['Type'], $a['Null']=='YES', $a['Default'] );
                        if( null === $oColObject ){
                                $this->SetError("Table column [$sTable.".$a['Field']."]: ".$this->GetError()." in ".__METHOD__." @ ".__LINE__);
                                $oClass = null; break;
                        }
                        $oProp = $oClass->AddProperty($a['Field'], $oColObject );
                        $oProp->SetNull($a['Null']=='YES');

                        // ID property
                        if( 'PRI' == $a['Key'] ){
                                // Check whether the old Class has the same ID Property
                                if( !$oClassOld->HasIdProperty($oProp->GetName()) ){
                                        $this->SetError("Table column [$sTable.".$a['Field']."]: Extended Class [$sClassNameOld] doesn't have the ID column [".$oProp->GetName()."] in ".__METHOD__." @ ".__LINE__);
                                        $oClass = null; break;
                                }
                                $oClass->AddIdProperty($oProp); // To se tam dostane z CreateClassByTable()
                                // Add this old Class' Property to the list of matched ID Properties
                                $oOldClass_IdProp = $oClassOld->GetProperty($oProp->GetName());
                                $aoMatchedIdProps[] = $oOldClass_IdProp;
                        }

                        $oProp->SetUnique( 'PRI' == $a['Key']  ||  'UNI' == $a['Key'] );
                }
                $oRes->FreeResult();

                // Check whether all ID Properties of old Class were matched
                foreach( $oClassOld->GetIdProperties() as $oOldClass_IdProp){
                        if( !in_array($oOldClass_IdProp, $aoMatchedIdProps) ){
                                $this->SetError("Extended Class [$sClassNameOld] doesn't have the ID column [".$oOldClass_IdProp->GetName()."] in ".__METHOD__." @ ".__LINE__);
                                $oClass = null; break;
                        }
                }

                if($oClass) $this->AddClassIntoDictionary($oClass);
                return $oClass;

        }// cDwObjectPersistence::ExtendClassByTable()



        /**<**********************************************************************************>
        *  Returns cDwOpColumnType object according to the definition in $sTypeString.        *
        **************************************************************************************/
        function CreateColumnTypeObject($sTypeString, $bNull, $sDefault){
                $oRet = null;

                //$sTypeName = substr( $sTypeString, 0, strpos($sTypeString, '(') );
                //if( !ereg( '(.*)(\\((.*)\\))?(.*)?', $sTypeString, $asParts ) ) return $oRet;
                //$asParts = array_map('strtoupper', array_map('trim', $asParts) );
                //echo "<pre>\$asParts: ".AdjustedPrintR($asParts)."</pre>";///
                // list($sTypeName, $sData, $sUnsigned) = $asParts;
                $sTypeName = strtoupper(trim(strtok($sTypeString, '(') ));
                $sData     =           (trim(strtok(')') ));
                $sUnsigned = strtoupper(trim(strtok('')  ));
                //echo "<pre>$sTypeName, $sData, $sUnsigned</pre>";///

                $sClassName = 'cDwOpColumnType_';

                /*switch($sTypeName){
                        case 'VARCHAR':
                        case '': $sClassName = '_'.$sTypeName;  break;
                }/**/

                $saImplementedColumnTypes = DWOP_IMPLEMENTED_COLUMN_TYPES;
                $asImplementedTypes = array_map('trim', explode(',', $saImplementedColumnTypes));
                if( !in_array( $sTypeName, $asImplementedTypes) ){ $this->SetError("Column type [$sTypeName] not implemented."); return null; }

                $sClassName .= $sTypeName;

                if( $sUnsigned == 'UNSIGNED' )
                        $sClassName .= '_UNSIGNED';

                if(!class_exists($sClassName)){ $this->SetError("Undefined column class [$sClassName]."); return null; }

                switch($sTypeName){
                        case 'ENUM': case 'SET':
                                $oRet = new $sClassName($sData); break;
                        case 'CHAR': case 'VARCHAR':
                                $oRet = new $sClassName((int)$sData); break;
                        default:
                                $oRet = new $sClassName(); break;
                }
                if( $oRet ){
                        $oRet->SetDefault($sDefault);
                        $oRet->SetNull($bNull);
                }


                return $oRet;

        }// cDwObjectPersistence::CreateColumnTypeObject()


        /**<***********************************************************************>
        *   Sets M : N relation between two classes using third class as a glue.   *
        ***************************************************************************/
        function SetMnRelation($oClass1, $oClassGlue, $oClass2){
                $bSucc = false;
                do{

                        // Convert class names to Class objects and check the existence of the class.
                        if( !($oClass1 = $this->ConvertAndCheckClass($oClass1)) ){ $this->SetError("!oClass1 in ".__METHOD__." @ ".__LINE__); break; }
                        if( !($oClass2 = $this->ConvertAndCheckClass($oClass2)) ){ $this->SetError("!oClass2 in ".__METHOD__." @ ".__LINE__); break; }
                        if( !($oClassGlue = $this->ConvertAndCheckClass($oClassGlue)) ){ $this->SetError("!oClassGlue in ".__METHOD__." @ ".__LINE__); break; }

                        // Check all classes whether suitable (PRIMARY KEYs etc.)
                        if( !$oClass1->IsSuitableForMnRelationSide() ){ $this->SetError("Class ".$oClass1->GetName()." not suitable for M:N side in ".__METHOD__." @ ".__LINE__); break; }
                        if( !$oClass2->IsSuitableForMnRelationSide() ){ $this->SetError("Class ".$oClass2->GetName()." not suitable for M:N side in ".__METHOD__." @ ".__LINE__); break; }
                        if( !$oClassGlue->IsSuitableForMnRelationGlue() ){ $this->SetError("Class ".$oClassGlue->GetName()." not suitable for M:N glue in ".__METHOD__." @ ".__LINE__); break; }

                        // Check whether the types are the same
                        $oIdProp1 = $oClass1->GetIdProperty();
                        $oIdProp2 = $oClass1->GetIdProperty();
                        $aoIdProps = $oClassGlue->GetIdProperties();

                        if( $oIdProp1->GetColType()->GetType() != $aoIdProps[0]->GetColType()->GetType() ){
                                $this->SetError('Property types mismatch: "'.$oIdProp1->GetName().'" and "'.$aoIdProps[0]->GetName().' in '.__METHOD__.' @ '.__LINE__); break; }
                        if( $oIdProp2->GetColType()->GetType() != $aoIdProps[1]->GetColType()->GetType() ){
                                $this->SetError('Property types mismatch: "'.$oIdProp2->GetName().'" and "'.$aoIdProps[1]->GetName().' in '.__METHOD__.' @ '.__LINE__); break; }

                        //$oIdProp1->CompareTo($aoIdProps[0]); // TODO

                        $b = $oClassGlue->SetMnRelationGlue($oClass1, $oClass2, $aoIdProps[0], $aoIdProps[1]);
                        if( !$b ){ $this->SetError("!\$oClassGlue->SetMnRelationGlue @ ".__LINE__); break; }

                        $bSucc = true;
                }while(false);
                return $bSucc;
        }


        /**<*********************************************************************************************>
  *                                                                                                *
                @param $oObject1 is an object of which related objects of $oClass2 should be returned.
                @param $oClass2 - can be:
                        (object)     - the Class will be found by it's class name.
                        (cDwOpClass) Class object - that's what we need
                        (string)     class name   - the Class will be found in the dictionary.
  *************************************************************************************************/
  function LoadObjectsByMnRelation($oObject, $oClassGlue, $oClass2, $xId=null){
        $bSucc = false;
        do{

                        // Convert class names to Class objects and check the existence of the class.
                        if( !($oClass1 = $this->ConvertAndCheckClass($oObject)) ){ $this->SetError("!oClass1 @ ".__LINE__); break; }
                        if( !($oClass2 = $this->ConvertAndCheckClass($oClass2)) ){ $this->SetError("!oClass2 @ ".__LINE__); break; }
                        if( !($oClassGlue = $this->ConvertAndCheckClass($oClassGlue)) ){ $this->SetError("!oClassGlue @ ".__LINE__); break; }

                        if( !$oClassGlue->IsMnRelationGlue() ){ $this->SetError($oClassGlue->GetName()." is not a M : N relation glue in ".__METHOD__." @ ".__LINE__); break; }

                        // Table names
                        $sTable1 = $oClass1->GetTable();
                        $sTable2 = $oClass2->GetTable();
                        $sTableGlue = $oClassGlue->GetTable();

                        // Column names
                        $oClass1IdProp = $oClass1->GetIdProperty();
                        $sIdCol1 = $oClass1IdProp->GetColName();

                        $sIdCol2 = $oClass2->GetIdProperty()->GetColName();
                        $sGluePropFrom = $oClassGlue->oMnRelationProp1->GetColName();
                        $sGluePropTo   = $oClassGlue->oMnRelationProp2->GetColName();

                        if( is_object($oObject) )
                                $xId = $oClass1IdProp->FormatForSql( $oObject->GetId() );

                        //if( !$xId ){ $this->SetError("Bad ID [$xId] in ".__METHOD__." @ ".__LINE__); break; }


                        // Create SQL
                        $sSQL = "
                        -- Vypise reklamy a k nim kanaly, ktere jsou s reklamou spojeny.
                        SELECT table_to.*
                        FROM $sTable1 AS table_from
                        INNER JOIN $sTableGlue AS glue ON table_from.$sIdCol1 = glue.$sGluePropFrom
                        LEFT JOIN $sTable2 AS table_to ON glue.$sGluePropTo = table_to.$sIdCol2
                        WHERE table_from.$sIdCol1 = $xId
                        ;";

                        $aoObjects = $this->LoadObjectsBySql($oClass2, $sSQL);

                        $bSucc = true;
        }while(false);

        return $aoObjects;
        }




}// cDwObjectPersistence




/**<***********************************************************************>
*  cDwOpClass                                                              *
***************************************************************************/
class cDwOpClass {

        var $sName;
        function GetName()      { return $this->sName; }
        function SetName($sName){ $this->sName = $sName; }

        var $sTable;
        function GetTable()       { return $this->sTable; }
        function SetTable($sTable){ $this->sTable = $sTable; }

        var $oParentClass = null;
        function GetParentClass()             { return $this->oParentClass; }
        function _SetParentClass($oParentClass){ $this->oParentClass = $oParentClass; }



        // -- Constructor -- //
        function cDwOpClass( $sName, $sTable ){ $this->sName = $sName; $this->SetTable($sTable); }

        // Properties //
        var $aoProperties;
        /** @returns cDwOpProperty[] array of this class' properties. */
        function GetProperties()             { return $this->aoProperties; }
        /** @returns cDwOpProperty this class' property of given name or NULL if the class does not have such. */
        function GetProperty($sColName){
                if( isset($this->aoProperties[$sColName]) )
                        $oRet = $this->aoProperties[$sColName];
                else $oRet = null;
                return $oRet;
        }
        function AddPropertyObject($oProperty){ return $this->aoProperties[$oProperty->GetColName()] = $oProperty; }
        function AddProperty($sColName, $oColType){
                $oProp = new cDwOpProperty($sColName, $oColType);
                $this->aoProperties[$sColName] = $oProp;
                return $oProp;
        }
        function RemProperty($sColName){
                if( !isset($this->aoProperties[$sColName]) ) return false;
                unset($this->aoProperties[$sColName]);
                return true;
        }
        function GetNonKeyProperties(){
                $aoProps = Array();
                foreach( $this->aoProperties as $oProp ){
                        //if( $oProp->IsKeyProp() ) continue;
                        $aoProps[] = $oProp;
                }
                return $aoProps;
        }



        // --- M : N relation stuff --- //


        // -- Is this class M : N relation "glue"? -- //
        var $bMnRelationGlue = false;
        function IsMnRelationGlue()            { return $this->bMnRelationGlue; }
        function UnsetMnRelationGlue(){ $this->bMnRelationGlue = false; }

        var $oMnRelationClass1 = null;
        var $oMnRelationClass2 = null;
        function GetMnRelationClass1()                  { return $this->oMnRelationClass1; }
        function GetMnRelationClass2()                  { return $this->oMnRelationClass2; }
        function GetMnRelationOtherClass($oClass){
                if( $oClass === $this->oMnRelationClass1 ) return $this->oMnRelationClass2;
                if( $oClass === $this->oMnRelationClass2 ) return $this->oMnRelationClass1;
                return null;
        }

        var $oMnRelationProp1 = null;
        var $oMnRelationProp2 = null;
        function GetMnRelationProp1()                 { return $this->oMnRelationProp1; }
        function GetMnRelationProp2()                 { return $this->oMnRelationProp2; }
        function GetMnRelationOtherProp($oProp){
                if( $oProp === $this->oMnRelationProp1 ) return $this->oMnRelationProp2;
                if( $oProp === $this->oMnRelationProp2 ) return $this->oMnRelationProp1;
                return null;
        }
        function GetMnRelationPropForClass($oClass){
                //App::Log($oClass->GetName()." ==? ".$this->oMnRelationClass1->GetName());///
                //App::Log($oClass->GetName()." ==? ".$this->oMnRelationClass2->GetName());///
                //if( $oClass === $this->oMnRelationClass1 ) return $this->oMnRelationProp1;
                //if( $oClass === $this->oMnRelationClass2 ) return $this->oMnRelationProp2;
                $asParents = class_parents($oClass->GetName());
                array_unshift($asParents, $oClass->GetName());
                //App::Log(implode(',',$asParents)." <==? ".$this->oMnRelationClass1->GetName());///
                //App::Log(implode(',',$asParents)." <==? ".$this->oMnRelationClass2->GetName());///
                if( in_array($this->oMnRelationClass1->GetName(), $asParents)){ return $this->oMnRelationProp1; }
                if( in_array($this->oMnRelationClass2->GetName(), $asParents)){ return $this->oMnRelationProp2; }
                return null;
        }




        function IsSuitableForMnRelationGlue(){ return Count($this->aoIdProperties) == 2; }
        function IsSuitableForMnRelationSide(){ return Count($this->aoIdProperties) == 1; }

        function SetMnRelationGlue($oClass1, $oClass2, $oProp1, $oProp2){
                $this->bMnRelationGlue = false;
                do{
                        // Check everything and return false on failure.
                        if( !$this->IsSuitableForMnRelationGlue() ) return false;
                        if( !$oClass1 || !($oClass1 instanceof cDwOpClass) ) return false;
                        if( !$oClass2 || !($oClass2 instanceof cDwOpClass) ) return false;

                        if( 1 != $oClass1->GetIdPropertiesCount() ) return false;
                        if( 1 != $oClass2->GetIdPropertiesCount() ) return false;

                        if( is_string($oProp1) ) $oProp1 = $this->GetProperty($oProp1);
                        if( is_string($oProp2) ) $oProp2 = $this->GetProperty($oProp2);

                        if( null == $oProp1  ||  !($oProp1 instanceof cDwOpProperty) ) return false;
                        if( null == $oProp2  ||  !($oProp2 instanceof cDwOpProperty) ) return false;

                        // Seems to be ok, set the relation for this Class... (we have to set the sides, too)
                        $this->oMnRelationClass1 = $oClass1;
                        $this->oMnRelationClass2 = $oClass2;
                        $this->oMnRelationProp1 = $oProp1;
                        $this->oMnRelationProp2 = $oProp2;

                        $this->bMnRelationGlue = true;

                }while(false);
                return $this->bMnRelationGlue;
        }



        // --- ID properties --- //

        // ID property //
        //var $oIdProperty;
        function GetIdProperty()            { reset($this->aoIdProperties); return current($this->aoIdProperties); }
        function SetIdProperty($oIdProperty){
                if(!$oIdProperty){ $this->aoIdProperties = Array(); return; }
                $this->aoIdProperties = Array( $oIdProperty );
                $oIdProperty->SetPrimaryKeyPart(true);
        }

        var $aoIdProperties = Array();
        function GetIdProperties()            { return $this->aoIdProperties; }

        function GetIdPropertiesCount()       { return Count($this->aoIdProperties); }


        /**<**********************************************************************************>
        *  @returns whether Class has given ID property  or ID property of given name         *
        **************************************************************************************/
        function HasIdProperty($xProp){
                if( is_string($xProp) ) $xProp = $this->GetProperty($xProp);
                if( !$xProp ) return false;
                if( in_array($xProp, $this->aoIdProperties) ) return true;
                return false;
        }

        function AddIdProperty($oProperty){ array_push($this->aoIdProperties, $oProperty); $oProperty->SetPrimaryKeyPart(true); }

        /**  @returns string Coma separated list of ID (PRIMARY KEY) values of the given object. */
        function GetIdPropertiesSql($oObject, $sGlue=', '){
                $asPropSqls = Array();
                foreach( $this->GetIdProperties() as $oProp ){
                        $asPropSqls[] = $oProp->GetColName() . " = " . $oProp->FormatForSql( $oObject->GetProperty($oProp->GetName()) );
                }
                $saPropSqls = implode($sGlue, $asPropSqls);
                //if( '' != $saPropSqls ) $saPropSqls = " WHERE $saPropSqls "; // We don't want this for ID - can be "ON ...", e.g.
                return $saPropSqls;
        }

        /**  @returns string Formatted list of ID (PRIMARY KEY) values of the given object. */
        function GetObjectMultiIdString($oObject, $iMode=DWOP_VALUE_DUMP_MODE__SQL){
                switch($iMode){
                        default: trigger_error("Bad param 2 - must be one of DWOP_VALUE_DUMP_MODE__* constants"); return null; break;
                        case DWOP_VALUE_DUMP_MODE__SQL:            $sGlue=', '; $sEq=' = '; $fFunc='sql'; break;
                        case DWOP_VALUE_DUMP_MODE__SQL_SHORT:      $sGlue=',';  $sEq='=';   $fFunc='sql'; break;
                        case DWOP_VALUE_DUMP_MODE__SQL_VALUES:     $sGlue=',';  $sEq='';    $fFunc='sql'; break;
                        case DWOP_VALUE_DUMP_MODE__ESCAPED_VALUES: $sGlue='\''; $sEq='';    $fFunc='addslashes'; break; // Shortest unique
                        case DWOP_VALUE_DUMP_MODE__COMA:           $sGlue=', '; $sEq='';    $fFunc=''; break;
                }
                $asPropSqls = Array();
                foreach( $this->GetIdProperties() as $oProp ){
                        $s = '';
                        if($sEq) $s .= $oProp->GetColName() . $sEq; // <col name> =

                        $xVal = $oObject->GetProperty($oProp->GetName());
                        if(!$fFunc) ;
                        elseif( 'sql' == $fFunc )  $xVal = $oProp->FormatForSql( $xVal );
                        else/*if( function_exists($fFunc) ) */$xVal = $fFunc($xVal);
                        //else ;
                        $s .= $xVal;

                        $asPropSqls[] = $s;
                }
                $saPropSqls = implode($sGlue, $asPropSqls);
                //if( '' != $saPropSqls ) $saPropSqls = " WHERE $saPropSqls "; // We don't want this for ID - can be "ON ...", e.g.
                return $saPropSqls;
        }


        /**  Sets ID properties.
             @param $xProperties can be:
                                        - a Property name
                            - coma separated list of Property names
                                        - array of Property objects
        */
        function SetIdProperties($xProperties){

                // If $xProperties param is string, convert to an array
                if( is_string($xProperties) )
                        $xProperties = array_map('trim', explode(',', $xProperties));

                // Now the $xProperties must be an array.
                if( !is_array($xProperties) ){
                        $this->aoIdProperties = Array();
                        return false;
                }



                // Go through all properties in the array and transform them to cDwOpIdingInfo objects.
                $aoIdProperties = Array();
                foreach( $xProperties as $xProperty ){
                        $bDesc = false;
                        // If the array item is a string, convert to a property object.
                        if( is_string($xProperty) )
                                $xProperty = $this->GetProperty($xProperty);

                        // If the $xProperty is not a cDwOpProperty object, move on.
                        if( null === $xProperty || !($xProperty instanceof cDwOpProperty) )
                                continue;

                        $xProperty->SetPrimaryKeyPart(true);
                        $aoIdProperties[] = $xProperty;
                }

                $this->aoIdProperties = $aoIdProperties;
                //echo "<pre>".AdjustedPrintR($aoOrderProperties)."</pre>";///

        }// SetIdProperties($xProperties)




        // --- Order properties --- //
        var $aoOrderProperties = Array();
        function GetOrderProperties()                  { return $this->aoOrderProperties; }


        /**  @returns string Coma separated list of Order Properties (column names). */
        function GetOrderPropertiesSql(){
                $asPropSqls = Array();
                foreach( $this->aoOrderProperties as $oOrderingInfo ){
                        $asPropSqls[] = $oOrderingInfo->oProperty->GetColName() . ($oOrderingInfo->bDesc ? ' DESC' : ' ASC');
                }
                $saPropSqls = implode(', ', $asPropSqls);
                if( '' != $saPropSqls ) $saPropSqls = " ORDER BY $saPropSqls ";
                return $saPropSqls;
        }


        /**<*************************************************************************>
        *  Sets the ordering information for this class. Previous info is replaced.  *
           @param $xProperties can be a coma separated list of Property names,
                                            an array of Property names, or
                                            an array of Property objects.
        *****************************************************************************/
        function SetOrderProperties($xProperties){
                // If $xProperties param is string, convert to an array
                if( is_string($xProperties) )
                        $xProperties = array_map('trim', explode(',', $xProperties));

                // Now the $xProperties must be an array.
                if( !is_array($xProperties) )
                        return false;

                // Go through all properties in the array and transform them to cDwOpOrderingInfo objects.
                $aoOrderProperties = Array();
                foreach( $xProperties as $xProperty ){
                        $bDesc = false;
                        // If the array item is a string, convert to a property object.
                        if( is_string($xProperty) ){
                                if( '' == $xProperty ) continue;
                                if( $xProperty[0] == '+' || $xProperty[0] == '-' ){
                                        $bDesc = ( $xProperty[0] == '-' );
                                        $xProperty = substr($xProperty, 1);
                                }
                                $xProperty = $this->GetProperty($xProperty);
                        }

                        // Instance of cDwOpProperty - convert to cDwOpOrderingInfo
                        if( $xProperty instanceof cDwOpProperty ){
                                $xProperty = new cDwOpOrderingInfo($xProperty, $bDesc);
                        }

                        // If the $xProperty is not a cDwOpProperty object, move on.
                        if( null === $xProperty || !($xProperty instanceof cDwOpOrderingInfo) )
                                continue;

                        $aoOrderProperties[] = $xProperty;
                }

                $this->aoOrderProperties = $aoOrderProperties;
                //echo "<pre>".AdjustedPrintR($aoOrderProperties)."</pre>";///

        }// SetOrderProperties($xProperties)


}// class cDwOpClass






/**<***********************************************************************>
*  Holds information about ordering property - for cDwOpClass.             *
***************************************************************************/
class cDwOpOrderingInfo {
        var $oProperty = null;
        var $bDesc = false;
        function cDwOpOrderingInfo($oProperty, $bDesc=false){
                $this->oProperty = $oProperty;
                $this->bDesc = $bDesc;
        }
}




/**<***********************************************************************>
*  cDwOpProperty                                                           *
***************************************************************************/
class cDwOpProperty {

        /**  Name - currently we use the same as a Property name and database column name.  */
        //var $sName = null;
        function GetName()         { return $this->sColName; }
        function SetName($sColName){ $this->sColName = $sColName; }

        /**  Column name - currently used also as the name of the property  */
        var $sColName = null;
        function GetColName()         { return $this->sColName; }
        function SetColName($sColName){ $this->sColName = $sColName; }

        /**  Column type - an object derived from cDwOpColumnType class.  */
        var $oColType = null;
        /** @returns cDwOpColumnType column type representing this property. */
        function GetColType()         { return $this->oColType; }
        function SetColType($oColType){ $this->oColType = $oColType; }

        /**  Whether the property is a part of a PRIMARY KEY for this Class.  */
        var $bPrimaryKeyPart = false;
        function IsPrimaryKeyPart()                { return $this->bPrimaryKeyPart; }
        function SetPrimaryKeyPart($bPrimaryKeyPart){ $this->bPrimaryKeyPart = $bPrimaryKeyPart; }

        /**  Whether the property is a FOREIGN KEY.  */
        var $bForeignKey = false;
        function IsForeignKey()            { return $this->bForeignKey; }
        function SetForeignKey($bForeignKey){ $this->bForeignKey = $bForeignKey; }

        /**  Whether the property is unique across all objects of this Class.  */
        var $bUnique = false;
        function IsUnique()         { return $this->bUnique; }
        function SetUnique($bUnique){ $this->bUnique = $bUnique; }

        /**  Whether this property can be NULL.  */
        var $bCanBeNull = false;
        function IsNull()            { return $this->bCanBeNull; }
        function SetNull($bCanBeNull){ $this->bCanBeNull = $bCanBeNull; }

        function IsKey(){
                return $this->IsPrimaryKeyPart() || $this->IsForeignKey() || $this->IsUnique();
        }


        // -- Constructor -- //
        function cDwOpProperty($sColName, $oColType, $bUnique=false){
                $this->SetColName($sColName);
                $this->SetColType($oColType);
                $this->SetUnique($bUnique);
        }

        /**  @returns SQL literal representing a value $xVal if it was held by this property. */
        function FormatForSql($xVal){
                if( null === $xVal  &&  $this->GetColType()->IsNull() )
                        return 'NULL';
                //echo "\n".CallInfo()."; Val: ".$xVal." ColType:".$this->GetColType()->GetName();///
                return $this->GetColType()->FormatForSql($xVal);
        }

}// class cDwOpProperty








/**<***********************************************************************>
*  cDwOpColumnType                                                         *
***************************************************************************/
abstract class cDwOpColumnType {

        var $sName;
        function GetName()      { return $this->sName; }
        function SetName($sName){ $this->sName = $sName; }

        var $iType;
        /** @returns  one of type constants: DWOP_TYPE_STR,DWOP_TYPE_NUM,DWOP_TYPE_BOOL,DWOP_TYPE_ENUM,DWOP_TYPE_SET, DWOP_TYPE_REAL */
        function GetType()      { return $this->iType; }
        /** @param iType  one of type constants: DWOP_TYPE_STR,DWOP_TYPE_NUM,DWOP_TYPE_BOOL,DWOP_TYPE_ENUM,DWOP_TYPE_SET, DWOP_TYPE_REAL */
        function SetType($iType){
                if(!in_array($iType, Array(DWOP_TYPE_STR,DWOP_TYPE_NUM,DWOP_TYPE_BOOL,DWOP_TYPE_ENUM,DWOP_TYPE_SET, DWOP_TYPE_REAL))){
                        user_error("cDwOpColumnType::SetType(): Param \$iType must be some of DWOP_TYPE_* constants.");
                }
                $this->iType = $iType;
        }

        // Can be NULL?
        var $bCanBeNull = true;
        function IsNull()            { return $this->bCanBeNull; }
        function SetNull($bCanBeNull){ $this->bCanBeNull = $bCanBeNull; }

        // Default value
        var $xDefault;
        function GetDefault()         { return $this->xDefault; }
        function SetDefault($xDefault){
                $bRet = true;
                if( (null === $xDefault && !$this->IsNull() )  // Default value being set is NULL and must not be null
                 || ( !$this->CheckValue($xDefault) )          // or  the default value is not allowed for this type,
                ){
                        $xDefault = $this->GetDefaultDefault();      // then set the default to the default default,
                        $bRet = false;                               // and indicate failure by returning false.
                }
                $this->xDefault = $xDefault;
                return $bRet;
        }
        //function GetDefaultDefault(){ user_error(__METHOD__.' must be overriden.'); return null; }
        function GetDefaultDefault(){
                switch( $this->GetType() ){
                        case DWOP_TYPE_STR:  return '';  break;
                        case DWOP_TYPE_NUM:  return 0;   break;
                        case DWOP_TYPE_BOOL: return false; break;
                        case DWOP_TYPE_ENUM: return '';  break;
                        case DWOP_TYPE_SET:  return '';  break;
                        case DWOP_TYPE_REAL: return 0.0; break;
                }
        }


        // Aditional data - possible values for ENUM and SET, etc
        //var $aData;



        /**<***********************************************************************>
        *  Constructor                                                             *
        ***************************************************************************/
        function cDwOpColumnType($iFlags=0, $xDefault=null){

                // Flags
                if( $iFlags & DWOP_NOT_NULL )
                        $this->SetNull(false);
                //if( $iFlags & DWOP_BINARY )
                //      $this->SetBinary(true);

                // Default value
                $this->SetDefault( $xDefault );

        }// cDwOpColumnType::cDwOpColumnType()


        // Useless
        /*function cDwOpColumnType($sName, $iType, $aData){
                $this->SetName($sName);
                $this->SetType($iType);
                $this->aData = $aData;
        }// cDwOpColumnType::cDwOpColumnType()
        /**/


        /**<***********************************************************************>
        *  @returns the value in $xVal in the format suitable for SQL.             *
        ***************************************************************************/
        function FormatForSql($xVal){
                //App::Log( CallInfo()."     \$xVal: (".gettype($xVal).") $xVal" );
                $sRet = null;

                if( null === $xVal ){
                        if( $this->IsNull() )
                                $sRet = 'NULL';
                        else
                                $sRet = 'nULl';
                }
                else switch($this->GetType()){
                        case DWOP_TYPE_STR:  $sRet = asq((string)$xVal); break;
                        case DWOP_TYPE_NUM:  $sRet = (double)$xVal; /*echo "<br>XXX".CallInfo(-2)."<br>".CallInfo(-3);*/ break;
                        case DWOP_TYPE_BOOL: $sRet = (integer)(boolean)$xRet; break;
                        case DWOP_TYPE_ENUM: $sRet = asq((string)$xVal); break;
                        case DWOP_TYPE_SET:  $sRet = asq((string)$xVal); break;
                        case DWOP_TYPE_REAL: $sRet = (double)$xVal; break;
                        //case DWOP_TYPE_:  $sRet = ; break;
                        default: trigger_error('Unknown value type! in '.__METHOD__.' @ '.__LINE__); $sRet = asq((string)$xVal); break;
                }
                //App::Log( CallInfo()."     \$sRet: $sRet" );
                return $sRet;
        }

        function ConvertFromResult($xVal){ return $xVal; }


        /**<***********************************************************************>
        *  Check whether the variable can fit in the column.                       *
        ***************************************************************************/
        function CheckValue($xVal /*, $oProperty*/){

                if( null === $xVal && !$this->IsNull() )
                        return false;

                $bFits = true;
                switch($this->GetType()){
                        case DWOP_TYPE_STR:  $bFits = is_string($xVal) || is_numeric($xVal); break;
                        case DWOP_TYPE_NUM:  $bFits = is_bool($xVal) || is_int($xVal); break;
                        case DWOP_TYPE_BOOL: $bFits = is_bool($xRet) || is_int($xVal); break;
                        //case DWOP_TYPE_ENUM: $bFits = is_string($xVal) /*&& in_array($xVal, $this->asMembers)/**/; break;
                        case DWOP_TYPE_SET:
                                $asMembers = array_map('trim', explode(',', $xVal));
                                foreach( $asMembers as $sMember ){
                                        if( !in_array($xVal, $this->aData) ){ $bFits = false; break; }
                                }
                        break;
                }
                return $bFits;
        }

}// class cDwOpColumnType






/**<***********************************************************************>
*  cDwOpColumnTypesRepository                                              *
   Skladiste casto pouzivanych typu sloupcu.
         Nema cenu sem davat stringy - lisi se delkou.
         Mozna je to uplne zbytecna trida...
***************************************************************************/
/*class cDwOpColumnTypesRepository {
        var $aoColumnTypes;
        function AddColumnType($sKey, &$oType){ $this->aoColumnTypes[$sKey] =& $oType; }


        function cDwOpColumnTypesRepository(){
                $this->aoColumnTypes = Array();

                $oType =& new cDwOpColumnType_INT();
                $this->AddColumnType('INT', $oType);
                $oType =& new cDwOpColumnType_INT_UNSIGNED();
                $this->AddColumnType('INT UNSIGNED', $oType);
                $oType =& new cDwOpColumnType_SMALLINT();
                $this->AddColumnType('SMALLINT', $oType);
                $oType =& new cDwOpColumnType_SMALLINT_UNSIGNED();
                $this->AddColumnType('SMALLINT UNSIGNED', $oType);


        }// cDwOpColumnTypesRepository()

        function &GetColumnType($sName){
                return isset( $this->aoColumnTypes[$sName] ) ? $this->aoColumnTypes[$sName] : null;
        }

}// class cDwOpColumnTypesRepository
/**/






/** ************************************************************************
*  Simple class that holds item's ID and a hashmap of parameters.          *
***************************************************************************/
abstract class DwFwIdAndProperties {

        function DwFwIdAndProperties($iId=null){
                $this->SetId($iId);
        }

        var $sId;
        function GetId()    { return $this->sId; }
        function SetId($sId){ $this->sId = $sId; }

        var $asParams = Array();
        function SetProperty($sName, $sValue){ $this->asParams[(string)$sName] = $sValue; }
        function GetProperty($sName){ return isset($this->asParams[(string)$sName]) ? $this->asParams[(string)$sName] : null; }
        //function HasProperty($sName){ return isset($this->asParams[(string)$sName]); }
        function HasProperty($sName){ return array_key_exists((string)$sName, $this->asParams); }
        function IsPropertySet($sName){ return $this->HasProperty($sName); }

        function GetProperties(){ return $this->asParams; }
        function SetProperties($asParams, $bOverwrite=true){
                foreach( $asParams as $sName => $sValue ){
                        if($bOverwrite || !isset($this->asParams[(string)$sName]))
                                $this->asParams[(string)$sName] = $sValue;
                }
        }

        function DwFwIdAndParams($sId=null){ $this->sId = $sId; }

}// class DwFwIdAndParams

Usage – model

/** ************************************************************************
*  Simple class that holds item's ID and a hashmap of parameters.          *
***************************************************************************/
abstract class DwFwIdAndProperties {

        function DwFwIdAndProperties($iId=null){
                $this->SetId($iId);
        }

        var $sId;
        function GetId()    { return $this->sId; }
        function SetId($sId){ $this->sId = $sId; }

        var $asParams = Array();
        function SetProperty($sName, $sValue){ $this->asParams[(string)$sName] = $sValue; }
        function GetProperty($sName){ return isset($this->asParams[(string)$sName]) ? $this->asParams[(string)$sName] : null; }
        //function HasProperty($sName){ return isset($this->asParams[(string)$sName]); }
        function HasProperty($sName){ return array_key_exists((string)$sName, $this->asParams); }
        function IsPropertySet($sName){ return $this->HasProperty($sName); }

        function GetProperties(){ return $this->asParams; }
        function SetProperties($asParams, $bOverwrite=true){
                foreach( $asParams as $sName => $sValue ){
                        if($bOverwrite || !isset($this->asParams[(string)$sName]))
                                $this->asParams[(string)$sName] = $sValue;
                }
        }

        function DwFwIdAndParams($sId=null){ $this->sId = $sId; }

}// class DwFwIdAndParams





/***************************************************************************
*  User                                                                    *
***************************************************************************/
class cObjectPersistenceTestClass_User extends DwFwIdAndProperties {
        function GetProperty($sName) {
                if( 'id' == $sName ) return $this->GetId();
                else                 return parent::GetProperty($sName);
        }
        function SetProperty($sName, $sValue){
                if( 'id' == $sName ) return $this->SetId($sValue);
                else                 return parent::SetProperty($sName, $sValue);
        }
}// class cObjectPersistenceTestClass_User

/***************************************************************************
*  Door                                                                    *
***************************************************************************/
class cObjectPersistence_TestClass_Door extends DwFwIdAndProperties {
        function GetProperty($sName) {
                if( 'id' == $sName ) return $this->GetId();
                else                 return parent::GetProperty($sName);
        }
        function SetProperty($sName, $sValue){
                if( 'id' == $sName ) return $this->SetId($sValue);
                else                 return parent::SetProperty($sName, $sValue);
        }
}// class cObjectPersistenceTestClass_User

/***************************************************************************
*  Key                                                                     *
***************************************************************************/
class cObjectPersistence_TestClass_Key extends DwFwIdAndProperties {
        function GetProperty($sName) {
                if( 'id' == $sName ) return $this->GetId();
                else                 return parent::GetProperty($sName);
        }
        function SetProperty($sName, $sValue){
                if( 'id' == $sName ) return $this->SetId($sValue);
                else                 return parent::SetProperty($sName, $sValue);
        }
}// class cObjectPersistenceTestClass_User

Usage – data manipulation

// Create DB object
$oDB = new cDBAccess_MySQL('localhost:3350', 'test', 'test', 'test', 'cp1250');
$oDB->SetSelectMode(CDBA_SELECT_RETURNS_CRESULT_ON_ERROR);
echo "Connect: ".///
        $oDB->Connect();
echo "<br/>\n";///

//$xVal = $oDB->SelectCell("SELECT NULL"); // Test what NULL looks like in PHP
//echo "\n".gettype($xVal)." ".ord($xVal); die();

// Create Object Persistence object
$oOP = new cDwObjectPersistence_DB($oDB);
//$oColTypes =& new cDwOpColumnTypesRepository();

Example SQL schema & data

CREATE TABLE `ap_users` (
  `id` int(10) unsigned NOT NULL auto_increment,
  `user` varchar(255) NOT NULL,
  `pass` varchar(40) default NULL,
  `fname` varchar(255) NOT NULL,
  `lname` varchar(255) NOT NULL,
  `addr` varchar(255) NOT NULL,
  `city` varchar(255) NOT NULL,
  `state` enum('good', 'bad') NOT NULL DEFAULT 'good',
  `psc` varchar(255) NOT NULL,
  `ctry` tinyint(3) unsigned NOT NULL,
  `ppal` varchar(255) default NULL
);
INSERT INTO ap_users (id, user) VALUES
  (10001, 'Ondra')
, (10002, 'Zdenek')
, (10003, 'Martin')
, (10004, 'Zuzka')
, (10005, 'David')
, (10006, 'Satan')
;

CREATE TABLE ap_doors (
        id INT UNSIGNED NOT NULL auto_increment PRIMARY KEY,
        txt VARCHAR(15)
);
INSERT INTO ap_doors VALUES (330, 'Nebe'), (118, 'Bazina'), (116, 'Peklo'), (110, 'Curaprox'), (222, 'Neznamo');

CREATE TABLE ap_keys (
        id_user INT UNSIGNED NOT NULL,
        id_door INT UNSIGNED NOT NULL,
        PRIMARY KEY pk( id_user, id_door )
);
INSERT INTO ap_keys VALUES
(10001, 330), (10001, 118), (10001, 110),    -- Ondra
(10002, 118), (10002, 116),                  -- Zdenek
(10003, 118), (10003, 330), (10003, 116),    -- Martin
(10004, 110), (10004, 330),                  -- Zuzka
(10006, 116), (10006, 118);                  -- Satan
$oClass = new cDwOpClass('cObjectPersistenceTestClass_User', 'ap_users');
//$oProp = $oClass->AddProperty('id', $oColTypes->GetColumnType('INT UNSIGNED'));
        $oProp = $oClass->AddProperty('id', new cDwOpColumnType_INT_UNSIGNED() );
        $oClass->SetIdProperty($oProp);
        $oProp = $oClass->AddProperty('user',  new cDwOpColumnType_VARCHAR(255, DWOP_NOT_NULL) );
        $oProp = $oClass->AddProperty('pass',  new cDwOpColumnType_VARCHAR(40,  DWOP_NOT_NULL) );
        $oProp = $oClass->AddProperty('fname', new cDwOpColumnType_VARCHAR(255, DWOP_NOT_NULL) );
        $oProp = $oClass->AddProperty('lname', new cDwOpColumnType_VARCHAR(255, DWOP_NOT_NULL) );
        $oProp = $oClass->AddProperty('addr',  new cDwOpColumnType_VARCHAR(255, DWOP_NOT_NULL) );
        $oProp = $oClass->AddProperty('city',  new cDwOpColumnType_VARCHAR(255, DWOP_NOT_NULL) );
        $oProp = $oClass->AddProperty('state', new cDwOpColumnType_VARCHAR(255, DWOP_NOT_NULL) );
        $oProp = $oClass->AddProperty('psc',   new cDwOpColumnType_VARCHAR(255, DWOP_NOT_NULL) );
        $oProp = $oClass->AddProperty('ctry',  new cDwOpColumnType_TINYINT_UNSIGNED() );
        $oProp = $oClass->AddProperty('ppal',  new cDwOpColumnType_VARCHAR(255) );
        $oClass->SetOrderProperties('lname, +fname, -id');
$oOP->AddClassIntoDictionary($oClass);
/*/
$oClass = $oOP->CreateClassByTable('cObjectPersistenceTestClass_User', 'ap_users');
$oClass->SetOrderProperties('lname, +fname, -id');
//file_put_contents('x1.txt', AdjustedPrintR($oClass));///
//echo "<pre>\$oClass: ".AdjustedPrintR($oClass)."</pre>";///

Usage – data manipulation

srand(time());

echo "<div>GetPoolCount(): ".$oOP->GetPoolCount()."</div>";

// Load by ID
echo "<h3>Load by ID</h3>";
$iID = 1;
echo "<pre>\$oObject = \$oOP->LoadObjectById(".$oClass->GetName().", $iID);</pre>";
$oUser = $oOP->LoadObjectById($oClass, $iID);
echo "<pre>oUser: [".gettype($oUser)."]".AdjustedPrintR($oUser)."</pre>";
echo "<div>GetPoolCount(): ".$oOP->GetPoolCount()."</div>";


// Load by ID, using class name string
echo "<h3>Load by ID, using class name string</h3>";
$iID = 1;
echo "<pre>\$oObject =& \$oOP->LoadObjectById('".$oClass->GetName()."', $iID);</pre>";
$oUser =& $oOP->LoadObjectById($oClass->GetName(), $iID);
echo "<pre>oUser: [".gettype($oUser)."]".AdjustedPrintR($oUser)."</pre>";
echo "<div>GetPoolCount(): ".$oOP->GetPoolCount()."</div>";



// Load by value
echo "<h3>Load by value</h3>";
$xVal = 'as';
echo "<pre>\$aoUsers = \$oOP->LoadObjectsByValue(".$oClass->GetName().", 'user', $xVal);</pre>";
$aoUsers = $oOP->LoadObjectsByValue($oClass, 'user', $xVal);
echo "<pre>\$aoUsers: [".gettype($aoUsers)."]".AdjustedPrintR($aoUsers)."</pre>";
echo "<div>GetPoolCount(): ".$oOP->GetPoolCount()."</div>";



// Save
echo "<h3>Save</h3>";
$oUser = new cObjectPersistenceTestClass_User();
$oUser->SetId(1);
$oUser->SetProperty('user', 'as'.rand());
$oUser->SetProperty('pass', 'as');
$oUser->SetProperty('fname', 'Astar');
$oUser->SetProperty('lname', 'Seran');
echo "<pre>oUser: [".gettype($oUser)."]".AdjustedPrintR($oUser)."</pre>";
echo "<pre>\$oOP->SaveObject(\$oUser);</pre>";
$bSucc = $oOP->SaveObject($oUser);
echo "<div>".($bSucc ? 'saved' : 'error')."</div>";
echo "<div>GetPoolCount(): ".$oOP->GetPoolCount()."</div>";


// Create
echo "<h3>Create</h3>";
$oUser = new cObjectPersistenceTestClass_User();
$oUser->SetId(null);
$oUser->SetProperty('user', 'as'.rand());
$oUser->SetProperty('pass', 'as');
$oUser->SetProperty('fname', 'Astar');
$oUser->SetProperty('lname', 'Seran');
echo "<pre>oUser: [".gettype($oUser)."]".AdjustedPrintR($oUser)."</pre>";
echo "<pre>\$oOP->SaveObject(\$oUser);</pre>";
$bSucc = $oOP->SaveObject($oUser);
echo "<div>".($bSucc ? 'saved' : 'error')."</div>";
if(!$bSucc) echo "<pre>".$oOP->GetDB()->GetLastErrorString()."</pre>";
echo "<pre>\$oUser: [".gettype($oUser)."]".AdjustedPrintR($oUser)."</pre>";
echo "<div>GetPoolCount(): ".$oOP->GetPoolCount()."</div>";


// Load by value - load object created above -> Object Pool hit
echo "<h3>Load by value 2</h3>";

$xVal = $oUser->GetProperty('user');
echo "<pre>\$aoUsers = \$oOP->LoadObjectsByValue(".$oClass->GetName().", 'user', $xVal);</pre>";
$aoUsers = $oOP->LoadObjectsByValue($oClass, 'user', $xVal);
echo "<pre>\$aoUsers: [".gettype($aoUsers)."]".AdjustedPrintR($aoUsers)."</pre>";
echo "<div>GetPoolCount(): ".$oOP->GetPoolCount()."</div>";



// Remove from pool - still that one object
echo "<h3>Remove from pool</h3>";

echo "<pre>\$bSucc = \$oOP->RemObjectFromPool(\$oUser);</pre>";
$bSucc = $oOP->RemObjectFromPool($oUser);
echo "<pre>\$oUser: [".gettype($oUser)."]".AdjustedPrintR($oUser)."</pre>";
echo "<div>GetPoolCount(): ".$oOP->GetPoolCount()."</div>";


// Load by value - that object again
echo "<h3>Load by value 3</h3>";

$xVal = $oUser->GetProperty('user');
echo "<pre>\$aoUsers = \$oOP->LoadObjectsByValue(".$oClass->GetName().", 'user', $xVal);</pre>";
$aoUsers = $oOP->LoadObjectsByValue($oClass, 'user', $xVal);
echo "<pre>\$aoUsers: [".gettype($aoUsers)."]".AdjustedPrintR($aoUsers)."</pre>";
echo "<div>GetPoolCount(): ".$oOP->GetPoolCount()."</div>";

// Delete that object from DB
echo "<h3>Delete object by ID</h3>";

echo "<pre>\$oOP->DeleteObject(".$oClass->GetName().", ".$oUser->GetId().");</pre>";
$bSucc = $oOP->DeleteObject($oClass, $oUser->GetId());
if(!$bSucc) echo '<div style="font-color: red;">Delete failed. Error: '.$oOP->GetError()."</pre>";
echo "<div>GetPoolCount(): ".$oOP->GetPoolCount()."</div>";



// Load all objects
echo "<h3>Load all objects</h3>";

echo "<pre>\$aoUsers = \$oOP->LoadObjects(".$oClass->GetName().");</pre>";
$aoUsers = $oOP->LoadObjects($oClass);
echo "<div>GetPoolCount(): ".$oOP->GetPoolCount()."</div>";
//echo "<pre>\$aoUsers: [".gettype($aoUsers)."]".AdjustedPrintR($aoUsers)."</pre>";




// M : N relation
$oClassUser = $oClass;
echo "<h2>M : N Relation</h2>";

echo "<div>\$oClassDoor = \$oOP->CreateClassByTable('cObjectPersistence_TestClass_Door', 'ap_doors');</div>";
$oClassDoor = $oOP->CreateClassByTable('cObjectPersistence_TestClass_Door', 'ap_doors');
file_put_contents('oClassDoor.txt', AdjustedPrintR($oClassDoor));///
echo "<div>\$oClassDoor->IsSuitableForMnRelationSide(): ".(int)$oClassDoor->IsSuitableForMnRelationSide()."</div>";
echo "<div>\$oClassUser->IsSuitableForMnRelationSide(): ".(int)$oClassUser->IsSuitableForMnRelationSide()."</div>";

echo "<div>\$oClassKey  = \$oOP->CreateClassByTable('cObjectPersistence_TestClass_Key', 'ap_keys');</div>";
$oClassKey  = $oOP->CreateClassByTable('cObjectPersistence_TestClass_Key', 'ap_keys');
file_put_contents('oClassKey.txt', AdjustedPrintR($oClassKey));///
echo "\$oClassKey->IsSuitableForMnRelationGlue(): ".(int)$oClassKey->IsSuitableForMnRelationGlue();

// Settin'up
echo '<pre>$oOP->SetMnRelation($oClassUser, $oClassKey, $oClassDoor);</pre>';
$bSucc = $oOP->SetMnRelation($oClassUser, $oClassKey, $oClassDoor);
file_put_contents('oClassKey2.txt', AdjustedPrintR($oClassKey));///
if(!$bSucc) echo "<div>Error: ".$oOP->GetError()."</div>";



// Load objects by M : N relation

/*/ Determines the Class and the ID from the $oUser object.
echo "<pre>\$aoDoors = \$oOP->LoadObjectsByMnRelation(\$oUser, \$oClassKey, \$oClassDoor)</pre>";
$aoDoors = $oOP->LoadObjectsByMnRelation($oUser, $oClassKey, $oClassDoor);
echo "<div>GetPoolCount(): ".$oOP->GetPoolCount()."</div>";
echo "<pre>\$aoDoors: (".gettype($aoDoors).") ".AdjustedPrintR($aoDoors)."</pre>";
/*/ // The same with ID explicitly set to 10001
echo "<pre>\$aoDoors = \$oOP->LoadObjectsByMnRelation('cObjectPersistenceTestClass_User', \$oClassKey, \$oClassDoor, 10001)</pre>";
$aoDoors = $oOP->LoadObjectsByMnRelation('cObjectPersistenceTestClass_User', $oClassKey, $oClassDoor, 10001);
echo "<div>GetPoolCount(): ".$oOP->GetPoolCount()."</div>";
echo "<pre>\$aoDoors: (".gettype($aoDoors).") ".AdjustedPrintR($aoDoors)."</pre>";

// Multiple ID properties SQL for an object:
echo "<div>\$oClassDoor->GetIdPropertiesSql(\$aoDoors[0]): ".$oClassDoor->GetIdPropertiesSql($aoDoors[0])."</pre>";

$aoKeys = $oOP->LoadObjectsByValue('cObjectPersistence_TestClass_Key', 'id_user', 10001);
echo "<div>\$oClassKey->GetIdPropertiesSql(\$aoDoors[0]): ".$oClassKey->GetIdPropertiesSql($aoKeys[0])."</pre>";



if( $oOP->GetError() )
echo '<div style="color: red">Error from the past: '.$oOP->GetError()."</div>";

0