<?php
/*
 * Copyright 2010-2011 by MODX, LLC.
 *
 * This file is part of xPDO.
 *
 * xPDO is free software; you can redistribute it and/or modify it under the
 * terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 *
 * xPDO is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * xPDO; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
 * Suite 330, Boston, MA 02111-1307 USA
 */

/**
 * The base persistent xPDO object classes.
 *
 * This file contains the base persistent object classes, which your user-
 * defined classes will extend when implementing an xPDO object model.
 *
 * @package xpdo
 * @subpackage om
 */

/**
 * The base persistent xPDO object class.
 *
 * This is the basis for the entire xPDO object model, and can also be used by a
 * class generator {@link xPDOGenerator}, ultimately allowing custom classes to
 * be user-defined in a web interface and framework-generated at runtime.
 *
 * @abstract This is an abstract class, and is not represented by an actual
 * table; it simply defines the member variables and functions needed for object
 * persistence. All xPDOObject derivatives must define both a PHP 4 style
 * constructor which calls a PHP 5 style __construct() method with the same
 * parameters. This is necessary to allow instantiation of further derived
 * classes  without knowing the name of the class ahead of time in PHP 4. Note
 * that this does not meet E_STRICT compliance in PHP 5, but is the only sane
 * way to achieve consistency between the PHP 4 and 5 inheritence models.
 *
 * @package xpdo
 * @subpackage om
 */
class xPDOObject {
    /**
     * A convenience reference to the xPDO object.
     * @var xPDO
     * @access public
     */
    public $xpdo= null;

    /**
     * Name of the data source container the object belongs to.
     * @var string
     * @access public
     */
    public $container= null;

    /**
     * Names of the fields in the data table, fully-qualified with a table name.
     *
     * NOTE: For use in table joins to qualify fields with the same name.
     *
     * @var array
     * @access public
     */
    public $fieldNames= null;

    /**
     * The actual class name of an instance.
     * @var string
     */
    public $_class= null;

    /**
     * The package the class is a part of.
     * @var string
     */
    public $_package= null;

    /**
     * An alias for this instance of the class.
     * @var string
     */
    public $_alias= null;

    /**
     * The primary key field (or an array of primary key fields) for this object.
     * @var string|array
     * @access public
     */
    public $_pk= null;

    /**
     * The php native type of the primary key field.
     *
     * NOTE: Will be an array if multiple primary keys are specified for the object.
     *
     * @var string|array
     * @access public
     */
    public $_pktype= null;

    /**
     * Name of the actual table representing this class.
     * @var string
     * @access public
     */
    public $_table= null;

    /**
     * An array of meta data for the table.
     * @var string
     * @access public
     */
    public $_tableMeta= null;

    /**
     * An array of field names that have been modified.
     * @var array
     * @access public
     */
    public $_dirty= array ();

    /**
     * An array of field names that have not been loaded from the source.
     * @var array
     * @access public
     */
    public $_lazy= array ();

    /**
     * An array of key-value pairs representing the fields of the instance.
     * @var array
     * @access public
     */
    public $_fields= array ();

    /**
     * An array of metadata definitions for each field in the class.
     * @var array
     * @access public
     */
    public $_fieldMeta= array ();

    /**
     * An array of aggregate foreign key relationships for the class.
     * @var array
     * @access public
     */
    public $_aggregates= array ();

    /**
     * An array of composite foreign key relationships for the class.
     * @var array
     * @access public
     */
    public $_composites= array ();

    /**
     * An array of object instances related to this object instance.
     * @var array
     * @access public
     */
    public $_relatedObjects= array ();

    /**
     * A validator object responsible for this object instance.
     * @var xPDOValidator
     * @access public
     */
    public $_validator = null;

    /**
     * An array of validation rules for this object instance.
     * @var array
     * @access public
     */
    public $_validationRules = array();

    /**
     * An array of field names that have been already validated.
     * @var array
     * @access public
     */
    public $_validated= array ();

    /**
     * Indicates if the validation map has been loaded.
     * @var boolean
     * @access public
     */
    public $_validationLoaded= false;

    /**
     * Indicates if the instance is transient (and thus new).
     * @var boolean
     * @access public
     */
    public $_new= true;

    /**
     * Indicates the cacheability of the instance.
     * @var boolean
     */
    public $_cacheFlag= true;

    /**
     * A collection of various options that can be used on the instance.
     * @var array
     */
    public $_options= array();

    /**
     * Responsible for loading a result set from the database.
     *
     * @static
     * @param xPDO &$xpdo A valid xPDO instance.
     * @param string $className Name of the class.
     * @param xPDOCriteria $criteria A valid xPDOCriteria instance.
     * @return PDOStatement A reference to a PDOStatement representing the
     * result set.
     */
    public static function & _loadRows(& $xpdo, $className, $criteria) {
        $rows= null;
        if ($criteria->prepare()) {
            if ($xpdo->getDebug() === true) $xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Attempting to execute query using PDO statement object: " . print_r($criteria->sql, true) . print_r($criteria->bindings, true));
            $tstart= $xpdo->getMicroTime();
            if (!$criteria->stmt->execute()) {
                $tend= $xpdo->getMicroTime();
                $totaltime= $tend - $tstart;
                $xpdo->queryTime= $xpdo->queryTime + $totaltime;
                $xpdo->executedQueries= $xpdo->executedQueries + 1;
                $errorInfo= $criteria->stmt->errorInfo();
                $xpdo->log(xPDO::LOG_LEVEL_ERROR, 'Error ' . $criteria->stmt->errorCode() . " executing statement: \n" . print_r($errorInfo, true));
                if (($errorInfo[1] == '1146' || $errorInfo[1] == '1') && $xpdo->getOption(xPDO::OPT_AUTO_CREATE_TABLES)) {
                    if ($xpdo->getManager() && $xpdo->manager->createObjectContainer($className)) {
                        $tstart= $xpdo->getMicroTime();
                        if (!$criteria->stmt->execute()) {
                            $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error " . $criteria->stmt->errorCode() . " executing statement: \n" . print_r($criteria->stmt->errorInfo(), true));
                        }
                        $tend= $xpdo->getMicroTime();
                        $totaltime= $tend - $tstart;
                        $xpdo->queryTime= $xpdo->queryTime + $totaltime;
                        $xpdo->executedQueries= $xpdo->executedQueries + 1;
                    } else {
                        $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error " . $xpdo->errorCode() . " attempting to create object container for class {$className}:\n" . print_r($xpdo->errorInfo(), true));
                    }
                }
            }
            $rows= & $criteria->stmt;
        } else {
            $errorInfo = $xpdo->errorInfo();
            $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error preparing statement for query: {$criteria->sql} - " . print_r($errorInfo, true));
            if (($errorInfo[1] == '1146' || $errorInfo[1] == '1') && $xpdo->getOption(xPDO::OPT_AUTO_CREATE_TABLES)) {
                if ($xpdo->getManager() && $xpdo->manager->createObjectContainer($className)) {
                    if (!$criteria->prepare()) {
                        $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error preparing statement for query: {$criteria->sql} - " . print_r($errorInfo, true));
                    } else {
                        $tstart= $xpdo->getMicroTime();
                        if (!$criteria->stmt->execute()) {
                            $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error " . $criteria->stmt->errorCode() . " executing statement: \n" . print_r($criteria->stmt->errorInfo(), true));
                        }
                        $tend= $xpdo->getMicroTime();
                        $totaltime= $tend - $tstart;
                        $xpdo->queryTime= $xpdo->queryTime + $totaltime;
                        $xpdo->executedQueries= $xpdo->executedQueries + 1;
                    }
                } else {
                    $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error " . $xpdo->errorCode() . " attempting to create object container for class {$className}:\n" . print_r($xpdo->errorInfo(), true));
                }
            }
        }
        return $rows;
    }

    /**
     * Loads an instance from an associative array.
     *
     * @static
     * @param xPDO &$xpdo A valid xPDO instance.
     * @param string $className Name of the class.
     * @param xPDOQuery|string $criteria A valid xPDOQuery instance or relation alias.
     * @param array $row The associative array containing the instance data.
     * @return xPDOObject A new xPDOObject derivative representing a data row.
     */
    public static function _loadInstance(& $xpdo, $className, $criteria, $row) {
        $rowPrefix= '';
        if (is_object($criteria) && $criteria instanceof xPDOQuery) {
            $alias = $criteria->getAlias();
            $actualClass = $criteria->getClass();
        } elseif (is_string($criteria) && !empty($criteria)) {
            $alias = $criteria;
            $actualClass = $className;
        } else {
            $alias = $className;
            $actualClass= $className;
        }
        if (isset($row["{$className}_class_key"])) {
            $actualClass= $row["{$className}_class_key"];
            $rowPrefix= $className . '_';
        }
        elseif (isset ($row["{$alias}_class_key"])) {
            $actualClass= $row["{$alias}_class_key"];
            $rowPrefix= $alias . '_';
        }
        elseif (isset ($row['class_key'])) {
            $actualClass= $row['class_key'];
        }
        $instance= $xpdo->newObject($actualClass);
        if (is_object($instance) && $instance instanceof xPDOObject) {
            if (strpos(strtolower(key($row)), strtolower($alias . '_')) === 0) {
                $rowPrefix= $alias . '_';
            }
            elseif (strpos(strtolower(key($row)), strtolower($className . '_')) === 0) {
                $rowPrefix= $className . '_';
            }
            else {
                $pk = $xpdo->getPK($actualClass);
                if (is_array($pk)) $pk = reset($pk);
                if (isset($row["{$alias}_{$pk}"])) {
                    $rowPrefix= $alias . '_';
                }
                elseif ($actualClass !== $className && $actualClass !== $alias && isset($row["{$actualClass}_{$pk}"])) {
                    $rowPrefix= $actualClass . '_';
                }
                elseif ($className !== $alias && isset($row["{$className}_{$pk}"])) {
                    $rowPrefix= $className . '_';
                }
            }
            if (!$instance instanceof $className) {
                $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Instantiated a derived class {$actualClass} that is not a subclass of the requested class {$className}");
            }
            $instance->_lazy= $actualClass !== $className ? array_keys($xpdo->getFields($className)) : array_keys($instance->_fields);
            $instance->fromArray($row, $rowPrefix, true, true);
            $instance->_dirty= array ();
            $instance->_new= false;
        }
        return $instance;
    }

