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); } }