Simplified

This commit is contained in:
julien
2026-03-09 14:17:05 +01:00
parent e65d79cccb
commit eff05b0971
18 changed files with 1012 additions and 523 deletions

View File

@@ -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

View File

@@ -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"
}
}
}

View File

@@ -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();

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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}
*/

View File

@@ -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;
}

View File

@@ -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]);
}
}

View File

@@ -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);
}
}

View 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");
}
}

View 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>&copy; ";
// 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");
}
}

View 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");
}
}

View 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");
}
}

View 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");
}
}

View File

@@ -3,28 +3,49 @@
{% block title %}Admin Gestion des articles{% endblock %}
{% block content %}
<h2>Gestion des articles</h2>
<h2>Gestion des articles</h2>
<!-- Lien dajout -->
<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 %}

View File

@@ -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 %}

View File

@@ -1,68 +1,55 @@
{% extends "layout.twig" %}
{% block title %}
{% if post is defined and post is not null and post.id is defined %}
Éditer larticle
{% 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 larticle
{% 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 à ladmin</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 %}