YetiForceCompany/YetiForceCRM

View on GitHub
app/Fields/Picklist/Item.php

Summary

Maintainability
F
4 days
Test Coverage
F
0%
<?php
/**
 * Picklist value item.
 *
 * @package App
 *
 * @copyright YetiForce S.A.
 * @license   YetiForce Public License 6.5 (licenses/LicenseEN.txt or yetiforce.com)
 * @author    Radosław Skrzypczak <r.skrzypczak@yetiforce.com>
 */

namespace App\Fields\Picklist;

/**
 * Picklist value item class.
 */
class Item extends \App\Base
{
    /** @var int Item ID */
    protected $id;
    /** @var string Item name */
    protected $name;
    /** @var int Permission level */
    protected $presence = 1;
    /** @var string Description */
    protected $description;
    /** @var string Prefix for numbering */
    protected $prefix;
    /** @var string Icon */
    protected $icon;
    /** @var string Color */
    protected $color;
    /** @var int Sort ID */
    protected $sortorderid;
    /** @var int Item ID for role */
    protected $valueid;
    /** @var int Record state */
    protected $record_state;
    /** @var int Time counting */
    protected $time_counting;
    /** @var int State for the record */
    protected $close_state;
    /** @var \Settings_Picklist_Field_Model */
    protected $fieldModel;
    /** @var array Changes */
    protected $changes = [];
    /** @var string[] Role IDs */
    protected $roles;

    /**
     * Get instance.
     *
     * @param \Vtiger_Field_Model $fieldModel
     * @param int|null            $id
     *
     * @return self
     */
    public static function getInstance(\Vtiger_Field_Model $fieldModel, ?int $id): self
    {
        $instance = new self();
        $instance->fieldModel = $fieldModel;
        if ($id) {
            $data = \App\Fields\Picklist::getValues($fieldModel->getName())[$id];
            $instance->id = $id;
            $instance->name = $data['picklistValue'];
            $instance->presence = (int) $data['presence'];
            $instance->description = $data['description'] ?? null;
            $instance->prefix = $data['prefix'] ?? null;
            $instance->icon = $data['icon'] ?? null;
            $instance->color = $data['color'] ?? null;
            $instance->sortorderid = (int) $data['sortorderid'];
            $instance->valueid = isset($data['picklist_valueid']) ? (int) $data['picklist_valueid'] : null;
            $instance->record_state = isset($data['record_state']) ? (int) $data['record_state'] : null;
            $instance->time_counting = isset($data['time_counting']) ? (int) $data['time_counting'] : null;
            $instance->close_state = (int) (new \App\Db\Query())->from('u_#__picklist_close_state')->where(['valueid' => $instance->valueid, 'fieldid' => $fieldModel->getId()])->exists();
        }

        return $instance;
    }

    /**
     * Function to get the Id.
     *
     * @return int
     */
    public function getId(): int
    {
        return (int) $this->id;
    }

    /** {@inheritdoc} */
    public function set($key, $value)
    {
        $propertyExists = \property_exists($this, $key);
        if ($this->getId() && !\in_array($key, ['id']) && ($propertyExists && $this->{$key} !== $value && (null !== $this->{$key} || '' !== $value)) && !\array_key_exists($key, $this->changes)) {
            $this->changes[$key] = $this->get($key);
        }
        return $propertyExists ? $this->{$key} = $value : parent::set($key, $value);
    }

    /** {@inheritdoc} */
    public function get($key)
    {
        return \property_exists($this, $key) ? $this->{$key} : parent::get($key);
    }

    /**
     * Gets field datatypes.
     *
     * @return array
     */
    public function getDbTypes(): array
    {
        return [
            'description' => \yii\db\Schema::TYPE_TEXT,
            'prefix' => [\yii\db\Schema::TYPE_STRING, 30],
            'color' => [\yii\db\Schema::TYPE_STRING, 25],
            'icon' => [\yii\db\Schema::TYPE_STRING, 255]
        ];
    }

