Вход Регистрация
Файл: library/XenForo/DataWriter.php
Строк: 1857
<?php

//TODO: Set raw

/**
* Abstract handler for data writing. Data writers focus on writing a unit of data
* to the database, including verifying all data to the application rules (including
* those set by the owner) and doing denormalized updates as necessary.
*
* The writer may also interact with the cache, if required.
*
* @package XenForo_Core
*/
abstract class XenForo_DataWriter
{
    
/**
    * Constant for error handling. Use this to trigger an exception when an error occurs.
    *
    * @var integer
    */
    
const ERROR_EXCEPTION 1;

    
/**
    * Constant for error handling. Use this to push errors onto an array. If you try
    * to save while there are errors, an exception will be thrown.
    *
    * @var integer
    */
    
const ERROR_ARRAY 2;

    
/**
    * Constant for error handling. Use this to push errors onto an array. If you try
    * to save while there are errors, the save will silently fail. Use this in places
    * where you aren't interested in handling errors.
    *
    * @var integer
    */
    
const ERROR_SILENT 3;

    
/**
     * Constant for data fields. Use this for 0/1 boolean integer fields.
     *
     * @var string
     */
    
const TYPE_BOOLEAN 'boolean';

    
/**
    * Constant for data fields. Use this for string fields. String fields are assumed
    * to be UTF-8 and limits will refer to characters.
    *
    * @var string
    */
    
const TYPE_STRING 'string';

    
/**
    * Constant for data fields. Use this for binary or ASCII-only fields. Limits
    * will refer to bytes.
    *
    * @var string
    */
    
const TYPE_BINARY 'binary';

    
/**
    * Constant for data fields. Use this for integer fields. Limits can be applied
    * on the range of valid values.
    *
    * @var string
    */
    
const TYPE_INT 'int';

    
/**
    * Constant for data fields. Use this for unsigned integer fields. Negative values
    * will always fail to be valid. Limits can be applied on the range of valid values.
    *
    * @var string
    */
    
const TYPE_UINT 'uint';

    
/**
     * Constant for data fields. Use this for unsigned integer fields. This differs from
     * TYPE_UINT in that negative values will be silently cast to 0. Limits can be
     * applied on the range of valid values.
     *
     * @var string
     */
    
const TYPE_UINT_FORCED 'uint_forced';

    
/**
    * Constant for data fields. Use this for float fields. Limits can be applied
    * on the range of valid values.
    *
    * @var string
    */
    
const TYPE_FLOAT 'float';

    
/**
     * Data is serialized. Ensures that if the data is not a string, it is serialized to ne.
     *
     * @var string
     */
    
const TYPE_SERIALIZED 'serialized';

    
/**
     * Data is serialized to JSON.
     *
     * @var string
     */
    
const TYPE_JSON 'json';

    
/**
     * Constant for data fields. Use this for fields that have a type that cannot be
     * known statically. Use this sparingly, as you must write code to ensure that
     * the value is a scalar before it is inserted into the DB. The behavior if you
     * don't do this is not defined!
     *
     * @var string
     */
    
const TYPE_UNKNOWN 'unknown';

    
/**
    * Cache object
    *
    * @var Zend_Cache_Core|Zend_Cache_Frontend
    */
    
protected $_cache null;

    
/**
    * Database object
    *
    * @var Zend_Db_Adapter_Abstract
    */
    
protected $_db null;

    
/**
    * Array of valid fields in the table. See {@link _getFields()} for more info.
    *
    * @var array
    */
    
protected $_fields = array();

    
/**
    * Options that change the behavior of the data writer. All options that a
    * data writer supports must be documented in this array, preferably
    * by the use of constants.
    *
    * @var array
    */
    
protected $_options = array();

    
/**
    * Extra data that the data writer can use. The DW should usually function without
    * any data in this array. Any data that DW supports should be documented in
    * this array, preferably by the use of constants.
    *
    * Required data (for example, a related phrase) can be included here if necessary.
    *
    * @var array
    */
    
protected $_extraData = array();

    
/**
    * Data that has just been set an is about to be saved.
    *
    * @var array
    */
    
protected $_newData = array();

    
/**
    * Existing data in the database. This is only populated when updating a record.
    *
    * @var array
    */
    
protected $_existingData = array();

    
/**
     * When enabled, preSave, postSave, and verification functions are disabled. To be used
     * when using the DW for importing data in bulk. Note that you are responsible for manually
     * replicating the post save effects.
     *
     * @var boolean
     */
    
protected $_importMode false;

    
/**
    * Type of error handler. See {@link ERROR_EXCEPTION} and related.
    *
    * @var integer
    */
    
protected $_errorHandler 0;

    
/**
    * Array of errors. This is always populated when an error occurs, though
    * an exception may be thrown as well.
    *
    * @var array
    */
    
protected $_errors = array();

    
/**
    * Tracks whether {@link _preSave()} was called. It can only be called once.
    *
    * @var boolean
    */
    
protected $_preSaveCalled false;

    
/**
    * Tracks whether {@link _preDelete()} was called. It can only be called once.
    *
    * @var boolean
    */
    
protected $_preDeleteCalled false;

    
/**
    * Options that can be passed into {@link set()}. Available options are:
    * (default value is listed in parentheses)
    *    * ignoreInvalidFields (false) - when true, an invalid field is simply ignored; otherwise an error is triggered
    *    * replaceInvalidWithDefault (false) - when true, invalid values are replaced with the default value (if there is one)
    *    * runVerificationCallback (true) - when true, verification callbacks are run when setting
    *    * setAfterPreSave (false) - when true, you may set data after preSave is called. This has very specific uses.
    *
    * @var array
    */
    
protected $_setOptions = array(
        
'ignoreInvalidFields' => false,
        
'replaceInvalidWithDefault' => false,
        
'runVerificationCallback' => true,
        
'setAfterPreSave' => false
    
);

    
/**
     * Title of the phrase that will be created when a call to set the
     * existing data fails (when the data doesn't exist).
     *
     * @var string
     */
    
protected $_existingDataErrorPhrase 'existing_data_required_data_writer_not_found';

    
/**
     * Standard approach to caching model objects for the lifetime of the data writer.
     * This is now a static cache to allow data to be reused between data writers, as they
     * are often used in bulk.
     *
     * @var array
     */
    
protected static $_modelCache = array();

    
/**
     * If true, a language rebuild is triggered at the very end of a save/delete process.
     *
     * @var boolean
     */
    
protected $_triggerLanguageRebuild false;

    
/**
    * Constructor.
    *
    * @param constant   Error handler. See {@link ERROR_EXCEPTION} and related.
    * @param array|null Dependency injector. Array keys available: db, cache.
    */
    
public function __construct($errorHandler self::ERROR_ARRAY, array $inject null)
    {
        
$this->_db = (isset($inject['db']) ? $inject['db'] : XenForo_Application::getDb());

        if (isset(
$inject['cache']))
        {
            
$this->_cache $inject['cache'];
        }

        
$this->setErrorHandler($errorHandler);

        
$fields $this->_getFields();
        if (
is_array($fields))
        {
            
$this->_fields $fields;
        }

        
$options $this->_getDefaultOptions();
        if (
is_array($options))
        {
            
$this->_options $options;
        }
    }

    
/**
    * Gets the fields that are defined for the table. This should return an array with
    * each key being a table name and a further array with each key being a field in database.
    * The value of each entry should be an array, which
    * may have the following keys:
    *    * type - one of the TYPE_* constants, this controls the type of data in the field
    *    * autoIncrement - set to true when the field is an auto_increment field. Used to populate this field after an insert
    *    * required - if set, inserts will be prevented if this field is not set or an empty string (0 is accepted)
    *   * requiredError - the phrase title that should be used if the field is not set (only if required)
    *    * default - the default value of the field; used if the field isn't set or if the set value is outside the constraints (if a {@link $_setOption} is set)
    *    * maxLength - for string/binary types only, the maximum length of the data. For strings, in characters; for binary, in bytes.
    *    * min - for numeric types only, the minimum value allowed (inclusive)
    *    * max - for numeric types only, the maximum value allowed (inclusive)
    *    * allowedValues - an array of allowed values for this field (commonly for enums)
    *    * verification - a callback to do more advanced verification. Callback function will take params for $value and $this.
    *
    * @return array
    */
    
abstract protected function _getFields();

