imageFields ?? []; } return []; } /** * Get the field that are wysiwyg, from the array in the model * * @param \Illuminate\Database\Eloquent\Model $model * @return array */ protected static function getWysiwygFields(Model $model): array { if (property_exists($model, 'wysiwygFields')) { return $model->wysiwygFields ?? []; } return []; } /** * Loop over the image or wysiwyg fields * * @param array $fields * @param \Illuminate\Database\Eloquent\Model $model * @param boolean $isTranslatable * @param mixed $locale * @param array $availableLocales * @param boolean $forceDeleteImage * @param string $fieldType * @return void */ protected static function loopOverFields( array $fields, Model $model, bool $isTranslatable, mixed $locale, array $availableLocales, bool $forceDeleteImage, string $fieldType = self::IMAGE ): void { foreach ($fields as $field) { $currentValue = data_get($model->getAttributes(), $field); $originalValue = data_get($model->getOriginal(), $field); if (is_null($currentValue) && is_null($originalValue)) { continue; } if (is_array($originalValue) && $isTranslatable) { $localesToProcess = ($forceDeleteImage) ? $availableLocales : [$locale]; foreach ($localesToProcess as $lang) { $oldLocaleVal = $originalValue[$lang] ?? []; $newLocaleVal = json_decode(data_get($model->getAttributes(), $field), true)[$lang] ?? []; static::traverseAndManage( $model, (array) $oldLocaleVal, (array) $newLocaleVal, "$field", $forceDeleteImage, $fieldType, $lang ); } continue; } //end if // TODO pas testé if (is_array($originalValue) && is_array($currentValue)) { static::traverseAndManage( $model, $originalValue, $currentValue, $field, $forceDeleteImage, $fieldType ); continue; } static::manageField($model, $originalValue, $currentValue, $field, $forceDeleteImage, $fieldType); } //end foreach } /** * Initializes the main loop to process image and WYSIWYG fields for a given model. * * Determines field types, handles localization, and delegates processing to `loopOverFields()` * for each supported field type. Used during update or forced delete events. * * @param \Illuminate\Database\Eloquent\Model $model The Eloquent model to process. * @param string $eventType The event type * (e.g. self::UPDATE_ACTION or self::FORCE_DELETE_ACTION). * @return void */ protected static function initLoop(Model $model, string $eventType = self::UPDATE_ACTION): void { $forceDeleteImage = $eventType === self::FORCE_DELETE_ACTION; $imagesFields = self::getImageFields($model); $wysiwygFields = self::getWysiwygFields($model); $isTranslatable = method_exists($model, 'getTranslations'); $locale = config('app.locale'); $availableLocales = array_keys(config('laravellocalization.supportedLocales', ['fr' => "Frenchouz"])); self::loopOverFields( $imagesFields, $model, $isTranslatable, $locale, $availableLocales, $forceDeleteImage, self::IMAGE, ); self::loopOverFields( $wysiwygFields, $model, $isTranslatable, $locale, $availableLocales, $forceDeleteImage, self::WYSIWYG, ); } /** * Recursively traverses and manages changes between two nested arrays of model fields. * * Compares old and new values, determines if fields are lists, and delegates handling * of each field to `manageField()`. Supports recursive traversal for nested arrays. * * @param \Illuminate\Database\Eloquent\Model $model The Eloquent model being processed. * @param array $old The previous state of the data. * @param array $new The new state of the data. * @param string $fieldPath The current dot-notated path of the field. * @param boolean $forceDeleteImage Whether to force deletion of old images. * @param string $fieldType The type of field being managed * (default: self::IMAGE). * @param string|null $locale The locale associated with the field, if any. * * @return void */ protected static function traverseAndManage( Model $model, array $old, array $new, string $fieldPath, bool $forceDeleteImage, string $fieldType = self::IMAGE, ?string $locale = null ): void { $allKeys = self::getFieldKeys($old, $new); // This boolean is used to know if we need to keep the index (key) of the array, // or not e.g do we want to keep content.fr.0 or just content.fr. $isList = self::isMeaningfulList($old) || self::isMeaningfulList($new); foreach ($allKeys as $key) { $oldValue = $old[$key] ?? null; $newValue = $new[$key] ?? null; $path = $isList ? "$fieldPath.$key" : (is_int($key) ? $fieldPath : "$fieldPath.$key"); // TODO cette récursivité n'a pas encore été testé. if (is_array($oldValue) || is_array($newValue)) { static::traverseAndManage( $model, (array) $oldValue, (array) $newValue, $path, $forceDeleteImage, $fieldType, $locale, ); } static::manageField($model, $oldValue, $newValue, $path, $forceDeleteImage, $fieldType); } } /** * Determine if the given array should be considered a meaningful list. * * A "meaningful list" is a numerically indexed array (0,1,2,...) * that contains at least one nested array — for example, a list of * objects, image blocks, or structured elements. * * This helps distinguish between true lists of complex data * and simple value arrays like ["foo", "bar"]. * * @param array $array * @return boolean */ private static function isMeaningfulList(array $array): bool { return array_is_list($array) && collect($array)->contains(fn($v) => is_array($v)); } /** * Get the unique set of keys present in both the old and new arrays. * * Merges the keys from the old and new data, removes duplicates, * and returns them as a clean, ordered list. * * Useful for comparing field values or recursively traversing * nested array structures. * * @param array $old * @param array $new * @return \Illuminate\Support\Collection */ protected static function getFieldKeys(array $old, array $new): \Illuminate\Support\Collection { return collect(array_keys($old)) ->merge(array_keys($new)) ->unique(); } /** * Get the image field and manage the store image. * * @param \Illuminate\Database\Eloquent\Model $model * @param mixed $oldValue * @param mixed $newValue * @param mixed $fieldName The fieldName, can be nested e.g content.image. * @param boolean $forceDeleteImage Does the image be force deleted, * even if old and new value are the same (on delete model most of case). * @param string $fieldType Choice between WYSIWYG and IMAGE. * @return void */ protected static function manageField( Model $model, mixed $oldValue, mixed $newValue, mixed $fieldName, bool $forceDeleteImage, string $fieldType = self::IMAGE ) { if ($fieldType === self::IMAGE) { // Cas 1 : Valeurs identiques -> only for force deleting model. if ((is_string($oldValue) === is_string($newValue)) && $forceDeleteImage) { // In this case we need to remove the actual file, not the old as we do in other conditions. $path = ToolboxHelper::mbReplace('storage/', '', $newValue); if (Storage::disk('public')->exists($path)) { Storage::disk('public')->delete($path); } } // // Cas 2 : ancienne = string, nouvelle = null → suppression. if (is_string($oldValue) && is_null($newValue)) { // Action suppression. FileHelper::removeOldFile($model, $fieldName); } // // Cas 3 : ancienne = null, nouvelle = string → ajout. // // Cas 4 : ancienne = string, nouvelle = autre string → remplacement. if (is_string($oldValue) && is_string($newValue) && $oldValue !== $newValue) { FileHelper::removeOldFile($model, $fieldName); } } //end if if ($fieldType === self::WYSIWYG) { $valueToPass = $forceDeleteImage ? '' : ($newValue ?? ''); if ($oldValue) { FileHelper::removeOldModelFilesFromHtml($valueToPass, $oldValue); } } } /** * Fills the model with attributes, handling uploaded files and nested arrays. * * Recursively processes attribute values to store uploaded files via `FileHelper::storeFile()`, * and applies the transformed data to the model using `fill()`. * * @param array $attributes The attributes to fill the model with. * @return void */ public function inertiaFill(array $attributes): void { if (!empty($attributes)) { $attributes = self::processContentFiles($attributes); } parent::fill($attributes); } /** * Store recursively all uploaded files in array. * * @param array $content * @return array */ private function processContentFiles(array $content): array { return collect($content) ->map(function ($value) { // Check the pattern of the key and the type of value. if ($value instanceof UploadedFile) { return FileHelper::storeFile($this, $value); } // Check if it's an array. if (is_array($value)) { return $this->processContentFiles($value); } return $value; }) ->all(); } }