    /**
     * Save.
     *
     * @return bool
     */
    public function save(): bool
    {
        try {
            $this->validate();
            $result = $this->saveToDb();
            if ($this->getPreviousValue('name')) {
                $this->rename();
            }
        } catch (\Throwable $e) {
            \App\Log::error($e->__toString());
            throw $e;
        }
        \App\Fields\Picklist::clearCache($this->fieldModel->getName(), $this->fieldModel->getModuleName());
        return $result;
    }

    /**
     * Save data to database.
     *
     * @return bool
     */
    public function saveToDb(): bool
    {
        $db = \App\Db::getInstance();
        $result = false;
        $fieldName = $this->fieldModel->getName();
        $primaryKey = \App\Fields\Picklist::getPickListId($fieldName);
        $baseTable = $this->getTableName();

        $dataForSave = $this->getValuesToSave();
        foreach ($this->getDbTypes() as $column => $type) {
            if (isset($dataForSave[$baseTable][$column])) {
                $length = null;
                if (\is_array($type)) {
                    [$type, $length] = $type;
                }
                $criteria = $db->getSchema()->createColumnSchemaBuilder($type, $length)->defaultValue('');
                \vtlib\Utils::addColumn($baseTable, $column, $criteria);
            }
        }

        $transaction = $db->beginTransaction();
        try {
            foreach ($dataForSave as $tableName => $tableData) {
                if (!$this->getId() && $baseTable === $tableName) {
                    if ($this->fieldModel->isRoleBased()) {
                        $vId = $db->getUniqueID('vtiger_picklistvalues');
                        $tableData['picklist_valueid'] = $vId;
                        $this->set('valueid', $vId);
                    }
                    $result = $db->createCommand()->insert($tableName, $tableData)->execute();
                    $this->id = $db->getLastInsertID($tableName . '_' . $primaryKey . '_seq');
                } elseif ($baseTable === $tableName) {
                    $db->createCommand()->update($tableName, $tableData, [$primaryKey => $this->getId()])->execute();
                } elseif ('u_#__picklist_close_state' === $tableName) {
                    $db->createCommand()->delete($tableName, ['fieldid' => $this->fieldModel->getId(), 'valueid' => $this->valueid])->execute();
                    if ($this->close_state) {
                        $db->createCommand()->insert($tableName, ['fieldid' => $this->fieldModel->getId(), 'valueid' => $this->valueid, 'value' => $this->name])->execute();
                    }
                }
            }
            $this->updateRolePermissions();

            $transaction->commit();
        } catch (\Throwable $ex) {
            $transaction->rollBack();
            \App\Log::error($ex->__toString());
            throw $ex;
        }
        return (bool) $result;
    }

    /**
     * Update role permissions.
     *
     * @return void
     */
    public function updateRolePermissions()
    {
        if ($this->valueid && null !== $this->roles) {
            $dbCommand = \App\Db::getInstance()->createCommand();
            $dbCommand->delete('vtiger_role2picklist', ['picklistvalueid' => $this->valueid])->execute();
            if ($this->roles) {
                $picklistId = \App\Fields\Picklist::getPicklistIdNr($this->fieldModel->getName());
                if ($insertValueList = array_map(fn ($roleid) => [$roleid, $this->valueid, $picklistId], $this->roles)) {
                    $dbCommand->batchInsert('vtiger_role2picklist', ['roleid', 'picklistvalueid', 'picklistid'], $insertValueList)->execute();
                }
            }
        }
    }

    /**
     * Get table name.
     *
     * @return string
     */
    public function getTableName(): string
    {
        return \App\Fields\Picklist::getPickListTableName($this->fieldModel->getName());
    }

    /**
     * Get next sequence number.
     *
     * @return int
     */
    public function getNextSeq(): int
    {
        return (int) (new \App\Db\Query())->from($this->getTableName())->max('sortorderid') + 1;
    }

    /**
     * Get writable fields.
     *
     * @return array
     */
    public function getWritableFields(): array
    {
        return ['name', 'presence', 'sortorderid', 'presence', 'description', 'prefix', 'icon', 'record_state', 'time_counting', 'close_state'];
    }

    /**
     * Get field model.
     *
     * @return \Settings_Picklist_Field_Model
     */
    public function getFieldModel(): \Settings_Picklist_Field_Model
    {
        return $this->fieldModel;
    }

