First commit
This commit is contained in:
132
app/Helpers/App.php
Normal file
132
app/Helpers/App.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// ── Core ────────────────────────────────────────────────────────────
|
||||
|
||||
function app_root(): string
|
||||
{
|
||||
return dirname(__DIR__, 2);
|
||||
}
|
||||
|
||||
function app_timezone(): string
|
||||
{
|
||||
$timezone = trim((string) Base::instance()->get('app.timezone'));
|
||||
return ($timezone !== '' && in_array($timezone, DateTimeZone::listIdentifiers(), true)) ? $timezone : 'UTC';
|
||||
}
|
||||
|
||||
function app_now(): string
|
||||
{
|
||||
return gmdate('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
function app_is_prod(): bool
|
||||
{
|
||||
return Base::instance()->get('app.env') === 'prod';
|
||||
}
|
||||
|
||||
// ── Fichiers et chemins ─────────────────────────────────────────────
|
||||
|
||||
function app_ensure_dir(string $path): void
|
||||
{
|
||||
if (!is_dir($path)) {
|
||||
mkdir($path, 0775, true);
|
||||
}
|
||||
}
|
||||
|
||||
function app_db_path(): string
|
||||
{
|
||||
return app_root() . '/db/app.sqlite';
|
||||
}
|
||||
|
||||
function app_logs_dir(): string
|
||||
{
|
||||
return app_root() . '/logs';
|
||||
}
|
||||
|
||||
function app_public_media_dir(): string
|
||||
{
|
||||
return app_root() . '/public/uploads/media';
|
||||
}
|
||||
|
||||
function app_media_url(string $fileName): string
|
||||
{
|
||||
return rtrim((string) Base::instance()->get('BASE'), '/') . '/uploads/media/' . rawurlencode($fileName);
|
||||
}
|
||||
|
||||
// ── Texte ───────────────────────────────────────────────────────────
|
||||
|
||||
function app_slugify(string $value): string
|
||||
{
|
||||
$slug = Web::instance()->slug(trim($value));
|
||||
return $slug !== '' ? $slug : 'article';
|
||||
}
|
||||
|
||||
function app_unique_slug(string $value, callable $exists): string
|
||||
{
|
||||
$base = app_slugify($value);
|
||||
if (!$exists($base)) {
|
||||
return $base;
|
||||
}
|
||||
|
||||
for ($i = 2; $i <= 1000; $i++) {
|
||||
$candidate = $base . '-' . $i;
|
||||
if (!$exists($candidate)) {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException('Impossible de générer un slug unique.');
|
||||
}
|
||||
|
||||
function app_format_datetime_fr(string $value): string
|
||||
{
|
||||
static $utc, $formatter;
|
||||
|
||||
$value = trim($value);
|
||||
if ($value === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
$utc ??= new DateTimeZone('UTC');
|
||||
$date = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $value, $utc);
|
||||
if (!$date instanceof DateTimeImmutable) {
|
||||
$date = new DateTimeImmutable($value, $utc);
|
||||
}
|
||||
|
||||
$date = $date->setTimezone(new DateTimeZone(date_default_timezone_get()));
|
||||
|
||||
if (class_exists('IntlDateFormatter')) {
|
||||
$formatter ??= new IntlDateFormatter(
|
||||
'fr_FR',
|
||||
IntlDateFormatter::LONG,
|
||||
IntlDateFormatter::SHORT,
|
||||
date_default_timezone_get(),
|
||||
IntlDateFormatter::GREGORIAN,
|
||||
"d MMMM yyyy 'à' HH:mm"
|
||||
);
|
||||
|
||||
$formatted = $formatter->format($date);
|
||||
if (is_string($formatted) && $formatted !== '') {
|
||||
return $formatted;
|
||||
}
|
||||
}
|
||||
|
||||
$months = [
|
||||
1 => 'janvier', 2 => 'février', 3 => 'mars', 4 => 'avril',
|
||||
5 => 'mai', 6 => 'juin', 7 => 'juillet', 8 => 'août',
|
||||
9 => 'septembre', 10 => 'octobre', 11 => 'novembre', 12 => 'décembre',
|
||||
];
|
||||
|
||||
return sprintf(
|
||||
'%d %s %d à %s',
|
||||
(int) $date->format('j'),
|
||||
$months[(int) $date->format('n')] ?? $date->format('F'),
|
||||
(int) $date->format('Y'),
|
||||
$date->format('H:i')
|
||||
);
|
||||
} catch (Throwable) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
162
app/Helpers/Error.php
Normal file
162
app/Helpers/Error.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
function app_error_meta(int $code): array
|
||||
{
|
||||
return match ($code) {
|
||||
400 => ['title' => 'Requête invalide', 'message' => 'La requête envoyée est invalide.'],
|
||||
403 => ['title' => 'Accès refusé', 'message' => 'Tu n\u2019as pas accès à cette ressource.'],
|
||||
404 => ['title' => 'Page introuvable', 'message' => 'La page demandée est introuvable.'],
|
||||
default => ['title' => 'Erreur serveur', 'message' => 'Une erreur est survenue.'],
|
||||
};
|
||||
}
|
||||
|
||||
function app_bootstrap_logging(): void
|
||||
{
|
||||
$dir = rtrim((string) Base::instance()->get('LOGS'), '/\\') . DIRECTORY_SEPARATOR;
|
||||
app_ensure_dir($dir);
|
||||
ini_set('log_errors', '1');
|
||||
ini_set('error_log', $dir . 'php-error.log');
|
||||
ini_set('display_errors', app_is_prod() ? '0' : '1');
|
||||
error_reporting(E_ALL);
|
||||
}
|
||||
|
||||
function app_request_summary(): string
|
||||
{
|
||||
$f3 = Base::instance();
|
||||
return sprintf(
|
||||
'request=%s %s ip=%s',
|
||||
(string) ($f3->get('VERB') ?? 'CLI'),
|
||||
(string) ($f3->get('URI') ?? '/'),
|
||||
(string) ($f3->get('IP') ?? '0.0.0.0')
|
||||
);
|
||||
}
|
||||
|
||||
function app_write_log(string $fileName, string $line): void
|
||||
{
|
||||
(new Log($fileName))->write($line);
|
||||
}
|
||||
|
||||
function app_log_error(int $code, string $status, string $text, ?Throwable $exception = null): void
|
||||
{
|
||||
if ($code === 404) {
|
||||
return;
|
||||
}
|
||||
|
||||
$level = $code >= 500 ? 'error' : ($code >= 400 ? 'warning' : 'info');
|
||||
$parts = [
|
||||
sprintf('level=%s code=%d status="%s"', $level, $code, $status),
|
||||
app_request_summary(),
|
||||
];
|
||||
|
||||
if ($text !== '') {
|
||||
$parts[] = 'message="' . str_replace(["\n", '"'], ['\\n', '\\"'], $text) . '"';
|
||||
}
|
||||
|
||||
if ($exception !== null) {
|
||||
$parts[] = sprintf('exception="%s" file="%s:%d"', $exception::class, $exception->getFile(), $exception->getLine());
|
||||
}
|
||||
|
||||
app_write_log('app.log', implode(' | ', $parts));
|
||||
}
|
||||
|
||||
function app_render_error_json(int $code): void
|
||||
{
|
||||
$f3 = Base::instance();
|
||||
$meta = app_error_meta($code);
|
||||
|
||||
while (ob_get_level() > 0) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
$f3->status($code);
|
||||
$f3->expire(0);
|
||||
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
echo json_encode(
|
||||
['error' => ['code' => $code, 'title' => $meta['title'], 'message' => $meta['message']]],
|
||||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
|
||||
);
|
||||
}
|
||||
|
||||
function app_render_error_fallback(int $code): void
|
||||
{
|
||||
$f3 = Base::instance();
|
||||
$meta = app_error_meta($code);
|
||||
$base = rtrim((string) $f3->get('BASE'), '/');
|
||||
|
||||
while (ob_get_level() > 0) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
$f3->status($code);
|
||||
$f3->expire(0);
|
||||
|
||||
if (!headers_sent()) {
|
||||
header('Content-Type: text/html; charset=UTF-8');
|
||||
header('Cache-Control: no-cache, no-store, must-revalidate');
|
||||
}
|
||||
|
||||
$title = htmlspecialchars((string) $meta['title'], ENT_QUOTES, 'UTF-8');
|
||||
$message = htmlspecialchars((string) $meta['message'], ENT_QUOTES, 'UTF-8');
|
||||
$href = htmlspecialchars($base . '/', ENT_QUOTES, 'UTF-8');
|
||||
|
||||
echo '<!doctype html><html lang="fr"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>' . $title . '</title></head><body><main><h1>' . $title . '</h1><p>' . $message . '</p><p><a href="' . $href . '">Retour à l\'accueil</a></p></main></body></html>';
|
||||
}
|
||||
|
||||
function app_bootstrap_errors(Base $f3): void
|
||||
{
|
||||
if (app_is_prod()) {
|
||||
register_shutdown_function(function (): void {
|
||||
$error = error_get_last();
|
||||
if ($error === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fatalTypes = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR];
|
||||
if (!in_array($error['type'] ?? 0, $fatalTypes, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
app_render_error_fallback(500);
|
||||
});
|
||||
}
|
||||
|
||||
$f3->set('ONERROR', function (Base $f3): void {
|
||||
$code = (int) ($f3->get('ERROR.code') ?? 500);
|
||||
$status = (string) ($f3->get('ERROR.status') ?? 'Internal Server Error');
|
||||
$text = (string) ($f3->get('ERROR.text') ?? '');
|
||||
|
||||
if (!app_is_prod() && (int) $f3->get('DEBUG') > 0) {
|
||||
$f3->status($code > 0 ? $code : 500);
|
||||
echo $text;
|
||||
return;
|
||||
}
|
||||
|
||||
$code = $code > 0 ? $code : 500;
|
||||
app_log_error($code, $status, $text);
|
||||
|
||||
if ($f3->get('AJAX')) {
|
||||
app_render_error_json($code);
|
||||
return;
|
||||
}
|
||||
|
||||
$f3->expire(0);
|
||||
$f3->status($code);
|
||||
|
||||
$meta = app_error_meta($code);
|
||||
$f3->mset([
|
||||
'errorCode' => $code,
|
||||
'errorTitle' => $meta['title'],
|
||||
'errorMessage' => $meta['message'],
|
||||
]);
|
||||
|
||||
try {
|
||||
echo Template::instance()->render('errors/error.html');
|
||||
} catch (Throwable $exception) {
|
||||
app_log_error(500, 'Internal Server Error', 'Error template rendering failed.', $exception);
|
||||
app_render_error_fallback($code);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user