Seditio Source
Root |
./othercms/xenForo 2.2.8/src/XF/Legacy/DataWriter.php
<?php

namespace XF\Legacy;

use
XF\Legacy\DataWriter\Phrase as XenForo_DataWriter_Phrase;

use function
array_key_exists, call_user_func_array, count, in_array, intval, is_array, is_integer, is_scalar, is_string, sizeof, strval;

abstract class
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 one.
     *
     * @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';

   
/**
    * Database object
    *
    * @var \XF\Db\AbstractAdapter
    */
   
protected $_db = null;

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

   
/**
    * 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 = [];

   
/**
    * 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 = [];

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

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

   
/**
     * 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 = [];

   
/**
    * 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 = [
       
'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 = [];

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

   
/**
    * Constructor.
    *
    * @param int $errorHandler Error handler. See {@link ERROR_EXCEPTION} and related.
    * @param array|null $inject Dependency injector. Array keys available: db, cache.
    */
   
public function __construct($errorHandler = self::ERROR_ARRAY, array $inject = null)
    {
       
$this->_db = \XF::db();

       
$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)
    *    * 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 = [];

        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 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 $tableName 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 $data Array of data containing the primary field or a scalar ID
    * @param string $primaryKeyField Primary key field, if data is an array
    * @param string $tableName 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 $dataArray Complete data record
     *
     * @return array
     */
   