    /**
     * Check if item is deletable.
     *
     * @return bool
     */
    public function isDeletable(): bool
    {
        return 1 === $this->presence && $this->fieldModel->isEditable();
    }

    /**
     * Rename item value in other data.
     *
     * @return void
     */
    public function rename()
    {
        $newValue = $this->name;
        $previousValue = $this->getPreviousValue('name');
        $fieldName = $this->fieldModel->getName();

        $dbCommand = \App\Db::getInstance()->createCommand();
        $dataReader = (new \App\Db\Query())->select(['tablename', 'columnname', 'fieldid', 'tabid'])
            ->from('vtiger_field')
            ->where(['fieldname' => $fieldName, 'uitype' => [15, 16, 33]])
            ->createCommand()->query();
        while ($row = $dataReader->read()) {
            $tableName = $row['tablename'];
            $columnName = $row['columnname'];
            $dbCommand->update($tableName, [$columnName => $newValue], [$columnName => $previousValue])
                ->execute();
            $dbCommand->update('vtiger_field', ['defaultvalue' => $newValue], ['defaultvalue' => $previousValue, 'fieldid' => $row['fieldid']])
                ->execute();
            $moduleName = \App\Module::getModuleName($row['tabid']);

            \App\Fields\Picklist::clearCache($fieldName, $moduleName);
            $eventHandler = new \App\EventHandler();
            $eventHandler->setParams([
                'fieldname' => $fieldName,
                'oldvalue' => $previousValue,
                'newvalue' => $newValue,
                'module' => $moduleName,
                'id' => $this->getId(),
            ]);
            $eventHandler->trigger('PicklistAfterRename');
        }
        \App\Fields\Picklist::clearCache($fieldName, $this->fieldModel->getModuleName());
    }

    /**
     * Delete item.
     *
     * @param int $replaceId Item ID
     *
     * @return void
     */
    public function delete(int $replaceId)
    {
        $db = \App\Db::getInstance();
        $fieldName = $this->fieldModel->getName();
        $transaction = $db->beginTransaction();
        try {
            $dbCommand = $db->createCommand();

            $primaryKey = \App\Fields\Picklist::getPickListId($this->fieldModel->getName());
            $replaceValue = \App\Purifier::decodeHtml((new \App\Db\Query())->select([$this->fieldModel->getName()])
                ->from($this->getTableName())
                ->where([$primaryKey => $replaceId])
                ->scalar());

            if ($this->fieldModel->isRoleBased()) {
                $dbCommand->delete('vtiger_role2picklist', ['picklistvalueid' => $this->valueid])->execute();
                $dbCommand->delete('u_#__picklist_close_state', ['valueid' => $this->valueid])->execute();
            }
            $dbCommand->delete($this->getTableName(), [$primaryKey => $this->getId()])->execute();
            $dependencyId = (new \App\Db\Query())->select(['s_#__picklist_dependency.id'])->from('s_#__picklist_dependency')->innerJoin('s_#__picklist_dependency_data', 's_#__picklist_dependency_data.id = s_#__picklist_dependency.id')
                ->where(['source_field' => $this->fieldModel->getId(), 'source_id' => $this->getId()])->column();
            if ($dependencyId) {
                $dbCommand->delete('s_#__picklist_dependency_data', ['id' => $dependencyId, 'source_id' => $this->getId()])->execute();
            }

            $dataReader = (new \App\Db\Query())->select(['tablename', 'columnname', 'fieldid', 'tabid'])
                ->from('vtiger_field')
                ->where(['fieldname' => $fieldName, 'uitype' => [15, 16, 33]])
                ->createCommand()->query();
            while ($row = $dataReader->read()) {
                $moduleName = \App\Module::getModuleName($row['tabid']);
                $tableName = $row['tablename'];
                $columnName = $row['columnname'];
                $dbCommand->update($tableName, [$columnName => $replaceValue], [$columnName => $this->name])
                    ->execute();
                $dbCommand->update('vtiger_field', ['defaultvalue' => $replaceValue], ['defaultvalue' => $this->name, 'fieldid' => $row['fieldid']])
                    ->execute();

                \App\Fields\Picklist::clearCache($fieldName, $moduleName);
                $eventHandler = new \App\EventHandler();
                $eventHandler->setParams([
                    'fieldname' => $fieldName,
                    'valuetodelete' => [$this->name],
                    'replacevalue' => $replaceValue,
                    'module' => $moduleName,
                ]);
                $eventHandler->trigger('PicklistAfterDelete');
            }
            $dataReader->close();

            $transaction->commit();
        } catch (\Throwable $ex) {
            $transaction->rollBack();
            \App\Log::error($ex->__toString());
            throw $ex;
        }

        \App\Fields\Picklist::clearCache($fieldName, $this->fieldModel->getModuleName());
    }

