157 lines
5.2 KiB
PHP
157 lines
5.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
class Media extends DB\SQL\Mapper
|
|
{
|
|
public function __construct(DB\SQL $db)
|
|
{
|
|
parent::__construct($db, 'media');
|
|
}
|
|
|
|
public static function bootstrap(DB\SQL $db): void
|
|
{
|
|
$db->exec('CREATE TABLE IF NOT EXISTS media (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
file_name TEXT NOT NULL UNIQUE,
|
|
alt TEXT NOT NULL DEFAULT \'\',
|
|
width INTEGER NOT NULL,
|
|
height INTEGER NOT NULL,
|
|
created_at TEXT NOT NULL
|
|
)');
|
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_media_created_at ON media(created_at DESC)');
|
|
}
|
|
|
|
public function all(): array
|
|
{
|
|
return array_map(
|
|
fn (self $m): array => $this->decorate($m->cast()),
|
|
$this->find(null, ['order' => 'created_at DESC, id DESC']) ?: []
|
|
);
|
|
}
|
|
|
|
public function findById(int $id): ?array
|
|
{
|
|
if ($id <= 0) {
|
|
return null;
|
|
}
|
|
|
|
$this->load(['id = ?', $id]);
|
|
return $this->dry() ? null : $this->decorate($this->cast());
|
|
}
|
|
|
|
public function findByFileName(string $fileName): ?array
|
|
{
|
|
$this->load(['file_name = ?', $fileName]);
|
|
return $this->dry() ? null : $this->decorate($this->cast());
|
|
}
|
|
|
|
// Reçoit le chemin absolu déposé par Web::receive() et le nom d'origine
|
|
// pour dériver un texte alternatif lisible.
|
|
public function upload(string $srcPath, string $originalName = ''): int
|
|
{
|
|
// Image::dump() gère le chargement et la compression.
|
|
// Attention : en JPEG, la transparence n'est pas conservée.
|
|
// $path='' indique à F3 que le chemin est absolu.
|
|
try {
|
|
$img = new Image($srcPath, false, '');
|
|
} catch (Throwable) {
|
|
throw new RuntimeException('Fichier image invalide ou format source non supporté.');
|
|
}
|
|
|
|
$data = $img->dump('jpeg', 85);
|
|
|
|
$fileName = bin2hex(random_bytes(16)) . '.jpg';
|
|
$target = app_public_media_dir() . '/' . $fileName;
|
|
|
|
if (!Base::instance()->write($target, $data)) {
|
|
throw new RuntimeException('Impossible d\'enregistrer cette image.');
|
|
}
|
|
|
|
// Supprimer le fichier intermédiaire déposé par Web::receive().
|
|
@unlink($srcPath);
|
|
|
|
$this->reset();
|
|
$this->file_name = $fileName;
|
|
$this->alt = $originalName !== '' ? self::altFromFilename($originalName) : '';
|
|
$this->width = $img->width();
|
|
$this->height = $img->height();
|
|
$this->created_at = app_now();
|
|
$this->save();
|
|
|
|
return (int) $this->get('id');
|
|
}
|
|
|
|
public function updateAlt(int $id, string $alt): void
|
|
{
|
|
$this->load(['id = ?', $id]);
|
|
if ($this->dry()) {
|
|
throw new RuntimeException('Image introuvable.');
|
|
}
|
|
|
|
$this->alt = trim($alt);
|
|
$this->save();
|
|
}
|
|
|
|
public function delete(int $id): void
|
|
{
|
|
$item = $this->findById($id);
|
|
if ($item === null) {
|
|
throw new RuntimeException('Image introuvable.');
|
|
}
|
|
|
|
if ($this->isUsed($item)) {
|
|
throw new RuntimeException('Cette image est encore utilisée par un article.');
|
|
}
|
|
|
|
$path = app_public_media_dir() . '/' . $item['file_name'];
|
|
|
|
$this->db->begin();
|
|
try {
|
|
$this->erase();
|
|
if (is_file($path) && !unlink($path)) {
|
|
throw new RuntimeException('Impossible de supprimer le fichier image.');
|
|
}
|
|
$this->db->commit();
|
|
} catch (Throwable $e) {
|
|
$this->db->rollback();
|
|
throw $e instanceof RuntimeException ? $e : new RuntimeException('Suppression impossible.');
|
|
}
|
|
}
|
|
|
|
// Dérive un texte alternatif lisible depuis le nom de fichier d'origine.
|
|
private static function altFromFilename(string $filename): string
|
|
{
|
|
$name = pathinfo($filename, PATHINFO_FILENAME);
|
|
$name = trim((string) preg_replace('/[-_]+/', ' ', $name));
|
|
// mb_ucfirst() n'existe qu'en PHP 8.4 — on l'émule.
|
|
return mb_strtoupper(mb_substr($name, 0, 1)) . mb_strtolower(mb_substr($name, 1));
|
|
}
|
|
|
|
// Une seule requête SQL pour les deux cas d'utilisation (couverture et body).
|
|
private function isUsed(array $item): bool
|
|
{
|
|
return $this->db->exec(
|
|
'SELECT 1 FROM posts WHERE cover_media_id = ? OR body_markdown LIKE ? LIMIT 1',
|
|
[$item['id'], '%media:' . $item['file_name'] . '%']
|
|
) !== [];
|
|
}
|
|
|
|
private function decorate(array $row): array
|
|
{
|
|
$alt = (string) $row['alt'];
|
|
|
|
return [
|
|
'id' => (int) $row['id'],
|
|
'file_name' => (string) $row['file_name'],
|
|
'alt' => $alt,
|
|
'width' => (int) $row['width'],
|
|
'height' => (int) $row['height'],
|
|
'created_at' => (string) $row['created_at'],
|
|
'created_at_label' => app_format_datetime_fr((string) $row['created_at']),
|
|
'url' => app_media_url((string) $row['file_name']),
|
|
'markdown' => '',
|
|
];
|
|
}
|
|
}
|