ldc-dam/app/Models/Media.php

849 lines
25 KiB
PHP
Raw Normal View History

2019-05-22 17:10:16 +02:00
<?php
namespace App\Models;
2019-05-22 17:10:16 +02:00
use App\Enums\MediaExtension;
2022-07-26 13:20:21 +02:00
use App\Enums\MediaStatut;
use App\Events\MediaChangedLabel;
use App\Events\MediaCreated;
use App\Events\MediaDestroyed;
use App\Events\MediaUpdated;
use App\Lib\ToolboxHelper;
use App\Traits\Models\BindedToElasticSearch;
2022-04-19 14:49:09 +02:00
use App\Models\User;
2019-05-22 17:10:16 +02:00
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Prunable;
2019-06-17 09:22:10 +02:00
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\ValidationException;
2022-08-01 16:27:22 +02:00
use Rennokki\QueryCache\Traits\QueryCacheable;
2019-05-22 17:10:16 +02:00
class Media extends Model
{
use BindedToElasticSearch;
use Prunable;
2022-08-01 16:27:22 +02:00
use QueryCacheable;
use SoftDeletes;
2022-08-01 16:27:22 +02:00
/**
* The timeout cache for this model
*
* @var integer
*/
protected $cacheFor = -1;
/**
* The cache driver to be used.
*
* @var string
*/
public $cacheDriver = 'redis';
2025-03-25 11:00:14 +01:00
/**
* Is the model a duplicate of another (only true before we save it in database)
*
* @var boolean
*/
public $isDuplicate = false;
/**
* The relations to eager load on every query.
*
* @var array
*/
protected $with = [
2025-03-25 11:00:14 +01:00
'owner',
'tags',
'etiquette',
'articles',
'sujet',
'typeMedia',
'societes',
'marques'
];
2019-05-22 17:10:16 +02:00
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
// ! id est obligatoire ici pour Jobs/XmlArcadiaMediasIn
'id',
'libelle',
'type_vue_id',
'variante_id',
'copyright_id',
'etiquette_id',
'langue_id',
'commentaire',
'version',
'statut',
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'isDefault' => 'bool',
2022-07-27 16:30:03 +02:00
'isLocked' => 'bool',
2022-07-26 13:20:21 +02:00
'statut' => MediaStatut::class,
'extension' => MediaExtension::class
];
/**
* ES The elasticsearch settings.
*
* @var array
*/
protected $indexSettings = [
'analysis' => [
'char_filter' => [
'replace' => [
'type' => 'mapping',
'mappings' => [
'&=> and '
],
],
],
'filter' => [
'word_delimiter' => [
'type' => 'word_delimiter',
'split_on_numerics' => false,
'split_on_case_change' => true,
'generate_word_parts' => true,
'generate_number_parts' => true,
'catenate_all' => true,
'preserve_original' => true,
'catenate_numbers' => true,
]
],
'analyzer' => [
'default' => [
'type' => 'custom',
'char_filter' => [
'html_strip',
'replace',
],
'tokenizer' => 'whitespace',
'filter' => [
2019-12-06 09:49:15 +01:00
'lowercase',
'word_delimiter',
],
],
],
2019-12-06 09:49:15 +01:00
'normalizer' => [
'my_normalizer' => [
'type' => 'custom',
'char_filter' => [],
'filter' => [
'lowercase'
]
]
]
],
];
/**
* ES Mapping Properties
*
* @var array
*/
protected $mappingProperties = array(
'libelle' => array(
'type' => 'text',
2019-12-06 09:49:15 +01:00
'analyzer' => 'default',
'fields' => [
'keyword' => [
2019-12-06 09:49:15 +01:00
'type' => 'keyword',
'normalizer' => 'my_normalizer'
]
],
'fielddata' => true
),
2020-05-19 15:56:30 +02:00
'commentaire' => array(
'type' => 'text',
'analyzer' => 'default',
'fields' => [
'keyword' => [
2020-05-19 15:56:30 +02:00
'type' => 'keyword',
'normalizer' => 'my_normalizer'
]
],
2020-05-19 15:56:30 +02:00
'fielddata' => true
),
'created_at' => [
'type' => 'date',
"format" => "yyyy-MM-dd HH:mm:ss"
],
'updated_at' => [
'type' => 'date',
"format" => "yyyy-MM-dd HH:mm:ss"
],
'version' => [
'type' => 'text',
'fielddata' => true
],
'arcadia_id' => [
'type' => 'text',
'analyzer' => 'standard'
],
'articles.noArt' => [
'type' => 'text',
'analyzer' => 'standard'
],
'articles.libCcial' => [
2019-12-06 09:49:15 +01:00
'type' => 'text',
'analyzer' => 'default',
'fields' => [
'keyword' => [
2019-12-06 09:49:15 +01:00
'type' => 'keyword',
'normalizer' => 'my_normalizer'
]
],
2019-12-06 09:49:15 +01:00
'fielddata' => true
]
);
2019-08-07 09:36:02 +02:00
/**
* Perform any actions required after the model boots.
*
* @return void
*/
protected static function booted()
{
static::created(function (self $model) {
2022-08-01 16:27:22 +02:00
// * Flush Eloquent cache.
self::flushQueryCache();
/** @var \App\Models\User */
$user = auth()->user();
MediaCreated::dispatch($user, $model);
2025-03-25 11:00:14 +01:00
if (empty($model->isDuplicate)) {
/** @throws \RuntimeException */
$model->addToElasticSearch();
}
});
static::updating(function (self $model) {
2022-07-26 13:20:21 +02:00
if ($model->statut === MediaStatut::brouillon and $model->arcadia_id) {
throw ValidationException::withMessages([
'statut' => trans('Impossible de changer le statut en brouillon quand la fiche est liée à arcadia')
]);
}
});
static::updated(function (self $model) {
2022-08-01 16:27:22 +02:00
// * Flush Eloquent cache.
self::flushQueryCache();
/** @var \App\Models\User */
$user = auth()->user();
2025-10-24 12:06:58 +02:00
// * If update is not running from console, we prevent soft delete before trigerring update event
if ($user and !\array_key_exists('deleted_at', $model->getChanges())) {
MediaUpdated::dispatch($user, $model);
}
if ($model->getRawOriginal('etiquette_id') != $model->etiquette_id) {
/** @var \App\Models\Etiquette|null */
$oldLabel = Etiquette::where('etiquette_id', $model->getRawOriginal('etiquette_id'))->first();
/** @var \App\Models\Etiquette|null */
$newLabel = Etiquette::where('etiquette_id', $model->etiquette_id)->first();
MediaChangedLabel::dispatch($user, $model, $oldLabel, $newLabel);
}
/** @throws \RuntimeException */
$model->addToElasticSearch();
});
static::deleted(function (self $model) {
2022-08-01 16:27:22 +02:00
// * Flush Eloquent cache.
self::flushQueryCache();
/** @var \App\Models\User */
$user = auth()->user();
MediaDestroyed::dispatch($user, $model);
/** @throws \RuntimeException */
$model->removeFromElasticSearch();
});
}
// * Laravel Behaviors
/**
* Get the route key for the model.
*
* @return string
*/
public function getRouteKeyName()
{
return 'uid';
}
2022-08-01 16:27:22 +02:00
/**
* Set the base cache tags that will be present
* on all queries.
*
* ! This is used to flush caching
*
* @return array
*/
protected function getCacheBaseTags(): array
{
return [
'dam_media',
];
}
/**
* Get the prunable model query.
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function prunable()
{
return static::query();
}
/**
* Prepare the model for pruning.
*
* @return void
* @throws \RuntimeException If pruning the model fails.
*/
protected function pruning()
{
$storage = Storage::disk('media');
if ($storage->exists($this->relative_path) and !$storage->delete($this->relative_path)) {
throw new \RuntimeException("Could not delete $this->relative_path from storage.");
}
if ($storage->exists($this->preview_relative_path) and !$storage->delete($this->preview_relative_path)) {
throw new \RuntimeException("Could not delete $this->preview_relative_path from storage.");
}
if ($storage->exists($this->thumb_relative_path) and !$storage->delete($this->thumb_relative_path)) {
throw new \RuntimeException("Could not delete $this->thumb_relative_path from storage.");
}
}
// * RELATIONSHIPS
/**
* BelongsTo dossier relation
2019-05-22 17:10:16 +02:00
* Get the dossier that owns the media.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
2019-05-22 17:10:16 +02:00
*/
public function dossier()
{
return $this->belongsTo(Dossier::class, 'dossier_id');
}
/**
* BelongsTo sujet relation
* Get the sujet that owns the media.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function sujet()
{
return $this->belongsTo(Sujet::class, 'sujet_id');
}
/**
* BelongsTo typeMedia relation
* Get the typeMedia that owns the media.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function typeMedia()
{
return $this->belongsTo(TypeMedia::class, 'type_media_id');
}
/**
* BelongsTo typevue relation
* Get the typevue that owns the media.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function typevue()
{
return $this->belongsTo(TypeVue::class);
}
/**
* BelongsTo langue relation
* Get the langue that owns the media.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function langue()
{
return $this->belongsTo(Langue::class, 'langue_id');
}
/**
* BelongsTo copyright relation
* Get the copyright that owns the media.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function copyright()
{
return $this->belongsTo(Copyright::class, 'copyright_id');
}
/**
* BelongsTo etiquette relation
* Get the etiquette that owns the media.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function etiquette()
{
return $this->belongsTo(Etiquette::class, 'etiquette_id');
}
/**
* BelongsToMany tags relation
* The tags that belong to the media.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function tags()
{
return $this->belongsToMany(Tag::class, 'media_tag');
}
/**
* BelongsToMany articles relation
* The articles that belong to the media.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function articles()
{
return $this->belongsToMany(Article::class, 'media_article');
2019-05-22 17:10:16 +02:00
}
/**
* BelongsToMany societes relation
* The societes that belong to the media.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function societes()
{
return $this->belongsToMany(Societe::class, 'media_societe');
}
/**
* BelongsToMany marques relation
* The marques that belong to the media.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function marques()
{
return $this->belongsToMany(Marque::class, 'media_marque', 'media_id', 'marque_id');
}
/**
* BelongsTo user relation
* Get the user that owns the media.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function owner()
{
return $this->belongsTo(User::class, 'created_by');
}
/**
* HasMany historiques relation
* Get the historiques for the media.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function historiques()
{
return $this->hasMany(Historique::class);
}
/**
* BelongsToMany paniers relation
* The paniers that belong to the media.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function paniers()
{
return $this->belongsToMany(Panier::class, 'panier_media');
}
2022-04-19 11:52:41 +02:00
// * METHODS
/**
* Get basename
*
* @return string
*/
public function getBasenameAttribute(): string
{
return "{$this->uid}.{$this->extension->value}";
}
/**
* Get preview_basename
*
* @return string
*/
public function getPreviewBasenameAttribute(): string
{
$extension = match ($this->extension) {
MediaExtension::pdf, MediaExtension::ai, MediaExtension::psd => MediaExtension::png,
default => $this->extension,
};
return "{$this->uid}_preview.{$extension->value}";
}
/**
* Get thumb_basename
*
* @return string
*/
public function getThumbBasenameAttribute(): string
{
$extension = match ($this->extension) {
MediaExtension::pdf, MediaExtension::ai, MediaExtension::psd => MediaExtension::png,
default => $this->extension,
};
return "{$this->uid}_thumb.{$extension->value}";
}
/**
* Get relative_path
*
* @return string
*/
public function getRelativePathAttribute(): string
{
return "{$this->folder}/{$this->basename}";
}
/**
* Get preview_relative_path
*
* @return string
*/
public function getPreviewRelativePathAttribute(): string
{
return "{$this->folder}/{$this->preview_basename}";
}
/**
* Get thumb_relative_path
*
* @return string
*/
public function getThumbRelativePathAttribute(): string
{
return "{$this->folder}/{$this->thumb_basename}";
}
/**
* Get full_path
*
* @return string
*/
public function getFullPathAttribute(): string
{
/** @var \Illuminate\Filesystem\FilesystemAdapter */
$storage = Storage::disk('media');
return $storage->path($this->relative_path);
}
/**
* Get preview_full_path
*
* @return string
*/
public function getPreviewFullPathAttribute(): string
{
/** @var \Illuminate\Filesystem\FilesystemAdapter */
$storage = Storage::disk('media');
return $storage->path($this->preview_relative_path);
}
/**
* Get thumb_full_path
*
* @return string
*/
public function getThumbFullPathAttribute(): string
{
/** @var \Illuminate\Filesystem\FilesystemAdapter */
$storage = Storage::disk('media');
return $storage->path($this->thumb_relative_path);
}
/**
* Delete files on path
* @return void
* @throws \RuntimeException If the media does not have a subject.
*/
public function deleteFilesOnAllPath()
{
/** @var \Illuminate\Filesystem\FilesystemAdapter */
$storage = Storage::disk('media');
$storage->delete($this->relative_path);
$storage->delete($this->thumb_relative_path);
$storage->delete($this->preview_relative_path);
if (!$this->sujet) {
throw new \RuntimeException('This medias does not have any subject wich is forbidden');
}
/** @var \Illuminate\Filesystem\FilesystemAdapter */
$storage = Storage::disk('media');
$this->sujet->formats->map(function (Format $format) use ($storage) {
$filePath = "{$this->folder}/{$this->uid}_{$format->libelle}.{$format->extension}";
if ($storage->exists($filePath)) {
$storage->delete($filePath);
}
});
}
/**
* Add this media into Elastic Search
*
* @return void
* @throws \RuntimeException If adding to ES failed.
*/
public function addToElasticSearch()
{
$this->removeFromElasticSearch();
try {
$this->refresh();
$this->addToIndex();
} catch (\Exception $e) {
report($e);
throw new \RuntimeException("Failed to add media '$this->arcadia_id' to ES", 0, $e);
}
}
/**
* Remove this media from ElasticSearch
*
* @return void
* @throws \RuntimeException If someting goes wrong.
*/
public function removeFromElasticSearch()
{
$esExist = self::searchByQuery(['match' => ['arcadia_id' => strval($this->arcadia_id)]]);
// * Remove all from elastic if exists
if ($esExist->totalHits() > 0) {
try {
$this->removeFromIndex();
} catch (\Exception $e) {
report($e);
throw new \RuntimeException("Failed to add media '$this->arcadia_id' to ES", 0, $e);
}
}
}
/**
* Create thumbnail image for a Media Subject on very of its formats.
* - Creation des vignettes en lien avec le sujet du média
*
* @return void
* @throws \ImagickException On errors.
* @throws \RuntimeException If media file is missing.
* @phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
*/
public function createSubjectThumbnails()
{
/** @var \Illuminate\Filesystem\FilesystemAdapter */
$storage = Storage::disk('media');
if (!$storage->exists($this->relative_path)) {
throw new \RuntimeException("Missing Media file at path '$this->relative_full_path'");
}
$im = new \Imagick();
$im->readImageBlob(
$storage->get($this->relative_path),
$this->basename
);
$im->setImageBackgroundColor('#ffffff');
$im->setImageAlphaChannel(\Imagick::ALPHACHANNEL_REMOVE);
$im->mergeImageLayers(\Imagick::LAYERMETHOD_FLATTEN);
$im->stripImage();
if (in_array($this->extension, [MediaExtension::pdf, MediaExtension::ai, MediaExtension::psd])) {
$im->setIteratorIndex(0);
}
/** @var \Illuminate\Support\Collection<\App\Models\Format> $formats */
$formats = optional($this->sujet)->formats ?? collect();
$formats->map(function (Format $format) use ($storage, $im) {
$targetWidth = \floatval($format->largeur);
$targetHeight = \floatval($format->hauteur) == 0 ?
\floatval(floor(($this->hauteur * $format->largeur) / $this->largeur)) :
\floatval($format->hauteur);
$im->setImageFormat($format->extension);
if ($this->largeur >= $targetWidth || $this->hauteur >= $targetHeight) {
$im->thumbnailImage($targetWidth, $targetHeight, true);
}
$storage->put(
"{$this->folder}/{$this->uid}_{$format->libelle}.{$format->extension}",
$im->getImageBlob()
);
});
$im->destroy();
unset($im);
}
/**
* Create thumbnail image for a Media.
* - Creation des vignettes de base
*
* @return void
* @throws \ImagickException On errors.
* @throws \RuntimeException If media file is missing.
* @phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
*/
public function createThumbnail()
{
/** @var \Illuminate\Filesystem\FilesystemAdapter */
$storage = Storage::disk('media');
if (!$storage->exists($this->relative_path)) {
throw new \RuntimeException("Missing Media file at path '$this->relative_full_path'");
}
$im = new \Imagick();
$im->readImageBlob(
$storage->get($this->relative_path),
$this->basename
);
if (in_array($this->extension, [MediaExtension::pdf, MediaExtension::ai, MediaExtension::psd])) {
$im->setIteratorIndex(0);
}
/*
Handle CMJN - DOESN'T WORK
if ($im->getImageColorspace() === \Imagick::COLORSPACE_CMYK){
$im->transformImageColorspace(\Imagick::COLORSPACE_RGB);
}
*/
// * create preview
/** @var \App\Models\Format $format */
$format = Format::withoutGlobalScope('App\Scopes\SystemScope')
2025-03-25 11:00:14 +01:00
->where('libelle', '=', 'preview')->firstOrFail();
if ($this->extension == MediaExtension::gif) {
if ($this->largeur >= $format->largeur || $this->hauteur >= $format->hauteur || $this->largeur == "") {
$im = $im->coalesceImages();
foreach ($im as $frame) {
$frame->thumbnailImage($format->largeur, $format->hauteur);
$frame->setImagePage($format->largeur, $format->hauteur, 0, 0);
}
$im = $im->deconstructImages();
}
2022-07-28 10:10:28 +02:00
$im->setImageFormat(MediaExtension::gif);
$storage->put(
$this->preview_relative_path,
$im->getImagesBlob()
);
} else {
2022-07-28 10:10:28 +02:00
$im->setImageFormat(\pathinfo($this->preview_relative_path, \PATHINFO_EXTENSION));
if ($this->largeur >= $format->largeur || $this->hauteur >= $format->hauteur || $this->largeur == "") {
$im->thumbnailImage($format->largeur, $format->hauteur, true);
}
$storage->put(
$this->preview_relative_path,
$im->getImageBlob()
);
} //end if
// * create thumb
/** @var \App\Models\Format $format */
$format = Format::withoutGlobalScope('App\Scopes\SystemScope')
2025-03-25 11:00:14 +01:00
->where('libelle', '=', 'thumb')->firstOrFail();
if ($this->extension != MediaExtension::png) {
$im->setImageBackgroundColor('#ffffff');
$im->setImageAlphaChannel(\Imagick::ALPHACHANNEL_REMOVE);
$im->mergeImageLayers(\Imagick::LAYERMETHOD_FLATTEN);
$im->stripImage();
}
$im->thumbnailImage($format->largeur, $format->hauteur, true);
$storage->put(
$this->thumb_relative_path,
$im->getImageBlob()
);
$im->destroy();
unset($im);
}
/**
* Renvoi le chemin attendu d'un média selon le format demandé
*
* @param \App\Models\Format $format
* @return string
* @throws \RuntimeException If Format is not handled.
*/
public function getPath(Format $format): string
{
/** @var \Illuminate\Filesystem\FilesystemAdapter */
$storage = Storage::disk('media');
// * format spécifique
if (!$format->system) {
if (in_array($this->extension, ['gif', 'jpeg', 'jpg', 'png', 'pdf', 'ai', 'psd'])) {
return $storage->path("/{$this->folder}/{$this->uid}_{$format->libelle}.{$format->extension->value}");
}
return $storage->path($this->relative_path);
}
// * format système (thumb et preview)
if ($format->system) {
if (in_array($this->extension, ['gif', 'jpeg', 'jpg', 'png'])) {
// * Gestion du cas de format qui fixe l'extension.
$file_extension = $format->extension != '' ? $format->extension : $this->extension;
return $storage->path("/{$this->folder}/{$this->uid}_{$format->libelle}.{$file_extension}");
} elseif (in_array($this->extension, ['pdf', 'ai', 'psd'])) {
return $storage->path("/{$this->folder}/{$this->uid}_{$format->libelle}.png");
}
return $storage->path("/{$this->folder}/{$this->uid}.{$this->extension->value}");
}
throw new \RuntimeException("Unhandled Format $format->id $format->libelle");
}
2022-04-19 11:52:41 +02:00
/**
* Convert the model instance to an array.
*
* @return array
*/
public function toArray()
{
/** @var \Illuminate\Filesystem\FilesystemAdapter */
$storage = Storage::disk('media');
2022-04-19 11:52:41 +02:00
return array_merge(parent::toArray(), [
'size' => ToolboxHelper::formatBytes($this->taille),
'url' => $storage->url($this->relative_path),
'preview_url' => $storage->url($this->preview_relative_path),
'thumb_url' => $storage->url($this->thumb_relative_path),
'jpeg_url' => $storage->url($this->jpeg_relative_path),
2022-04-19 11:52:41 +02:00
'created_at' => $this->created_at->formatLocalized('%Y-%m-%d %H:%M:%S'),
'updated_at' => $this->updated_at->formatLocalized('%Y-%m-%d %H:%M:%S')
]);
}
2019-05-22 17:10:16 +02:00
}