Simplified
This commit is contained in:
10
.env.example
10
.env.example
@@ -1,10 +1,2 @@
|
||||
# Environnement de l'application
|
||||
# Environnement de l'application (development ou production)
|
||||
APP_ENV=development
|
||||
|
||||
# Chemin de cache Twig (laisser vide en développement pour désactiver le cache)
|
||||
# Exemples :
|
||||
# TWIG_CACHE=var/cache/twig
|
||||
# TWIG_CACHE=/srv/www/myapp/var/cache/twig
|
||||
TWIG_CACHE=
|
||||
|
||||
# (Optionnel) autres variables d'environnement peuvent être ajoutées ici
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
"slim/psr7": "*",
|
||||
"twig/twig": "*",
|
||||
"slim/twig-view": "^3.4",
|
||||
"catfan/medoo": "2.*",
|
||||
"vlucas/phpdotenv": "^5.6"
|
||||
"catfan/medoo": "2.*"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
@@ -26,4 +25,4 @@
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.94"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,28 +6,85 @@ require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use Slim\Factory\AppFactory;
|
||||
use Slim\Views\TwigMiddleware;
|
||||
use Slim\Views\Twig;
|
||||
use Medoo\Medoo;
|
||||
use App\Controllers\PostController;
|
||||
|
||||
// Charger et créer les services centralisés
|
||||
$services = App\Factories\ServiceFactory::createServices();
|
||||
// ============================================
|
||||
// Configuration
|
||||
// ============================================
|
||||
|
||||
/** @var \Slim\Views\Twig $twig */
|
||||
$twig = $services['view'];
|
||||
$env = $_ENV['APP_ENV'] ?? 'production';
|
||||
$isDev = strtolower($env) === 'development';
|
||||
|
||||
// Dossier de cache Twig (false en dev, chemin en prod)
|
||||
$twigCache = $isDev ? false : __DIR__ . '/../var/cache/twig';
|
||||
if ($twigCache && !is_dir($twigCache)) {
|
||||
@mkdir($twigCache, 0755, true);
|
||||
}
|
||||
|
||||
// Chemin base de données
|
||||
$dbFile = __DIR__ . '/../database/app.sqlite';
|
||||
$dbDir = dirname($dbFile);
|
||||
if (!is_dir($dbDir)) {
|
||||
@mkdir($dbDir, 0755, true);
|
||||
}
|
||||
if (!file_exists($dbFile)) {
|
||||
@touch($dbFile);
|
||||
@chmod($dbFile, 0664);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Initialisation des services
|
||||
// ============================================
|
||||
|
||||
// Twig
|
||||
$twig = Twig::create(
|
||||
__DIR__ . '/../views',
|
||||
['cache' => $twigCache]
|
||||
);
|
||||
|
||||
// Medoo (SQLite)
|
||||
$db = new Medoo([
|
||||
'type' => 'sqlite',
|
||||
'database' => $dbFile,
|
||||
]);
|
||||
|
||||
// Créer la table si elle n'existe pas
|
||||
$db->pdo->exec("
|
||||
CREATE TABLE IF NOT EXISTS post (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
");
|
||||
|
||||
// ============================================
|
||||
// Slim App
|
||||
// ============================================
|
||||
|
||||
// Slim app
|
||||
$app = AppFactory::create();
|
||||
|
||||
// Middlewares essentiels
|
||||
$app->addBodyParsingMiddleware();
|
||||
$app->add(TwigMiddleware::create($app, $twig));
|
||||
|
||||
// Charger les routes
|
||||
$routesPath = __DIR__ . '/../src/Routes/web.php';
|
||||
if (file_exists($routesPath)) {
|
||||
/** @var callable $routes */
|
||||
$routes = require $routesPath;
|
||||
$routes($app);
|
||||
}
|
||||
// ============================================
|
||||
// Routes
|
||||
// ============================================
|
||||
|
||||
$errorMiddleware = $app->addErrorMiddleware(true, true, true);
|
||||
$controller = new PostController($twig, $db);
|
||||
|
||||
$app->get('/', [$controller, 'index']);
|
||||
$app->get('/admin', [$controller, 'admin']);
|
||||
$app->get('/admin/edit/{id}', [$controller, 'form']);
|
||||
$app->post('/admin/create', [$controller, 'create']);
|
||||
$app->post('/admin/edit/{id}', [$controller, 'update']);
|
||||
$app->post('/admin/delete/{id}', [$controller, 'delete']);
|
||||
|
||||
// ============================================
|
||||
// Run
|
||||
// ============================================
|
||||
|
||||
$errorMiddleware = $app->addErrorMiddleware($isDev, $isDev, $isDev);
|
||||
$app->run();
|
||||
|
||||
@@ -7,8 +7,7 @@ namespace App\Controllers;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Slim\Views\Twig;
|
||||
use App\Repositories\PostRepositoryInterface as PostRepository;
|
||||
use App\Requests\PostRequest;
|
||||
use Medoo\Medoo;
|
||||
use App\Models\Post;
|
||||
|
||||
/**
|
||||
@@ -16,93 +15,133 @@ use App\Models\Post;
|
||||
*/
|
||||
class PostController
|
||||
{
|
||||
private Twig $view;
|
||||
private PostRepository $repo;
|
||||
|
||||
public function __construct(Twig $view, PostRepository $repo)
|
||||
public function __construct(private Twig $view, private Medoo $db)
|
||||
{
|
||||
$this->view = $view;
|
||||
$this->repo = $repo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche la page d'accueil avec tous les articles.
|
||||
*/
|
||||
public function index(Request $req, Response $res): Response
|
||||
{
|
||||
$posts = $this->repo->allDesc(); // Post[]
|
||||
$rows = $this->db->select('post', '*', ['ORDER' => ['id' => 'DESC']]);
|
||||
$posts = array_map(fn ($row) => Post::fromArray($row), $rows ?: []);
|
||||
|
||||
return $this->view->render($res, 'pages/home.twig', ['posts' => $posts]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche la page d'administration.
|
||||
*/
|
||||
public function admin(Request $req, Response $res): Response
|
||||
{
|
||||
$posts = $this->repo->allDesc();
|
||||
$rows = $this->db->select('post', '*', ['ORDER' => ['id' => 'DESC']]);
|
||||
$posts = array_map(fn ($row) => Post::fromArray($row), $rows ?: []);
|
||||
|
||||
return $this->view->render($res, 'pages/admin.twig', ['posts' => $posts]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formulaire de création / édition.
|
||||
*
|
||||
* @param Request $req
|
||||
* @param Response $res
|
||||
* @param array $args
|
||||
* @return Response
|
||||
* Affiche le formulaire de création/édition.
|
||||
*/
|
||||
public function form(Request $req, Response $res, array $args): Response
|
||||
{
|
||||
$id = (int)($args['id'] ?? 0);
|
||||
$post = $id ? $this->repo->find($id) : null;
|
||||
$post = null;
|
||||
|
||||
// Si id fourni mais post introuvable -> 404
|
||||
if ($id > 0 && $post === null) {
|
||||
$res->getBody()->write('Article non trouvé');
|
||||
return $res->withStatus(404);
|
||||
if ($id > 0) {
|
||||
$row = $this->db->get('post', '*', ['id' => $id]);
|
||||
if (!$row) {
|
||||
$res->getBody()->write('Article non trouvé');
|
||||
return $res->withStatus(404);
|
||||
}
|
||||
$post = Post::fromArray($row);
|
||||
}
|
||||
|
||||
// Twig peut accéder aux getters (post.title, post.content)
|
||||
$action = $id ? "/admin/edit/{$id}" : "/admin/create";
|
||||
return $this->view->render($res, 'pages/post_form.twig', ['post' => $post, 'action' => $action]);
|
||||
return $this->view->render($res, 'pages/post_form.twig', [
|
||||
'post' => $post,
|
||||
'action' => $action,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un nouvel article.
|
||||
*/
|
||||
public function create(Request $req, Response $res): Response
|
||||
{
|
||||
$postRequest = PostRequest::fromArray($req->getParsedBody());
|
||||
if (! $postRequest->isValid()) {
|
||||
$data = $req->getParsedBody();
|
||||
$title = trim((string)($data['title'] ?? ''));
|
||||
$content = trim((string)($data['content'] ?? ''));
|
||||
|
||||
// Créer un objet Post pour valider
|
||||
try {
|
||||
$post = new Post(0, $title, $content);
|
||||
} catch (\InvalidArgumentException) {
|
||||
// Validation échouée, retour à l'admin
|
||||
return $res->withHeader('Location', '/admin')->withStatus(302);
|
||||
}
|
||||
|
||||
// Utilisation du helper toModel() pour construire l'entité Post
|
||||
$post = $postRequest->toModel(0);
|
||||
$this->repo->create($post);
|
||||
// Persister en DB
|
||||
$this->db->insert('post', [
|
||||
'title' => $post->getTitle(),
|
||||
'content' => $post->getContent(),
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
return $res->withHeader('Location', '/admin')->withStatus(302);
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour un article existant.
|
||||
*/
|
||||
public function update(Request $req, Response $res, array $args): Response
|
||||
{
|
||||
$id = (int)$args['id'];
|
||||
$existing = $this->repo->find($id);
|
||||
if ($existing === null) {
|
||||
$existing = $this->db->get('post', 'id', ['id' => $id]);
|
||||
|
||||
if (!$existing) {
|
||||
$res->getBody()->write('Article non trouvé');
|
||||
return $res->withStatus(404);
|
||||
}
|
||||
|
||||
$postRequest = PostRequest::fromArray($req->getParsedBody());
|
||||
if (! $postRequest->isValid()) {
|
||||
$data = $req->getParsedBody();
|
||||
$title = trim((string)($data['title'] ?? ''));
|
||||
$content = trim((string)($data['content'] ?? ''));
|
||||
|
||||
// Créer un objet Post pour valider
|
||||
try {
|
||||
$post = new Post($id, $title, $content);
|
||||
} catch (\InvalidArgumentException) {
|
||||
// Validation échouée, retour à l'admin
|
||||
return $res->withHeader('Location', '/admin')->withStatus(302);
|
||||
}
|
||||
|
||||
$post = $postRequest->toModel($id);
|
||||
$this->repo->update($id, $post);
|
||||
// Persister en DB
|
||||
$this->db->update('post', [
|
||||
'title' => $post->getTitle(),
|
||||
'content' => $post->getContent(),
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
], ['id' => $id]);
|
||||
|
||||
return $res->withHeader('Location', '/admin')->withStatus(302);
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime un article.
|
||||
*/
|
||||
public function delete(Request $req, Response $res, array $args): Response
|
||||
{
|
||||
$id = (int)$args['id'];
|
||||
$existing = $this->repo->find($id);
|
||||
if ($existing === null) {
|
||||
$existing = $this->db->get('post', 'id', ['id' => $id]);
|
||||
|
||||
if (!$existing) {
|
||||
$res->getBody()->write('Article non trouvé');
|
||||
return $res->withStatus(404);
|
||||
}
|
||||
|
||||
$this->repo->delete($id);
|
||||
$this->db->delete('post', ['id' => $id]);
|
||||
return $res->withHeader('Location', '/admin')->withStatus(302);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Factories;
|
||||
|
||||
use App\Controllers\PostController;
|
||||
use Slim\Views\Twig;
|
||||
use App\Repositories\PostRepositoryInterface;
|
||||
|
||||
/**
|
||||
* Fabrique d'instance de PostController pour faciliter l'intégration avec un container.
|
||||
*/
|
||||
final class PostControllerFactory
|
||||
{
|
||||
/**
|
||||
* Crée une instance de PostController depuis le tableau de services renvoyé par ServiceFactory.
|
||||
*
|
||||
* @param array<string,mixed> $services
|
||||
* @return PostController
|
||||
*/
|
||||
public static function create(array $services): PostController
|
||||
{
|
||||
/** @var Twig $view */
|
||||
$view = $services['view'];
|
||||
/** @var PostRepositoryInterface $repo */
|
||||
$repo = $services['postRepository'];
|
||||
|
||||
return new PostController($view, $repo);
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Factories;
|
||||
|
||||
use Slim\Views\Twig;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
use Medoo\Medoo;
|
||||
use Dotenv\Dotenv;
|
||||
use App\Repositories\PostRepositoryMedoo;
|
||||
use App\Repositories\PostRepositoryInterface;
|
||||
|
||||
/**
|
||||
* Fabrique des services de l'application.
|
||||
*
|
||||
* Cette fabrique centralise la création des dépendances (Twig, DB, repositories...)
|
||||
* pour garder public/index.php minimal et faciliter les tests/unit.
|
||||
*/
|
||||
final class ServiceFactory
|
||||
{
|
||||
/**
|
||||
* Crée et retourne un tableau associatif de services.
|
||||
*
|
||||
* Clefs retournées :
|
||||
* - 'view' => Twig
|
||||
* - 'postRepository' => PostRepositoryInterface
|
||||
*
|
||||
* @param array<string,mixed>|null $overrides Permet d'injecter des remplacements pour les tests.
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public static function createServices(?array $overrides = null): array
|
||||
{
|
||||
// Charger .env si présent (tolérant l'absence)
|
||||
$dotenv = Dotenv::createImmutable(__DIR__ . '/../../');
|
||||
$dotenv->safeLoad();
|
||||
|
||||
// Config basique (idem que précédemment mais centralisé)
|
||||
$env = strtolower((string) ($_ENV['APP_ENV'] ?? $_SERVER['APP_ENV'] ?? 'production'));
|
||||
$devEnvs = ['development', 'dev'];
|
||||
$twigCacheEnv = $_ENV['TWIG_CACHE'] ?? $_SERVER['TWIG_CACHE'] ?? null;
|
||||
if ($twigCacheEnv !== null && $twigCacheEnv !== '') {
|
||||
$twigCache = (string)$twigCacheEnv;
|
||||
} else {
|
||||
$twigCache = in_array($env, $devEnvs, true) ? false : __DIR__ . '/../../var/cache/twig';
|
||||
}
|
||||
|
||||
$dbFile = $_ENV['DB_FILE'] ?? __DIR__ . '/../../database/app.sqlite';
|
||||
$dbFileMode = 0664;
|
||||
|
||||
// Créer dossier cache si nécessaire
|
||||
if ($twigCache && $twigCache !== false && !is_dir((string) $twigCache)) {
|
||||
@mkdir((string) $twigCache, 0755, true);
|
||||
}
|
||||
|
||||
// Twig
|
||||
$loader = new FilesystemLoader(__DIR__ . '/../../views');
|
||||
$twig = new Twig($loader, ['cache' => $twigCache]);
|
||||
|
||||
// Medoo (SQLite)
|
||||
$dbDir = dirname($dbFile);
|
||||
if (!is_dir($dbDir)) {
|
||||
@mkdir($dbDir, 0755, true);
|
||||
}
|
||||
if (!file_exists($dbFile)) {
|
||||
@touch($dbFile);
|
||||
@chmod($dbFile, $dbFileMode);
|
||||
}
|
||||
|
||||
$medooOptions = [
|
||||
'database_type' => 'sqlite',
|
||||
'database_name' => $dbFile,
|
||||
'charset' => 'utf8',
|
||||
];
|
||||
$database = new Medoo($medooOptions);
|
||||
|
||||
// Repository
|
||||
$postRepository = new PostRepositoryMedoo($database);
|
||||
|
||||
$services = [
|
||||
'view' => $twig,
|
||||
'database' => $database,
|
||||
'postRepository' => $postRepository,
|
||||
];
|
||||
|
||||
// Appliquer overrides si fournis (utile pour tests)
|
||||
if (is_array($overrides)) {
|
||||
$services = array_merge($services, $overrides);
|
||||
}
|
||||
|
||||
return $services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,67 +4,151 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use DateTime;
|
||||
|
||||
/**
|
||||
* Représente un post (DTO / entité légère).
|
||||
* Modèle Post pour un blog en production.
|
||||
*
|
||||
* Cette classe est immuable par simplicité : on construit une instance
|
||||
* depuis les données (DB ou formulaire) et on récupère ses valeurs via des getters.
|
||||
* Encapsule la logique métier et la validation des articles.
|
||||
*/
|
||||
final class Post
|
||||
{
|
||||
private int $id;
|
||||
private string $title;
|
||||
private string $content;
|
||||
private DateTime $createdAt;
|
||||
private DateTime $updatedAt;
|
||||
|
||||
/**
|
||||
* @param int $id Identifiant (0 si non persisté)
|
||||
* @param string $title Titre de l'article
|
||||
* @param string $content Contenu HTML ou texte de l'article
|
||||
*/
|
||||
public function __construct(int $id, string $title, string $content)
|
||||
{
|
||||
$this->id = $id;
|
||||
$this->title = $title;
|
||||
$this->content = $content;
|
||||
public function __construct(
|
||||
private readonly int $id,
|
||||
private readonly string $title,
|
||||
private readonly string $content,
|
||||
?DateTime $createdAt = null,
|
||||
?DateTime $updatedAt = null,
|
||||
) {
|
||||
$this->createdAt = $createdAt ?? new DateTime();
|
||||
$this->updatedAt = $updatedAt ?? new DateTime();
|
||||
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une instance depuis un tableau (par ex. ligne DB ou formulaire).
|
||||
* Crée une instance depuis un tableau (ligne DB ou formulaire).
|
||||
*
|
||||
* Ce helper facilite la migration depuis le format tableau existant.
|
||||
*
|
||||
* @param array<string,mixed> $data Clé 'id' (optionnelle), 'title', 'content'
|
||||
* @param array<string,mixed> $data
|
||||
* @return self
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
$id = isset($data['id']) ? (int)$data['id'] : 0;
|
||||
$title = isset($data['title']) ? (string)$data['title'] : '';
|
||||
$content = isset($data['content']) ? (string)$data['content'] : '';
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
$title = (string)($data['title'] ?? '');
|
||||
$content = (string)($data['content'] ?? '');
|
||||
|
||||
return new self($id, $title, $content);
|
||||
$createdAt = isset($data['created_at'])
|
||||
? new DateTime($data['created_at'])
|
||||
: new DateTime();
|
||||
|
||||
$updatedAt = isset($data['updated_at'])
|
||||
? new DateTime($data['updated_at'])
|
||||
: new DateTime();
|
||||
|
||||
return new self($id, $title, $content, $createdAt, $updatedAt);
|
||||
}
|
||||
|
||||
/** @return int */
|
||||
/**
|
||||
* Valide les données du post.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
private function validate(): void
|
||||
{
|
||||
if (empty($this->title)) {
|
||||
throw new \InvalidArgumentException('Le titre ne peut pas être vide');
|
||||
}
|
||||
|
||||
if (mb_strlen($this->title) > 255) {
|
||||
throw new \InvalidArgumentException('Le titre ne peut pas dépasser 255 caractères');
|
||||
}
|
||||
|
||||
if (empty($this->content)) {
|
||||
throw new \InvalidArgumentException('Le contenu ne peut pas être vide');
|
||||
}
|
||||
|
||||
if (mb_strlen($this->content) > 65535) {
|
||||
throw new \InvalidArgumentException('Le contenu ne peut pas dépasser 65535 caractères');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Getters
|
||||
// ============================================
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function getTitle(): string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function getContent(): string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): DateTime
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): DateTime
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Logique métier
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Retourne un tableau simple utile pour la persistance.
|
||||
* Retourne un extrait du contenu (sans HTML).
|
||||
*
|
||||
* @param int $length Longueur maximale en caractères
|
||||
* @return string
|
||||
*/
|
||||
public function getExcerpt(int $length = 150): string
|
||||
{
|
||||
$text = strip_tags($this->content);
|
||||
return mb_strlen($text) > $length
|
||||
? mb_substr($text, 0, $length) . '...'
|
||||
: $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne un slug (URL-friendly) du titre.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSlug(): string
|
||||
{
|
||||
$slug = strtolower($this->title);
|
||||
$slug = preg_replace('/[^a-z0-9]+/', '-', $slug);
|
||||
return trim($slug, '-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Indique si l'article a été créé récemment.
|
||||
*
|
||||
* @param int $days Nombre de jours
|
||||
* @return bool
|
||||
*/
|
||||
public function isRecent(int $days = 7): bool
|
||||
{
|
||||
$limit = new DateTime("-{$days} days");
|
||||
return $this->createdAt > $limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne les données prêtes à persister en DB.
|
||||
*
|
||||
* @return array{title:string,content:string}
|
||||
*/
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Models\Post;
|
||||
|
||||
/**
|
||||
* Interface décrivant les opérations sur les posts.
|
||||
*/
|
||||
interface PostRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* Retourne tous les posts triés par id descendant.
|
||||
*
|
||||
* @return Post[] Tableau d'objets Post
|
||||
*/
|
||||
public function allDesc(): array;
|
||||
|
||||
/**
|
||||
* Trouve un post par son id.
|
||||
*
|
||||
* @param int $id
|
||||
* @return Post|null
|
||||
*/
|
||||
public function find(int $id): ?Post;
|
||||
|
||||
/**
|
||||
* Crée un post.
|
||||
*
|
||||
* @param Post $post Instance contenant les données à insérer (id ignoré)
|
||||
* @return int id inséré
|
||||
*/
|
||||
public function create(Post $post): int;
|
||||
|
||||
/**
|
||||
* Met à jour un post.
|
||||
*
|
||||
* @param int $id
|
||||
* @param Post $post Données à mettre à jour (id ignoré)
|
||||
* @return void
|
||||
*/
|
||||
public function update(int $id, Post $post): void;
|
||||
|
||||
/**
|
||||
* Supprime un post.
|
||||
*
|
||||
* @param int $id
|
||||
* @return void
|
||||
*/
|
||||
public function delete(int $id): void;
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use Medoo\Medoo;
|
||||
use App\Models\Post;
|
||||
|
||||
/**
|
||||
* Repository pour "post" basé sur Medoo.
|
||||
*
|
||||
* Cette implémentation convertit les lignes DB en objets App\Models\Post.
|
||||
*/
|
||||
class PostRepositoryMedoo implements PostRepositoryInterface
|
||||
{
|
||||
private Medoo $db;
|
||||
|
||||
public function __construct(Medoo $db)
|
||||
{
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function allDesc(): array
|
||||
{
|
||||
$rows = $this->db->select('post', ['id', 'title', 'content'], ['ORDER' => ['id' => 'DESC']]);
|
||||
if (!is_array($rows)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_map(function ($r) {
|
||||
return new Post(
|
||||
(int)($r['id'] ?? 0),
|
||||
(string)($r['title'] ?? ''),
|
||||
(string)($r['content'] ?? '')
|
||||
);
|
||||
}, $rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function find(int $id): ?Post
|
||||
{
|
||||
$row = $this->db->get('post', ['id', 'title', 'content'], ['id' => $id]);
|
||||
|
||||
if (empty($row) || $row === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Post(
|
||||
(int)($row['id'] ?? 0),
|
||||
(string)($row['title'] ?? ''),
|
||||
(string)($row['content'] ?? '')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function create(Post $post): int
|
||||
{
|
||||
$data = $post->toPersistableArray();
|
||||
|
||||
$this->db->insert('post', $data);
|
||||
return (int)$this->db->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function update(int $id, Post $post): void
|
||||
{
|
||||
$data = $post->toPersistableArray();
|
||||
$this->db->update('post', $data, ['id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function delete(int $id): void
|
||||
{
|
||||
$this->db->delete('post', ['id' => $id]);
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Requests;
|
||||
|
||||
use App\Models\Post;
|
||||
|
||||
/**
|
||||
* Classe simple responsable de la validation / sanitation des données
|
||||
* provenant des formulaires de post.
|
||||
*
|
||||
* Usage : $valid = PostRequest::fromArray($data)->validated();
|
||||
*/
|
||||
final class PostRequest
|
||||
{
|
||||
private string $title;
|
||||
private string $content;
|
||||
|
||||
private function __construct(string $title, string $content)
|
||||
{
|
||||
$this->title = $title;
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une instance à partir d'un tableau (ex: $request->getParsedBody()).
|
||||
*
|
||||
* @param mixed $input
|
||||
* @return self
|
||||
*/
|
||||
public static function fromArray($input): self
|
||||
{
|
||||
$title = isset($input['title']) ? (string)$input['title'] : '';
|
||||
$content = isset($input['content']) ? (string)$input['content'] : '';
|
||||
|
||||
$title = trim($title);
|
||||
$content = trim($content);
|
||||
|
||||
// Limites raisonnables (configurable si besoin)
|
||||
$title = mb_substr($title, 0, 255);
|
||||
$content = mb_substr($content, 0, 65535);
|
||||
|
||||
return new self($title, $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne les données validées/sanitizées prêtes à persister.
|
||||
*
|
||||
* @return array{title:string,content:string}
|
||||
*/
|
||||
public function validated(): array
|
||||
{
|
||||
// Ici on pourrait ajouter des validations plus strictes (ex: required, longueur minimale)
|
||||
return [
|
||||
'title' => $this->title,
|
||||
'content' => $this->content,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indique si le formulaire est suffisamment rempli.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid(): bool
|
||||
{
|
||||
return $this->title !== '' && $this->content !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit la requête validée en modèle App\Models\Post.
|
||||
*
|
||||
* Ce helper facilite la construction d'un Post directement depuis la requête.
|
||||
*
|
||||
* @param int $id Identifiant (0 si nouvel enregistrement)
|
||||
* @return Post
|
||||
*/
|
||||
public function toModel(int $id = 0): Post
|
||||
{
|
||||
return new Post($id, $this->title, $this->content);
|
||||
}
|
||||
}
|
||||
72
var/cache/twig/2c/2c6a63a35910781ed2464d3430983db2.php
vendored
Normal file
72
var/cache/twig/2c/2c6a63a35910781ed2464d3430983db2.php
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
use Twig\Environment;
|
||||
use Twig\Error\LoaderError;
|
||||
use Twig\Error\RuntimeError;
|
||||
use Twig\Extension\CoreExtension;
|
||||
use Twig\Extension\SandboxExtension;
|
||||
use Twig\Markup;
|
||||
use Twig\Sandbox\SecurityError;
|
||||
use Twig\Sandbox\SecurityNotAllowedTagError;
|
||||
use Twig\Sandbox\SecurityNotAllowedFilterError;
|
||||
use Twig\Sandbox\SecurityNotAllowedFunctionError;
|
||||
use Twig\Source;
|
||||
use Twig\Template;
|
||||
use Twig\TemplateWrapper;
|
||||
|
||||
/* partials/_header.twig */
|
||||
class __TwigTemplate_722bfa8935490dbfa02149909963bc2e extends Template
|
||||
{
|
||||
private Source $source;
|
||||
/**
|
||||
* @var array<string, Template>
|
||||
*/
|
||||
private array $macros = [];
|
||||
|
||||
public function __construct(Environment $env)
|
||||
{
|
||||
parent::__construct($env);
|
||||
|
||||
$this->source = $this->getSourceContext();
|
||||
|
||||
$this->parent = false;
|
||||
|
||||
$this->blocks = [
|
||||
];
|
||||
}
|
||||
|
||||
protected function doDisplay(array $context, array $blocks = []): iterable
|
||||
{
|
||||
$macros = $this->macros;
|
||||
// line 1
|
||||
yield "<header>
|
||||
<h1>
|
||||
<a href=\"/\">Mon Blog</a> |
|
||||
<a href=\"/admin\">Admin</a>
|
||||
</h1>
|
||||
</header>
|
||||
";
|
||||
yield from [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function getTemplateName(): string
|
||||
{
|
||||
return "partials/_header.twig";
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function getDebugInfo(): array
|
||||
{
|
||||
return array ( 42 => 1,);
|
||||
}
|
||||
|
||||
public function getSourceContext(): Source
|
||||
{
|
||||
return new Source("", "partials/_header.twig", "/home/julien/Documents/Git/julien/blog-slim/views/partials/_header.twig");
|
||||
}
|
||||
}
|
||||
84
var/cache/twig/3c/3c39c0e6430a5103c42c27b4478a8b72.php
vendored
Normal file
84
var/cache/twig/3c/3c39c0e6430a5103c42c27b4478a8b72.php
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
use Twig\Environment;
|
||||
use Twig\Error\LoaderError;
|
||||
use Twig\Error\RuntimeError;
|
||||
use Twig\Extension\CoreExtension;
|
||||
use Twig\Extension\SandboxExtension;
|
||||
use Twig\Markup;
|
||||
use Twig\Sandbox\SecurityError;
|
||||
use Twig\Sandbox\SecurityNotAllowedTagError;
|
||||
use Twig\Sandbox\SecurityNotAllowedFilterError;
|
||||
use Twig\Sandbox\SecurityNotAllowedFunctionError;
|
||||
use Twig\Source;
|
||||
use Twig\Template;
|
||||
use Twig\TemplateWrapper;
|
||||
|
||||
/* partials/_footer.twig */
|
||||
class __TwigTemplate_f24e585e1ab9b18bf721f5ade85d5894 extends Template
|
||||
{
|
||||
private Source $source;
|
||||
/**
|
||||
* @var array<string, Template>
|
||||
*/
|
||||
private array $macros = [];
|
||||
|
||||
public function __construct(Environment $env)
|
||||
{
|
||||
parent::__construct($env);
|
||||
|
||||
$this->source = $this->getSourceContext();
|
||||
|
||||
$this->parent = false;
|
||||
|
||||
$this->blocks = [
|
||||
];
|
||||
}
|
||||
|
||||
protected function doDisplay(array $context, array $blocks = []): iterable
|
||||
{
|
||||
$macros = $this->macros;
|
||||
// line 1
|
||||
yield "<footer class=\"site-footer\">
|
||||
<p>© ";
|
||||
// line 2
|
||||
yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape($this->extensions['Twig\Extension\CoreExtension']->formatDate("now", "Y"), "html", null, true);
|
||||
yield " Mon Blog – Tous droits réservés.</p>
|
||||
<nav>
|
||||
<a href=\"\">À propos</a> |
|
||||
<a href=\"\">Contact</a>
|
||||
</nav>
|
||||
</footer>
|
||||
";
|
||||
yield from [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function getTemplateName(): string
|
||||
{
|
||||
return "partials/_footer.twig";
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function isTraitable(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function getDebugInfo(): array
|
||||
{
|
||||
return array ( 45 => 2, 42 => 1,);
|
||||
}
|
||||
|
||||
public function getSourceContext(): Source
|
||||
{
|
||||
return new Source("", "partials/_footer.twig", "/home/julien/Documents/Git/julien/blog-slim/views/partials/_footer.twig");
|
||||
}
|
||||
}
|
||||
151
var/cache/twig/47/47cab4dc7982a59d8be24ee5dbfcb55a.php
vendored
Normal file
151
var/cache/twig/47/47cab4dc7982a59d8be24ee5dbfcb55a.php
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
use Twig\Environment;
|
||||
use Twig\Error\LoaderError;
|
||||
use Twig\Error\RuntimeError;
|
||||
use Twig\Extension\CoreExtension;
|
||||
use Twig\Extension\SandboxExtension;
|
||||
use Twig\Markup;
|
||||
use Twig\Sandbox\SecurityError;
|
||||
use Twig\Sandbox\SecurityNotAllowedTagError;
|
||||
use Twig\Sandbox\SecurityNotAllowedFilterError;
|
||||
use Twig\Sandbox\SecurityNotAllowedFunctionError;
|
||||
use Twig\Source;
|
||||
use Twig\Template;
|
||||
use Twig\TemplateWrapper;
|
||||
|
||||
/* pages/home.twig */
|
||||
class __TwigTemplate_9471f8175ca69249c24a559aa9930d1d extends Template
|
||||
{
|
||||
private Source $source;
|
||||
/**
|
||||
* @var array<string, Template>
|
||||
*/
|
||||
private array $macros = [];
|
||||
|
||||
public function __construct(Environment $env)
|
||||
{
|
||||
parent::__construct($env);
|
||||
|
||||
$this->source = $this->getSourceContext();
|
||||
|
||||
$this->blocks = [
|
||||
'title' => [$this, 'block_title'],
|
||||
'content' => [$this, 'block_content'],
|
||||
];
|
||||
}
|
||||
|
||||
protected function doGetParent(array $context): bool|string|Template|TemplateWrapper
|
||||
{
|
||||
// line 1
|
||||
return "layout.twig";
|
||||
}
|
||||
|
||||
protected function doDisplay(array $context, array $blocks = []): iterable
|
||||
{
|
||||
$macros = $this->macros;
|
||||
$this->parent = $this->load("layout.twig", 1);
|
||||
yield from $this->parent->unwrap()->yield($context, array_merge($this->blocks, $blocks));
|
||||
}
|
||||
|
||||
// line 3
|
||||
/**
|
||||
* @return iterable<null|scalar|\Stringable>
|
||||
*/
|
||||
public function block_title(array $context, array $blocks = []): iterable
|
||||
{
|
||||
$macros = $this->macros;
|
||||
yield "Mon Blog";
|
||||
yield from [];
|
||||
}
|
||||
|
||||
// line 5
|
||||
/**
|
||||
* @return iterable<null|scalar|\Stringable>
|
||||
*/
|
||||
public function block_content(array $context, array $blocks = []): iterable
|
||||
{
|
||||
$macros = $this->macros;
|
||||
// line 6
|
||||
$context['_parent'] = $context;
|
||||
$context['_seq'] = CoreExtension::ensureTraversable(($context["posts"] ?? null));
|
||||
$context['_iterated'] = false;
|
||||
foreach ($context['_seq'] as $context["_key"] => $context["post"]) {
|
||||
// line 7
|
||||
yield "<article class=\"post\">
|
||||
<h2>";
|
||||
// line 8
|
||||
yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(CoreExtension::getAttribute($this->env, $this->source, $context["post"], "title", [], "any", false, false, false, 8), "html", null, true);
|
||||
yield "</h2>
|
||||
|
||||
<div class=\"post-meta\">
|
||||
<small>Publié le ";
|
||||
// line 11
|
||||
yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape($this->extensions['Twig\Extension\CoreExtension']->formatDate(CoreExtension::getAttribute($this->env, $this->source, $context["post"], "createdAt", [], "any", false, false, false, 11), "d/m/Y à H:i"), "html", null, true);
|
||||
yield "</small>
|
||||
";
|
||||
// line 12
|
||||
if ((($tmp = CoreExtension::getAttribute($this->env, $this->source, $context["post"], "isRecent", [7], "method", false, false, false, 12)) && $tmp instanceof Markup ? (string) $tmp : $tmp)) {
|
||||
// line 13
|
||||
yield " <span class=\"badge badge-new\">Nouveau</span>
|
||||
";
|
||||
}
|
||||
// line 15
|
||||
yield " </div>
|
||||
|
||||
<p>";
|
||||
// line 17
|
||||
yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(CoreExtension::getAttribute($this->env, $this->source, $context["post"], "getExcerpt", [200], "method", false, false, false, 17), "html", null, true);
|
||||
yield "</p>
|
||||
|
||||
<p>
|
||||
<a href=\"/article/";
|
||||
// line 20
|
||||
yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(CoreExtension::getAttribute($this->env, $this->source, $context["post"], "getSlug", [], "method", false, false, false, 20), "html", null, true);
|
||||
yield "\">Lire la suite →</a>
|
||||
</p>
|
||||
</article>
|
||||
";
|
||||
$context['_iterated'] = true;
|
||||
}
|
||||
// line 23
|
||||
if (!$context['_iterated']) {
|
||||
// line 24
|
||||
yield "<p>Aucun article publié.</p>
|
||||
";
|
||||
}
|
||||
$_parent = $context['_parent'];
|
||||
unset($context['_seq'], $context['_key'], $context['post'], $context['_parent'], $context['_iterated']);
|
||||
$context = array_intersect_key($context, $_parent) + $_parent;
|
||||
yield from [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function getTemplateName(): string
|
||||
{
|
||||
return "pages/home.twig";
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function isTraitable(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function getDebugInfo(): array
|
||||
{
|
||||
return array ( 114 => 24, 112 => 23, 104 => 20, 98 => 17, 94 => 15, 90 => 13, 88 => 12, 84 => 11, 78 => 8, 75 => 7, 70 => 6, 63 => 5, 52 => 3, 41 => 1,);
|
||||
}
|
||||
|
||||
public function getSourceContext(): Source
|
||||
{
|
||||
return new Source("", "pages/home.twig", "/home/julien/Documents/Git/julien/blog-slim/views/pages/home.twig");
|
||||
}
|
||||
}
|
||||
158
var/cache/twig/5f/5feb7baabf556b5d769418b4ed8398c5.php
vendored
Normal file
158
var/cache/twig/5f/5feb7baabf556b5d769418b4ed8398c5.php
vendored
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
use Twig\Environment;
|
||||
use Twig\Error\LoaderError;
|
||||
use Twig\Error\RuntimeError;
|
||||
use Twig\Extension\CoreExtension;
|
||||
use Twig\Extension\SandboxExtension;
|
||||
use Twig\Markup;
|
||||
use Twig\Sandbox\SecurityError;
|
||||
use Twig\Sandbox\SecurityNotAllowedTagError;
|
||||
use Twig\Sandbox\SecurityNotAllowedFilterError;
|
||||
use Twig\Sandbox\SecurityNotAllowedFunctionError;
|
||||
use Twig\Source;
|
||||
use Twig\Template;
|
||||
use Twig\TemplateWrapper;
|
||||
|
||||
/* layout.twig */
|
||||
class __TwigTemplate_589a65738a57deabe1e7c4440aeb0288 extends Template
|
||||
{
|
||||
private Source $source;
|
||||
/**
|
||||
* @var array<string, Template>
|
||||
*/
|
||||
private array $macros = [];
|
||||
|
||||
public function __construct(Environment $env)
|
||||
{
|
||||
parent::__construct($env);
|
||||
|
||||
$this->source = $this->getSourceContext();
|
||||
|
||||
$this->parent = false;
|
||||
|
||||
$this->blocks = [
|
||||
'title' => [$this, 'block_title'],
|
||||
'content' => [$this, 'block_content'],
|
||||
'scripts' => [$this, 'block_scripts'],
|
||||
];
|
||||
}
|
||||
|
||||
protected function doDisplay(array $context, array $blocks = []): iterable
|
||||
{
|
||||
$macros = $this->macros;
|
||||
// line 1
|
||||
yield "<!DOCTYPE html>
|
||||
<html lang=\"fr\">
|
||||
<head>
|
||||
<meta charset=\"UTF-8\">
|
||||
<title>";
|
||||
// line 5
|
||||
yield from $this->unwrap()->yieldBlock('title', $context, $blocks);
|
||||
yield "</title>
|
||||
";
|
||||
// line 7
|
||||
yield " <style>
|
||||
body {font-family: Arial, sans-serif; margin: 2rem;}
|
||||
.post {border-bottom: 1px solid #ccc; padding: 1rem 0;}
|
||||
.admin-actions a,
|
||||
.admin-actions form {margin-right: .5rem;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
";
|
||||
// line 17
|
||||
yield " ";
|
||||
yield from $this->load("partials/_header.twig", 17)->unwrap()->yield($context);
|
||||
// line 18
|
||||
yield "
|
||||
";
|
||||
// line 20
|
||||
yield " <main>
|
||||
";
|
||||
// line 21
|
||||
yield from $this->unwrap()->yieldBlock('content', $context, $blocks);
|
||||
// line 22
|
||||
yield " </main>
|
||||
|
||||
";
|
||||
// line 25
|
||||
yield " ";
|
||||
yield from $this->load("partials/_footer.twig", 25)->unwrap()->yield($context);
|
||||
// line 26
|
||||
yield "
|
||||
";
|
||||
// line 28
|
||||
yield " ";
|
||||
// line 29
|
||||
yield " ";
|
||||
yield from $this->unwrap()->yieldBlock('scripts', $context, $blocks);
|
||||
// line 30
|
||||
yield "</body>
|
||||
|
||||
</html>
|
||||
";
|
||||
yield from [];
|
||||
}
|
||||
|
||||
// line 5
|
||||
/**
|
||||
* @return iterable<null|scalar|\Stringable>
|
||||
*/
|
||||
public function block_title(array $context, array $blocks = []): iterable
|
||||
{
|
||||
$macros = $this->macros;
|
||||
yield "Mon Blog";
|
||||
yield from [];
|
||||
}
|
||||
|
||||
// line 21
|
||||
/**
|
||||
* @return iterable<null|scalar|\Stringable>
|
||||
*/
|
||||
public function block_content(array $context, array $blocks = []): iterable
|
||||
{
|
||||
$macros = $this->macros;
|
||||
yield from [];
|
||||
}
|
||||
|
||||
// line 29
|
||||
/**
|
||||
* @return iterable<null|scalar|\Stringable>
|
||||
*/
|
||||
public function block_scripts(array $context, array $blocks = []): iterable
|
||||
{
|
||||
$macros = $this->macros;
|
||||
yield from [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function getTemplateName(): string
|
||||
{
|
||||
return "layout.twig";
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function isTraitable(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function getDebugInfo(): array
|
||||
{
|
||||
return array ( 121 => 29, 111 => 21, 100 => 5, 92 => 30, 89 => 29, 87 => 28, 84 => 26, 81 => 25, 77 => 22, 75 => 21, 72 => 20, 69 => 18, 66 => 17, 55 => 7, 51 => 5, 45 => 1,);
|
||||
}
|
||||
|
||||
public function getSourceContext(): Source
|
||||
{
|
||||
return new Source("", "layout.twig", "/home/julien/Documents/Git/julien/blog-slim/views/layout.twig");
|
||||
}
|
||||
}
|
||||
182
var/cache/twig/e0/e0b9aff4ba5a590eadfd3a590206e0b2.php
vendored
Normal file
182
var/cache/twig/e0/e0b9aff4ba5a590eadfd3a590206e0b2.php
vendored
Normal file
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
use Twig\Environment;
|
||||
use Twig\Error\LoaderError;
|
||||
use Twig\Error\RuntimeError;
|
||||
use Twig\Extension\CoreExtension;
|
||||
use Twig\Extension\SandboxExtension;
|
||||
use Twig\Markup;
|
||||
use Twig\Sandbox\SecurityError;
|
||||
use Twig\Sandbox\SecurityNotAllowedTagError;
|
||||
use Twig\Sandbox\SecurityNotAllowedFilterError;
|
||||
use Twig\Sandbox\SecurityNotAllowedFunctionError;
|
||||
use Twig\Source;
|
||||
use Twig\Template;
|
||||
use Twig\TemplateWrapper;
|
||||
|
||||
/* pages/admin.twig */
|
||||
class __TwigTemplate_9953ee20db26573f1b0e2b3758f3423d extends Template
|
||||
{
|
||||
private Source $source;
|
||||
/**
|
||||
* @var array<string, Template>
|
||||
*/
|
||||
private array $macros = [];
|
||||
|
||||
public function __construct(Environment $env)
|
||||
{
|
||||
parent::__construct($env);
|
||||
|
||||
$this->source = $this->getSourceContext();
|
||||
|
||||
$this->blocks = [
|
||||
'title' => [$this, 'block_title'],
|
||||
'content' => [$this, 'block_content'],
|
||||
];
|
||||
}
|
||||
|
||||
protected function doGetParent(array $context): bool|string|Template|TemplateWrapper
|
||||
{
|
||||
// line 1
|
||||
return "layout.twig";
|
||||
}
|
||||
|
||||
protected function doDisplay(array $context, array $blocks = []): iterable
|
||||
{
|
||||
$macros = $this->macros;
|
||||
$this->parent = $this->load("layout.twig", 1);
|
||||
yield from $this->parent->unwrap()->yield($context, array_merge($this->blocks, $blocks));
|
||||
}
|
||||
|
||||
// line 3
|
||||
/**
|
||||
* @return iterable<null|scalar|\Stringable>
|
||||
*/
|
||||
public function block_title(array $context, array $blocks = []): iterable
|
||||
{
|
||||
$macros = $this->macros;
|
||||
yield "Admin – Gestion des articles";
|
||||
yield from [];
|
||||
}
|
||||
|
||||
// line 5
|
||||
/**
|
||||
* @return iterable<null|scalar|\Stringable>
|
||||
*/
|
||||
public function block_content(array $context, array $blocks = []): iterable
|
||||
{
|
||||
$macros = $this->macros;
|
||||
// line 6
|
||||
yield "<h2>Gestion des articles</h2>
|
||||
|
||||
<!-- Lien d'ajout -->
|
||||
<p>
|
||||
<a href=\"/admin/edit/0\" class=\"btn btn-primary\">+ Ajouter un article</a>
|
||||
</p>
|
||||
|
||||
";
|
||||
// line 13
|
||||
if ((($tmp = !Twig\Extension\CoreExtension::testEmpty(($context["posts"] ?? null))) && $tmp instanceof Markup ? (string) $tmp : $tmp)) {
|
||||
// line 14
|
||||
yield "<table class=\"admin-table\">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Titre</th>
|
||||
<th>Créé le</th>
|
||||
<th>Modifié le</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
";
|
||||
// line 24
|
||||
$context['_parent'] = $context;
|
||||
$context['_seq'] = CoreExtension::ensureTraversable(($context["posts"] ?? null));
|
||||
foreach ($context['_seq'] as $context["_key"] => $context["post"]) {
|
||||
// line 25
|
||||
yield " <tr>
|
||||
<td>
|
||||
<strong>";
|
||||
// line 27
|
||||
yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(CoreExtension::getAttribute($this->env, $this->source, $context["post"], "title", [], "any", false, false, false, 27), "html", null, true);
|
||||
yield "</strong>
|
||||
";
|
||||
// line 28
|
||||
if ((($tmp = CoreExtension::getAttribute($this->env, $this->source, $context["post"], "isRecent", [7], "method", false, false, false, 28)) && $tmp instanceof Markup ? (string) $tmp : $tmp)) {
|
||||
// line 29
|
||||
yield " <span class=\"badge badge-new\">Nouveau</span>
|
||||
";
|
||||
}
|
||||
// line 31
|
||||
yield " </td>
|
||||
<td>";
|
||||
// line 32
|
||||
yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape($this->extensions['Twig\Extension\CoreExtension']->formatDate(CoreExtension::getAttribute($this->env, $this->source, $context["post"], "createdAt", [], "any", false, false, false, 32), "d/m/Y H:i"), "html", null, true);
|
||||
yield "</td>
|
||||
<td>";
|
||||
// line 33
|
||||
yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape($this->extensions['Twig\Extension\CoreExtension']->formatDate(CoreExtension::getAttribute($this->env, $this->source, $context["post"], "updatedAt", [], "any", false, false, false, 33), "d/m/Y H:i"), "html", null, true);
|
||||
yield "</td>
|
||||
<td class=\"admin-actions\">
|
||||
<a href=\"/admin/edit/";
|
||||
// line 35
|
||||
yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(CoreExtension::getAttribute($this->env, $this->source, $context["post"], "id", [], "any", false, false, false, 35), "html", null, true);
|
||||
yield "\" class=\"btn btn-sm btn-secondary\">Éditer</a>
|
||||
|
||||
<form method=\"post\" action=\"/admin/delete/";
|
||||
// line 37
|
||||
yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(CoreExtension::getAttribute($this->env, $this->source, $context["post"], "id", [], "any", false, false, false, 37), "html", null, true);
|
||||
yield "\" style=\"display:inline;\">
|
||||
<button type=\"submit\" class=\"btn btn-sm btn-danger\"
|
||||
onclick=\"return confirm('Supprimer cet article ?')\">
|
||||
Supprimer
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
";
|
||||
}
|
||||
$_parent = $context['_parent'];
|
||||
unset($context['_seq'], $context['_key'], $context['post'], $context['_parent']);
|
||||
$context = array_intersect_key($context, $_parent) + $_parent;
|
||||
// line 46
|
||||
yield " </tbody>
|
||||
</table>
|
||||
";
|
||||
} else {
|
||||
// line 49
|
||||
yield "<p><em>Aucun article à gérer.</em></p>
|
||||
";
|
||||
}
|
||||
yield from [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function getTemplateName(): string
|
||||
{
|
||||
return "pages/admin.twig";
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function isTraitable(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function getDebugInfo(): array
|
||||
{
|
||||
return array ( 148 => 49, 143 => 46, 128 => 37, 123 => 35, 118 => 33, 114 => 32, 111 => 31, 107 => 29, 105 => 28, 101 => 27, 97 => 25, 93 => 24, 81 => 14, 79 => 13, 70 => 6, 63 => 5, 52 => 3, 41 => 1,);
|
||||
}
|
||||
|
||||
public function getSourceContext(): Source
|
||||
{
|
||||
return new Source("", "pages/admin.twig", "/home/julien/Documents/Git/julien/blog-slim/views/pages/admin.twig");
|
||||
}
|
||||
}
|
||||
@@ -3,28 +3,49 @@
|
||||
{% block title %}Admin – Gestion des articles{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Gestion des articles</h2>
|
||||
<h2>Gestion des articles</h2>
|
||||
|
||||
<!-- Lien d’ajout -->
|
||||
<a href="/admin/edit/0">+ Ajouter un article</a>
|
||||
<!-- Lien d'ajout -->
|
||||
<p>
|
||||
<a href="/admin/edit/0" class="btn btn-primary">+ Ajouter un article</a>
|
||||
</p>
|
||||
|
||||
{% for post in posts %}
|
||||
<div class="post">
|
||||
<h3>{{ post.title }}</h3>
|
||||
<p>{{ post.content|raw }}</p>
|
||||
|
||||
<div class="admin-actions">
|
||||
<a href="/admin/edit/{{ post.id }}">Éditer</a>
|
||||
{% if posts is not empty %}
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Titre</th>
|
||||
<th>Créé le</th>
|
||||
<th>Modifié le</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for post in posts %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ post.title }}</strong>
|
||||
{% if post.isRecent(7) %}
|
||||
<span class="badge badge-new">Nouveau</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ post.createdAt|date("d/m/Y H:i") }}</td>
|
||||
<td>{{ post.updatedAt|date("d/m/Y H:i") }}</td>
|
||||
<td class="admin-actions">
|
||||
<a href="/admin/edit/{{ post.id }}" class="btn btn-sm btn-secondary">Éditer</a>
|
||||
|
||||
<form method="post" action="/admin/delete/{{ post.id }}" style="display:inline;">
|
||||
<button type="submit"
|
||||
onclick="return confirm('Supprimer cet article ?')">
|
||||
<button type="submit" class="btn btn-sm btn-danger"
|
||||
onclick="return confirm('Supprimer cet article ?')">
|
||||
Supprimer
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>Aucun article à gérer.</p>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p><em>Aucun article à gérer.</em></p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -3,12 +3,24 @@
|
||||
{% block title %}Mon Blog{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% for post in posts %}
|
||||
<div class="post">
|
||||
<h2>{{ post.title }}</h2>
|
||||
<p>{{ post.content|raw }}</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>Aucun article publié.</p>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
{% for post in posts %}
|
||||
<article class="post">
|
||||
<h2>{{ post.title }}</h2>
|
||||
|
||||
<div class="post-meta">
|
||||
<small>Publié le {{ post.createdAt|date("d/m/Y à H:i") }}</small>
|
||||
{% if post.isRecent(7) %}
|
||||
<span class="badge badge-new">Nouveau</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<p>{{ post.getExcerpt(200) }}</p>
|
||||
|
||||
<p>
|
||||
<a href="/article/{{ post.getSlug() }}">Lire la suite →</a>
|
||||
</p>
|
||||
</article>
|
||||
{% else %}
|
||||
<p>Aucun article publié.</p>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@@ -1,68 +1,55 @@
|
||||
{% extends "layout.twig" %}
|
||||
|
||||
{% block title %}
|
||||
{% if post is defined and post is not null and post.id is defined %}
|
||||
Éditer l’article
|
||||
{% else %}
|
||||
Créer un article
|
||||
{% endif %}
|
||||
{% if post is defined and post is not null and post.id > 0 %}
|
||||
Éditer l'article
|
||||
{% else %}
|
||||
Créer un article
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>
|
||||
{% if post is defined and post is not null and post.id is defined %}
|
||||
Éditer l’article
|
||||
{% else %}
|
||||
Créer un article
|
||||
{% endif %}
|
||||
</h2>
|
||||
|
||||
{# Formulaire identifié pour le script JavaScript #}
|
||||
<form id="articleForm" method="post" action="{{ action }}">
|
||||
<p>
|
||||
<label>Titre<br>
|
||||
<input type="text" name="title"
|
||||
value="{{ post.title|default('') }}" required>
|
||||
</label>
|
||||
</p>
|
||||
{% if post is defined and post is not null and post.id > 0 %}
|
||||
Éditer l'article
|
||||
{% else %}
|
||||
Créer un article
|
||||
{% endif %}
|
||||
|
||||
<p>
|
||||
<label>Contenu<br>
|
||||
<textarea id="editor" name="content" rows="6">{{ post.content|default('') }}</textarea>
|
||||
</label>
|
||||
</p>
|
||||
{# Formulaire identifié pour le script JavaScript #}
|
||||
<form id="articleForm" method="post" action="{{ action }}">
|
||||
<p>
|
||||
<label for="title">Titre<br>
|
||||
<input type="text" id="title" name="title" value="{{ post.title|default('') }}" required maxlength="255">
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<button type="submit">
|
||||
{% if post is defined and post is not null and post.id is defined %}Mettre à jour{% else %}Enregistrer{% endif %}
|
||||
<p>
|
||||
<label for="editor">Contenu<br>
|
||||
<textarea id="editor" name="content" rows="6" required>{{ post.content|default('') }}</textarea>
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{% if post is defined and post is not null and post.id > 0 %}
|
||||
Mettre à jour
|
||||
{% else %}
|
||||
Enregistrer
|
||||
{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
<a href="/admin" class="btn btn-secondary">Annuler</a>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
{% if post is defined and post is not null and post.id > 0 %}
|
||||
<hr>
|
||||
<small>
|
||||
Créé le : {{ post.createdAt|date("d/m/Y à H:i") }}<br>
|
||||
Modifié le : {{ post.updatedAt|date("d/m/Y à H:i") }}
|
||||
</small>
|
||||
{% endif %}
|
||||
|
||||
<p><a href="/admin">Retour à l’admin</a></p>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/js/tinymce/tinymce.min.js" referrerpolicy="origin" crossorigin="anonymous"></script>
|
||||
<script>
|
||||
tinymce.init({
|
||||
selector: '#editor',
|
||||
base_url: '/js/tinymce',
|
||||
license_key: 'gpl',
|
||||
height: 400,
|
||||
menubar: false,
|
||||
plugins: [
|
||||
'advlist', 'autolink', 'lists', 'link', 'image',
|
||||
'charmap', 'preview', 'anchor', 'searchreplace',
|
||||
'visualblocks', 'code', 'fullscreen',
|
||||
'insertdatetime', 'media', 'table',
|
||||
'help', 'wordcount'
|
||||
],
|
||||
toolbar: [
|
||||
'undo redo | formatselect | bold italic underline |',
|
||||
'alignleft aligncenter alignright alignjustify |',
|
||||
'bullist numlist outdent indent | link image media |',
|
||||
'removeformat | help'
|
||||
].join(' '),
|
||||
content_style: 'body {font-family:Helvetica,Arial,sans-serif;font-size:14px}'
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
Reference in New Issue
Block a user