public function getTablesDataFromArray(array $dataArray)
    {
       
$data = [];

       
/**
         * 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] = [];

            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 [];
    }

   
/**
    * 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 Name of the option to set
    * @param mixed  $value Value of the option
    */
   
public function setOption($name, $value)
    {
        if (!
array_key_exists($name, $this->_options))
        {
            throw new \
InvalidArgumentException('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 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 \
InvalidArgumentException('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 Name of the data
    * @param mixed  $value 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 Data that can uniquely ID this item
    * @param boolean $trustInputAsPrimary 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 = [$this->_getPrimaryTable() => $data];
        }
        else
        {
           
$existing = $this->_getExistingData($data);
            if (
is_array($existing) && !array_key_exists($this->_getPrimaryTable(), $existing))
            {
                throw new \
InvalidArgumentException('_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 $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(\XF::phrase($this->_existingDataErrorPhrase));
    }

   
/**
    * Sets a field. This value will be updated when save is called.
    *
    * @param string $field Name of field to update
    * @param string $value Value to update with
    * @param string $tableName Table name, if empty then all tables with that column
    * @param array  $options 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 \
LogicException('Set cannot be called after preSave has been called.');
        }

       
$validField = 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);
                }
            }
        }

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

        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 $field Name of field to update
     * @param string $newValue Value to update with
     * @param string $tableName Table name, if empty then all tables with that column
     * @param array  $options Options. See {@link $_setOptions).
     *
     * @return boolean
     */
   
protected function _setPostSave($field, $newValue, $tableName = '', array $options = [])
    {
       
$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 $fieldName Name of the field.
    * @param array  $fieldData Data about the field (includes type, contraints, callback, etc)
    * @param mixed  $value Value for the field
    * @param array  $options Options. Uses {@link $_setOptions}.
    *
    * @return boolean
    */
   
protected function _isFieldValueValid($fieldName, array $fieldData, &$value, array $options = [])
    {
       
$fieldType = $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, $fieldName, false);
                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 $fieldData 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 < 0 ? 0 : $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 ? 1 : 0);

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

                if (@
unserialize($value) === false && $value != serialize(false))
                {
                    throw new \
InvalidArgumentException('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 \
InvalidArgumentException('Value cannot be JSON decoded');
                }

                return
$value;

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

           
default:
                throw new \
InvalidArgumentException((
                    (
$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 $fieldType Type of the field. See the TYPE_* constants.
    * @param mixed  $value Value for the field.
    * @param array  $extraLimits Extra constraints
    *
    * @return boolean|string Either TRUE or an error message
    */
   
protected function _applyFieldValueLimits($fieldType, &$value, array $extraLimits = [])
    {
       
// 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($value, 0, $extraLimits['maxLength']);
                        }
                        else
                        {
                           
$value = substr($value, 0, $extraLimits['maxLength']);
                        }
                    }
                    else
                    {
                        return \
XF::phrase('please_enter_value_using_x_characters_or_fewer', ['count' => $extraLimits['maxLength']]);
                    }
                }
                break;

            case
self::TYPE_UINT_FORCED:
            case
self::TYPE_UINT:
                if (
$value < 0)
                {
                    if (
$this->_importMode)
                    {
                       
$value = 0;
                    }
                    else
                    {
                        return \
XF::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 \
XF::phrase('please_enter_number_that_is_at_least_x', ['min' => $extraLimits['min']]);
                    }
                }

                if (isset(
$extraLimits['max']) && $value > $extraLimits['max'])
                {
                    if (
$this->_importMode)
                    {
                       
$value = $extraLimits['max'];
                    }
                    else
                    {
                        return \
XF::phrase('please_enter_number_that_is_no_more_than_x', ['max' => $extraLimits['max']]);
                    }
                }
                break;
        }

        if (isset(
$extraLimits['allowedValues']) && is_array($extraLimits['allowedValues']) && !in_array($value, $extraLimits['allowedValues'], true))
        {
            return \
XF::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 Callback to run. Use an array with a string '$this' to callback to this object.
    * @param mixed    $value Value to verify
    * @param array    $fieldData Information about the field, including all constraints to be applied
    * @param string $fieldName
    *
    * @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,
            [&
$value, $this, $fieldName, $fieldData]
        );
    }

   
/**
    * Helper method to bulk set values from an array.
    *
    * @param array $fields Key-value pairs of fields and values.
    * @param array $options 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 Field name
    * @param string $tableName 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 = [];

       
// 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 = [];

       
// 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|\XF\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 \
XF\PrintableException($error);
        }
    }

   
/**
     * 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 (
$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 = [];

       
// 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')
    {
        return
0;
    }

   
/**
    * 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 \
XF\PrintableException($this->_errors);
            }
        }

        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 $tableName 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 \
LogicException('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, $field, false);
        }
        else if (isset(
$this->_fields[$tableName][$field]['required']) && is_string($this->_fields[$tableName][$field]['required']))
        {
           
$this->error(\XF::phrase($this->_fields[$tableName][$field]['required']), $field, false);
        }
        else
        {

           
$this->error(\XF::phrase('please_enter_value_for_required_field_x', ['field' => $field]), $field, false);
        }
    }

   
/**
     * 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|\XF\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(), $tableName, true);
        }
    }

   
/**
    * 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 $insertId Auto-increment value from 0.
    * @param string $tableName Name of the table set the auto increment field in
    * @param bool $updateAll 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 $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 $tableName 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 (
$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 ? [$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 \
LogicException('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 = [])
    {
       
$this->_getPhraseModel()->insertOrUpdateMasterPhrase($title, $text, $addOnId, $extra,  [
           
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,  [
           
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, [
           
//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()
    {
       
$this->_db->beginTransaction();
        return
true;
    }

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

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

   
/**
     * Performs a language rebuild if needed.
     */
   
protected function _performLanguageRebuild()
    {
    }

   
/**
     * 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 \XF\Legacy\Model
     */
   
public function getModelFromCache($class)
    {
        if (!isset(
self::$_modelCache[$class]))
        {
           
self::$_modelCache[$class] = \XF\Legacy\Model::create($class);
        }

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

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

   
/**
     * @return \XF\Legacy\Model\UserIgnore
     */
   
protected function _getUserIgnoreModel()
    {
        return
$this->getModelFromCache('XenForo_Model_UserIgnore');
    }

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

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

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

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

   
/**
    * 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 \InvalidArgumentException
    *
    * @return \XF\Legacy\DataWriter
    */
   
public static function create($class, $errorHandler = self::ERROR_ARRAY, array $inject = null)
    {
        if (
strpos($class, ':'))
        {
           
$createClass = \XF::stringToClass($class, '%s\Legacy\DataWriter\%s');
        }
        else if (
strpos($class, '\\'))
        {
           
$createClass = $class;
        }
        else
        {
           
$createClass = $class;
           
$createClass = preg_replace('/^XenForo_/', 'XF\\', $createClass);
           
$createClass = preg_replace('/(_|\\\\)DataWriter_/', '\\Legacy\\DataWriter\\', $createClass);
           
$createClass = str_replace('_', '\\', $createClass);
        }

        return new
$createClass($errorHandler, $inject);
    }
}