    /**
     * Responsible for loading an instance into a collection.
     *
     * @static
     * @param xPDO &$xpdo A valid xPDO instance.
     * @param array &$objCollection The collection to load the instance into.
     * @param string $className Name of the class.
     * @param mixed $criteria A valid primary key, criteria array, or xPDOCriteria instance.
     * @param boolean|integer $cacheFlag Indicates if the objects should be cached and
     * optionally, by specifying an integer value, for how many seconds.
     */
    public static function _loadCollectionInstance(xPDO & $xpdo, array & $objCollection, $className, $criteria, $row, $fromCache, $cacheFlag=true) {
        $loaded = false;
        if ($obj= xPDOObject :: _loadInstance($xpdo, $className, $criteria, $row)) {
            if (($cacheKey= $obj->getPrimaryKey()) && !$obj->isLazy()) {
                if (is_array($cacheKey)) {
                    $pkval= implode('-', $cacheKey);
                } else {
                    $pkval= $cacheKey;
                }
                if ($xpdo->getOption(xPDO::OPT_CACHE_DB_COLLECTIONS, array(), 1) == 2 && $xpdo->_cacheEnabled && $cacheFlag) {
                    if (!$fromCache) {
                        $pkCriteria = $xpdo->newQuery($className, $cacheKey, $cacheFlag);
                        $xpdo->toCache($pkCriteria, $obj, $cacheFlag);
                    } else {
                        $obj->_cacheFlag= true;
                    }
                }
                $objCollection[$pkval]= $obj;
                $loaded = true;
            } else {
                $objCollection[]= $obj;
                $loaded = true;
            }
        }
        return $loaded;
    }

    /**
     * Load an instance of an xPDOObject or derivative class.
     *
     * @static
     * @param xPDO &$xpdo A valid xPDO instance.
     * @param string $className Name of the class.
     * @param mixed $criteria A valid primary key, criteria array, or
     * xPDOCriteria instance.
     * @param boolean|integer $cacheFlag Indicates if the objects should be
     * cached and optionally, by specifying an integer value, for how many
     * seconds.
     * @return object|null An instance of the requested class, or null if it
     * could not be instantiated.
     */
    public static function load(xPDO & $xpdo, $className, $criteria, $cacheFlag= true) {
        $instance= null;
        $fromCache= false;
        if ($className= $xpdo->loadClass($className)) {
            if (!is_object($criteria)) {
                $criteria= $xpdo->getCriteria($className, $criteria, $cacheFlag);
            }
            if (is_object($criteria)) {
                $criteria = $xpdo->addDerivativeCriteria($className, $criteria);
                $row= null;
                if ($xpdo->_cacheEnabled && $criteria->cacheFlag && $cacheFlag) {
                    $row= $xpdo->fromCache($criteria, $className);
                }
                if ($row === null || !is_array($row)) {
                    if ($rows= xPDOObject :: _loadRows($xpdo, $className, $criteria)) {
                        $row= $rows->fetch(PDO::FETCH_ASSOC);
                        $rows->closeCursor();
                    }
                } else {
                    $fromCache= true;
                }
                if (!is_array($row)) {
                    if ($xpdo->getDebug() === true) $xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Fetched empty result set from statement: " . print_r($criteria->sql, true) . " with bindings: " . print_r($criteria->bindings, true));
                } else {
                    $instance= xPDOObject :: _loadInstance($xpdo, $className, $criteria, $row);
                    if (is_object($instance)) {
                        if (!$fromCache && $cacheFlag && $xpdo->_cacheEnabled) {
                            $xpdo->toCache($criteria, $instance, $cacheFlag);
                            if ($xpdo->getOption(xPDO::OPT_CACHE_DB_OBJECTS_BY_PK) && ($cacheKey= $instance->getPrimaryKey()) && !$instance->isLazy()) {
                                $pkCriteria = $xpdo->newQuery($className, $cacheKey, $cacheFlag);
                                $xpdo->toCache($pkCriteria, $instance, $cacheFlag);
                            }
                        }
                        if ($xpdo->getDebug() === true) $xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Loaded object instance: " . print_r($instance->toArray('', true), true));
                    }
                }
            } else {
                $xpdo->log(xPDO::LOG_LEVEL_ERROR, 'No valid statement could be found in or generated from the given criteria.');
            }
        } else {
            $xpdo->log(xPDO::LOG_LEVEL_ERROR, 'Invalid class specified: ' . $className);
        }
        return $instance;
    }

    /**
     * Load a collection of xPDOObject instances.
     *
     * @static
     * @param xPDO &$xpdo A valid xPDO instance.
     * @param string $className Name of the class.
     * @param mixed $criteria A valid primary key, criteria array, or xPDOCriteria instance.
     * @param boolean|integer $cacheFlag Indicates if the objects should be
     * cached and optionally, by specifying an integer value, for how many
     * seconds.
     * @return array An array of xPDOObject instances or an empty array if no instances are loaded.
     */
    public static function loadCollection(xPDO & $xpdo, $className, $criteria= null, $cacheFlag= true) {
        $objCollection= array ();
        $fromCache = false;
        if (!$className= $xpdo->loadClass($className)) return $objCollection;
        $rows= false;
        $fromCache= false;
        $collectionCaching = (integer) $xpdo->getOption(xPDO::OPT_CACHE_DB_COLLECTIONS, array(), 1);
        if (!is_object($criteria)) {
            $criteria= $xpdo->getCriteria($className, $criteria, $cacheFlag);
        }
        if (is_object($criteria)) {
            $criteria = $xpdo->addDerivativeCriteria($className, $criteria);
        }
        if ($collectionCaching > 0 && $xpdo->_cacheEnabled && $cacheFlag) {
            $rows= $xpdo->fromCache($criteria);
            $fromCache = (is_array($rows) && !empty($rows));
        }
        if (!$fromCache && is_object($criteria)) {
            $rows= xPDOObject :: _loadRows($xpdo, $className, $criteria);
        }
        if (is_array ($rows)) {
            foreach ($rows as $row) {
                xPDOObject :: _loadCollectionInstance($xpdo, $objCollection, $className, $criteria, $row, $fromCache, $cacheFlag);
            }
        } elseif (is_object($rows)) {
            $cacheRows = array();
            while ($row = $rows->fetch(PDO::FETCH_ASSOC)) {
                xPDOObject :: _loadCollectionInstance($xpdo, $objCollection, $className, $criteria, $row, $fromCache, $cacheFlag);
                if ($collectionCaching > 0 && $xpdo->_cacheEnabled && $cacheFlag && !$fromCache) $cacheRows[] = $row;
            }
            if ($collectionCaching > 0 && $xpdo->_cacheEnabled && $cacheFlag && !$fromCache) $rows =& $cacheRows;
        }
        if (!$fromCache && $xpdo->_cacheEnabled && $collectionCaching > 0 && $cacheFlag && !empty($rows)) {
            $xpdo->toCache($criteria, $rows, $cacheFlag);
        }
        return $objCollection;
    }

    /**
     * Load a collection of xPDOObject instances and a graph of related objects.
     *
     * @static
     * @param xPDO &$xpdo A valid xPDO instance.
     * @param string $className Name of the class.
     * @param string|array $graph A related object graph in array or JSON
     * format, e.g. array('relationAlias'=>array('subRelationAlias'=>array()))
     * or {"relationAlias":{"subRelationAlias":{}}}.  Note that the empty arrays
     * are necessary in order for the relation to be recognized.
     * @param mixed $criteria A valid primary key, criteria array, or xPDOCriteria instance.
     * @param boolean|integer $cacheFlag Indicates if the objects should be
     * cached and optionally, by specifying an integer value, for how many
     * seconds.
     * @return array An array of xPDOObject instances or an empty array if no instances are loaded.
     */
    public static function loadCollectionGraph(xPDO & $xpdo, $className, $graph, $criteria, $cacheFlag) {
        $objCollection = array();
        if ($query= $xpdo->newQuery($className, $criteria, $cacheFlag)) {
            $query = $xpdo->addDerivativeCriteria($className, $query);
            $query->bindGraph($graph);
            $rows = array();
            $fromCache = false;
            $collectionCaching = (integer) $xpdo->getOption(xPDO::OPT_CACHE_DB_COLLECTIONS, array(), 1);
            if ($collectionCaching > 0 && $xpdo->_cacheEnabled && $cacheFlag) {
                $rows= $xpdo->fromCache($query);
                $fromCache = !empty($rows);
            }
            if (!$fromCache) {
                $stmt= $query->prepare();
                if ($stmt && $stmt->execute()) {
                    $objCollection= $query->hydrateGraph($stmt, $cacheFlag);
                }
            } elseif (!empty($rows)) {
                $objCollection= $query->hydrateGraph($rows, $cacheFlag);
            }
        }
        return $objCollection;
    }