    public function 
getFields()
    {
        return 
$this->_getFields();
    }

    public function 
getFieldNames($tableName null)
    {
        
$tables $this->_getFields();

        if (!empty(
$tableName))
        {
            if (empty(
$tables[$tableName]))
            {
                
$this->error("No fields are defined for table '{$tableName}'.");
            }

            return 
array_keys($tables[$tableName]);
        }

        
$fieldNames = array();

        foreach (
$tables AS $fields)
        {
            foreach (
$fields AS $fieldName => $fieldInfo)
            {
                
$fieldNames[] = $fieldName;
            }
        }

        return 
array_unique($fieldNames);
    }

    
/**
    * Gets the actual existing data out of data that was passed in. This data
    * may be a scalar or an array. If it's a scalar, assume that it is the primary
    * key (if there is one); if it is an array, attempt to extract the primary key
    * (or some other unique identifier). Then fetch the correct data from a model.
    *
    * @param mixed Data that can uniquely ID this item
    *
    * @return array|false
    */
    
abstract protected function _getExistingData($data);

    
/**
    * Gets SQL condition to update the existing record. Should read from {@link _existingData}.
    *
    * @param string Table name
    *
    * @return string
    */
    
abstract protected function _getUpdateCondition($tableName);

    
/**
    * Get the primary key from either an array of data or just the scalar value
    *
    * @param mixed Array of data containing the primary field or a scalar ID
    * @param string Primary key field, if data is an array
    * @param string Table name, if empty the first table defined in fields is used
    *
    * @return string|false
    */
    
protected function _getExistingPrimaryKey($data$primaryKeyField ''$tableName '')
    {
        if (!
$tableName)
        {
            
$tableName $this->_getPrimaryTable();
        }

        if (!
$primaryKeyField)
        {
            
$primaryKeyField $this->_getAutoIncrementField($tableName);
        }

        if (!isset(
$this->_fields[$tableName][$primaryKeyField]))
        {
            return 
false;
        }

        if (
is_array($data))
        {
            if (
array_key_exists($primaryKeyField$data))
            {
                return 
$data[$primaryKeyField];
            }
            else
            {
                return 
false;
            }
        }
        else
        {
            return 
$data;
        }
    }

    
/**
     * Splits a (complete) data record array into its constituent tables using the _fields array for use in _getExistingData
     *
     * In order for this to work, the following must be true:
     * 1) The input array must provide all data that this datawriter requires
     * 2) There can be no overlapping field names in the tables that define this data, unless those fields all store the same value
     * 3) IMPORTANT: If the input array does *not* include all the required data fields, it is your responsibility to provide it after this function returns.
     *
     * @param array Complete data record
     *
     * @return array
     */
    
public function getTablesDataFromArray(array $dataArray)
    {
        
$data = array();

        
/**
         * there are no dupe fieldnames between the tables,
         * so we might as well populate the existing data using the existing _fields array
         */
        
foreach ($this->_fields AS $tableName => $field)
        {
            
$data[$tableName] = array();

            foreach (
$field AS $fieldName => $fieldInfo)
            {
                
/**
                 * don't attempt to set fields that are not provided by $dataArray,
                 * it is your own responsibility to resolve this afterwards the function returns
                 */
                
if (isset($dataArray[$fieldName]))
                {
                    
$data[$tableName][$fieldName] = $dataArray[$fieldName];
                }
            }
        }

        return 
$data;
    }

    
/**
    * Gets the default set of options for this data writer. This is automatically
    * called in the constructor. If you wish to set options in your data writer,
    * override this function. As this is handled at run time rather than compile time,
    * you may use dynamic behaviors to set the option defaults.
    *
    * @return array
    */
    
protected function _getDefaultOptions()
    {
        return array();
    }

    
/**
    * Sets the default options for {@link set()}. See {@link $_setOptions} for
    * the available options.
    *
    * @param array
    */
    
public function setDefaultSetOptions(array $setOptions)
    {
        
$this->_setOptions array_merge($this->_setOptions$setOptions);
    }

    
/**
    * Sets an option. The meaning of this option is completely domain specific.
    * If an unknown option specified, an exception will be triggered.
    *
    * @param string Name of the option to set
    * @param mixed  Value of the option
    */
    
public function setOption($name$value)
    {
        if (!
array_key_exists($name$this->_options))
        {
            throw new 
XenForo_Exception('Cannot set unknown option');
        }

        
$this->_options[$name] = $value;
    }

    
/**
    * Gets an option. The meaning of this option is completely domain specific.
    * If an unknown option specified, an exception will be triggered.
    *
    * @param string Name of the option to get
    *
    * @return mixed  Value of the option
    */
    
public function getOption($name)
    {
        if (!
array_key_exists($name$this->_options))
        {
            throw new 
XenForo_Exception('Cannot get unknown option ' $name);
        }

        return 
$this->_options[$name];
    }

    
/**
    * Sets extra data that the DW can use to help it. The DW should be able
    * to function without this data.
    *
    * @param string Name of the data
    * @param mixed  Value of the data
    */
    
public function setExtraData($name$value)
    {
        
$this->_extraData[$name] = $value;
    }

    
/**
     * Gets the named piece of extra data.
     *
     * @param string $name
     *
     * @return mixed
     */
    
public function getExtraData($name)
    {
        return 
$this->isExtraDataSet($name) ? $this->_extraData[$name] : null;
    }

    
/**
     * Determines whether or not the specified extra data field has been set at all
     *
     * @param string $name
     *
     * @return boolean
     */
    
public function isExtraDataSet($name)
    {
        return 
array_key_exists($name$this->_extraData);
    }

    
/**
     * Sets the import mode. When enabled, preSave, postSave, and verification functions are disabled.
     *
     * @param boolean$mode
     */
    
public function setImportMode($mode)
    {
        
$this->_importMode $mode;
    }

    
/**
    * Sets the existing data. This causes the system to do an update instead of an insert.
    * This function triggers an error if no data can be fetched from the provided data.
    *
    * @param mixed   Data that can uniquely ID this item
    * @param boolean If true, trust the passed data to be based on data in DB; if false, the data is used as is (if it's an array)
    *
    * @return boolean
    */
    
public function setExistingData($data$trustInputAsPrimary false)
    {
        if (
$trustInputAsPrimary && is_array($data) && sizeof(array_keys($this->_fields)) == 1)
        {
            
// don't force standardization and given an array, so use it as is
            
$existing = array($this->_getPrimaryTable() => $data);
        }
        else
        {
            
$existing $this->_getExistingData($data);
            if (
is_array($existing) && !array_key_exists($this->_getPrimaryTable(), $existing))
            {
                throw new 
XenForo_Exception('_getExistingData returned an array but did not include data for the primary table');
            }
        }

        
// data is only valid if the data is an array and every entry (table) is an array as well
        
$validData true;
        if (!
is_array($existing))
        {
            
$validData false;
        }
        else
        {
            foreach (
$existing AS $table => $tableData)
            {
                if (!
is_array($tableData))
                {
                    
$validData false;
                    break;
                }
            }
        }

        if (
$validData)
        {
            
$this->_existingData $existing;
            return 
true;
        }
        else
        {
            
$this->_triggerInvalidExistingDataError();
            return 
false;
        }
    }

    
/**
     * Triggers the error for when invalid existing data was requested. This can (and generally
     * should) be extended by concrete DWs to give an error that is more specific to
     * their content type.
     */
    
protected function _triggerInvalidExistingDataError()
    {
        
$this->error(new XenForo_Phrase($this->_existingDataErrorPhrase));
    }

    
/**
    * Sets a field. This value will be updated when save is called.
    *
    * @param string Name of field to update
    * @param string Value to update with
    * @param string Table name, if empty then all tables with that column
    * @param array  Options. See {@link $_setOptions).
    *
    * @return boolean
    */
    
public function set($field$value$tableName '', array $options null)
    {
        
$options = (is_array($options) ? array_merge($this->_setOptions$options) : $this->_setOptions);

        if (
$this->_preSaveCalled && empty($options['setAfterPreSave']))
        {
            throw new 
XenForo_Exception('Set cannot be called after preSave has been called.');
        }

        
$validField false;
        
$dataSet false;
        foreach (
$this->_fields AS $table => $fields)
        {
            if (
$tableName && $tableName != $table)
            {
                continue;
            }

            if (isset(
$fields[$field]) && is_array($fields[$field]))
            {
                
$validField true;
                
$newValue $value;

                if (
$this->_isFieldValueValid($field$fields[$field], $newValue$options))
                {
                    
$this->_setInternal($table$field$newValue);
                    
$dataSet true;
                }
            }
        }

        if (!
$validField && empty($options['ignoreInvalidFields']))
        {
            
$this->error("The field '$field' was not recognised."$fieldfalse);
        }

        return 
$validField;
    }

    
/**
     * Internal function to set a field without any validation or type casting.
     * Use only when you're sure validation, etc isn't needed. The field will only
     * be set if the value has changed.
     *
     * @param string $table Table the field belongs to
     * @param string $field Name of the field
     * @param mixed $newValue Value for the field
     * @param boolean $forceSet If true, the set always goes through
     */
    
protected function _setInternal($table$field$newValue$forceSet false)
    {
        
$existingValue $this->get($field$table);
        if (
$forceSet
            
|| $existingValue === null
            
|| !is_scalar($newValue)
            || !
is_scalar($existingValue)
            || 
strval($newValue) !== strval($existingValue)
        )
        {
            if (
$newValue === $this->getExisting($field$table))
            {
                unset(
$this->_newData[$table][$field]);
            }
            else
            {
                
$this->_newData[$table][$field] = $newValue;
            }
        }
    }

    
/**
     * Internal helper for calling set after save and not triggering an error.
     *
     * @param string Name of field to update
     * @param string Value to update with
     * @param string Table name, if empty then all tables with that column
     * @param array  Options. See {@link $_setOptions).
     *
     * @return boolean
     */
    
protected function _setPostSave($field$newValue$tableName '', array $options = array())
    {
        
$options['setAfterPreSave'] = true;

        return 
$this->set($field$newValue$tableName$options);
    }

    
/**
    * Determines if the provided value for a field is valid. The value may be
    * modified based on type, contraints, or other verification methods.
    *
    * @param string Name of the field.
    * @param array  Data about the field (includes type, contraints, callback, etc)
    * @param mixed  Value for the field
    * @param array  Options. Uses {@link $_setOptions}.
    *
    * @return boolean
    */
    
protected function _isFieldValueValid($fieldName, array $fieldData, &$value, array $options = array())
    {
        
$fieldType = isset($fieldData['type']) ? $fieldData['type'] : self::TYPE_BINARY;
        
$value $this->_castValueToType($fieldType$value$fieldName$fieldData);

        if (!empty(
$options['runVerificationCallback']) && !empty($fieldData['verification']))
        {
            if (!
$this->_runVerificationCallback($fieldData['verification'], $value$fieldData$fieldName))
            {
                
// verification callbacks are responsible for throwing errors
                
return false;
            }
        }

        
$checkLimits $this->_applyFieldValueLimits($fieldType$value$fieldData);

        if (
$checkLimits !== true)
        {
            if (empty(
$options['replaceInvalidWithDefault']) || !array_key_exists('default'$fieldData))
            {
                
$this->error($checkLimits$fieldNamefalse);
                return 
false;
            }
            else
            {
                
$value $fieldData['default'];
            }
        }

        return 
true;
    }

    
/**
     * Casts the field value based on the specified type (TYPE_* constants).
     *
     * @param string $fieldType Type to cast to
     * @param mixed $value Value to cast
     * @param string $fieldName Name of the field being cast
     * @param array Array of all field data information, for extra options
     *
     * @return mixed
     */
    
protected function _castValueToType($fieldType$value$fieldName, array $fieldData)
    {
        switch (
$fieldType)
        {
            case 
self::TYPE_STRING:
                if (isset(
$fieldData['noTrim']))
                {
                    return 
strval($value);
                }
                else
                {
                    return 
trim(strval($value));
                }

            case 
self::TYPE_BINARY:
                return 
strval($value);

            case 
self::TYPE_UINT_FORCED:
                
$value intval($value);
                return (
$value $value);

            case 
self::TYPE_UINT:
            case 
self::TYPE_INT:
                return 
intval($value);

            case 
self::TYPE_FLOAT:
                return 
strval($value) + 0;

            case 
self::TYPE_BOOLEAN:
                return (
$value 0);

            case 
self::TYPE_SERIALIZED:
                if (!
is_string($value))
                {
                    return 
serialize($value);
                }

                if (@
unserialize($value) === false && $value != serialize(false))
                {
                    throw new 
XenForo_Exception('Value is not unserializable');
                }

                return 
$value;

            case 
self::TYPE_JSON:
                if (!
is_string($value))
                {
                    return 
json_encode($value);
                }

                if (
json_decode($value) === null)
                {
                    throw new 
XenForo_Exception('Value cannot be JSON decoded');
                }

                return 
$value;

            case 
self::TYPE_UNKNOWN:
                return 
$value// unmodified

            
default:
                throw new 
XenForo_Exception((
                    (
$fieldName === false)
                    ? 
"There is no field type '$fieldType'."
                    
"The field type specified for '$fieldName' is not valid ($fieldType)."
                
));
        }
    }

    
/**
    * Applies value limits to a field based on type and other constraints.
    * Returns true if the field meets the constraints. The passed in value will
    * be modified by reference.
    *
    * @param string Type of the field. See the TYPE_* constants.
    * @param mixed  Value for the field.
    * @param array  Extra constraints
    *
    * @return boolean|string Either TRUE or an error message
    */
    
protected function _applyFieldValueLimits($fieldType, &$value, array $extraLimits = array())
    {
        
// constraints
        
switch ($fieldType)
        {
            case 
self::TYPE_STRING:
            case 
self::TYPE_BINARY:
                
$strlenFunc = ($fieldType == self::TYPE_STRING 'utf8_strlen' 'strlen');

                if (isset(
$extraLimits['maxLength']) && $strlenFunc($value) > $extraLimits['maxLength'])
                {
                    if (
$this->_importMode)
                    {
                        if (
$strlenFunc == 'utf8_strlen')
                        {
                            
$value utf8_substr($value0$extraLimits['maxLength']);
                        }
                        else
                        {
                            
$value substr($value0$extraLimits['maxLength']);
                        }
                    }
                    else
                    {
                        return new 
XenForo_Phrase('please_enter_value_using_x_characters_or_fewer', array('count' => $extraLimits['maxLength']));
                    }
                }
                break;

            case 
self::TYPE_UINT_FORCED:
            case 
self::TYPE_UINT:
                if (
$value 0)
                {
                    if (
$this->_importMode)
                    {
                        
$value 0;
                    }
                    else
                    {
                        return new 
XenForo_Phrase('please_enter_positive_whole_number');
                    }
                }
                else if (
$this->_importMode && $value 4294967295)
                {
                    
$value 4294967295;
                }
                if (!
array_key_exists('max'$extraLimits))
                {
                    
$extraLimits['max'] = 4294967295;
                }
                break;

            case 
self::TYPE_INT:
                if (
$this->_importMode)
                {
                    if (
$value 2147483647)
                    {
                        
$value 2147483647;
                    }
                    else if (
$value < -2147483648)
                    {
                        
$value = -2147483648;
                    }
                }
                if (!
array_key_exists('min'$extraLimits))
                {
                    
$extraLimits['min'] = -2147483648;
                }
                if (!
array_key_exists('max'$extraLimits))
                {
                    
$extraLimits['max'] = 2147483647;
                }
                break;
        }

        switch (
$fieldType)
        {
            case 
self::TYPE_UINT_FORCED:
            case 
self::TYPE_UINT:
            case 
self::TYPE_INT:
            case 
self::TYPE_FLOAT:
                if (isset(
$extraLimits['min']) && $value $extraLimits['min'])
                {
                    if (
$this->_importMode)
                    {
                        
$value $extraLimits['min'];
                    }
                    else
                    {
                        return new 
XenForo_Phrase('please_enter_number_that_is_at_least_x', array('min' => $extraLimits['min']));
                    }
                }

                if (isset(
$extraLimits['max']) && $value $extraLimits['max'])
                {
                    if (
$this->_importMode)
                    {
                        
$value $extraLimits['max'];
                    }
                    else
                    {
                        return new 
XenForo_Phrase('please_enter_number_that_is_no_more_than_x', array('max' => $extraLimits['max']));
                    }
                }
                break;
        }

        if (isset(
$extraLimits['allowedValues']) && is_array($extraLimits['allowedValues']) && !in_array($value$extraLimits['allowedValues']))
        {
            return new 
XenForo_Phrase('please_enter_valid_value');
        }

        return 
true;
    }

    
/**
    * Runs the verification callback. This callback may modify the value if it chooses to.
    * Callback receives 2 params: the value and this object. The callback must return true
    * if the value was valid.
    *
    * Returns true if the verification was successful.
    *
    * @param callback Callback to run. Use an array with a string '$this' to callback to this object.
    * @param mixed    Value to verify
    * @param array    Information about the field, including all constraints to be applied
    *
    * @return boolean
    */
    
protected function _runVerificationCallback($callback, &$value, array $fieldData$fieldName false)
    {
        if (
$this->_importMode)
        {
            return 
true;
        }

        if (
is_array($callback) && isset($callback[0]) && $callback[0] == '$this')
        {
            
$callback[0] = $this;
        }

        return (boolean)
call_user_func_array($callback,
            array(&
$value$this$fieldName$fieldData)
        );
    }

    
/**
    * Helper method to bulk set values from an array.
    *
    * @param array Key-value pairs of fields and values.
    * @param array Options to pass into {@link set()}. See {@link $_setOptions}.
    */
    
public function bulkSet(array $fields, array $options null)
    {
        foreach (
$fields AS $field => $value)
        {
            
$this->set($field$value''$options);
        }
    }

    
/**
     * Sets the error handler type.
     *
     * @param integer $errorHandler
     */
    
public function setErrorHandler($errorHandler)
    {
        
$this->_errorHandler intval($errorHandler);
    }

    
/**
    * Gets data related to this object regardless of where it is defined (new or old).
    *
    * @param string Field name
    * @param string Table name, if empty loops through tables until first match
    *
    * @return mixed Returns null if the specified field could not be found.
    */
    
public function get($field$tableName '')
    {
        
$tables $this->_getTableList($tableName);

        foreach (
$tables AS $tableName)
        {
            if (isset(
$this->_newData[$tableName]) && array_key_exists($field$this->_newData[$tableName]))
            {
                return 
$this->_newData[$tableName][$field];
            }
            else if (isset(
$this->_existingData[$tableName]) && array_key_exists($field$this->_existingData[$tableName]))
            {
                return 
$this->_existingData[$tableName][$field];
            }
        }

        return 
null;
    }

    
/**
    * Explictly gets data from the new data array. Returns null if not set.
    *
    * @param string $field Field name
    * @param string $tableName Table name, if empty loops through tables until first match
    * @param boolean $exists
    *
    * @return mixed
    */
    
public function getNew($field$tableName '', &$exists null)
    {
        
$tables $this->_getTableList($tableName);

        foreach (
$tables AS $tableName)
        {
            if (isset(
$this->_newData[$tableName]) && array_key_exists($field$this->_newData[$tableName]))
            {
                
$exists true;
                return 
$this->_newData[$tableName][$field];
            }
        }

        
$exists false;
        return 
null;
    }

    
/**
     * Returns true if changes have been made to this data.
     *
     * @return boolean
     */
    
public function hasChanges()
    {
        return (!empty(
$this->_newData));
    }

    
/**
    * Determines whether the named field has been changed/set.
    *
    * @param string $field Field name
    * @param string $tableName Table name, if empty loops through tables until first match
    *
    * @return boolean
    */
    
public function isChanged($field$tableName '')
    {
        
$this->getNew($field$tableName$exists);
        return 
$exists;
    }

    
/**
    * Explictly gets data from the existing data array. Returns null if not set.
    *
    * @param string $field Field name
    * @param string $tableName Table name, if empty loops through tables until first match
    * @param boolean $exists
    *
    * @return mixed
    */
    
public function getExisting($field$tableName '', &$exists null)
    {
        
$tables $this->_getTableList($tableName);

        foreach (
$tables AS $tableName)
        {
            if (isset(
$this->_existingData[$tableName]) && array_key_exists($field$this->_existingData[$tableName]))
            {
                
$exists true;
                return 
$this->_existingData[$tableName][$field];
            }
        }

        
$exists false;
        return 
null;
    }

    
/**
    * Merges the new and existing data to show a "final" view of the data. This will
    * generally reflect what is in the database.
    *
    * If no table is specified, all data will be flattened into one array. New data
    * takes priority over existing data, and earlier tables in the list take priority
    * over later tables.
    *
    * @param string $tableName
    *
    * @return array
    */
    
public function getMergedData($tableName '')
    {
        
$tables $this->_getTableList($tableName);

        
$output = array();

        
// loop through all tables and use the first value that comes up for a field.
        // this assumes that the more "primary" tables come first
        
foreach ($tables AS $tableName)
        {
            if (isset(
$this->_newData[$tableName]))
            {
                
$output += $this->_newData[$tableName];
            }

            if (isset(
$this->_existingData[$tableName]))
            {
                
$output += $this->_existingData[$tableName];
            }
        }

        return 
$output;
    }

    
/**
     * Gets the existing data only into a flat view of the data.
     *
     * @param string $tableName
     *
     * @return array
     */
    
public function getMergedExistingData($tableName '')
    {
        
$tables $this->_getTableList($tableName);

        
$output = array();

        
// loop through all tables and use the first value that comes up for a field.
        // this assumes that the more "primary" tables come first
        
foreach ($tables AS $tableName)
        {
            if (isset(
$this->_existingData[$tableName]))
            {
                
$output += $this->_existingData[$tableName];
            }
        }

        return 
$output;
    }

    
/**
    * Trigger an error with the specified string (or phrase object). Depending on the
    * type of error handler chosen, this may throw an exception.
    *
    * @param string|XenForo_Phrase $error Error message
    * @param string|false $errorKey Unique key for the error. Used to prevent multiple errors from the same field being displayed.
    * @param boolean $specificError If true and error key specified, overwrites an existing error with this name
    */
    
public function error($error$errorKey false$specificError true)
    {
        if (
$errorKey !== false)
        {
            if (
$specificError || !isset($this->_errors[strval($errorKey)]))
            {
                
$this->_errors[strval($errorKey)] = $error;
            }
        }
        else
        {
            
$this->_errors[] = $error;
        }

        if (
$this->_errorHandler == self::ERROR_EXCEPTION)
        {
            throw new 
XenForo_Exception($errortrue);
        }
    }

    
/**
     * Merges a set of errors into this DW.
     *
     * @param array $errors
     */
    
public function mergeErrors(array $errors)
    {
        foreach (
$errors AS $errorKey => $error)
        {
            if (
is_integer($errorKey))
            {
                
$errorKey false;
            }
            
$this->error($error$errorKey);
        }
    }

    
/**
    * Gets all errors that have been triggered. If {@link preSave()} has been
    * called and this returns an empty array, a {@link save()} should go through.
    *
    * @return array
    */
    
public function getErrors()
    {
        return 
$this->_errors;
    }

    
/**
     * Determines if this DW has errors.
     *
     * @return boolean
     */
    
public function hasErrors()
    {
        return (
count($this->_errors) > 0);
    }

    
/**
     * Gets the specific named error if reported.
     *
     * @param string $errorKey
     *
     * @return string
     */
    
public function getError($errorKey)
    {
        return (isset(
$this->_errors[$errorKey]) ? $this->_errors[$errorKey] : false);
    }

    
/**
     * Gets all new data that has been set to date
     *
     * @return array
     */
    
public function getNewData()
    {
        return 
$this->_newData;
    }

    
/**
     * Gets all new data that has been set to date, merged through all tables.
     *
     * @param string $tableName
     */
    
public function getMergedNewData($tableName '')
    {
        
$tables $this->_getTableList($tableName);

        
$output = array();

        
// loop through all tables and use the first value that comes up for a field.
        // this assumes that the more "primary" tables come first
        
foreach ($tables AS $tableName)
        {
            if (isset(
$this->_newData[$tableName]))
            {
                
$output += $this->_newData[$tableName];
            }
        }

        return 
$output;
    }

    
/**
     * Updates the version ID field for the specified record. Th add-on ID
     * is determined from a previously set value, so it should not be updated
     * after this is called.
     *
     * @param string $versionIdField Name of the field the version ID will be written to
     * @param string $versionStringField Name of the field where the version string will be written to
     * @param string $addOnIdField Name of the field the add-on ID is kept in
     *
     * @return integer Version ID to use
     */
    
public function updateVersionId($versionIdField 'version_id'$versionStringField 'version_string'$addOnIdField 'addon_id')
    {
        
$addOnId $this->get($addOnIdField);
        if (
$addOnId)
        {
            
$version $this->getModelFromCache('XenForo_Model_AddOn')->getAddOnVersion($addOnId);
            if (!
$version)
            {
                
$this->set($addOnIdField''); // no add-on found, make it custom

                
$versionId 0;
                
$versionString '';
            }
            else
            {
                
$versionId $version['version_id'];
                
$versionString $version['version_string'];
            }
        }
        else
        {
            
$versionId 0;
            
$versionString '';
        }

        
$this->set($versionIdField$versionId);
        
$this->set($versionStringField$versionString);
        return 
$versionId;
    }

    
/**
    * Determines if we have errors that would prevent a save. If the silent error
    * handler is used, errors simply trigger a return of true (yes, we have errors);
    * otherwise, errors trigger an exception. Generally, if errors are to be handled,
    * save shouldn't be called.
    *
    * @return boolean True if there are errors
    */
    
protected function _haveErrorsPreventSave()
    {
        if (
$this->_errors)
        {
            if (
$this->_errorHandler == self::ERROR_SILENT)
            {
                return 
true;
            }
            else
            {
                throw new 
XenForo_Exception($this->_errorstrue);
            }
        }

        return 
false;
    }

    
/**
    * External handler to get the SQL update/delete condition for this data. If there is no
    * existing data, this always returns an empty string, otherwise it proxies
    * to {@link _getUpdateCondition()}, which is an abstract function.
    *
    * @param string Name of the table to fetch the condition for
    *
    * @return string
    */
    
public function getUpdateCondition($tableName)
    {
        if (!
$this->_existingData || !isset($this->_existingData[$tableName]))
        {
            return 
'';
        }
        else
        {
            return 
$this->_getUpdateCondition($tableName);
        }
    }

    
/**
    * Saves the changes to the data. This either updates an existing record or inserts
    * a new one, depending whether {@link setExistingData} was called.
    *
    * After a successful insert with an auto_increment field, the value will be stored
    * into the new data array with a field marked as autoIncrement. Use {@link get()}
    * to get access to it.
    *
    * @return boolean True on success
    */
    
public function save()
    {
        
$this->preSave();

        if (
$this->_haveErrorsPreventSave())
        {
            return 
false;
        }

        if (!
$this->_newData)
        {
            
// nothing to change; error if insert, act as if everything is ok on update
            
if (!$this->_existingData)
            {
                throw new 
XenForo_Exception('Cannot save item when no data has been set');
            }
        }

        
$this->_beginDbTransaction();

        try
        {
            
$this->_save();

            if (!
$this->_importMode)
            {
                
$this->_postSave();
                
$this->_performLanguageRebuild();
            }
        }
        catch (
Exception $e)
        {
            
$this->_rollbackDbTransaction();
            throw 
$e;
        }

        
$this->_commitDbTransaction();

        if (!
$this->_importMode)
        {
            
$this->_postSaveAfterTransaction();
        }

        return 
true;
    }

    
/**
    * Public facing handler for final verification before a save. Any verification
    * or complex defaults may be set by this.
    *
    * It is generally not advisable to override this function. If you just want to
    * add pre-save behaviors, override {@link _preSave()}.
    */
    
public function preSave()
    {
        if (
$this->_preSaveCalled)
        {
            return;
        }

        
$this->_preSaveDefaults();
        if (!
$this->_importMode)
        {
            
$this->_preSave();

            if (!
$this->_existingData)
            {
                
$this->_resolveDefaultsAndRequiredFieldsForInsert();
            }
            else
            {
                
$this->_checkRequiredFieldsForUpdate();
            }
        }
        else if (!
$this->_existingData)
        {
            
$this->_resolveDefaultsAndRequiredFieldsForInsert(false);
        }

        
$this->_preSaveCalled true;
    }

    
/**
     * Method designed to be overridden by child classes to add pre-save behaviors that
     * set dynamic defaults. This is still called in import mode.
     */
    
protected function _preSaveDefaults()
    {
    }

    
/**
    * Method designed to be overridden by child classes to add pre-save behaviors. This
    * is not callbed in import mode.
    */
    
protected function _preSave()
    {
    }

    
/**
    * This resolves unset fields to their defaults (if available) and the checks
    * for required fields that are unset or empty. If a required field is not
    * set properly, an error is thrown.
    *
    * @param boolean $checkRequired If true, checks required fields
    */
    
protected function _resolveDefaultsAndRequiredFieldsForInsert($checkRequired true)
    {
        foreach (
$this->_fields AS $tableName => $fields)
        {
             foreach (
$fields AS $field => $fieldData)
            {
                
// when default is an array it references another column in an earlier table
                
if ((!isset($this->_newData[$tableName]) || !array_key_exists($field$this->_newData[$tableName]))
                    && 
array_key_exists('default'$fieldData)
                    && !
is_array($fieldData['default'])
                )
                {
                    
$this->_setInternal($tableName$field$fieldData['default']);
                }

                if (
$checkRequired
                    
&& !empty($fieldData['required'])
                    && (!isset(
$this->_newData[$tableName])
                        || !
array_key_exists($field$this->_newData[$tableName])
                        || 
$this->_newData[$tableName][$field] === ''
                    
)
                )
                {
                    
// references an externalID, we can't resolve this
                    
if (isset($fieldData['default']) && is_array($fieldData['default']))
                    {
                        continue;
                    }
                    
$this->_triggerRequiredFieldError($tableName$field);
                }
            }
        }
    }

    
/**
     * Checks that required field values are still maintained on updates.
     */
    
protected function _checkRequiredFieldsForUpdate()
    {
        foreach (
$this->_fields AS $tableName => $fields)
        {
             foreach (
$fields AS $field => $fieldData)
            {
                if (!isset(
$this->_newData[$tableName]) || !array_key_exists($field$this->_newData[$tableName]))
                {
                    continue;
                }

                if (!empty(
$fieldData['required']) && $this->_newData[$tableName][$field] === '')
                {
                    
$this->_triggerRequiredFieldError($tableName$field);
                }
            }
        }
    }

    
/**
     * Triggers the error for a required field not being specified.
     *
     * @param string $tableName
     * @param string $field
     */
    
protected function _triggerRequiredFieldError($tableName$field)
    {
        
$errorText $this->_getSpecificRequiredFieldErrorText($tableName$field);
        if (
$errorText)
        {
            
$this->error($errorText$fieldfalse);
        }
        else if (isset(
$this->_fields[$tableName][$field]['requiredError']))
        {
            
$this->error(new XenForo_Phrase($this->_fields[$tableName][$field]['requiredError']), $fieldfalse);
        }
        else
        {

            
$this->error(new XenForo_Phrase('please_enter_value_for_required_field_x', array('field' => $field)), $fieldfalse);
        }
    }

    
/**
     * Gets the error text (or phrase) for a specific required field. Concrete DWs
     * may override this to get nicer error messages for specific fields.
     *
     * @param string $tableName
     * @param string $field
     *
     * @return false|string|XenForo_Phrase
     */
    
protected function _getSpecificRequiredFieldErrorText($tableName$field)
    {
        return 
false;
    }

    
/**
     * Returns true if this DW is updating a record, rather than inserting one.
     *
     * @return boolean
     */
    
public function isUpdate()
    {
        return !empty(
$this->_existingData);
    }

    
/**
     * Returns true if this DW is inserting a record, rather than updating one.
     *
     * @return boolean
     */
    
public function isInsert()
    {
        return !
$this->isUpdate();
    }

    
/**
    * Internal save handler. Deals with both updates and inserts.
    */
    
protected function _save()
    {
        if (
$this->isUpdate())
        {
            
$this->_update();
        }
        else
        {
            
$this->_insert();
        }
    }

    
/**
    * Internal save handler.
    */
    
protected function _insert()
    {
        foreach (
$this->_getTableList() AS $tableName)
        {
            
$this->_db->insert($tableName$this->_newData[$tableName]);
            
$this->_setAutoIncrementValue($this->_db->lastInsertId(), $tableNametrue);
        }
    }

    
/**
    * Internal update handler.
    */
    
protected function _update()
    {
        foreach (
$this->_getTableList() AS $tableName)
        {
            if (!(
$update $this->getUpdateCondition($tableName)) || empty($this->_newData[$tableName]))
            {
                continue;
            }
            
$this->_db->update($tableName$this->_newData[$tableName], $update);
        }
    }

    
/**
    * Sets the auto-increment value to the auto increment field, if there is one.
    * If the ID passed in is 0, nothing will be updated.
    *
    * @param integer Auto-increment value from 0.
    * @param string Name of the table set the auto increment field in
    * @param bool Update all tables with cross referenced auto increment fields
    *
    * @return boolean True on update
    */
    
protected function _setAutoIncrementValue($insertId$tableName$updateAll false)
    {
        if (!
$insertId)
        {
            return 
false;
        }

        
$field $this->_getAutoIncrementField($tableName);
        if (!
$field)
        {
            return 
false;
        }

        
$insertId += 0;

        
$this->_newData[$tableName][$field] = $insertId;
        if (
$updateAll)
        {
            foreach (
$this->_fields AS $table => $fieldData)
            {
                foreach (
$fieldData AS $fieldName => $fieldType)
                {
                    if (!isset(
$fieldType['default']) || !is_array($fieldType['default']))
                    {
                        continue;
                    }

                    if (
$fieldType['default'][0] == $tableName && !$this->get($field$table))
                    {
                        
$this->_newData[$table][$field] = $insertId;
                    }
                }
            }
        }
        return 
true;
    }

    
/**
    * Finds the auto-increment field in the list of fields. This field is simply
    * the first field tagged with the autoIncrement flag.
    *
    * @param string Name of the table to obtain the field for
    *
    * @return string|false Name of the field if found or false
    */
    
protected function _getAutoIncrementField($tableName)
    {
        foreach (
$this->_fields[$tableName] AS $field => $fieldData)
        {
            if (!empty(
$fieldData['autoIncrement']))
            {
                return 
$field;
            }
        }

        return 
false;
    }

    
/**
    * Finds the first table in the _fields array
    *
    * @return string|false Name of the table or false
    */
    
protected function _getPrimaryTable()
    {
        
$tables array_keys($this->_fields);
        return (isset(
$tables[0]) ? $tables[0] : false);
    }

    
/**
     * Gets an array with a list of all table names or, if provided, only
     * the specified table.
     *
     * @param string $tableName Optional table to limit results to.
     *
     * @return array
     */
    
protected function _getTableList($tableName '')
    {
        return (
$tableName ? array($tableName) : array_keys($this->_fields));
    }

    
/**
     * @return array
     */
    
public function getTables()
    {
        return 
array_keys($this->_fields);
    }

    
/**
    * Method designed to be overridden by child classes to add post-save behaviors.
    * This is not called in import mode.
    */
    
protected function _postSave()
    {
    }

    
/**
     * Method designed to be overridden by child classes to add post-save behaviors
     * that should be run after the transaction is committed. This is not called in
     * import mode.
     */
    
protected function _postSaveAfterTransaction()
    {

    }

    
/**
    * Deletes the record that was selected by a call to {@link setExistingData()}.
    *
    * @return boolean True on success
    */
    
public function delete()
    {
        
$this->preDelete();

        if (
$this->_haveErrorsPreventSave())
        {
            return 
false;
        }

        
$this->_beginDbTransaction();

        
$this->_delete();
        
$this->_postDelete();
        
$this->_performLanguageRebuild();

        
$this->_commitDbTransaction();

        return 
true;
    }

    
/**
    * Public facing handler for final verification before a delete. Any verification
    * or complex defaults may be set by this.
    *
    * It is generally not advisable to override this function. If you just want to
    * add pre-delete behaviors, override {@link _preDelete()}.
    */
    
public function preDelete()
    {
        if (
$this->_preDeleteCalled)
        {
            return;
        }

        
$this->_preDelete();

        
$this->_preDeleteCalled true;
    }

    
/**
    * Method designed to be overridden by child classes to add pre-delete behaviors.
    */
    
protected function _preDelete()
    {
    }

    
/**
    * Internal handler for a delete action. Actually does the delete.
    */
    
protected function _delete()
    {
        foreach (
$this->_getTableList() AS $tableName)
        {
            
$condition $this->getUpdateCondition($tableName);
            if (!
$condition)
            {
                throw new 
XenForo_Exception('Cannot delete data without a condition');
            }
            
$this->_db->delete($tableName$condition);
        }
    }

    
/**
    * Method designed to be overridden by child classes to add pre-delete behaviors.
    */
    
protected function _postDelete()
    {
    }

    
/**
     * Inserts or updates a master (language 0) phrase. Errors will be silently ignored.
     *
     * @param string $title
     * @param string $text
     * @param string $addOnId
     * @param array $extra
     */
    
protected function _insertOrUpdateMasterPhrase($title$text$addOnId '', array $extra = array())
    {
        
$this->_getPhraseModel()->insertOrUpdateMasterPhrase($title$text$addOnId$extra,  array(
            
XenForo_DataWriter_Phrase::OPTION_REBUILD_LANGUAGE_CACHE => false,
            
XenForo_DataWriter_Phrase::OPTION_RECOMPILE_TEMPLATE => false
        
));
        
$this->_triggerLanguageRebuild true;
    }

    
/**
     * Deletes the named master phrase if it exists.
     *
     * @param string $title
     */
    
protected function _deleteMasterPhrase($title)
    {
        
$this->_getPhraseModel()->deleteMasterPhrase($title,  array(
            
XenForo_DataWriter_Phrase::OPTION_REBUILD_LANGUAGE_CACHE => false,
            
XenForo_DataWriter_Phrase::OPTION_RECOMPILE_TEMPLATE => false
        
));
        
$this->_triggerLanguageRebuild true;
    }

    
/**
     * Renames a master phrase. If you get a conflict, it will
     * be silently ignored.
     *
     * @param string $oldName
     * @param string $newName
     */
    
protected function _renameMasterPhrase($oldName$newName)
    {
        
$this->_getPhraseModel()->renameMasterPhrase($oldName$newName, array(
            
//XenForo_DataWriter_Phrase::OPTION_REBUILD_LANGUAGE_CACHE => false,
            
XenForo_DataWriter_Phrase::OPTION_RECOMPILE_TEMPLATE => false
        
));
        
$this->_triggerLanguageRebuild true;
    }

    
/**
     * Changes the add-on for a phrase. This should be called when you have phrases
     * that are attached to a DW entity that itself is attached to an add-on.
     *
     * @param string $phraseName
     * @param string $addOnId
     */
    
protected function _changePhraseAddOn($phraseName$addOnId)
    {
        
$this->_getPhraseModel()->changePhraseAddOn($phraseName$addOnId);
    }

    
/**
    * Starts a new database transaction.
    */
    
protected function _beginDbTransaction()
    {
        
XenForo_Db::beginTransaction($this->_db);
        return 
true;
    }

    
/**
    * Commits a new database transaction.
    */
    
protected function _commitDbTransaction()
    {
        
XenForo_Db::commit($this->_db);
        return 
true;
    }

    
/**
    * Rolls a database transaction back.
    */
    
protected function _rollbackDbTransaction()
    {
        
XenForo_Db::rollback($this->_db);
        return 
true;
    }

    
/**
     * Performs a language rebuild if needed.
     */
    
protected function _performLanguageRebuild()
    {
        if (
$this->_triggerLanguageRebuild)
        {
            
$this->getModelFromCache('XenForo_Model_Language')->rebuildLanguageCaches();
        }
    }

    
/**
     * Gets the specified model object from the cache. If it does not exist,
     * it will be instantiated.
     *
     * @param string $class Name of the class to load
     *
     * @return XenForo_Model
     */
    
public function getModelFromCache($class)
    {
        if (!isset(
self::$_modelCache[$class]))
        {
            
self::$_modelCache[$class] = XenForo_Model::create($class);
        }

        return 
self::$_modelCache[$class];
    }

    
/**
     * Returns the user model
     *
     * @return XenForo_Model_User
     */
    
protected function _getUserModel()
    {
        return 
$this->getModelFromCache('XenForo_Model_User');
    }

    
/**
     * @return XenForo_Model_UserIgnore
     */
    
protected function _getUserIgnoreModel()
    {
        return 
$this->getModelFromCache('XenForo_Model_UserIgnore');
    }

    
/**
     * Returns the phrase model
     *
     * @return XenForo_Model_Phrase
     */
    
protected function _getPhraseModel()
    {
        return 
$this->getModelFromCache('XenForo_Model_Phrase');
    }

    
/**
     * Returns the news feed model
     *
     * @return XenForo_Model_NewsFeed
     */
    
protected function _getNewsFeedModel()
    {
        return 
$this->getModelFromCache('XenForo_Model_NewsFeed');
    }

    
/**
     * Returns the alert model
     *
     * @return XenForo_Model_Alert
     */
    
protected function _getAlertModel()
    {
        return 
$this->getModelFromCache('XenForo_Model_Alert');
    }

    
/**
     * Returns the admin search model
     *
     * @return XenForo_Model_AdminSearch
     */
    
protected function _getAdminSearchModel()
    {
        return 
$this->getModelFromCache('XenForo_Model_AdminSearch');
    }

    
/**
    * Helper method to get the cache object.
    *
    * @return Zend_Cache_Core|Zend_Cache_Frontend|false
    */
    
protected function _getCache()
    {
        if (
$this->_cache === null)
        {
            
$this->_cache XenForo_Application::getCache();
        }

        return 
$this->_cache;
    }

    
/**
    * Factory method to get the named data writer. The class must exist or be autoloadable
    * or an exception will be thrown.
    *
    * @param string     $class Class to load
    * @param mixed      $errorHandler Error handler. See {@link ERROR_EXCEPTION} and related.
    * @param array|null $inject Dependencies to inject. See {@link __construct()}.
    *
    * @throws XenForo_Exception
    *
    * @return XenForo_DataWriter
    */
    
public static function create($class$errorHandler self::ERROR_ARRAY, array $inject null)
    {
        
$createClass XenForo_Application::resolveDynamicClass($class'datawriter');
        if (!
$createClass)
        {
            throw new 
XenForo_Exception("Invalid data writer '$class' specified");
        }

        return new 
$createClass($errorHandler$inject);
    }
}
Онлайн: 2
Реклама