Files
animal_management/models/forms/AnimalForm.php
2026-04-04 13:13:00 -04:00

1025 lines
36 KiB
PHP

<?php
namespace humhub\modules\animal_management\models\forms;
use humhub\modules\animal_management\models\Animal;
use humhub\modules\animal_management\models\AnimalFieldValue;
use humhub\modules\animal_management\models\AnimalGalleryItem;
use humhub\modules\content\components\ContentContainerActiveRecord;
use humhub\modules\rescue_foundation\components\UploadStandards;
use humhub\modules\rescue_foundation\models\RescueFieldDefinition;
use humhub\modules\space\models\Space;
use Yii;
use yii\base\Model;
use yii\helpers\FileHelper;
use yii\helpers\Json;
use yii\web\UploadedFile;
class AnimalForm extends Model
{
public const MODULE_ID = 'animal_management';
public ContentContainerActiveRecord $contentContainer;
public string $name = '';
public string $species = '';
public string $breed = '';
public string $sex = '';
public string $status = Animal::STATUS_INTAKE;
public bool $in_possession = true;
public string $location_name = '';
public string $city = '';
public string $state = '';
public string $zip = '';
public string $public_summary = '';
public string $medical_notes = '';
public UploadedFile|string|null $coverImageFile = null;
public UploadedFile|string|null $profileImageFile = null;
public bool $removeCoverImage = false;
public bool $removeProfileImage = false;
public string $coverImageGalleryPath = '';
public string $profileImageGalleryPath = '';
public array $tileDisplayFields = [];
public array $heroDisplayFields = [];
public array $customFields = [];
private array $fieldConfig = [];
private array $customFieldDefinitions = [];
private ?Animal $animal = null;
private ?string $existingCoverImagePath = null;
private ?string $existingProfileImagePath = null;
public function init()
{
parent::init();
$this->bootstrapFieldConfig();
}
public function rules()
{
return [
[['species', 'breed', 'sex', 'location_name', 'city', 'state', 'zip'], 'string', 'max' => 120],
[['name'], 'string', 'max' => 190],
[['status'], 'in', 'range' => array_keys(Animal::statusOptions())],
[['public_summary', 'medical_notes'], 'string'],
[['in_possession'], 'boolean'],
[['state'], 'string', 'max' => 2],
[['zip'], 'string', 'max' => 10],
[['removeCoverImage', 'removeProfileImage'], 'boolean'],
[['coverImageGalleryPath', 'profileImageGalleryPath'], 'string', 'max' => 500],
[['coverImageGalleryPath', 'profileImageGalleryPath'], 'validateGalleryImageSelection'],
[['tileDisplayFields', 'heroDisplayFields'], 'default', 'value' => []],
[['tileDisplayFields', 'heroDisplayFields'], 'each', 'rule' => ['in', 'range' => array_keys(DisplaySettingsForm::fieldOptions())]],
[['coverImageFile', 'profileImageFile'], 'file',
'skipOnEmpty' => true,
'extensions' => UploadStandards::imageExtensions(),
'checkExtensionByMimeType' => true,
'mimeTypes' => UploadStandards::imageMimeTypes(),
'maxSize' => UploadStandards::IMAGE_MAX_BYTES,
],
[['customFields'], 'safe'],
[['name', 'species', 'breed', 'sex', 'status', 'location_name', 'city', 'state', 'zip', 'public_summary', 'medical_notes'], 'validateConfiguredRequiredFields'],
[['customFields'], 'validateCustomFieldValues'],
];
}
public function save(): bool
{
if (!$this->validate()) {
return false;
}
$transaction = Yii::$app->db->beginTransaction();
try {
$animal = $this->animal ?: new Animal();
$animal->contentcontainer_id = $this->contentContainer->contentcontainer_id;
$animal->name = trim($this->name);
$animal->species = trim($this->species);
$animal->breed = trim($this->breed);
$animal->sex = trim($this->sex);
$animal->status = $this->status;
$animal->in_possession = $this->in_possession ? 1 : 0;
$animal->public_summary = trim($this->public_summary);
$animal->medical_notes = trim($this->medical_notes);
if ($this->in_possession) {
$animal->location_name = $this->getDefaultLocationName();
$animal->city = '';
$animal->state = '';
$animal->zip = '';
} else {
$animal->location_name = trim($this->location_name);
$animal->city = trim($this->city);
$animal->state = strtoupper(trim($this->state));
$animal->zip = trim($this->zip);
}
if (!$animal->save()) {
$this->addErrors($animal->getErrors());
$transaction->rollBack();
return false;
}
if (!$this->saveCustomFieldValues($animal)) {
$transaction->rollBack();
return false;
}
if (!$this->saveImageFields($animal)) {
$transaction->rollBack();
return false;
}
if (!$this->saveDisplayFieldOverrides($animal)) {
$transaction->rollBack();
return false;
}
$transaction->commit();
$this->animal = $animal;
return true;
} catch (\Throwable $e) {
$transaction->rollBack();
Yii::error($e, 'animal_management.animal_form_save');
$message = (string)$e->getMessage();
if (stripos($message, 'Permission denied') !== false) {
$this->addError('customFields', Yii::t('AnimalManagementModule.base', 'Could not save uploaded image due to file permission restrictions. Please contact your administrator.'));
} else {
$this->addError('customFields', Yii::t('AnimalManagementModule.base', 'Unexpected error while saving animal profile.'));
}
return false;
}
}
public function getAnimal(): ?Animal
{
return $this->animal;
}
public function setAnimal(Animal $animal): void
{
$this->animal = $animal;
$this->name = (string)$animal->name;
$this->species = (string)$animal->species;
$this->breed = (string)$animal->breed;
$this->sex = (string)$animal->sex;
$this->status = (string)$animal->status;
$this->in_possession = ((int)$animal->in_possession) === 1;
$this->location_name = (string)$animal->location_name;
$this->city = (string)$animal->city;
$this->state = (string)$animal->state;
$this->zip = (string)$animal->zip;
$this->public_summary = (string)$animal->public_summary;
$this->medical_notes = (string)$animal->medical_notes;
$this->loadCustomFieldValues($animal);
$this->existingCoverImagePath = $this->getImageFieldValue($animal, 'cover_image_url');
$this->existingProfileImagePath = $this->getImageFieldValue($animal, 'profile_image_url');
$this->coverImageGalleryPath = (string)($this->existingCoverImagePath ?? '');
$this->profileImageGalleryPath = (string)($this->existingProfileImagePath ?? '');
$this->tileDisplayFields = $this->readDisplayFieldOverride($animal, 'tile_display_fields');
$this->heroDisplayFields = $this->readDisplayFieldOverride($animal, 'hero_display_fields');
}
public function getStatusOptions(): array
{
return Animal::statusOptions();
}
public function isFieldActive(string $fieldKey): bool
{
if (!isset($this->fieldConfig[$fieldKey])) {
return true;
}
return (int)$this->fieldConfig[$fieldKey]['is_active'] === 1;
}
public function isFieldRequired(string $fieldKey): bool
{
if (!isset($this->fieldConfig[$fieldKey])) {
return false;
}
return (int)$this->fieldConfig[$fieldKey]['required'] === 1;
}
public function getFieldLabel(string $fieldKey): string
{
if (!isset($this->fieldConfig[$fieldKey])) {
return $this->getAttributeLabel($fieldKey);
}
$label = trim((string)$this->fieldConfig[$fieldKey]['label']);
return $label !== '' ? $label : $this->getAttributeLabel($fieldKey);
}
public function getCustomFieldDefinitions(): array
{
return $this->customFieldDefinitions;
}
public function getCustomFieldSelectOptions(string $fieldKey): array
{
if (!isset($this->customFieldDefinitions[$fieldKey])) {
return [];
}
return $this->parseSelectOptions((string)$this->customFieldDefinitions[$fieldKey]['options']);
}
public function getExistingCoverImagePath(): ?string
{
return $this->existingCoverImagePath;
}
public function getExistingProfileImagePath(): ?string
{
return $this->existingProfileImagePath;
}
public function getGalleryImageOptions(): array
{
if (!$this->animal instanceof Animal || Yii::$app->db->schema->getTableSchema('rescue_animal_gallery_item', true) === null) {
return [];
}
$items = AnimalGalleryItem::find()
->where(['animal_id' => (int)$this->animal->id])
->orderBy(['id' => SORT_DESC])
->all();
$options = [];
foreach ($items as $item) {
$url = trim((string)$item->getImageUrl());
if ($url === '' || isset($options[$url]) || !$this->isResolvableImageUrl($url)) {
continue;
}
$options[$url] = '#' . (int)$item->id . ' - ' . basename((string)parse_url($url, PHP_URL_PATH));
}
return $options;
}
public function validateConfiguredRequiredFields(string $attribute): void
{
if (!$this->isFieldActive($attribute) || !$this->isFieldRequired($attribute)) {
return;
}
if ($this->in_possession && in_array($attribute, ['location_name', 'city', 'state', 'zip'], true)) {
return;
}
$value = $this->{$attribute};
if (is_string($value)) {
$value = trim($value);
}
if ($value === '' || $value === null) {
$this->addError($attribute, Yii::t('AnimalManagementModule.base', '{field} is required.', [
'{field}' => $this->getFieldLabel($attribute),
]));
}
}
public function validateCustomFieldValues(string $attribute): void
{
foreach ($this->customFieldDefinitions as $fieldKey => $definition) {
$value = $this->customFields[$fieldKey] ?? null;
$label = (string)$definition['label'];
$inputType = (string)$definition['input_type'];
$required = ((int)$definition['required']) === 1;
if ($required && $this->isCustomValueEmpty($value, $inputType)) {
$this->addError($attribute, Yii::t('AnimalManagementModule.base', '{field} is required.', ['{field}' => $label]));
continue;
}
if ($this->isCustomValueEmpty($value, $inputType)) {
continue;
}
$normalized = is_array($value) ? '' : trim((string)$value);
if ($inputType === 'number' && !is_numeric($normalized)) {
$this->addError($attribute, Yii::t('AnimalManagementModule.base', '{field} must be a valid number.', ['{field}' => $label]));
continue;
}
if (($inputType === 'date' || $inputType === 'datetime') && strtotime($normalized) === false) {
$this->addError($attribute, Yii::t('AnimalManagementModule.base', '{field} must be a valid date/time.', ['{field}' => $label]));
continue;
}
if ($inputType === 'select') {
$options = $this->getCustomFieldSelectOptions($fieldKey);
if (!empty($options) && !array_key_exists($normalized, $options)) {
$this->addError($attribute, Yii::t('AnimalManagementModule.base', 'Invalid selection for {field}.', ['{field}' => $label]));
}
}
}
}
public function validateGalleryImageSelection(string $attribute): void
{
$selectedPath = trim((string)$this->{$attribute});
if ($selectedPath === '') {
return;
}
if (!$this->animal instanceof Animal) {
$this->addError($attribute, Yii::t('AnimalManagementModule.base', 'Gallery image selection is available after creating the animal profile.'));
return;
}
if (Yii::$app->db->schema->getTableSchema('rescue_animal_gallery_item', true) === null) {
$this->addError($attribute, Yii::t('AnimalManagementModule.base', 'Gallery storage is unavailable.'));
return;
}
$options = $this->getGalleryImageOptions();
$exists = isset($options[$selectedPath]);
if (!$exists) {
$this->addError($attribute, Yii::t('AnimalManagementModule.base', 'Selected gallery image is not available for this animal.'));
}
}
private function bootstrapFieldConfig(): void
{
$this->fieldConfig = $this->defaultFieldConfig();
if (!class_exists(RescueFieldDefinition::class)) {
return;
}
if (Yii::$app->db->schema->getTableSchema('rescue_field_definition', true) === null) {
return;
}
$definitions = RescueFieldDefinition::find()
->where(['module_id' => self::MODULE_ID])
->orderBy(['sort_order' => SORT_ASC, 'id' => SORT_ASC])
->all();
foreach ($definitions as $definition) {
$fieldKey = (string)$definition->field_key;
if (isset($this->fieldConfig[$fieldKey])) {
$this->fieldConfig[$fieldKey] = [
'label' => (string)$definition->label,
'required' => (int)$definition->required,
'is_active' => (int)$definition->is_active,
];
continue;
}
if ((int)$definition->is_active !== 1) {
continue;
}
if ((string)$definition->group_key !== 'animal_profile') {
continue;
}
if (in_array($fieldKey, ['cover_image_url', 'profile_image_url', 'photo_url', 'image_url', 'profile_image', 'photo', 'tile_display_fields', 'hero_display_fields'], true)) {
continue;
}
$this->customFieldDefinitions[$fieldKey] = [
'id' => (int)$definition->id,
'field_key' => $fieldKey,
'label' => trim((string)$definition->label) !== '' ? (string)$definition->label : $fieldKey,
'input_type' => (string)$definition->input_type,
'required' => (int)$definition->required,
'visibility' => (string)$definition->visibility,
'options' => (string)$definition->options,
'sort_order' => (int)$definition->sort_order,
];
}
}
private function saveCustomFieldValues(Animal $animal): bool
{
if (empty($this->customFieldDefinitions)) {
return true;
}
foreach ($this->customFieldDefinitions as $fieldKey => $definition) {
$fieldDefinitionId = (int)$definition['id'];
if ($fieldDefinitionId === 0) {
continue;
}
$inputType = (string)$definition['input_type'];
$rawValue = $this->customFields[$fieldKey] ?? null;
$valueText = $this->normalizeCustomValue($rawValue, $inputType);
$record = AnimalFieldValue::findOne([
'animal_id' => (int)$animal->id,
'field_definition_id' => $fieldDefinitionId,
]);
if ($this->isCustomValueEmpty($valueText, $inputType)) {
if ($record instanceof AnimalFieldValue) {
$record->delete();
}
continue;
}
if (!$record instanceof AnimalFieldValue) {
$record = new AnimalFieldValue();
$record->animal_id = (int)$animal->id;
$record->field_definition_id = $fieldDefinitionId;
}
$record->value_text = $valueText;
$record->value_json = null;
if (!$record->save()) {
$this->addError('customFields', Yii::t('AnimalManagementModule.base', 'Could not save custom field {field}.', [
'{field}' => (string)$definition['label'],
]));
foreach ($record->getFirstErrors() as $error) {
$this->addError('customFields', $error);
}
}
}
if ($this->hasErrors('customFields')) {
return false;
}
return true;
}
private function loadCustomFieldValues(Animal $animal): void
{
if (empty($this->customFieldDefinitions)) {
return;
}
$existingValues = AnimalFieldValue::find()
->where(['animal_id' => (int)$animal->id])
->indexBy('field_definition_id')
->all();
foreach ($this->customFieldDefinitions as $fieldKey => $definition) {
$fieldDefinitionId = (int)$definition['id'];
if ($fieldDefinitionId === 0 || !isset($existingValues[$fieldDefinitionId])) {
continue;
}
$value = (string)$existingValues[$fieldDefinitionId]->value_text;
if ((string)$definition['input_type'] === 'boolean') {
$this->customFields[$fieldKey] = in_array(strtolower($value), ['1', 'true', 'yes', 'on'], true) ? '1' : '0';
} else {
$this->customFields[$fieldKey] = $value;
}
}
}
private function normalizeCustomValue($value, string $inputType): ?string
{
if ($inputType === 'boolean') {
return !empty($value) ? '1' : '0';
}
if (is_array($value)) {
return Json::encode($value);
}
return trim((string)$value);
}
private function isCustomValueEmpty($value, string $inputType): bool
{
if ($inputType === 'boolean') {
return false;
}
if (is_array($value)) {
return empty($value);
}
return trim((string)$value) === '';
}
private function parseSelectOptions(string $options): array
{
$options = trim($options);
if ($options === '') {
return [];
}
$decoded = null;
try {
$decoded = Json::decode($options, true);
} catch (\Throwable $e) {
$decoded = null;
}
if (is_array($decoded)) {
$result = [];
if (array_values($decoded) === $decoded) {
foreach ($decoded as $item) {
$item = (string)$item;
if ($item === '') {
continue;
}
$result[$item] = $item;
}
return $result;
}
foreach ($decoded as $key => $value) {
$key = (string)$key;
if ($key === '') {
continue;
}
$result[$key] = (string)$value;
}
return $result;
}
$result = [];
foreach (preg_split('/[\r\n,]+/', $options) as $item) {
$item = trim((string)$item);
if ($item === '') {
continue;
}
$result[$item] = $item;
}
return $result;
}
private function defaultFieldConfig(): array
{
return [
'name' => ['label' => 'Name', 'required' => 0, 'is_active' => 1],
'species' => ['label' => 'Species', 'required' => 1, 'is_active' => 1],
'breed' => ['label' => 'Breed', 'required' => 0, 'is_active' => 1],
'sex' => ['label' => 'Sex', 'required' => 0, 'is_active' => 1],
'status' => ['label' => 'Status', 'required' => 1, 'is_active' => 1],
'in_possession' => ['label' => 'In possession', 'required' => 1, 'is_active' => 1],
'location_name' => ['label' => 'Location name', 'required' => 0, 'is_active' => 1],
'city' => ['label' => 'City', 'required' => 0, 'is_active' => 1],
'state' => ['label' => 'State', 'required' => 0, 'is_active' => 1],
'zip' => ['label' => 'ZIP', 'required' => 0, 'is_active' => 1],
'public_summary' => ['label' => 'Public summary', 'required' => 0, 'is_active' => 1],
'medical_notes' => ['label' => 'Medical notes', 'required' => 0, 'is_active' => 1],
];
}
private function getDefaultLocationName(): string
{
if ($this->contentContainer instanceof Space) {
return trim((string)$this->contentContainer->name);
}
return '';
}
private function saveImageFields(Animal $animal): bool
{
if ($this->removeCoverImage && !empty($this->existingCoverImagePath)) {
$this->deleteStoredFile($this->existingCoverImagePath, (int)$animal->id);
if (!$this->setImageFieldValue($animal, 'cover_image_url', null, 'Cover image URL', 230)) {
return false;
}
$this->existingCoverImagePath = null;
}
if ($this->removeProfileImage && !empty($this->existingProfileImagePath)) {
$this->deleteStoredFile($this->existingProfileImagePath, (int)$animal->id);
if (!$this->setImageFieldValue($animal, 'profile_image_url', null, 'Profile image URL', 240)) {
return false;
}
$this->existingProfileImagePath = null;
}
if ($this->coverImageFile instanceof UploadedFile) {
if (!empty($this->existingCoverImagePath)) {
$this->deleteStoredFile($this->existingCoverImagePath, (int)$animal->id);
}
$stored = $this->storeImage($this->coverImageFile, 'cover', $animal);
if ($stored === null) {
$this->addError('coverImageFile', Yii::t('AnimalManagementModule.base', 'Could not upload cover image.'));
return false;
}
if (!$this->setImageFieldValue($animal, 'cover_image_url', $stored, 'Cover image URL', 230)) {
return false;
}
$this->existingCoverImagePath = $stored;
}
$selectedCoverFromGallery = trim((string)$this->coverImageGalleryPath);
if (!$this->removeCoverImage && $selectedCoverFromGallery !== '' && !($this->coverImageFile instanceof UploadedFile)) {
if (!$this->setImageFieldValue($animal, 'cover_image_url', $selectedCoverFromGallery, 'Cover image URL', 230)) {
return false;
}
$this->existingCoverImagePath = $selectedCoverFromGallery;
}
if ($this->profileImageFile instanceof UploadedFile) {
if (!empty($this->existingProfileImagePath)) {
$this->deleteStoredFile($this->existingProfileImagePath, (int)$animal->id);
}
$stored = $this->storeImage($this->profileImageFile, 'profile', $animal);
if ($stored === null) {
$this->addError('profileImageFile', Yii::t('AnimalManagementModule.base', 'Could not upload profile image.'));
return false;
}
if (!$this->setImageFieldValue($animal, 'profile_image_url', $stored, 'Profile image URL', 240)) {
return false;
}
$this->existingProfileImagePath = $stored;
}
$selectedProfileFromGallery = trim((string)$this->profileImageGalleryPath);
if (!$this->removeProfileImage && $selectedProfileFromGallery !== '' && !($this->profileImageFile instanceof UploadedFile)) {
if (!$this->setImageFieldValue($animal, 'profile_image_url', $selectedProfileFromGallery, 'Profile image URL', 240)) {
return false;
}
$this->existingProfileImagePath = $selectedProfileFromGallery;
}
if (!$this->syncPrimaryImagesToGallery($animal)) {
return false;
}
return true;
}
private function readDisplayFieldOverride(Animal $animal, string $fieldKey): array
{
$raw = $this->getImageFieldValue($animal, $fieldKey);
if ($raw === null) {
return [];
}
$decoded = json_decode((string)$raw, true);
if (!is_array($decoded)) {
$decoded = array_map('trim', explode(',', (string)$raw));
}
$allowed = array_keys(DisplaySettingsForm::fieldOptions());
$normalized = [];
foreach ($decoded as $field) {
$field = trim((string)$field);
if ($field === '' || !in_array($field, $allowed, true)) {
continue;
}
if (!in_array($field, $normalized, true)) {
$normalized[] = $field;
}
}
return $normalized;
}
private function saveDisplayFieldOverrides(Animal $animal): bool
{
$tileFields = $this->normalizeDisplayFields($this->tileDisplayFields);
$heroFields = $this->normalizeDisplayFields($this->heroDisplayFields);
if (!$this->setImageFieldValue($animal, 'tile_display_fields', empty($tileFields) ? null : json_encode($tileFields), 'Tile Display Fields', 250)) {
return false;
}
if (!$this->setImageFieldValue($animal, 'hero_display_fields', empty($heroFields) ? null : json_encode($heroFields), 'Hero Display Fields', 251)) {
return false;
}
return true;
}
private function normalizeDisplayFields(array $fields): array
{
$allowed = array_keys(DisplaySettingsForm::fieldOptions());
$normalized = [];
foreach ($fields as $field) {
$field = trim((string)$field);
if ($field === '' || !in_array($field, $allowed, true)) {
continue;
}
if (!in_array($field, $normalized, true)) {
$normalized[] = $field;
}
}
return $normalized;
}
private function storeImage(UploadedFile $file, string $prefix, Animal $animal): ?string
{
$random = Yii::$app->security->generateRandomString(8);
$extension = strtolower((string)$file->extension);
$fileName = $prefix . '-' . time() . '-' . $random . '.' . $extension;
$candidateDirs = [
'/uploads/animal-management/animals/' . (int)$animal->id,
'/uploads/animal-management-runtime/animals/' . (int)$animal->id,
'/uploads/animal-media/animals/' . (int)$animal->id,
];
foreach ($candidateDirs as $relativeDir) {
$absoluteDir = Yii::getAlias('@webroot') . $relativeDir;
try {
FileHelper::createDirectory($absoluteDir, 0775, true);
} catch (\Throwable $e) {
Yii::warning($e->getMessage(), 'animal_management.image_upload_dir');
continue;
}
if (!is_dir($absoluteDir) || !is_writable($absoluteDir)) {
continue;
}
$absolutePath = $absoluteDir . '/' . $fileName;
if ($file->saveAs($absolutePath)) {
return $relativeDir . '/' . $fileName;
}
}
return null;
}
private function deleteStoredFile(?string $relativePath, int $animalId): void
{
if (empty($relativePath)) {
return;
}
if ($this->isGalleryReferencedPath($animalId, $relativePath)) {
return;
}
$absolutePath = Yii::getAlias('@webroot') . $relativePath;
if (is_file($absolutePath)) {
@unlink($absolutePath);
}
}
private function getImageFieldValue(Animal $animal, string $fieldKey): ?string
{
if (!class_exists(RescueFieldDefinition::class)
|| Yii::$app->db->schema->getTableSchema('rescue_field_definition', true) === null
|| Yii::$app->db->schema->getTableSchema('rescue_animal_field_value', true) === null
) {
return null;
}
$definition = RescueFieldDefinition::findOne([
'module_id' => self::MODULE_ID,
'group_key' => 'animal_profile',
'field_key' => $fieldKey,
]);
if (!$definition instanceof RescueFieldDefinition) {
return null;
}
$fieldValue = AnimalFieldValue::findOne([
'animal_id' => (int)$animal->id,
'field_definition_id' => (int)$definition->id,
]);
if (!$fieldValue instanceof AnimalFieldValue) {
return null;
}
$value = trim((string)$fieldValue->value_text);
return $value !== '' ? $value : null;
}
private function setImageFieldValue(Animal $animal, string $fieldKey, ?string $path, string $label, int $sortOrder): bool
{
if (!class_exists(RescueFieldDefinition::class)
|| Yii::$app->db->schema->getTableSchema('rescue_field_definition', true) === null
|| Yii::$app->db->schema->getTableSchema('rescue_animal_field_value', true) === null
) {
return true;
}
$definition = RescueFieldDefinition::findOne([
'module_id' => self::MODULE_ID,
'group_key' => 'animal_profile',
'field_key' => $fieldKey,
]);
$isInternalDisplayOverride = in_array($fieldKey, ['tile_display_fields', 'hero_display_fields'], true);
if (!$definition instanceof RescueFieldDefinition) {
$definition = new RescueFieldDefinition();
$definition->module_id = self::MODULE_ID;
$definition->group_key = 'animal_profile';
$definition->field_key = $fieldKey;
$definition->label = $label;
$definition->input_type = 'text';
$definition->required = 0;
$definition->is_core = $isInternalDisplayOverride ? 1 : 0;
$definition->is_active = 1;
$definition->visibility = $isInternalDisplayOverride ? 'internal' : 'public';
$definition->options = '{}';
$definition->sort_order = $sortOrder;
$definition->created_at = date('Y-m-d H:i:s');
$definition->updated_at = date('Y-m-d H:i:s');
if (!$definition->save()) {
$this->addError('customFields', Yii::t('AnimalManagementModule.base', 'Could not create image field definition.'));
return false;
}
} elseif ($isInternalDisplayOverride) {
$definition->label = $label;
$definition->input_type = 'text';
$definition->required = 0;
$definition->is_core = 1;
$definition->is_active = 1;
$definition->visibility = 'internal';
$definition->sort_order = $sortOrder;
$definition->updated_at = date('Y-m-d H:i:s');
if (!$definition->save()) {
$this->addError('customFields', Yii::t('AnimalManagementModule.base', 'Could not update display override field definition.'));
return false;
}
}
$record = AnimalFieldValue::findOne([
'animal_id' => (int)$animal->id,
'field_definition_id' => (int)$definition->id,
]);
$path = trim((string)$path);
if ($path === '') {
if ($record instanceof AnimalFieldValue) {
$record->delete();
}
return true;
}
if (!$record instanceof AnimalFieldValue) {
$record = new AnimalFieldValue();
$record->animal_id = (int)$animal->id;
$record->field_definition_id = (int)$definition->id;
}
$record->value_text = $path;
$record->value_json = null;
if (!$record->save()) {
$this->addError('customFields', Yii::t('AnimalManagementModule.base', 'Could not save image path field value.'));
return false;
}
return true;
}
private function syncPrimaryImagesToGallery(Animal $animal): bool
{
if (Yii::$app->db->schema->getTableSchema('rescue_animal_gallery_item', true) === null) {
return true;
}
$coverPath = $this->getImageFieldValue($animal, 'cover_image_url');
$profilePath = $this->getImageFieldValue($animal, 'profile_image_url');
if (!$this->ensureGalleryItemForPath($animal, $coverPath, 'cover')) {
return false;
}
if (!$this->ensureGalleryItemForPath($animal, $profilePath, 'profile')) {
return false;
}
return true;
}
private function ensureGalleryItemForPath(Animal $animal, ?string $imagePath, string $sourceType): bool
{
$imagePath = trim((string)$imagePath);
if ($imagePath === '') {
return true;
}
if (!$this->isResolvableImageUrl($imagePath)) {
return true;
}
$targetHash = $this->computeImageContentHash($imagePath);
$exists = AnimalGalleryItem::find()
->where(['animal_id' => (int)$animal->id, 'file_path' => $imagePath])
->exists();
if (!$exists) {
$items = AnimalGalleryItem::find()->where(['animal_id' => (int)$animal->id])->all();
foreach ($items as $item) {
$itemUrl = trim((string)$item->getImageUrl());
if ($itemUrl === '') {
continue;
}
if ($itemUrl === $imagePath) {
$exists = true;
break;
}
if ($targetHash !== null) {
$itemHash = $this->computeImageContentHash($itemUrl);
if ($itemHash !== null && hash_equals($targetHash, $itemHash)) {
$exists = true;
break;
}
}
}
}
if ($exists) {
return true;
}
$item = new AnimalGalleryItem();
$item->animal_id = (int)$animal->id;
$item->file_path = $imagePath;
$item->source_type = $sourceType;
$item->created_by = Yii::$app->user->isGuest ? null : (int)Yii::$app->user->id;
if (!$item->save()) {
$this->addError('customFields', Yii::t('AnimalManagementModule.base', 'Could not add profile image to gallery.'));
return false;
}
return true;
}
private function isGalleryReferencedPath(int $animalId, string $relativePath): bool
{
if (Yii::$app->db->schema->getTableSchema('rescue_animal_gallery_item', true) === null) {
return false;
}
return AnimalGalleryItem::find()
->where(['animal_id' => $animalId, 'file_path' => $relativePath])
->exists();
}
private function computeImageContentHash(string $pathOrUrl): ?string
{
$pathOrUrl = trim($pathOrUrl);
if ($pathOrUrl === '') {
return null;
}
$absolutePath = $pathOrUrl;
if (substr($absolutePath, 0, 1) === '/') {
$absolutePath = Yii::getAlias('@webroot') . $absolutePath;
}
if (!is_file($absolutePath) || !is_readable($absolutePath)) {
return null;
}
$hash = @hash_file('sha1', $absolutePath);
return is_string($hash) && $hash !== '' ? $hash : null;
}
private function isResolvableImageUrl(string $pathOrUrl): bool
{
$pathOrUrl = trim($pathOrUrl);
if ($pathOrUrl === '') {
return false;
}
if (preg_match('/^https?:\/\//i', $pathOrUrl)) {
return true;
}
if (substr($pathOrUrl, 0, 1) !== '/') {
return false;
}
$absolutePath = Yii::getAlias('@webroot') . $pathOrUrl;
return is_file($absolutePath) && is_readable($absolutePath);
}
}