    /**
     * Get a set of column names from an xPDOObject for use in SQL queries.
     *
     * @static
     * @param xPDO &$xpdo A reference to an initialized xPDO instance.
     * @param string $className The class name to get columns from.
     * @param string $tableAlias An optional alias for the table in the query.
     * @param string $columnPrefix An optional prefix to prepend to each column name.
     * @param array $columns An optional array of field names to include or exclude
     * (include is default behavior).
     * @param boolean $exclude Determines if any specified columns should be included
     * or excluded from the set of results.
     * @return string A comma-delimited list of the field names for use in a SELECT clause.
     */
    public static function getSelectColumns(xPDO & $xpdo, $className, $tableAlias= '', $columnPrefix= '', $columns= array (), $exclude= false) {
        $columnarray= array ();
        $aColumns= $xpdo->getFields($className);
        if ($aColumns) {
            if (!empty ($tableAlias)) {
                $tableAlias= $xpdo->escape($tableAlias);
                $tableAlias.= '.';
            }
            foreach (array_keys($aColumns) as $k) {
                if ($exclude && in_array($k, $columns)) {
                    continue;
                }
                elseif (empty ($columns)) {
                    $columnarray[$k]= "{$tableAlias}" . $xpdo->escape($k);
                }
                elseif ($exclude || in_array($k, $columns)) {
                    $columnarray[$k]= "{$tableAlias}" . $xpdo->escape($k);
                } else {
                    continue;
                }
                if (!empty ($columnPrefix)) {
                    $columnarray[$k]= $columnarray[$k] . " AS " . $xpdo->escape("{$columnPrefix}{$k}");
                }
            }
        }
        return implode(', ', $columnarray);
    }

    /**
     * Constructor
     *
     * Do not call the constructor directly; see {@link xPDO::newObject()}.
     *
     * All derivatives of xPDOObject must redeclare this method, and must call
     * the parent method explicitly before any additional logic is executed, e.g.
     *
     * <code>
     * public function __construct(xPDO & $xpdo) {
     *     parent  :: __construct($xpdo);
     *     // Any additional constructor tasks here
     * }
     * </code>
     *
     * @access public
     * @param xPDO &$xpdo A reference to a valid xPDO instance.
     * @return xPDOObject
     */
    public function __construct(xPDO & $xpdo) {
        $this->xpdo= & $xpdo;
        $this->container= $xpdo->config['dbname'];
        $this->_class= get_class($this);
        $pos= strrpos($this->_class, '_');
        if ($pos !== false && substr($this->_class, $pos + 1) == $xpdo->config['dbtype']) {
            $this->_class= substr($this->_class, 0, $pos);
        }
        $this->_package= $xpdo->getPackage($this->_class);
        $this->_alias= $this->_class;
        $this->_table= $xpdo->getTableName($this->_class);
        $this->_tableMeta= $xpdo->getTableMeta($this->_class);
        $this->_fields= $xpdo->getFields($this->_class);
        $this->_fieldMeta= $xpdo->getFieldMeta($this->_class);
        $this->_aggregates= $xpdo->getAggregates($this->_class);
        $this->_composites= $xpdo->getComposites($this->_class);
        $classVars= array ();
        if ($relatedObjs= array_merge($this->_aggregates, $this->_composites)) {
            if ($this->getOption(xPDO::OPT_HYDRATE_RELATED_OBJECTS)) $classVars= get_object_vars($this);
            foreach ($relatedObjs as $aAlias => $aMeta) {
                if (!array_key_exists($aAlias, $this->_relatedObjects)) {
                    if ($aMeta['cardinality'] == 'many') {
                        $this->_relatedObjects[$aAlias]= array ();
                    }
                    else {
                        $this->_relatedObjects[$aAlias]= null;
                    }
                }
                if ($this->getOption(xPDO::OPT_HYDRATE_RELATED_OBJECTS) && !array_key_exists($aAlias, $classVars)) {
                    $this->$aAlias= & $this->_relatedObjects[$aAlias];
                    $classVars[$aAlias]= 1;
                }
            }
        }
        if ($this->getOption(xPDO::OPT_HYDRATE_FIELDS)) {
            if (!$this->getOption(xPDO::OPT_HYDRATE_RELATED_OBJECTS)) $classVars= get_object_vars($this);
            foreach ($this->_fields as $fldKey => $fldVal) {
                if (!array_key_exists($fldKey, $classVars)) {
                    $this->$fldKey= & $this->_fields[$fldKey];
                }
            }
        }
        $this->setDirty();
    }

    /**
     * Get an option value for this instance.
     *
     * @param string $key The option key to retrieve a value for.
     * @param array|null $options An optional array to search for a value in first.
     * @param mixed $default A default value to return if no value is found; null is the default.
     * @return mixed The value of the option or the provided default if it is not set.
     */
    public function getOption($key, $options = null, $default = null) {
        if (is_array($options) && array_key_exists($key, $options)) {
            $value= $options[$key];
        } elseif (array_key_exists($key, $this->_options)) {
            $value= $this->_options[$key];
        } else {
            $value= $this->xpdo->getOption($key, null, $default);
        }
        return $value;
    }

    /**
     * Set an option value for this instance.
     *
     * @param string $key The option key to set a value for.
     * @param mixed $value A value to assign to the option.
     */
    public function setOption($key, $value) {
        $this->_options[$key]= $value;
    }

