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']); return [ 'items' => array_map(fn(self $row): array => $this->decorate($row->cast()), $result['subset'] ?: []), 'pagination' => [ 'page' => max(1, min($page, $result['count'] ?: 1)), 'pages' => max(1, (int) ($result['count'] ?: 1)), ], ]; } public function recent(int $limit): array { return array_map( fn(self $row): array => $this->decorate($row->cast()), $this->find(null, ['order' => 'created_at DESC, id DESC', 'limit' => $limit]) ?: [] ); } 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 $path, string $originalName = ''): int { if (!is_file($path)) { throw new RuntimeException('Fichier image introuvable.'); } $info = @getimagesize($path); if (!is_array($info)) { @unlink($path); throw new RuntimeException('Fichier image invalide.'); } $mime = strtolower((string) ($info['mime'] ?? '')); $extension = match ($mime) { 'image/jpeg' => 'jpg', 'image/png' => 'png', default => null, }; if ($extension === null) { @unlink($path); throw new RuntimeException('Format non supporté. Utilise JPG ou PNG.'); } $fileName = bin2hex(random_bytes(16)) . '.' . $extension; $target = rtrim((string) Base::instance()->get('paths.media_dir'), '/\\') . DIRECTORY_SEPARATOR . $fileName; if (!@rename($path, $target)) { if (!@copy($path, $target)) { @unlink($path); throw new RuntimeException('Impossible d’enregistrer cette image.'); } @unlink($path); } $this->reset(); $this->file_name = $fileName; $this->alt = $this->altFromName($originalName); $this->width = (int) $info[0]; $this->height = (int) $info[1]; $this->created_at = app_now(); $this->save(); 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.'); } $path = rtrim((string) Base::instance()->get('paths.media_dir'), '/\\') . DIRECTORY_SEPARATOR . $this->file_name; $this->erase(); if (is_file($path)) { @unlink($path); } } private function decorate(array $row): array { $file = (string) $row['file_name']; $alt = (string) $row['alt']; return [ 'id' => (int) $row['id'], 'file_name' => $file, 'alt' => $alt, 'width' => (int) $row['width'], 'height' => (int) $row['height'], 'created_at' => (string) $row['created_at'], 'url' => rtrim((string) Base::instance()->get('BASE'), '/') . rtrim((string) Base::instance()->get('paths.media_base'), '/') . '/' . rawurlencode($file), 'markdown' => '![' . $alt . '](media:' . $file . ')', ]; } private function altFromName(string $name): string { $name = trim(pathinfo($name, PATHINFO_FILENAME)); $name = preg_replace('/[-_]+/', ' ', $name) ?: ''; return trim($name); } }