377 lines
14 KiB
PHP
377 lines
14 KiB
PHP
<?php
|
|
|
|
namespace App\Traits\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Uwil\LaravelCommon\Helpers\FileHelper;
|
|
use Uwil\LaravelCommon\Helpers\ToolboxHelper;
|
|
|
|
trait HasUwilImages
|
|
{
|
|
/**
|
|
* LES CAS TRAITÉ ET NON TRAITÉ
|
|
* traité :
|
|
* - champs basique racine - image et wisiwig
|
|
* - champs basique translatable - image et wisigiw
|
|
* - prise en compte du trait softDetele
|
|
*
|
|
* Non testé :
|
|
* - la suppression totale des images dans un champs translatable
|
|
* (ça fonctionne mais il y a des changements à mettre dans FileHelper)
|
|
* - le cas ou le champs (ancien et current) ne sont que des tableaux (non translatable)
|
|
* - la récursivité dans traverseAndManage,
|
|
* je ne suis pour l'instant passé qu'une fois dans cette méthode, via un champs translatable
|
|
*
|
|
* TODO :
|
|
* - ajouter la possibilité de viser un nest array dans les tableau de fields
|
|
* - job pour nettoyer les models
|
|
*/
|
|
|
|
protected const UPDATE_ACTION = 'update';
|
|
protected const FORCE_DELETE_ACTION = 'forceDelete';
|
|
|
|
protected const WYSIWYG = "wysiwyg";
|
|
protected const IMAGE = "image";
|
|
|
|
/**
|
|
* Perform any actions required after the model boots.
|
|
*
|
|
* @return void
|
|
*/
|
|
protected static function bootHasUwilImages(): void
|
|
{
|
|
/** @phpstan-ignore-next-line */
|
|
$hadSoftDelete = method_exists(static::class, 'initializeSoftDeletes');
|
|
|
|
if ($hadSoftDelete) {
|
|
/** @phpstan-ignore-next-line */
|
|
static::forceDeleted(function (self $model) {
|
|
self::initLoop($model, self::FORCE_DELETE_ACTION);
|
|
});
|
|
} else {
|
|
static::deleted(function (self $model) {
|
|
self::initLoop($model, self::FORCE_DELETE_ACTION);
|
|
});
|
|
}
|
|
static::updated(function (self $model) {
|
|
self::initLoop($model, self::UPDATE_ACTION);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the field that are images, from the array in the model
|
|
*
|
|
* @param \Illuminate\Database\Eloquent\Model $model
|
|
* @return array
|
|
*/
|
|
protected static function getImageFields(Model $model): array
|
|
{
|
|
if (property_exists($model, 'imageFields')) {
|
|
return $model->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();
|
|
}
|
|
}
|