    /**
     * Function formats data for saving.
     *
     * @return array
     */
    private function getValuesToSave(): array
    {
        $forSave = [];
        $tableName = $this->getTableName();
        if (!$this->getId()) {
            $forSave[$this->getTableName()] = [
                'sortorderid' => $this->getNextSeq(),
                'presence' => $this->presence,
            ];
        }
        $fields = $this->getId() ? array_keys($this->changes) : $this->getWritableFields();
        foreach ($fields as $name) {
            $itemPropertyModel = $this->getFieldInstanceByName($name);
            if ($itemPropertyModel && isset($this->{$name}) && ($this->getId() || (!$this->getId() && '' !== $this->{$name}))) {
                $this->validateValue($name, $this->{$name});
                $forSave[$itemPropertyModel->getTableName()][$itemPropertyModel->getColumnName()] = $this->{$name};
            } elseif (isset($this->{$name}) && ($this->getId() || (!$this->getId() && '' !== $this->{$name}))) {
                $this->validateValue($name, $this->{$name});
                $forSave[$tableName][$name] = $this->{$name};
            }
        }

        return $forSave;
    }

    /**
     * Get pervious value by field.
     *
     * @param string $fieldName
     *
     * @return mixed
     */
    public function getPreviousValue(?string $fieldName = '')
    {
        return $fieldName ? ($this->changes[$fieldName] ?? null) : $this->changes;
    }

    /** {@inheritdoc} */
    public function getData()
    {
        $data = [];
        foreach (get_object_vars($this) as $name => $value) {
            if (\is_object($value) || 'value' === $name || 'changes' === $name || null === $value) {
                continue;
            }
            if (\is_array($value)) {
                $value = implode(',', $value);
            }
            $data[$name] = $value;
        }

        return $data;
    }

    /**
     * Get fields for edit.
     *
     * @return array
     */
    public function getEditFields(): array
    {
        $fields = [];
        $editFields = ['name'];
        $editFields[] = 'icon';
        if ($this->fieldModel->getModule()->isEntityModule()) {
            if (!$this->getId()) {
                $editFields[] = 'roles';
            }
            $editFields[] = 'description';
            $editFields[] = 'prefix';
            if ($this->fieldModel->getFieldParams()['isProcessStatusField'] ?? false) {
                if (\App\Db::getInstance()->getTableSchema($this->getTableName())->getColumn('time_counting')) {
                    $editFields[] = 'time_counting';
                }
                $editFields[] = 'record_state';
            }
            if (15 === $this->fieldModel->getUIType()) {
                $editFields[] = 'close_state';
            }
        }

        foreach ($editFields as $fieldName) {
            $propertyModel = $this->getFieldInstanceByName($fieldName);
            if (null !== $this->get($fieldName)) {
                $propertyModel->set('fieldvalue', $this->get($fieldName));
            } elseif (($defaultValue = $propertyModel->get('defaultvalue')) !== null) {
                $propertyModel->set('fieldvalue', $defaultValue);
            }
            $fields[$fieldName] = $propertyModel;
        }

        return $fields;
    }

    /**
     * Basic validation.
     *
     * @return array
     */
    public function validate(): array
    {
        $response = [];
        if ($this->isDuplicateValue()) {
            $response[] = [
                'result' => false,
                'message' => \App\Language::translate('LBL_DUPLICATE', 'Settings:Picklist')
            ];
        }
        return $response;
    }

