Added Models

This commit is contained in:
julien
2026-03-09 12:22:50 +01:00
parent 18eabb0560
commit 4342751c70
7 changed files with 146 additions and 44 deletions

View File

@@ -9,6 +9,7 @@ use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Views\Twig; use Slim\Views\Twig;
use App\Repositories\PostRepositoryInterface as PostRepository; use App\Repositories\PostRepositoryInterface as PostRepository;
use App\Requests\PostRequest; use App\Requests\PostRequest;
use App\Models\Post;
/** /**
* Contrôleur pour les posts. * Contrôleur pour les posts.
@@ -26,7 +27,7 @@ class PostController
public function index(Request $req, Response $res): Response public function index(Request $req, Response $res): Response
{ {
$posts = $this->repo->allDesc(); $posts = $this->repo->allDesc(); // Post[]
return $this->view->render($res, 'pages/home.twig', ['posts' => $posts]); return $this->view->render($res, 'pages/home.twig', ['posts' => $posts]);
} }
@@ -51,9 +52,11 @@ class PostController
// Si id fourni mais post introuvable -> 404 // Si id fourni mais post introuvable -> 404
if ($id > 0 && $post === null) { if ($id > 0 && $post === null) {
return $res->withStatus(404)->write('Article non trouvé'); $res->getBody()->write('Article non trouvé');
return $res->withStatus(404);
} }
// Twig peut accéder aux getters (post.title, post.content)
$action = $id ? "/admin/edit/{$id}" : "/admin/create"; $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]);
} }
@@ -62,12 +65,12 @@ class PostController
{ {
$postRequest = PostRequest::fromArray($req->getParsedBody()); $postRequest = PostRequest::fromArray($req->getParsedBody());
if (! $postRequest->isValid()) { if (! $postRequest->isValid()) {
// Simple gestion d'erreur : rediriger vers admin (on peut ajouter flash messages plus tard)
return $res->withHeader('Location', '/admin')->withStatus(302); return $res->withHeader('Location', '/admin')->withStatus(302);
} }
$data = $postRequest->validated(); // Utilisation du helper toModel() pour construire l'entité Post
$this->repo->create($data); $post = $postRequest->toModel(0);
$this->repo->create($post);
return $res->withHeader('Location', '/admin')->withStatus(302); return $res->withHeader('Location', '/admin')->withStatus(302);
} }
@@ -76,7 +79,8 @@ class PostController
$id = (int)$args['id']; $id = (int)$args['id'];
$existing = $this->repo->find($id); $existing = $this->repo->find($id);
if ($existing === null) { if ($existing === null) {
return $res->withStatus(404)->write('Article non trouvé'); $res->getBody()->write('Article non trouvé');
return $res->withStatus(404);
} }
$postRequest = PostRequest::fromArray($req->getParsedBody()); $postRequest = PostRequest::fromArray($req->getParsedBody());
@@ -84,8 +88,8 @@ class PostController
return $res->withHeader('Location', '/admin')->withStatus(302); return $res->withHeader('Location', '/admin')->withStatus(302);
} }
$data = $postRequest->validated(); $post = $postRequest->toModel($id);
$this->repo->update($id, $data); $this->repo->update($id, $post);
return $res->withHeader('Location', '/admin')->withStatus(302); return $res->withHeader('Location', '/admin')->withStatus(302);
} }
@@ -94,7 +98,8 @@ class PostController
$id = (int)$args['id']; $id = (int)$args['id'];
$existing = $this->repo->find($id); $existing = $this->repo->find($id);
if ($existing === null) { if ($existing === null) {
return $res->withStatus(404)->write('Article non trouvé'); $res->getBody()->write('Article non trouvé');
return $res->withStatus(404);
} }
$this->repo->delete($id); $this->repo->delete($id);

78
src/Models/Post.php Normal file
View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace App\Models;
/**
* Représente un post (DTO / entité légère).
*
* 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.
*/
final class Post
{
private int $id;
private string $title;
private string $content;
/**
* @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;
}
/**
* Crée une instance depuis un tableau (par ex. ligne DB ou formulaire).
*
* Ce helper facilite la migration depuis le format tableau existant.
*
* @param array<string,mixed> $data Clé 'id' (optionnelle), 'title', 'content'
* @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'] : '';
return new self($id, $title, $content);
}
/** @return int */
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;
}
/**
* Retourne un tableau simple utile pour la persistance.
*
* @return array{title:string,content:string}
*/
public function toPersistableArray(): array
{
return [
'title' => $this->title,
'content' => $this->content,
];
}
}

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace App\Repositories; namespace App\Repositories;
use App\Models\Post;
/** /**
* Interface décrivant les opérations sur les posts. * Interface décrivant les opérations sur les posts.
*/ */
@@ -12,7 +14,7 @@ interface PostRepositoryInterface
/** /**
* Retourne tous les posts triés par id descendant. * Retourne tous les posts triés par id descendant.
* *
* @return array<int, array{id:int, title:string, content:string}> * @return Post[] Tableau d'objets Post
*/ */
public function allDesc(): array; public function allDesc(): array;
@@ -20,26 +22,26 @@ interface PostRepositoryInterface
* Trouve un post par son id. * Trouve un post par son id.
* *
* @param int $id * @param int $id
* @return array{id:int, title:string, content:string}|null * @return Post|null
*/ */
public function find(int $id): ?array; public function find(int $id): ?Post;
/** /**
* Crée un post. * Crée un post.
* *
* @param array{title:string,content:string} $data * @param Post $post Instance contenant les données à insérer (id ignoré)
* @return int id inséré * @return int id inséré
*/ */
public function create(array $data): int; public function create(Post $post): int;
/** /**
* Met à jour un post. * Met à jour un post.
* *
* @param int $id * @param int $id
* @param array{title:string,content:string} $data * @param Post $post Données à mettre à jour (id ignoré)
* @return void * @return void
*/ */
public function update(int $id, array $data): void; public function update(int $id, Post $post): void;
/** /**
* Supprime un post. * Supprime un post.

View File

@@ -5,10 +5,12 @@ declare(strict_types=1);
namespace App\Repositories; namespace App\Repositories;
use Medoo\Medoo; use Medoo\Medoo;
use App\Models\Post;
/** /**
* Repository pour "post" basé sur Medoo. * Repository pour "post" basé sur Medoo.
* Retourne et consomme des tableaux associatifs. *
* Cette implémentation convertit les lignes DB en objets App\Models\Post.
*/ */
class PostRepositoryMedoo implements PostRepositoryInterface class PostRepositoryMedoo implements PostRepositoryInterface
{ {
@@ -25,19 +27,23 @@ class PostRepositoryMedoo implements PostRepositoryInterface
public function allDesc(): array public function allDesc(): array
{ {
$rows = $this->db->select('post', ['id', 'title', 'content'], ['ORDER' => ['id' => 'DESC']]); $rows = $this->db->select('post', ['id', 'title', 'content'], ['ORDER' => ['id' => 'DESC']]);
return is_array($rows) ? array_map(function ($r) { if (!is_array($rows)) {
return [ return [];
'id' => (int)($r['id'] ?? 0), }
'title' => (string)($r['title'] ?? ''),
'content' => (string)($r['content'] ?? ''), return array_map(function ($r) {
]; return new Post(
}, $rows) : []; (int)($r['id'] ?? 0),
(string)($r['title'] ?? ''),
(string)($r['content'] ?? '')
);
}, $rows);
} }
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function find(int $id): ?array public function find(int $id): ?Post
{ {
$row = $this->db->get('post', ['id', 'title', 'content'], ['id' => $id]); $row = $this->db->get('post', ['id', 'title', 'content'], ['id' => $id]);
@@ -45,34 +51,31 @@ class PostRepositoryMedoo implements PostRepositoryInterface
return null; return null;
} }
return [ return new Post(
'id' => (int)($row['id'] ?? 0), (int)($row['id'] ?? 0),
'title' => (string)($row['title'] ?? ''), (string)($row['title'] ?? ''),
'content' => (string)($row['content'] ?? ''), (string)($row['content'] ?? '')
]; );
} }
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function create(array $data): int public function create(Post $post): int
{ {
$this->db->insert('post', [ $data = $post->toPersistableArray();
'title' => $data['title'] ?? '',
'content' => $data['content'] ?? '', $this->db->insert('post', $data);
]);
return (int)$this->db->id(); return (int)$this->db->id();
} }
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function update(int $id, array $data): void public function update(int $id, Post $post): void
{ {
$this->db->update('post', [ $data = $post->toPersistableArray();
'title' => $data['title'] ?? '', $this->db->update('post', $data, ['id' => $id]);
'content' => $data['content'] ?? '',
], ['id' => $id]);
} }
/** /**

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace App\Requests; namespace App\Requests;
use App\Models\Post;
/** /**
* Classe simple responsable de la validation / sanitation des données * Classe simple responsable de la validation / sanitation des données
* provenant des formulaires de post. * provenant des formulaires de post.
@@ -65,4 +67,17 @@ final class PostRequest
{ {
return $this->title !== '' && $this->content !== ''; 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

@@ -10,5 +10,4 @@
</div> </div>
{% else %} {% else %}
<p>Aucun article publié.</p> <p>Aucun article publié.</p>
{% endfor %}
{% endblock %} {% endblock %}

View File

@@ -28,7 +28,7 @@
<p> <p>
<label>Contenu<br> <label>Contenu<br>
<textarea id="editor" name="content" rows="6" {# required <- removed because conflicts with TinyMCE#}>{{ post.content|default('') }}</textarea> <textarea id="editor" name="content" rows="6" >{{ post.content|default('') }}</textarea>
</label> </label>
</p> </p>
@@ -45,7 +45,7 @@
<script> <script>
tinymce.init({ tinymce.init({
selector: '#editor', selector: '#editor',
base_url: '/js/tinymce', // assure le bon chemin relatif base_url: '/js/tinymce',
license_key: 'gpl', license_key: 'gpl',
height: 400, height: 400,
menubar: false, menubar: false,