    /**
     * Set a field value by the field key or name.
     *
     * @todo Define and implement field validation.
     *
     * @param string $k The field key or name.
     * @param mixed $v The value to set the field to.
     * @param string|callable $vType A string indicating the format of the
     * provided value parameter, or a callable function that should be used to
     * set the field value, overriding the default behavior.
     * @return boolean Determines whether the value was set successfully and was
     * determined to be dirty (i.e. different from the previous value).
     */
    public function set($k, $v= null, $vType= '') {
        $set= false;
        $callback= '';
        $callable= !empty($vType) && is_callable($vType, false, $callback) ? true : false;
        $oldValue= null;
        if (is_string($k) && !empty($k)) {
            if (array_key_exists($k, $this->_fieldMeta)) {
                $oldValue= $this->_fields[$k];
                if (isset ($this->_fieldMeta[$k]['index']) && $this->_fieldMeta[$k]['index'] === 'pk' && isset ($this->_fieldMeta[$k]['generated'])) {
                    if (!$this->_fieldMeta[$k]['generated'] === 'callback') {
                        return false;
                    }
                }
                if ($callable && $callback) {
                    $set = $callback($k, $v, $this);
                } else {
                    if (is_string($v) && $this->getOption(xPDO::OPT_ON_SET_STRIPSLASHES)) {
                        $v= stripslashes($v);
                    }
                    if ($oldValue !== $v) {
                        //type validation
                        $phptype= $this->_fieldMeta[$k]['phptype'];
                        $dbtype= $this->_fieldMeta[$k]['dbtype'];
                        $allowNull= isset($this->_fieldMeta[$k]['null']) ? (boolean) $this->_fieldMeta[$k]['null'] : true;
                        if ($v === null) {
                            if ($allowNull) {
                                $this->_fields[$k]= null;
                                $set= true;
                            } else {
                                $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "{$this->_class}: Attempt to set NOT NULL field {$k} to NULL");
                            }
                        }
                        else {
                            switch ($phptype) {
                                case 'timestamp' :
                                case 'datetime' :
                                    $ts= false;
                                    if (preg_match('/int/i', $dbtype)) {
                                        if (strtolower($vType) == 'integer' || is_int($v) || $v == '0') {
                                            $ts= (integer) $v;
                                        } else {
                                            $ts= strtotime($v);
                                        }
                                        if ($ts === false) {
                                            $ts= 0;
                                        }
                                        $this->_fields[$k]= $ts;
                                        $set= true;
                                    } else {
                                        if ($vType == 'utc' || in_array($v, $this->xpdo->driver->_currentTimestamps) || $v === '0000-00-00 00:00:00') {
                                            $this->_fields[$k]= (string) $v;
                                            $set= true;
                                        } else {
                                            if (strtolower($vType) == 'integer' || is_int($v)) {
                                                $ts= intval($v);
                                            } elseif (is_string($v) && !empty($v)) {
                                                $ts= strtotime($v);
                                            }
                                            if ($ts !== false) {
                                                $this->_fields[$k]= strftime('%Y-%m-%d %H:%M:%S', $ts);
                                                $set= true;
                                            }
                                        }
                                    }
                                    break;
                                case 'date' :
                                    if (preg_match('/int/i', $dbtype)) {
                                        if (strtolower($vType) == 'integer' || is_int($v) || $v == '0') {
                                            $ts= (integer) $v;
                                        } else {
                                            $ts= strtotime($v);
                                        }
                                        if ($ts === false) {
                                            $ts= 0;
                                        }
                                        $this->_fields[$k]= $ts;
                                        $set= true;
                                    } else {
                                        if ($vType == 'utc' || in_array($v, $this->xpdo->driver->_currentDates) || $v === '0000-00-00') {
                                            $this->_fields[$k]= $v;
                                            $set= true;
                                        } else {
                                            if (strtolower($vType) == 'integer' || is_int($v)) {
                                                $ts= intval($v);
                                            } elseif (is_string($v) && !empty($v)) {
                                                $ts= strtotime($v);
                                            }
                                            $ts= strtotime($v);
                                            if ($ts !== false) {
                                                $this->_fields[$k]= strftime('%Y-%m-%d', $ts);
                                                $set= true;
                                            }
                                        }
                                    }
                                    break;
                                case 'boolean' :
                                    $this->_fields[$k]= intval($v);
                                    $set= true;
                                    break;
                                case 'integer' :
                                    $this->_fields[$k]= intval($v);
                                    $set= true;
                                    break;
                                case 'array' :
                                    if (is_object($v) && $v instanceof xPDOObject) {
                                        $v = $v->toArray();
                                    }
                                    if (is_array($v)) {
                                        $this->_fields[$k]= serialize($v);
                                        $set= true;
                                    }
                                    break;
                                case 'json' :
                                    if (is_object($v) && $v instanceof xPDOObject) {
                                        $v = $v->toArray();
                                    }
                                    if (is_string($v)) {
                                        $v= $this->xpdo->fromJSON($v, true);
                                    }
                                    if (is_array($v)) {
                                        $this->_fields[$k]= $this->xpdo->toJSON($v);
                                        $set= true;
                                    }
                                    break;
                                default :
                                    $this->_fields[$k]= $v;
                                    $set= true;
                            }
                        }
                    }
                }
            } elseif ($this->getOption(xPDO::OPT_HYDRATE_ADHOC_FIELDS)) {
                $oldValue= isset($this->_fields[$k]) ? $this->_fields[$k] : null;
                if ($callable) {
                    $set = $callback($k, $v, $this);
                } else {
                    $this->_fields[$k]= $v;
                    $set= true;
                }
            }
            if ($set && $oldValue !== $this->_fields[$k]) {
                $this->setDirty($k);
            } else {
                $set= false;
            }
            if ($set && $this->getOption(xPDO::OPT_HYDRATE_FIELDS) && !isset ($this->$k)) {
                $this->$k= & $this->_fields[$k];
            }
        } else {
            $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, 'xPDOObject - Called set() with an invalid field name: ' . print_r($k, 1));
        }
        return $set;
    }

    /**
     * Get a field value (or a set of values) by the field key(s) or name(s).
     *
     * Warning: do not use the $format parameter if retrieving multiple values of
     * different types, as the format string will be applied to all types, most
     * likely with unpredicatable results.  Optionally, you can supply an associate
     * array of format strings with the field key as the key for the format array.
     *
     * @param string|array $k A string (or an array of strings) representing the field
     * key or name.
     * @param string|array $format An optional variable (or an array of variables) to
     * format the return value(s).
     * @param mixed $formatTemplate An additional optional variable that can be used in
     * formatting the return value(s).
     * @return mixed The value(s) of the field(s) requested.
     */
    public function get($k, $format = null, $formatTemplate= null) {
        $value= null;
        if (is_array($k)) {
            if ($this->isLazy()) {
                $this->_loadFieldData($k);
            }
            foreach ($k as $key) {
                if (array_key_exists($key, $this->_fields)) {
                    if (is_array($format) && isset ($format[$key])) {
                        $formatTpl= null;
                        if (is_array ($formatTemplate) && isset ($formatTemplate[$key])) {
                            $formatTpl= $formatTemplate[$key];
                        }
                        $value[$key]= $this->get($key, $format[$key], $formatTpl);
                    } elseif (!empty ($format) && is_string($format)) {
                        $value[$key]= $this->get($key, $format, $formatTemplate);
                    } else {
                        $value[$key]= $this->get($key);
                    }
                }
            }
        } elseif (is_string($k) && !empty($k)) {
            if (array_key_exists($k, $this->_fields)) {
                if ($this->isLazy($k)) {
                    $this->_loadFieldData($k);
                }
                $dbType= $this->_getDataType($k);
                $fieldType= $this->_getPHPType($k);
                $value= $this->_fields[$k];
                if ($value !== null) {
                    switch ($fieldType) {
                        case 'boolean' :
                            $value= (boolean) $value;
                            break;
                        case 'integer' :
                            $value= intval($value);
                            if (is_string($format) && !empty ($format)) {
                                if (strpos($format, 're:') === 0) {
                                    if (!empty ($formatTemplate) && is_string($formatTemplate)) {
                                        $value= preg_replace(substr($format, 3), $formatTemplate, $value);
                                    }
                                } else {
                                    $value= sprintf($format, $value);
                                }
                            }
                            break;
                        case 'float' :
                            $value= (float) $value;
                            if (is_string($format) && !empty ($format)) {
                                if (strpos($format, 're:') === 0) {
                                    if (!empty ($formatTemplate) && is_string($formatTemplate)) {
                                        $value= preg_replace(substr($format, 3), $formatTemplate, $value);
                                    }
                                } else {
                                    $value= sprintf($format, $value);
                                }
                            }
                            break;
                        case 'timestamp' :
                        case 'datetime' :
                            if (preg_match('/int/i', $dbType)) {
                                $ts= intval($value);
                            } elseif (in_array($value, $this->xpdo->driver->_currentTimestamps)) {
                                $ts= time();
                            } else {
                                $ts= strtotime($value);
                            }
                            if ($ts !== false && !empty($value)) {
                                if (is_string($format) && !empty ($format)) {
                                    if (strpos($format, 're:') === 0) {
                                        $value= date('Y-m-d H:M:S', $ts);
                                        if (!empty ($formatTemplate) && is_string($formatTemplate)) {
                                            $value= preg_replace(substr($format, 3), $formatTemplate, $value);
                                        }
                                    } elseif (strpos($format, '%') === false) {
                                        $value= date($format, $ts);
                                    } else {
                                        $value= strftime($format, $ts);
                                    }
                                } else {
                                    $value= strftime('%Y-%m-%d %H:%M:%S', $ts);
                                }
                            }
                            break;
                        case 'date' :
                            if (preg_match('/int/i', $dbType)) {
                                $ts= intval($value);
                            } elseif (in_array($value, $this->xpdo->driver->_currentDates)) {
                                $ts= time();
                            } else {
                                $ts= strtotime($value);
                            }
                            if ($ts !== false && !empty($value)) {
                                if (is_string($format) && !empty ($format)) {
                                    if (strpos($format, 're:') === 0) {
                                        $value= strftime('%Y-%m-%d', $ts);
                                        if (!empty ($formatTemplate) && is_string($formatTemplate)) {
                                            $value= preg_replace(substr($format, 3), $formatTemplate, $value);
                                        }
                                    } elseif (strpos($format, '%') === false) {
                                        $value= date($format, $ts);
                                    } elseif ($ts !== false) {
                                        $value= strftime($format, $ts);
                                    }
                                } else {
                                    $value= strftime('%Y-%m-%d', $ts);
                                }
                            }
                            break;
                        case 'array' :
                            if (is_string($value)) {
                                $value= unserialize($value);
                            }
                            break;
                        case 'json' :
                            if (is_string($value) && strlen($value) > 1) {
                                $value= $this->xpdo->fromJSON($value, true);
                            }
                            break;
                        default :
                            if (is_string($format) && !empty ($format)) {
                                if (strpos($format, 're:') === 0) {
                                    if (!empty ($formatTemplate) && is_string($formatTemplate)) {
                                        $value= preg_replace(substr($format, 3), $formatTemplate, $value);
                                    }
                                } else {
                                    $value= sprintf($format, $value);
                                }
                            }
                            break;
                    }
                }
            }
        }
        return $value;
    }

    /**
     * Gets an object related to this instance by a foreign key relationship.
     *
     * Use this for 1:? (one:zero-or-one) or 1:1 relationships, which you can
     * distinguish by setting the nullability of the field representing the
     * foreign key.
     *
     * For all 1:* relationships for this instance, see {@link getMany()}.
     *
     * @see xPDOObject::getMany()
     * @see xPDOObject::addOne()
     * @see xPDOObject::addMany()
     *
     * @param string $alias Alias of the foreign class representing the related
     * object.
     * @param object $criteria xPDOCriteria object to get the related objects
     * @param boolean|integer $cacheFlag Indicates if the object should be
     * cached and optionally, by specifying an integer value, for how many
     * seconds.
     * @return xPDOObject|null The related object or null if no instance exists.
     */
    public function & getOne($alias, $criteria= null, $cacheFlag= true) {
        $object= null;
        if ($fkdef= $this->getFKDefinition($alias)) {
            $k= $fkdef['local'];
            $fk= $fkdef['foreign'];
            if (isset ($this->_relatedObjects[$alias])) {
                if (is_object($this->_relatedObjects[$alias])) {
                    $object= & $this->_relatedObjects[$alias];
                    return $object;
                }
            }
            if ($criteria === null) {
                $criteria= array ($fk => $this->get($k));
            }
            if ($object= $this->xpdo->getObject($fkdef['class'], $criteria, $cacheFlag)) {
                $this->_relatedObjects[$alias]= $object;
            }
        } else {
            $this->xpdo->log(xPDO::LOG_LEVEL_WARN, "Could not getOne: foreign key definition for alias {$alias} not found.");
        }
        return $object;
    }

    /**
     * Gets a collection of objects related by aggregate or composite relations.
     *
     * @see xPDOObject::getOne()
     * @see xPDOObject::addOne()
     * @see xPDOObject::addMany()
     *
     * @param string $alias Alias of the foreign class representing the related
     * object.
     * @param object $criteria xPDOCriteria object to get the related objects
     * @param boolean|integer $cacheFlag Indicates if the objects should be
     * cached and optionally, by specifying an integer value, for how many
     * seconds.
     * @return array A collection of related objects or an empty array.
     */
    public function & getMany($alias, $criteria= null, $cacheFlag= true) {
        $collection= $this->_getRelatedObjectsByFK($alias, $criteria, $cacheFlag);
        return $collection;
    }

    /**
     * Adds an object related to this instance by a foreign key relationship.
     *
     * @see xPDOObject::getOne()
     * @see xPDOObject::getMany()
     * @see xPDOObject::addMany()
     *
     * @param mixed &$obj A single object to be related to this instance.
     * @param string $alias The relation alias of the related object (only
     * required if more than one relation exists to the same foreign class).
     * @return boolean True if the related object was added to this object.
     */
    public function addOne(& $obj, $alias= '') {
        $added= false;
        if (is_object($obj)) {
            if (empty ($alias)) {
                if ($obj->_alias == $obj->_class) {
                    $aliases = $this->_getAliases($obj->_class, 1);
                    if (!empty($aliases)) {
                        $obj->_alias = reset($aliases);
                    }
                }
                $alias= $obj->_alias;
            }
            $fkMeta= $this->getFKDefinition($alias);
            if ($fkMeta && $fkMeta['cardinality'] === 'one') {
                $obj->_alias= $alias;
                $fk= $fkMeta['foreign'];
                $key= $fkMeta['local'];
                $owner= isset ($fkMeta['owner']) ? $fkMeta['owner'] : 'local';
                $kval= $this->get($key);
                $fkval= $obj->get($fk);

                if ($owner == 'local') {
                    $obj->set($fk, $kval);
                }
                else {
                    $this->set($key, $fkval);
                }

                $this->_relatedObjects[$obj->_alias]= $obj;
                $this->setDirty($key);
                $added= true;
            } else {
                $this->xpdo->log(xPDO::LOG_LEVEL_WARN, "Foreign key definition for class {$obj->class}, alias {$obj->_alias} not found, or cardinality is not 'one'.");
            }
        } else {
            $this->xpdo->log(xPDO::LOG_LEVEL_WARN, "Attempt to add a non-object to a relation with alias ({$alias})");
        }
        if (!$added) {
            $this->xpdo->log(xPDO::LOG_LEVEL_WARN, "Could not add related object! " . (is_object($obj) ? print_r($obj->toArray(), true) : ''));
        }
        return $added;
    }

    /**
     * Adds an object or collection of objects related to this class.
     *
     * This method adds an object or collection of objects in a one-to-
     * many foreign key relationship with this object to the internal list of
     * related objects.  By adding these related objects, you can cascade
     * {@link xPDOObject::save()}, {@link xPDOObject::remove()}, and other
     * operations based on the type of relationships defined.
     *
     * @see xPDOObject::addOne()
     * @see xPDOObject::getOne()
     * @see xPDOObject::getMany()
     *
     * @param mixed &$obj A single object or collection of objects to be related
     * to this instance via the intersection class.
     * @param string $alias An optional alias, required only for instances where
     * you have more than one relation defined to the same class.
     * @return boolean Indicates if the addMany was successful.
     */
    public function addMany(& $obj, $alias= '') {
        $added= false;
        if (!is_array($obj)) {
            if (is_object($obj)) {
                if (empty ($alias)) {
                    if ($obj->_alias == $obj->_class) {
                        $aliases = $this->_getAliases($obj->_class, 1);
                        if (!empty($aliases)) {
                            $obj->_alias = reset($aliases);
                        }
                    }
                    $alias= $obj->_alias;
                }
                if ($fkMeta= $this->getFKDefinition($alias)) {
                    $obj->_alias= $alias;
                    if ($fkMeta['cardinality'] === 'many') {
                        if ($obj->_new) {
                            $objpk= '__new' . (isset ($this->_relatedObjects[$alias]) ? count($this->_relatedObjects[$alias]) : 0);
                        } else {
                            $objpk= $obj->getPrimaryKey();
                            if (is_array($objpk)) {
                                $objpk= implode('-', $objpk);
                            }
                        }
                        $this->_relatedObjects[$alias][$objpk]= $obj;
                        if ($this->xpdo->getDebug() === true) $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, 'Added related object with alias: ' . $alias . ' and pk: ' . $objpk . "\n" . print_r($obj->toArray('', true), true));
                        $added= true;
                    }
                }
            }
        } else {
            foreach ($obj as $o) {
                $added= $this->addMany($o, $alias);
            }
        }
        return $added;
    }

    /**
     * Persist new or changed objects to the database container.
     *
     * Inserts or updates the database record representing this object and any
     * new or changed related object records.  Both aggregate and composite
     * related objects will be saved as appropriate, before or following the
     * save operation on the controlling instance.
     *
     * @param boolean|integer $cacheFlag Indicates if the saved object(s) should
     * be cached and optionally, by specifying an integer value, for how many
     * seconds before expiring.  Overrides the cacheFlag for the object(s).
     * @return boolean Returns true on success, false on failure.
     */
    public function save($cacheFlag= null) {
        if ($this->isLazy()) {
            $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, 'Attempt to save lazy object: ' . print_r($this->toArray('', true), 1));
            return false;
        }
        $result= true;
        $sql= '';
        $pk= $this->getPrimaryKey();
        $pkn= $this->getPK();
        $pkGenerated= false;
        if ($this->isNew()) {
            $this->setDirty();
        }
        if ($this->getOption(xPDO::OPT_VALIDATE_ON_SAVE)) {
            if (!$this->validate()) {
                return false;
            }
        }
        $this->_saveRelatedObjects();
        if (!empty ($this->_dirty)) {
            $cols= array ();
            $bindings= array ();
            $updateSql= array ();
            foreach (array_keys($this->_dirty) as $_k) {
                if (!array_key_exists($_k, $this->_fieldMeta)) {
                    continue;
                }
                if (isset($this->_fieldMeta[$_k]['generated'])) {
                    if (!$this->_new || !isset($this->_fields[$_k]) || empty($this->_fields[$_k])) {
                        $pkGenerated= true;
                        continue;
                    }
                }
                if ($this->_fieldMeta[$_k]['phptype'] === 'password') {
                    $this->_fields[$_k]= $this->encode($this->_fields[$_k], 'password');
                }
                $fieldType= PDO::PARAM_STR;
                $fieldValue= $this->_fields[$_k];
                if (in_array($this->_fieldMeta[$_k]['phptype'], array ('datetime', 'timestamp')) && !empty($this->_fieldMeta[$_k]['attributes']) && $this->_fieldMeta[$_k]['attributes'] == 'ON UPDATE CURRENT_TIMESTAMP') {
                    $this->_fields[$_k]= strftime('%Y-%m-%d %H:%M:%S');
                    continue;
                }
                elseif ($fieldValue === null || $fieldValue === 'NULL') {
                    if ($this->_new) continue;
                    $fieldType= PDO::PARAM_NULL;
                    $fieldValue= null;
                }
                elseif (in_array($this->_fieldMeta[$_k]['phptype'], array ('timestamp', 'datetime')) && in_array($fieldValue, $this->xpdo->driver->_currentTimestamps, true)) {
                    $this->_fields[$_k]= strftime('%Y-%m-%d %H:%M:%S');
                    continue;
                }
                elseif (in_array($this->_fieldMeta[$_k]['phptype'], array ('date')) && in_array($fieldValue, $this->xpdo->driver->_currentDates, true)) {
                    $this->_fields[$_k]= strftime('%Y-%m-%d');
                    continue;
                }
                elseif ($this->_fieldMeta[$_k]['phptype'] == 'timestamp' && preg_match('/int/i', $this->_fieldMeta[$_k]['dbtype'])) {
                    $fieldType= PDO::PARAM_INT;
                }
                elseif (!in_array($this->_fieldMeta[$_k]['phptype'], array ('string','password','datetime','timestamp','date','time','array','json'))) {
                    $fieldType= PDO::PARAM_INT;
                }
                if ($this->_new) {
                    $cols[$_k]= $this->xpdo->escape($_k);
                    $bindings[":{$_k}"]['value']= $fieldValue;
                    $bindings[":{$_k}"]['type']= $fieldType;
                } else {
                    $bindings[":{$_k}"]['value']= $fieldValue;
                    $bindings[":{$_k}"]['type']= $fieldType;
                    $updateSql[]= $this->xpdo->escape($_k) . " = :{$_k}";
                }
            }
            if ($this->_new) {
                $sql= "INSERT INTO {$this->_table} (" . implode(', ', array_values($cols)) . ") VALUES (" . implode(', ', array_keys($bindings)) . ")";
            } else {
                if ($pk && $pkn) {
                    if (is_array($pkn)) {
                        $iteration= 0;
                        $where= '';
                        foreach ($pkn as $k => $v) {
                            $vt= PDO::PARAM_INT;
                            if ($this->_fieldMeta[$k]['phptype'] == 'string') {
                                $vt= PDO::PARAM_STR;
                            }
                            if ($iteration) {
                                $where .= " AND ";
                            }
                            $where .= $this->xpdo->escape($k) . " = :{$k}";
                            $bindings[":{$k}"]['value']= $this->_fields[$k];
                            $bindings[":{$k}"]['type']= $vt;
                            $iteration++;
                        }
                    } else {
                        $pkn= $this->getPK();
                        $pkt= PDO::PARAM_INT;
                        if ($this->_fieldMeta[$pkn]['phptype'] == 'string') {
                            $pkt= PDO::PARAM_STR;
                        }
                        $bindings[":{$pkn}"]['value']= $pk;
                        $bindings[":{$pkn}"]['type']= $pkt;
                        $where= $this->xpdo->escape($pkn) . ' = :' . $pkn;
                    }
                    if (!empty ($updateSql)) {
                        $sql= "UPDATE {$this->_table} SET " . implode(',', $updateSql) . " WHERE {$where}";
                    }
                }
            }
            if (!empty ($sql) && $criteria= new xPDOCriteria($this->xpdo, $sql)) {
                if ($criteria->prepare()) {
                    if (!empty ($bindings)) {
                        $criteria->bind($bindings, true, false);
                    }
                    if ($this->xpdo->getDebug() === true) $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Executing SQL:\n{$sql}\nwith bindings:\n" . print_r($bindings, true));
                    if (!$result= $criteria->stmt->execute()) {
                        $errorInfo= $criteria->stmt->errorInfo();
                        $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error " . $criteria->stmt->errorCode() . " executing statement:\n" . $criteria->toSQL() . "\n" . print_r($errorInfo, true));
                        if (($errorInfo[1] == '1146' || $errorInfo[1] == '1') && $this->getOption(xPDO::OPT_AUTO_CREATE_TABLES)) {
                            if ($this->xpdo->getManager() && $this->xpdo->manager->createObjectContainer($this->_class) === true) {
                                if (!$result= $criteria->stmt->execute()) {
                                    $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error " . $criteria->stmt->errorCode() . " executing statement:\n{$sql}\n");
                                }
                            } else {
                                $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error " . $this->xpdo->errorCode() . " attempting to create object container for class {$this->_class}:\n" . print_r($this->xpdo->errorInfo(), true));
                            }
                        }
                    }
                } else {
                    $result= false;
                }
                if ($result) {
                    if ($pkn && !$pk) {
                        if ($pkGenerated) {
                            $this->_fields[$this->getPK()]= $this->xpdo->lastInsertId();
                        }
                        $pk= $this->getPrimaryKey();
                    }
                    if ($pk || !$this->getPK()) {
                        $this->_dirty= array();
                        $this->_validated= array();
                        $this->_new= false;
                    }
                    $callback = $this->getOption(xPDO::OPT_CALLBACK_ON_SAVE);
                    if ($callback && is_callable($callback)) {
                        call_user_func($callback, array('className' => $this->_class, 'criteria' => $criteria, 'object' => $this));
                    }
                    if ($this->xpdo->_cacheEnabled && $pk && ($cacheFlag || ($cacheFlag === null && $this->_cacheFlag))) {
                        $cacheKey= is_array($pk) ? implode('_', $pk) : $pk;
                        if (is_bool($cacheFlag)) {
                            $expires= 0;
                        } else {
                            $expires= intval($cacheFlag);
                        }
                        $this->xpdo->toCache($this->xpdo->getTableClass($this->_class) . '_' . $cacheKey, $this, $expires, array('modified' => true));
                    }
                }
            }
        }
        $this->_saveRelatedObjects();
        if ($result) {
            $this->_dirty= array ();
            $this->_validated= array ();
        }
        return $result;
    }

    /**
     * Searches for any related objects with pending changes to save.
     *
     * @access protected
     * @uses xPDOObject::_saveRelatedObject()
     * @return integer The number of related objects processed.
     */
    protected function _saveRelatedObjects() {
        $saved= 0;
        if (!empty ($this->_relatedObjects)) {
            foreach ($this->_relatedObjects as $alias => $ro) {
                $objects= array ();
                if (is_object($ro)) {
                    $primaryKey= $ro->_new ? '__new' : $ro->getPrimaryKey();
                    if (is_array($primaryKey)) $primaryKey= implode('-', $primaryKey);
                    $objects[$primaryKey]= & $ro;
                    $cardinality= 'one';
                }
                elseif (is_array($ro)) {
                    $objects= $ro;
                    $cardinality= 'many';
                }
                if (!empty($objects)) {
                    foreach ($objects as $pk => $obj) {
                        if ($fkMeta= $this->getFKDefinition($alias)) {
                            if ($this->_saveRelatedObject($obj, $fkMeta)) {
                                if ($cardinality == 'many') {
                                    $newPk= $obj->getPrimaryKey();
                                    if (is_array($newPk)) $newPk= implode('-', $newPk);
                                    if ($pk != $newPk) {
                                        $this->_relatedObjects[$alias][$newPk]= $obj;
                                        unset($this->_relatedObjects[$alias][$pk]);
                                    }
                                }
                                $saved++;
                            }
                        }
                    }
                }
            }
        }
        return $saved;
    }

    /**
     * Save a related object with pending changes.
     *
     * This function is also responsible for setting foreign keys when new
     * related objects are being saved, as well as local keys when the host
     * object is new and needs the foreign key.
     *
     * @access protected
     * @param xPDOObject &$obj A reference to the related object.
     * @param array $fkMeta The meta data representing the relation.
     * @return boolean True if a related object was dirty and saved successfully.
     */
    protected function _saveRelatedObject(& $obj, $fkMeta) {
        $saved= false;
        $local= $fkMeta['local'];
        $foreign= $fkMeta['foreign'];
        $cardinality= $fkMeta['cardinality'];
        $owner= isset ($fkMeta['owner']) ? $fkMeta['owner'] : '';
        if (!$owner) {
            $owner= $cardinality === 'many' ? 'foreign' : 'local';
        }
        if ($owner === 'local' && $fk= $this->get($local)) {
            $obj->set($foreign, $fk);
            $saved= $obj->save();
        } elseif ($owner === 'foreign') {
            if ($obj->isNew() || !empty($obj->_dirty)) {
                $saved= $obj->save();
            }
            $fk= $obj->get($foreign);
            if ($fk) {
                $this->set($local, $fk);
            }
        }
        if ($this->xpdo->getDebug() === true) $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG , ($saved ? 'Successfully saved' : 'Could not save') . " related object\nMain object: " . print_r($this->toArray('', true), true) . "\nRelated Object: " . print_r($obj->toArray('', true), true));
        return $saved;
    }

    /**
     * Remove the persistent instance of an object permanently.
     *
     * Deletes the persistent object isntance stored in the database when
     * called, including any dependent objects defined by composite foreign key
     * relationships.
     *
     * @todo Implement some way to reassign ownership of related composite
     * objects when remove is called, perhaps by passing another object
     * instance as an optional parameter, or creating a separate method.
     *
     * @param array $ancestors Keeps track of classes which have already been
     * removed to prevent loop with circular references.
     * @return boolean Returns true on success, false on failure.
     */
    public function remove(array $ancestors= array ()) {
        $result= false;
        if ($pk= $this->getPrimaryKey()) {
            if (!empty ($this->_composites)) {
                $current= array ($this->_class, $this->_alias);
                foreach ($this->_composites as $compositeAlias => $composite) {
                    if (in_array($compositeAlias, $ancestors) || in_array($composite['class'], $ancestors)) {
                        continue;
                    }
                    if ($composite['cardinality'] === 'many') {
                        if ($many= $this->getMany($compositeAlias)) {
                            foreach ($many as $one) {
                                $ancestors[]= $compositeAlias;
                                $newAncestors= $ancestors + $current;
                                if (!$one->remove($newAncestors)) {
                                    $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error removing dependent object: " . print_r($one->toArray('', true), true));
                                }
                            }
                            unset($many);
                        }
                    }
                    elseif ($one= $this->getOne($compositeAlias)) {
                        $ancestors[]= $compositeAlias;
                        $newAncestors= $ancestors + $current;
                        if (!$one->remove($newAncestors)) {
                            $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error removing dependent object: " . print_r($one->toArray('', true), true));
                        }
                        unset($one);
                    }
                }
            }
            $delete= $this->xpdo->newQuery($this->_class);
            $delete->command('DELETE');
            $delete->where($pk);
            // $delete->limit(1);
            $stmt= $delete->prepare();
            if (is_object($stmt)) {
                if (!$result= $stmt->execute()) {
                    $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, 'Could not delete from ' . $this->_table . '; primary key specified was ' . print_r($pk, true) . "\n" . print_r($stmt->errorInfo(), true));
                } else {
                    $callback = $this->getOption(xPDO::OPT_CALLBACK_ON_REMOVE);
                    if ($callback && is_callable($callback)) {
                        call_user_func($callback, array('className' => $this->_class, 'criteria' => $delete, 'object' => $this));
                    }
                    if ($this->xpdo->_cacheEnabled) {
                        $cacheKey= is_array($pk) ? implode('_', $pk) : $pk;
                        $this->xpdo->toCache($this->xpdo->getTableClass($this->_class) . '_' . $cacheKey, null, 0, array('modified' => true));
                    }
                    $this->xpdo->log(xPDO::LOG_LEVEL_INFO, "Removed {$this->_class} instance with primary key " . print_r($pk, true));
                }
            } else {
                $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, 'Could not build criteria to delete from ' . $this->_table . '; primary key specified was ' . print_r($pk, true));
            }
        }
        return $result;
    }

    /**
     * Gets the value (or values) of the primary key field(s) for the object.
     *
     * @param boolean $validateCompound If any of the keys in a compound primary key are empty
     * or null, and the default value is not allowed to be null, do not return an array, instead
     * return null; the default is true
     * @return mixed The string (or an array) representing the value(s) of the
     * primary key field(s) for this instance.
     */
    public function getPrimaryKey($validateCompound= true) {
        $value= null;
        if ($pk= $this->getPK()) {
            if (is_array($pk)) {
                foreach ($pk as $k) {
                    $_pk= $this->get($k);
                    if (($_pk && strtolower($_pk) !== 'null') || !$validateCompound) {
                        $value[]= $_pk;
                    } else {
                        if (isset ($this->_fieldMeta[$k]['default'])) {
                            $value[]= $this->_fieldMeta[$k]['default'];
                            $this->_fields[$k]= $this->_fieldMeta[$k]['default'];
                        }
                        elseif (isset ($this->_fieldMeta[$k]['null']) && $this->_fieldMeta[$k]['null']) {
                            $value[]= null;
                        }
                        else {
                            $value= null;
                            break;
                        }
                    }
                }
            } else {
                $value= $this->get($pk);
            }
        }
        if (!$value && $this->xpdo->getDebug() === true) {
            $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "No primary key value found for pk definition: " . print_r($this->getPK(), true));
        }
        return $value;
    }

    /**
     * Gets the name (or names) of the primary key field(s) for the object.
     *
     * @return mixed The string (or an array of strings) representing the name(s)
     * of the primary key field(s) for this instance.
     */
    public function getPK() {
        if ($this->_pk === null) {
            $this->_pk= $this->xpdo->getPK($this->_class);
        }
        return $this->_pk;
    }

    /**
     * Gets the type of the primary key field for the object.
     *
     * @return string The type of the primary key field for this instance.
     */
    public function getPKType() {
        if ($this->_pktype === null) {
            if ($this->_pk === null) {
                $this->getPK();
            }
            $this->_pktype= $this->xpdo->getPKType($this->_class);
        }
        return $this->_pktype;
    }

    /**
     * Get the name of a class related by foreign key to a specified field key.
     *
     * This is generally used to lookup classes involved in one-to-one
     * relationships with the current object.
     *
     * @param string $k The field name or key to lookup a related class for.
     */
    public function getFKClass($k) {
        $fkclass= null;
        if (is_string($k)) {
            if (!empty ($this->_aggregates)) {
                foreach ($this->_aggregates as $aggregateAlias => $aggregate) {
                    if ($aggregate['local'] === $k) {
                        $fkclass= $aggregate['class'];
                        break;
                    }
                }
            }
            if (!$fkclass && !empty ($this->_composites)) {
                foreach ($this->_composites as $compositeAlias => $composite) {
                    if ($composite['local'] === $k) {
                        $fkclass= $composite['class'];
                        break;
                    }
                }
            }
            $fkclass= $this->xpdo->loadClass($fkclass);
        }
        if ($this->xpdo->getDebug() === true) $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Returning foreign key class {$fkclass} for column {$k}");
        return $fkclass;
    }

    /**
     * Get a foreign key definition for a specific classname.
     *
     * This is generally used to lookup classes in a one-to-many relationship
     * with the current object.
     *
     * @param string $alias Alias of the related class to lookup a foreign key
     * definition from.
     * @return array A foreign key definition.
     */
    public function getFKDefinition($alias) {
        return $this->xpdo->getFKDefinition($this->_class, $alias);
    }

    /**
     * Gets a field name as represented in the database container.
     *
     * This gets the name of the field, fully-qualified by either the object
     * table name or a specified alias, and properly quoted.
     *
     * @param string $k The simple name of the field.
     * @param string $alias An optional alias for the table in a specific query.
     * @return string The name of the field, qualified with the table name or an
     * optional table alias.
     */
    public function getFieldName($k, $alias= null) {
        if ($this->fieldNames === null) {
            $this->_initFields();
        }
        $name= null;
        if (is_string($k) && isset ($this->fieldNames[$k])) {
            $name= $this->fieldNames[$k];
        }
        if ($name !== null && $alias !== null) {
            $name= str_replace("{$this->_table}.", "{$alias}.", $name);
        }
        return $name;
    }

    /**
     * Copies the object fields and corresponding values to an associative array.
     *
     * @param string $keyPrefix An optional prefix to prepend to the field values.
     * @param boolean $rawValues An optional flag indicating if you want the raw values instead of
     * those returned by the {@link xPDOObject::get()} function.
     * @param boolean $excludeLazy An option flag indicating if you want to exclude lazy fields from
     * the resulting array; the default behavior is to include them which means the object will
     * query the database for the lazy fields before providing the value.
     * @return array An array representation of the object fields/values.
     */
    public function toArray($keyPrefix= '', $rawValues= false, $excludeLazy= false) {
        $objarray= null;
        if (is_array($this->_fields)) {
            $keys= array_keys($this->_fields);
            if (!$excludeLazy && $this->isLazy()) {
                $this->_loadFieldData($this->_lazy);
            }
            foreach ($keys as $key) {
                if (!$excludeLazy || !$this->isLazy($key)) {
                    $objarray[$keyPrefix . $key]= $rawValues ? $this->_fields[$key] : $this->get($key);
                }
            }
        }
        return $objarray;
    }

    /**
     * Sets object fields from an associative array of key => value pairs.
     *
     * @param array $fldarray An associative array of key => values.
     * @param string $keyPrefix Specify an optional prefix to strip from all array
     * keys in fldarray.
     * @param boolean $setPrimaryKeys Optional param to set generated primary keys.
     * @param boolean $rawValues Optional way to set values without calling the
     * {@link xPDOObject::set()} method.
     * @param boolean $adhocValues Optional way to set adhoc values so that all the values of
     * fldarray become object vars.
     */
    public function fromArray($fldarray, $keyPrefix= '', $setPrimaryKeys= false, $rawValues= false, $adhocValues= false) {
        if (is_array($fldarray)) {
            $pkSet= false;
            $generatedKey= false;
            reset($fldarray);
            while (list ($key, $val)= each($fldarray)) {
                if (!empty ($keyPrefix)) {
                    $prefixPos= strpos($key, $keyPrefix);
                    if ($prefixPos === 0) {
                        $key= substr($key, strlen($keyPrefix));
                    } else {
                        continue;
                    }
                    if ($this->xpdo->getDebug() === true) $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Stripped prefix {$keyPrefix} to produce key {$key}");
                }
                if (isset ($this->_fieldMeta[$key]['index']) && $this->_fieldMeta[$key]['index'] == 'pk') {
                    if ($setPrimaryKeys) {
                        if (isset ($this->_fieldMeta[$key]['generated'])) {
                            $generatedKey= true;
                        }
                        if ($this->_new) {
                            if ($rawValues || $generatedKey) {
                                $this->_setRaw($key, $val);
                            } else {
                                $this->set($key, $val);
                            }
                            $pkSet= true;
                        }
                    }
                }
                elseif (isset ($this->_fieldMeta[$key])) {
                    if ($rawValues) {
                        $this->_setRaw($key, $val);
                    } else {
                        $this->set($key, $val);
                    }
                }
                elseif ($adhocValues || $this->getOption(xPDO::OPT_HYDRATE_ADHOC_FIELDS)) {
                    if ($rawValues) {
                        $this->_setRaw($key, $val);
                        if (!isset ($this->$key)) {
                            if ($this->getOption(xPDO::OPT_HYDRATE_RELATED_OBJECTS) && is_object($val) || $this->getOption(xPDO::OPT_HYDRATE_FIELDS) && !is_object($val)) {
                                $this->$key= & $this->_fields[$key];
                            }
                        }
                    } else {
                        $this->set($key, $val);
                    }
                }
                if ($this->isLazy($key)) {
                    $this->_lazy = array_diff($this->_lazy, array($key));
                }
            }
        }
    }

    /**
     * Add a validation rule to an object field for this instance.
     *
     * @param string $field The field key to apply the rule to.
     * @param string $name A name to identify the rule.
     * @param string $type The type of rule.
     * @param string $rule The rule definition.
     * @param array $parameters Any input parameters for the rule.
     */
    public function addValidationRule($field, $name, $type, $rule, array $parameters= array()) {
        if (!$this->_validationLoaded) $this->_loadValidation();
        if (!isset($this->_validationRules[$field])) $this->_validationRules[$field]= array();
        $this->_validationRules[$field][$name]= array(
            'type' => $type,
            'rule' => $rule,
            'parameters' => array()
        );
        foreach ($parameters as $paramKey => $paramValue) {
            $this->_validationRules[$field][$name]['parameters'][$paramKey]= $paramValue;
        }
    }

    /**
     * Remove one or more validation rules from this instance.
     *
     * @param string $field An optional field name to remove rules from. If not
     * specified or null, all rules from all columns will be removed.
     * @param array $rules An optional array of rule names to remove if a single
     * field is specified.  If $field is null, this parameter is ignored.
     */
    public function removeValidationRules($field = null, array $rules = array()) {
        if (!$this->_validationLoaded) $this->_loadValidation();
        if (empty($rules) && is_string($field)) {
            unset($this->_validationRules[$field]);
        } elseif (empty($rules) && is_null($field)) {
            $this->_validationRules = array();
        } elseif (is_array($rules) && !empty($rules) && is_string($field)) {
            foreach ($rules as $name) {
                unset($this->_validationRules[$field][$name]);
            }
        }
    }

    /**
     * Get the xPDOValidator class configured for this instance.
     *
     * @return string|boolean The xPDOValidator instance or false if it could
     * not be loaded.
     */
    public function getValidator() {
        if (!is_object($this->_validator)) {
            $validatorClass = $this->xpdo->loadClass('validation.xPDOValidator', XPDO_CORE_PATH, true, true);
            if ($derivedClass = $this->getOption(xPDO::OPT_VALIDATOR_CLASS, null, '')) {
                if ($derivedClass = $this->xpdo->loadClass($derivedClass, '', false, true)) {
                    $validatorClass = $derivedClass;
                }
            }
            if ($validatorClass) {
                $this->_validator= new $validatorClass($this);
            }
        }
        return $this->_validator;
    }

    /**
     * Used to load validation from the object map.
     *
     * @access public
     * @param boolean $reload Indicates if the schema validation rules should be
     * reloaded.
     */
    public function _loadValidation($reload= false) {
        if (!$this->_validationLoaded || $reload == true) {
            $validation= $this->xpdo->getValidationRules($this->_class);
            $this->_validationLoaded= true;
            foreach ($validation as $column => $rules) {
                foreach ($rules as $name => $rule) {
                    $parameters = array_diff($rule, array($rule['type'], $rule['rule']));
                    $this->addValidationRule($column, $name, $rule['type'], $rule['rule'], $parameters);
                }
            }
        }
    }

    /**
     * Validate the field values using an xPDOValidator.
     *
     * @param array $options An array of options to pass to the validator.
     * @return boolean True if validation was successful.
     */
    public function validate(array $options = array()) {
        $validated= false;
        if ($validator= $this->getValidator()) {
            $validated= $this->_validator->validate($options);
            if ($this->xpdo->getDebug() === true) {
                $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Validator class executed, result = " . print_r($validated, true));
            }
        } else {
            if ($this->xpdo->getDebug() === true) {
                $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "No validator found for {$this->_class} instance.");
            }
            $validated= true;
        }
        return $validated;
    }

    /**
     * Indicates if the object or specified field has been validated.
     *
     * @param string $key Optional key to check for specific validation.
     * @return boolean True if the object or specified field has been fully
     * validated successfully.
     */
    public function isValidated($key= '') {
        $validated = false;
        $unvalidated = array_diff($this->_dirty, $this->_validated);
        if (empty($key)) {
            $validated = (count($unvalidated) > 0);
        } else {
            $validated = !in_array($key, $unvalidated);
        }
        return $validated;
    }

    /**
     * Indicates if the object or specified field is lazy.
     *
     * @param string $key Optional key to check for laziness.
     * @return boolean True if the field specified or if any field is lazy if no
     * field is specified.
     */
    public function isLazy($key= '') {
        $lazy = false;
        if (empty($key)) {
            $lazy = (count($this->_lazy) > 0);
        } else {
            $lazy = in_array($key, $this->_lazy);
        }
        return $lazy;
    }

    /**
     * Gets related objects by a foreign key and specified criteria.
     *
     * @access protected
     * @param string $alias The alias representing the relationship.
     * @param mixed An optional xPDO criteria expression.
     * @param boolean|integer Indicates if the saved object(s) should
     * be cached and optionally, by specifying an integer value, for how many
     * seconds before expiring.  Overrides the cacheFlag for the object.
     * @return array A collection of objects matching the criteria.
     */
    protected function & _getRelatedObjectsByFK($alias, $criteria= null, $cacheFlag= true) {
        $collection= array ();
        if (isset($this->_relatedObjects[$alias]) && (is_object($this->_relatedObjects[$alias]) || (is_array($this->_relatedObjects[$alias]) && !empty ($this->_relatedObjects[$alias])))) {
            $collection= & $this->_relatedObjects[$alias];
        } else {
            $fkMeta= $this->getFKDefinition($alias);
            if ($fkMeta) {
                if ($criteria === null) {
                    $criteria= array($fkMeta['foreign'] => $this->get($fkMeta['local']));
                } else {
                    $criteria= $this->xpdo->newQuery($fkMeta['class'], $criteria);
                    $criteria->andCondition(array("{$criteria->getAlias()}.{$fkMeta['foreign']}" => $this->get($fkMeta['local'])));
                }
                if ($collection= $this->xpdo->getCollection($fkMeta['class'], $criteria, $cacheFlag)) {
                    $this->_relatedObjects[$alias]= array_merge($this->_relatedObjects[$alias], $collection);
                }
            }
        }
        if ($this->xpdo->getDebug() === true) {
            $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "_getRelatedObjectsByFK :: {$alias} :: " . (is_object($criteria) ? print_r($criteria->sql, true)."\n".print_r($criteria->bindings, true) : 'no criteria'));
        }
        return $collection;
    }

    /**
     * Initializes the field names with the qualified table name.
     *
     * Once this is called, you can lookup the qualified name by the field name
     * itself in {@link xPDOObject::$fieldNames}.
     *
     * @access protected
     */
    protected function _initFields() {
        reset($this->_fieldMeta);
        while (list ($k, $v)= each($this->_fieldMeta)) {
            $this->fieldNames[$k]= $this->xpdo->escape($this->_table) . '.' . $this->xpdo->escape($k);
        }
    }

    /**
     * Returns a JSON representation of the object.
     *
     * @param string $keyPrefix An optional prefix to prepend to the field keys.
     * @param boolean $rawValues An optional flag indicating if the field values
     * should be returned raw or via {@link xPDOObject::get()}.
     * @return string A JSON string representing the object.
     */
    public function toJSON($keyPrefix= '', $rawValues= false) {
        $json= '';
        $array= $this->toArray($keyPrefix, $rawValues);
        if ($array) {
            $json= $this->xpdo->toJSON($array);
        }
        return $json;
    }

    /**
     * Sets the object fields from a JSON object string.
     *
     * @param string $jsonSource A JSON object string.
     * @param string $keyPrefix An optional prefix to strip from the keys.
     * @param boolean $setPrimaryKeys Indicates if primary key fields should be set.
     * @param boolean $rawValues Indicates if values should be set raw or via
     * {@link xPDOObject::set()}.
     * @param boolean $adhocValues Indicates if ad hoc fields should be added to the
     * xPDOObject from the source object.
     */
    public function fromJSON($jsonSource, $keyPrefix= '', $setPrimaryKeys= false, $rawValues= false, $adhocValues= false) {
        $array= $this->xpdo->fromJSON($jsonSource, true);
        if ($array) {
            $this->fromArray($array, $keyPrefix, $setPrimaryKeys, $rawValues, $adhocValues);
        } else {
            $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, 'xPDOObject::fromJSON() -- Could not convert jsonSource to a PHP array.');
        }
    }

    /**
     * Encodes a string using the specified algorithm.
     *
     * NOTE: This implementation currently only implements md5.  To implement additional
     * algorithms, override this function in your xPDOObject derivative classes.
     *
     * @param string $source The string source to encode.
     * @param string $type The type of encoding algorithm to apply, md5 by default.
     * @return string The encoded string.
     */
    public function encode($source, $type= 'md5') {
        if (!is_string($source) || empty ($source)) {
            $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, 'xPDOObject::encode() -- Attempt to encode source data that is not a string (or is empty); encoding skipped.');
            return $source;
        }
        switch ($type) {
            case 'password':
            case 'md5':
                $encoded= md5($source);
                break;
            default :
                $encoded= $source;
                $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "xPDOObject::encode() -- Attempt to encode source data using an unsupported encoding algorithm ({$type}).");
                break;
        }
        return $encoded;
    }

    /**
     * Indicates if an object field has been modified (or never saved).
     *
     * @access public
     * @param string $key The field name to check.
     * @return boolean True if the field exists and either has been modified or the object is new.
     */
    public function isDirty($key) {
        $dirty= false;
        if (array_key_exists($key, $this->_fields)) {
            if (array_key_exists($key, $this->_dirty) || $this->_new) {
                $dirty= true;
            }
        } else {
            $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "xPDOObject::isDirty() -- Attempt to check if an unknown field ({$key}) has been modified.");
        }
        return $dirty;
    }

    /**
     * Add the field to a collection of field keys that have been modified.
     *
     * This function also clears any validation flag associated with the field.
     *
     * @param string $key The key of the field to set dirty.
     */
    public function setDirty($key= '') {
        if (empty($key)) {
            foreach (array_keys($this->_fields) as $fIdx => $fieldKey) {
                $this->setDirty($fieldKey);
            }
        }
        elseif (array_key_exists($key, $this->_fields)) {
            $this->_dirty[$key]= $key;
            if (isset($this->_validated[$key])) unset($this->_validated[$key]);
        }
    }

    /**
     * Indicates if the instance is new, and has not yet been persisted.
     *
     * @return boolean True if the object has not been saved or was loaded from
     * the database.
     */
    public function isNew() {
        return (boolean) $this->_new;
    }

    /**
     * Gets the database data type for the specified field.
     *
     * @access protected
     * @param string $key The field name to get the data type for.
     * @return string The DB data type of the field.
     */
    protected function _getDataType($key) {
        $type= 'text';
        if (isset ($this->_fieldMeta[$key]['dbtype'])) {
            $type= strtolower($this->_fieldMeta[$key]['dbtype']);
        } elseif ($this->xpdo->getDebug() === true) {
            $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "xPDOObject::_getDataType() -- No data type specified for field ({$key}), using `text`.");
        }
        return $type;
    }

    /**
     * Gets the php data type for the specified field.
     *
     * @access protected
     * @param string $key The field name to get the data type for.
     * @return string The PHP data type of the field.
     */
    protected function _getPHPType($key) {
        $type= 'string';
        if (isset ($this->_fieldMeta[$key]['phptype'])) {
            $type= strtolower($this->_fieldMeta[$key]['phptype']);
        } elseif ($this->xpdo->getDebug() === true) {
            $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "xPDOObject::_getPHPType() -- No PHP type specified for field ({$key}), using `string`.");
        }
        return $type;
    }

    /**
     * Load persistent data from the source for the field(s) indicated.
     *
     * @access protected
     * @param string|array $fields A field name or array of field names to load
     * from the data source.
     */
    protected function _loadFieldData($fields) {
        if (!is_array($fields)) $fields= array($fields);
        else $fields= array_values($fields);
        $criteria= $this->xpdo->newQuery($this->_class, $this->getPrimaryKey());
        $criteria->select($fields);
        if ($rows= xPDOObject :: _loadRows($this->xpdo, $this->_class, $criteria)) {
            $row= reset($rows);
            $this->fromArray($row, '', false, true);
            $this->_lazy= array_diff($this->_lazy, $fields);
        }
    }

    /**
     * Set a raw value on a field converted to the appropriate type.
     *
     * @access protected
     * @param string $key The key identifying the field to set.
     * @param mixed $val The value to set.
     * @return boolean Returns true if the value was set, false otherwise.
     */
    protected function _setRaw($key, $val) {
        $set = false;
        if ($val === null) {
            $this->_fields[$key] = null;
            $set = true;
        } else {
            $phptype = $this->_getPHPType($key);
            $dbtype = $this->_getDataType($key);
            switch ($phptype) {
                case 'int':
                case 'integer':
                case 'boolean':
                    $this->_fields[$key] = (integer) $val;
                    $set = true;
                    break;
                case 'float':
                    $this->_fields[$key] = (float) $val;
                    $set = true;
                    break;
                case 'array':
                    if (is_array($val)) {
                        $this->_fields[$key]= serialize($val);
                        $set = true;
                    } elseif (is_string($val)) {
                        $this->_fields[$key]= $val;
                        $set = true;
                    } elseif (is_object($val) && $val instanceof xPDOObject) {
                        $this->_fields[$key]= serialize($val->toArray());
                        $set = true;
                    }
                    break;
                case 'json':
                    if (!is_string($val)) {
                        $v = $val;
                        if (is_array($v)) {
                            $this->_fields[$key] = $this->xpdo->toJSON($v);
                            $set = true;
                        } elseif (is_object($v) && $v instanceof xPDOObject) {
                            $this->_fields[$key] = $this->xpdo->toJSON($v->toArray());
                            $set = true;
                        }
                    } else {
                        $this->_fields[$key]= $val;
                        $set = true;
                    }
                    break;
                case 'date':
                case 'datetime':
                case 'timestamp':
                    if (preg_match('/int/i', $dbtype)) {
                        $this->_fields[$key] = (integer) $val;
                        $set = true;
                        break;
                    }
                default:
                    $this->_fields[$key] = $val;
                    $set = true;
            }
        }
        if ($set) $this->setDirty($key);
        return $set;
    }

    /**
     * Find aliases for any defined object relations of the specified class.
     *
     * @access protected
     * @param string $class The name of the class to find aliases from.
     * @param int $limit An optional limit on the number of aliases to return;
     * default is 0, i.e. no limit.
     * @return array An array of aliases or an empty array if none are found.
     */
    protected function _getAliases($class, $limit = 0) {
        $aliases = array();
        $limit = intval($limit);
        $array = array('aggregates' => $this->_aggregates, 'composites' => $this->_composites);
        foreach ($array as $relType => $relations) {
            foreach ($relations as $alias => $def) {
                if (isset($def['class']) && $def['class'] == $class) {
                    $aliases[] = $alias;
                    if ($limit > 0 && count($aliases) > $limit) break;
                }
            }
        }
        return $aliases;
    }
}

/**
 * Extend to define a class with a native integer primary key field named id.
 *
 * @see xpdo/om/mysql/xpdosimpleobject.map.inc.php
 * @package xpdo
 * @subpackage om
 */
class xPDOSimpleObject extends xPDOObject {}