    /**
     * Check if picklist value exists.
     *
     * @return bool
     */
    public function isDuplicateValue(): bool
    {
        $picklistValues = \App\Fields\Picklist::getValuesName($this->fieldModel->getName());
        if ($this->id) {
            unset($picklistValues[$this->id]);
        }

        return \in_array(strtolower($this->name), array_map('strtolower', $picklistValues));
    }

    /**
     * Validate item data.
     *
     * @param string $fieldName
     * @param mixed  $value
     *
     * @return void
     */
    public function validateValue(string $fieldName, $value)
    {
        switch ($fieldName) {
            case 'name':
                $itemPropertyModel = $this->getFieldInstanceByName($fieldName);
                $itemPropertyModel->getUITypeModel()->validate($value, false);
                if (empty($value)) {
                    throw new \App\Exceptions\IllegalValue("LBL_NOT_FILLED_MANDATORY_FIELDS||{$fieldName}", 512);
                }
                if (preg_match('/[\<\>\"\#]/', $value)) {
                    throw new \App\Exceptions\IllegalValue("ERR_SPECIAL_CHARACTERS_NOT_ALLOWED||{$fieldName}||{$value}", 512);
                }
                if ($itemPropertyModel->getMaxValue() && \strlen($value) > $itemPropertyModel->getMaxValue()) {
                    throw new \App\Exceptions\IllegalValue("ERR_EXCEEDED_NUMBER_CHARACTERS||{$fieldName}||{$value}", 406);
                }
                if ($this->isDuplicateValue($value, $this->getId())) {
                    throw new \App\Exceptions\IllegalValue("ERR_DUPLICATES_VALUES_FOUND||{$fieldName}||{$value}", 513);
                }
                break;
            case 'color':
            case 'description':
            case 'prefix':
            case 'close_state':
            case 'icon':
                $itemPropertyModel = $this->getFieldInstanceByName($fieldName);
                $itemPropertyModel->getUITypeModel()->validate($value, false);
                break;
            case 'time_counting':
            case 'record_state':
            case 'sortorderid':
                if (!\App\Validator::integer($value)) {
                    throw new \App\Exceptions\IllegalValue("ERR_NOT_ALLOWED_VALUE||{$fieldName}||{$value}", 406);
                }
                break;
            case 'presence':
                if (1 !== $value && 0 !== $value) {
                    throw new \App\Exceptions\IllegalValue("ERR_NOT_ALLOWED_VALUE||{$fieldName}||{$value}", 406);
                }
                break;
            default:
                throw new \App\Exceptions\IllegalValue("ERR_NOT_ALLOWED_VALUE||{$fieldName}||{$value}", 406);
        }
    }

