'Text', 'textarea' => 'Textarea', 'boolean' => 'Boolean', 'select' => 'Select', 'number' => 'Number', 'date' => 'Date', 'datetime' => 'Date/Time', ]; } public static function visibilityOptions(): array { return [ 'public' => 'Public', 'private' => 'Private', 'internal' => 'Internal', ]; } public function loadRows(): void { if (!$this->canUseFieldDefinition()) { return; } $this->rows = []; $definitions = RescueFieldDefinition::find() ->where(['module_id' => self::MODULE_ID]) ->orderBy(['group_key' => SORT_ASC, 'sort_order' => SORT_ASC, 'id' => SORT_ASC]) ->all(); foreach ($definitions as $definition) { $this->rows[] = [ 'id' => (int)$definition->id, 'field_key' => (string)$definition->field_key, 'label' => (string)$definition->label, 'group_key' => (string)$definition->group_key, 'input_type' => (string)$definition->input_type, 'required' => (int)$definition->required, 'is_core' => (int)$definition->is_core, 'is_active' => (int)$definition->is_active, 'visibility' => (string)$definition->visibility, 'sort_order' => (int)$definition->sort_order, ]; } } public function save(): bool { if (!$this->canUseFieldDefinition()) { $this->addError('rows', Yii::t('AnimalManagementModule.base', 'Field definition storage is unavailable.')); return false; } $this->clearValidationErrors(); $byId = []; foreach (RescueFieldDefinition::find()->where(['module_id' => self::MODULE_ID])->all() as $definition) { $byId[(int)$definition->id] = $definition; } $transaction = Yii::$app->db->beginTransaction(); try { $now = date('Y-m-d H:i:s'); foreach ($this->rows as $index => $row) { $id = (int)($row['id'] ?? 0); if ($id === 0 || !isset($byId[$id])) { continue; } $definition = $byId[$id]; $isCore = ((int)$definition->is_core) === 1; $remove = !empty($row['remove']); if ($remove && !$isCore) { if (!$definition->delete()) { $this->addError('rows', Yii::t('AnimalManagementModule.base', 'Could not remove field {field}.', [ '{field}' => (string)$definition->field_key, ])); } continue; } $label = trim((string)($row['label'] ?? '')); $groupKey = trim((string)($row['group_key'] ?? $definition->group_key)); $visibility = trim((string)($row['visibility'] ?? $definition->visibility)); $sortOrder = (int)($row['sort_order'] ?? $definition->sort_order); $required = !empty($row['required']) ? 1 : 0; $isActive = !empty($row['is_active']) ? 1 : 0; $inputType = trim((string)($row['input_type'] ?? $definition->input_type)); $options = trim((string)$definition->options); if ($label === '') { $this->addError('rows', Yii::t('AnimalManagementModule.base', 'Field label cannot be empty for {field}.', [ '{field}' => (string)$definition->field_key, ])); continue; } if ($groupKey === '') { $this->addError('rows', Yii::t('AnimalManagementModule.base', 'Group key cannot be empty for {field}.', [ '{field}' => (string)$definition->field_key, ])); continue; } if (!array_key_exists($visibility, self::visibilityOptions())) { $this->addError('rows', Yii::t('AnimalManagementModule.base', 'Invalid visibility for {field}.', [ '{field}' => (string)$definition->field_key, ])); continue; } if (!$isCore && !$this->validateSelectOptions($inputType, $options, (string)$definition->field_key)) { continue; } $definition->label = $label; $definition->group_key = $groupKey; $definition->visibility = $visibility; $definition->sort_order = $sortOrder; $definition->required = $isCore ? (int)$definition->required : $required; $definition->is_active = $isCore ? (int)$definition->is_active : $isActive; $definition->updated_at = $now; if (!$definition->save()) { $this->addError('rows', Yii::t('AnimalManagementModule.base', 'Could not save field {field}.', [ '{field}' => (string)$definition->field_key, ])); foreach ($definition->getFirstErrors() as $error) { $this->addError('rows', $error); } } $this->rows[$index]['is_core'] = (int)$definition->is_core; } if ($this->hasErrors()) { $transaction->rollBack(); return false; } if (!$this->saveNewField($now)) { $transaction->rollBack(); return false; } $transaction->commit(); return true; } catch (\Throwable $e) { $transaction->rollBack(); Yii::error($e, 'animal_management.field_definition_settings'); $this->addError('rows', Yii::t('AnimalManagementModule.base', 'Unexpected error while saving field settings.')); return false; } } public function canUseFieldDefinition(): bool { if (!class_exists(RescueFieldDefinition::class)) { return false; } return Yii::$app->db->schema->getTableSchema('rescue_field_definition', true) !== null; } private function saveNewField(string $now): bool { $newFieldKey = strtolower(trim($this->new_field_key)); $newLabel = trim($this->new_label); if ($newFieldKey === '' && $newLabel === '') { return true; } if ($newFieldKey === '' || $newLabel === '') { $this->addError('rows', Yii::t('AnimalManagementModule.base', 'Both new field key and label are required.')); return false; } if (!preg_match('/^[a-z0-9_]+$/', $newFieldKey)) { $this->addError('rows', Yii::t('AnimalManagementModule.base', 'New field key must contain only lowercase letters, numbers, and underscores.')); return false; } if (!array_key_exists($this->new_input_type, self::inputTypeOptions())) { $this->addError('rows', Yii::t('AnimalManagementModule.base', 'Invalid input type for new field.')); return false; } if (!array_key_exists($this->new_visibility, self::visibilityOptions())) { $this->addError('rows', Yii::t('AnimalManagementModule.base', 'Invalid visibility for new field.')); return false; } $groupKey = trim($this->new_group_key); if ($groupKey === '') { $this->addError('rows', Yii::t('AnimalManagementModule.base', 'New field group key is required.')); return false; } $options = trim($this->new_options); if (!$this->validateSelectOptions($this->new_input_type, $options, $newFieldKey)) { return false; } $exists = RescueFieldDefinition::find() ->where(['module_id' => self::MODULE_ID, 'field_key' => $newFieldKey]) ->exists(); if ($exists) { $this->addError('rows', Yii::t('AnimalManagementModule.base', 'Field key {key} already exists.', ['{key}' => $newFieldKey])); return false; } $field = new RescueFieldDefinition(); $field->module_id = self::MODULE_ID; $field->group_key = $groupKey; $field->field_key = $newFieldKey; $field->label = $newLabel; $field->input_type = $this->new_input_type; $field->required = $this->new_required ? 1 : 0; $field->is_core = 0; $field->is_active = 1; $field->visibility = $this->new_visibility; $field->sort_order = (int)$this->new_sort_order; $field->options = $options !== '' ? $options : '{}'; $field->created_at = $now; $field->updated_at = $now; if (!$field->save()) { $this->addError('rows', Yii::t('AnimalManagementModule.base', 'Could not create new field.')); foreach ($field->getFirstErrors() as $error) { $this->addError('rows', $error); } return false; } $this->new_field_key = ''; $this->new_label = ''; $this->new_input_type = 'text'; $this->new_group_key = 'animal_profile'; $this->new_visibility = 'public'; $this->new_required = 0; $this->new_sort_order = 500; $this->new_options = ''; return true; } private function clearValidationErrors(): void { foreach (array_keys($this->errors) as $attribute) { $this->clearErrors($attribute); } } private function validateSelectOptions(string $inputType, string $options, string $fieldKey): bool { if ($inputType !== 'select') { return true; } if ($options === '' || $options === '{}') { $this->addError('rows', Yii::t('AnimalManagementModule.base', 'Select field {field} requires options JSON or comma/newline list.', [ '{field}' => $fieldKey, ])); return false; } $decoded = json_decode($options, true); if (json_last_error() === JSON_ERROR_NONE) { if (!is_array($decoded) || empty($decoded)) { $this->addError('rows', Yii::t('AnimalManagementModule.base', 'Select field {field} options JSON must be a non-empty array/object.', [ '{field}' => $fieldKey, ])); return false; } return true; } $parts = preg_split('/[\r\n,]+/', $options); $parts = array_filter(array_map(static function ($item) { return trim((string)$item); }, $parts)); if (empty($parts)) { $this->addError('rows', Yii::t('AnimalManagementModule.base', 'Select field {field} requires at least one option.', [ '{field}' => $fieldKey, ])); return false; } return true; } }