176 lines
5.3 KiB
PHP
176 lines
5.3 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
class Media extends DB\SQL\Mapper
|
||
{
|
||
public function __construct()
|
||
{
|
||
parent::__construct(Base::instance()->get('DB'), 'media');
|
||
}
|
||
|
||
public static function bootstrap(DB\SQL $db): void
|
||
{
|
||
if ($db->schema('media', null, 0)) {
|
||
return;
|
||
}
|
||
|
||
$db->exec('CREATE TABLE 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 idx_media_created_at ON media(created_at DESC)');
|
||
}
|
||
|
||
public function page(int $page, int $perPage): array
|
||
{
|
||
$result = $this->paginate(max(0, $page - 1), $perPage, null, ['order' => 'created_at DESC, id DESC']);
|
||
$items = array_map(fn(self $row): array => $this->decorate($row->cast()), $result['subset'] ?: []);
|
||
|
||
return [
|
||
'items' => $items,
|
||
'pagination' => [
|
||
'page' => max(1, min($page, $result['count'] ?: 1)),
|
||
'pages' => max(1, (int) ($result['count'] ?: 1)),
|
||
],
|
||
];
|
||
}
|
||
|
||
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());
|
||
}
|
||
|
||
public function upload(string $temporaryPath, string $originalName = ''): int
|
||
{
|
||
$f3 = Base::instance();
|
||
|
||
if (!is_file($temporaryPath)) {
|
||
throw new RuntimeException('Fichier image introuvable.');
|
||
}
|
||
|
||
$binary = $f3->read($temporaryPath);
|
||
$image = new Image();
|
||
if ($binary === '' || $image->load($binary) === false) {
|
||
@unlink($temporaryPath);
|
||
throw new RuntimeException('Fichier image invalide.');
|
||
}
|
||
|
||
$info = @getimagesizefromstring($binary);
|
||
$mime = strtolower((string) ($info['mime'] ?? ''));
|
||
$extension = match ($mime) {
|
||
'image/jpeg' => 'jpg',
|
||
'image/png' => 'png',
|
||
default => null,
|
||
};
|
||
|
||
if ($extension === null) {
|
||
@unlink($temporaryPath);
|
||
throw new RuntimeException('Format non supporté. Utilise JPG ou PNG.');
|
||
}
|
||
|
||
$encoded = match ($extension) {
|
||
'jpg' => $image->dump('jpeg', 90),
|
||
'png' => $image->dump('png'),
|
||
};
|
||
|
||
$fileName = bin2hex(random_bytes(16)) . '.' . $extension;
|
||
$target = $this->storagePath($fileName);
|
||
|
||
try {
|
||
$f3->write($target, $encoded);
|
||
} catch (Throwable $e) {
|
||
@unlink($temporaryPath);
|
||
throw new RuntimeException('Impossible d’enregistrer cette image.', 0, $e);
|
||
}
|
||
|
||
@unlink($temporaryPath);
|
||
|
||
try {
|
||
$this->reset();
|
||
$this->file_name = $fileName;
|
||
$this->alt = $this->guessAlt($originalName);
|
||
$this->width = $image->width();
|
||
$this->height = $image->height();
|
||
$this->created_at = app_now();
|
||
$this->save();
|
||
} catch (Throwable $e) {
|
||
@unlink($target);
|
||
throw new RuntimeException('Impossible de finaliser cette image.', 0, $e);
|
||
}
|
||
|
||
return (int) $this->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 deleteById(int $id): void
|
||
{
|
||
$this->load(['id = ?', $id]);
|
||
if ($this->dry()) {
|
||
throw new RuntimeException('Image introuvable.');
|
||
}
|
||
|
||
try {
|
||
$this->erase();
|
||
} catch (Throwable $e) {
|
||
throw new RuntimeException('Impossible de supprimer cette image.', 0, $e);
|
||
}
|
||
}
|
||
|
||
private function decorate(array $row): array
|
||
{
|
||
$fileName = (string) $row['file_name'];
|
||
$alt = (string) $row['alt'];
|
||
$base = rtrim((string) Base::instance()->get('BASE'), '/');
|
||
$mediaBase = rtrim((string) Base::instance()->get('paths.media_base'), '/');
|
||
|
||
return [
|
||
'id' => (int) $row['id'],
|
||
'file_name' => $fileName,
|
||
'alt' => $alt,
|
||
'width' => (int) $row['width'],
|
||
'height' => (int) $row['height'],
|
||
'created_at' => (string) $row['created_at'],
|
||
'url' => $base . $mediaBase . '/' . rawurlencode($fileName),
|
||
'markdown' => '',
|
||
];
|
||
}
|
||
|
||
private function storagePath(string $fileName): string
|
||
{
|
||
return rtrim((string) Base::instance()->get('paths.media_dir'), '/\\') . DIRECTORY_SEPARATOR . $fileName;
|
||
}
|
||
|
||
private function guessAlt(string $originalName): string
|
||
{
|
||
$label = trim(pathinfo($originalName, PATHINFO_FILENAME));
|
||
$label = preg_replace('/[-_]+/', ' ', $label) ?: '';
|
||
return trim($label);
|
||
}
|
||
}
|