    /**
     * Get fields instance by name.
     *
     * @param string $name
     *
     * @return Vtiger_Field_Model
     */
    public function getFieldInstanceByName($name)
    {
        $params = [];
        $qualifiedModuleName = 'Settings:Picklist';
        $tableName = $this->getTableName();
        switch ($name) {
            case 'name':
                $params = [
                    'name' => $name,
                    'column' => $this->fieldModel->getName(),
                    'label' => 'LBL_ITEM_VALUE',
                    'uitype' => 1,
                    'typeofdata' => 'V~M',
                    'maximumlength' => $this->fieldModel->getMaxValue(),
                    'purifyType' => \App\Purifier::TEXT,
                    'table' => $tableName,
                    'validator' => [['name' => 'FieldLabel']]
                ];
                if (1 !== $this->presence || !$this->fieldModel->isEditable()) {
                    $params['isEditableReadOnly'] = true;
                }
                break;
            case 'description':
                $params = [
                    'name' => $name,
                    'column' => $name,
                    'label' => 'LBL_DESCRIPTION',
                    'uitype' => 300,
                    'typeofdata' => 'V~O',
                    'maximumlength' => '65535',
                    'purifyType' => \App\Purifier::HTML,
                    'tooltip' => 'LBL_DESCRIPTION_VALUE_LIST',
                    'table' => $tableName
                ];
                break;
            case 'prefix':
                $params = [
                    'name' => $name,
                    'column' => $name,
                    'label' => 'LBL_PREFIX',
                    'uitype' => 1,
                    'typeofdata' => 'V~O',
                    'maximumlength' => '25',
                    'purifyType' => \App\Purifier::TEXT,
                    'tooltip' => 'LBL_DESCRIPTION_PREFIXES',
                    'table' => $tableName
                ];
                break;
            case 'close_state':
                $params = [
                    'name' => $name,
                    'column' => $name,
                    'label' => 'LBL_CLOSES_RECORD',
                    'uitype' => 56,
                    'typeofdata' => 'C~O',
                    'maximumlength' => '5',
                    'purifyType' => \App\Purifier::BOOL,
                    'tooltip' => 'LBL_BLOCKED_RECORD_INFO',
                    'table' => 'u_#__picklist_close_state'
                ];
                break;
            case 'icon':
                $params = [
                    'name' => $name,
                    'column' => $name,
                    'label' => 'LBL_ICON',
                    'uitype' => 62,
                    'typeofdata' => 'V~O',
                    'maximumlength' => '255',
                    'purifyType' => \App\Purifier::TEXT,
                    'table' => $tableName
                ];
                break;
            case 'time_counting':
                $params = [
                    'name' => $name,
                    'column' => $name,
                    'label' => 'LBL_TIME_COUNTING',
                    'uitype' => 16,
                    'typeofdata' => 'V~M',
                    'maximumlength' => '250',
                    'purifyType' => \App\Purifier::INTEGER,
                    'tooltip' => 'LBL_TIME_COUNTING_INFO',
                    'defaultvalue' => 0,
                    'picklistValues' => [
                        0 => \App\Language::translate('LBL_NONE', '_Base'),
                        \App\RecordStatus::TIME_COUNTING_REACTION => \App\Language::translate('LBL_TIME_COUNTING_REACTION', $qualifiedModuleName),
                        \App\RecordStatus::TIME_COUNTING_RESOLVE => \App\Language::translate('LBL_TIME_COUNTING_RESOLVE', $qualifiedModuleName),
                        \App\RecordStatus::TIME_COUNTING_IDLE => \App\Language::translate('LBL_TIME_COUNTING_IDLE', $qualifiedModuleName)
                    ],
                    'table' => $tableName
                ];
                break;
            case 'record_state':
                $params = [
                    'name' => $name,
                    'column' => $name,
                    'label' => 'LBL_RECORD_STATE',
                    'uitype' => 16,
                    'typeofdata' => 'V~M',
                    'maximumlength' => '250',
                    'purifyType' => \App\Purifier::INTEGER,
                    'tooltip' => 'LBL_RECORD_STATE_INFO',
                    'defaultvalue' => \App\RecordStatus::RECORD_STATE_NO_CONCERN,
                    'picklistValues' => [],
                    'table' => $tableName
                ];
                foreach (\App\RecordStatus::getLabels() as $key => $value) {
                    $params['picklistValues'][$key] = \App\Language::translate($value, $qualifiedModuleName);
                }
                break;
            case 'roles':
                $params = [
                    'name' => $name,
                    'column' => $name,
                    'label' => 'LBL_ASSIGN_TO_ROLE',
                    'uitype' => 33,
                    'typeofdata' => 'V~O',
                    'maximumlength' => '500',
                    'purifyType' => \App\Purifier::TEXT,
                    'defaultvalue' => 'all',
                    'picklistValues' => [
                        'all' => \App\Language::translate('LBL_ALL_ROLES', $qualifiedModuleName)
                    ],
                    'table' => $tableName
                ];
                foreach (\Settings_Roles_Record_Model::getAll() as $key => $roleModel) {
                    $params['picklistValues'][$key] = \App\Language::translate($roleModel->get('rolename'), 'Settings:Roles');
                }
                break;
            default:
                break;
        }

        return $params ? \Vtiger_Field_Model::init($qualifiedModuleName, $params, $name)->set('sourceFieldModel', $this->fieldModel) : null;
    }
}