Switching from NetBean to Medoo

This commit is contained in:
julien
2026-03-09 00:33:30 +01:00
parent 8d7aba5447
commit 001dc204af
10 changed files with 146 additions and 359 deletions

View File

@@ -13,10 +13,8 @@
"require": { "require": {
"slim/slim": "4.*", "slim/slim": "4.*",
"slim/psr7": "*", "slim/psr7": "*",
"gabordemooij/redbean": "^5.7",
"twig/twig": "*", "twig/twig": "*",
"slim/twig-view": "*", "catfan/medoo": "^2.1"
"php-di/php-di": "^7.1"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

View File

@@ -2,90 +2,67 @@
declare(strict_types=1); declare(strict_types=1);
/* -------------------------------------------------
* Autoload Composer + notre configuration RedBean
* ------------------------------------------------- */
require __DIR__ . '/../vendor/autoload.php'; require __DIR__ . '/../vendor/autoload.php';
require __DIR__ . '/../src/Config/redbean.php';
/* -------------------------------------------------
* Imports (facades, DI, Slim, Twig, etc.)
* ------------------------------------------------- */
use DI\ContainerBuilder;
use Slim\Factory\AppFactory; use Slim\Factory\AppFactory;
use Slim\Views\Twig; use Slim\Views\Twig;
use Slim\Views\TwigMiddleware; use Slim\Views\TwigMiddleware;
use Twig\Loader\FilesystemLoader; use Twig\Loader\FilesystemLoader;
use RedBeanPHP\R; // façade RedBean (nécessaire pour le service DB) use Medoo\Medoo;
/* ------------------------------------------------- // -------------------------
* Construction du conteneur DI // DB SQLite + fichier
* ------------------------------------------------- */ // -------------------------
$containerBuilder = new ContainerBuilder(); function ensureDatabaseFile(string $path): void
{
if (!file_exists($path)) {
if (!is_dir(dirname($path))) {
mkdir(dirname($path), 0755, true);
}
touch($path);
chmod($path, 0664);
}
}
$dbFile = __DIR__ . '/../database/app.sqlite';
ensureDatabaseFile($dbFile);
$containerBuilder->addDefinitions([ // -------------------------
// Instancier Medoo (SQLite)
/* ------------------------------------------------- // -------------------------
* Service Twig (template engine) $database = new Medoo([
* ------------------------------------------------- */ 'database_type' => 'sqlite',
Twig::class => function () { 'database_file' => $dbFile,
$loader = new FilesystemLoader(__DIR__ . '/../views'); 'error' => PDO::ERRMODE_EXCEPTION,
// En développement on désactive le cache pour voir les changements immédiatement 'charset' => 'utf8',
return new Twig($loader, ['cache' => false]);
},
/* -------------------------------------------------
* Service « db » connexion RedBean
* ------------------------------------------------- */
'db' => function () {
// Le chemin du fichier SQLite est déterminé dans redbean.php
$dbPath = getDefaultDbPath(); // ex. …/database/app.sqlite ou autre via DB_FILE
initRedBean($dbPath); // crée le fichier + lance R::setup()
// Retourner ladaptateur (facultatif, on le garde pour que le service existe)
return R::getDatabaseAdapter();
},
// -----------------------------------------------------------------
// Ajoutez dautres services ici (logger, repository, etc.)
// -----------------------------------------------------------------
]); ]);
/* ------------------------------------------------- // Créer la table si nécessaire (schéma minimal)
* Build du conteneur $database->query(
* ------------------------------------------------- */ <<<'SQL'
$container = $containerBuilder->build(); CREATE TABLE IF NOT EXISTS post (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL
);
SQL
);
/* ------------------------------------------------- // -------------------------
* Initialise la connexion DB dès le bootstrap // Services
* ------------------------------------------------- */ // -------------------------
$container->get('db'); // déclenche initRedBean() avant le chargement des routes $services = [];
$services['twig'] = new Twig(new FilesystemLoader(__DIR__ . '/../views'), ['cache' => false]);
$services['db'] = $database;
$services['post_repository'] = new App\Repositories\PostRepositoryMedoo($database);
/* ------------------------------------------------- // -------------------------
* Instanciation de lapplication Slim avec le conteneur // Slim app
* ------------------------------------------------- */ // -------------------------
AppFactory::setContainer($container);
$app = AppFactory::create(); $app = AppFactory::create();
/* -------------------------------------------------
* Middleware derreur (affiche les traces en dev)
* ------------------------------------------------- */
$app->addErrorMiddleware(true, true, true); $app->addErrorMiddleware(true, true, true);
$app->add(TwigMiddleware::create($app, $services['twig']));
/* ------------------------------------------------- // Charger routes et injecter services
* Middleware Twig rend les vues disponibles (require __DIR__ . '/../src/Routes/web.php')($app, $services);
* ------------------------------------------------- */
$twig = $container->get(Twig::class);
$app->add(TwigMiddleware::create($app, $twig));
/* -------------------------------------------------
* Chargement dynamique de toutes les définitions de routes
* ------------------------------------------------- */
foreach (glob(__DIR__ . '/../src/Routes/*.routes.php') as $file) {
// Chaque fichier retourne une closure qui reçoit $app
(require $file)($app);
}
/* -------------------------------------------------
* Démarrage de lapplication
* ------------------------------------------------- */
$app->run(); $app->run();

View File

@@ -1,69 +0,0 @@
<?php
declare(strict_types=1);
use RedBeanPHP\R;
/**
* -----------------------------------------------------------------
* Helper crée le fichier SQLite sil nexiste pas.
* -----------------------------------------------------------------
*
* @param string $path Chemin complet du fichier SQLite.
*/
function ensureDatabaseFile(string $path): void
{
if (!file_exists($path)) {
// crée un fichier vide
touch($path);
// permissions rwrwr-- (0664) suffisantes pour le serveur web
chmod($path, 0664);
}
}
/**
* -----------------------------------------------------------------
* Initialise RedBeanPHP avec le driver SQLite.
* -----------------------------------------------------------------
*
* Cette fonction :
* 1. Sassure que le fichier SQLite existe (via ensureDatabaseFile()).
* 2. Lance R::setup() avec le préfixe « sqlite: ».
* 3. Définit le mode « freeze » (false en dev, true en prod).
*
* @param string $dbPath Chemin complet du fichier SQLite.
*/
function initRedBean(string $dbPath): void
{
// 1. création éventuelle du fichier
ensureDatabaseFile($dbPath);
// 2. connexion à la base SQLite
R::setup('sqlite:' . $dbPath);
// 3. pendant le développement on laisse freeze à false
// (RedBean crée/modifie les tables automatiquement)
R::freeze(false);
}
/**
* -----------------------------------------------------------------
* Retourne le chemin par défaut du fichier SQLite.
* -----------------------------------------------------------------
*
* Le nom du fichier peut être surchargé via la variable denvironnement
* DB_FILE (ou via un .env chargé par lapplication). Ainsi on évite
* de coder en dur le nom du fichier dans le bootstrap.
*
* @return string Chemin absolu du fichier SQLite.
*/
function getDefaultDbPath(): string
{
// Le répertoire « database » se trouve deux niveaux au-dessus de ce fichier
$baseDir = realpath(__DIR__ . '/../../database');
// Nom du fichier valeur par défaut « app.sqlite »
$fileName = getenv('DB_FILE') ?: 'app.sqlite';
return $baseDir . DIRECTORY_SEPARATOR . $fileName;
}

View File

@@ -7,74 +7,56 @@ namespace App\Controllers;
use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Views\Twig; use Slim\Views\Twig;
use App\Models\Post; use App\Repositories\PostRepositoryMedoo as PostRepository;
/**
* Contrôleur dédié à la gestion des articles.
*/
class PostController class PostController
{ {
private Twig $view; private Twig $view;
private PostRepository $repo;
public function __construct(Twig $view) public function __construct(Twig $view, PostRepository $repo)
{ {
$this->view = $view; $this->view = $view;
$this->repo = $repo;
} }
/** Page publique liste des articles */ public function index(Request $req, Response $res): Response
public function index(Request $request, Response $response): Response
{ {
$posts = Post::allDesc(); $posts = $this->repo->allDesc();
return $this->view->render($response, 'pages/home.twig', ['posts' => $posts]); return $this->view->render($res, 'pages/home.twig', ['posts' => $posts]);
} }
/** Tableau de bord admin */ public function admin(Request $req, Response $res): Response
public function admin(Request $request, Response $response): Response
{ {
$posts = Post::allDesc(); $posts = $this->repo->allDesc();
return $this->view->render($response, 'pages/admin.twig', ['posts' => $posts]); return $this->view->render($res, 'pages/admin.twig', ['posts' => $posts]);
} }
/** Formulaire création / édition */ public function form(Request $req, Response $res, array $args): Response
public function form(Request $request, Response $response, array $args): Response
{ {
$id = (int)($args['id'] ?? 0); $id = (int)($args['id'] ?? 0);
$post = $id ? Post::find($id) : null; // id=0 → création $post = $id ? $this->repo->find($id) : null;
$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($response, 'pages/post_form.twig', [
'action' => $action,
'post' => $post,
]);
} }
/** Enregistrement dun nouvel article */ public function create(Request $req, Response $res): Response
public function create(Request $request, Response $response): Response
{ {
$data = $request->getParsedBody(); $data = $req->getParsedBody();
$post = Post::make($data); $this->repo->create($data);
$post->save(); return $res->withHeader('Location', '/admin')->withStatus(302);
return $response->withHeader('Location', '/admin')->withStatus(302);
} }
/** Mise à jour dun article existant */ public function update(Request $req, Response $res, array $args): Response
public function update(Request $request, Response $response, array $args): Response
{ {
$post = Post::find((int)$args['id']); $id = (int)$args['id'];
$post->updateFromArray($request->getParsedBody()); $this->repo->update($id, $req->getParsedBody());
$post->save(); return $res->withHeader('Location', '/admin')->withStatus(302);
return $response->withHeader('Location', '/admin')->withStatus(302);
} }
/** Suppression dun article */ public function delete(Request $req, Response $res, array $args): Response
public function delete(Request $request, Response $response, array $args): Response
{ {
$post = Post::find((int)$args['id']); $this->repo->delete((int)$args['id']);
$post->delete(); return $res->withHeader('Location', '/admin')->withStatus(302);
return $response->withHeader('Location', '/admin')->withStatus(302);
} }
} }

View File

@@ -1,3 +0,0 @@
<?php
// Non utilisé pour l'instant

View File

@@ -1,128 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Models;
use RedBeanPHP\R;
use RedBeanPHP\OODBBean;
/**
* Modèle RedBeanPHP minimal pour l'entité «post».
*
* Chaque instance encapsule un bean RedBean (`OODBBean`).
* Les opérations CRUD sont déléguées à RedBean, tandis que les
* getters/setters offrent une API typée.
*/
class Post
{
/** @var OODBBean le bean réel géré par RedBean */
private OODBBean $bean;
/** Constructeur privé on crée/charge les objets via les factories */
private function __construct(OODBBean $bean)
{
$this->bean = $bean;
}
/* -------------------------------------------------
FACTORIES
------------------------------------------------- */
/** Crée un nouveau post à partir dun tableau de données */
public static function make(array $data): self
{
$bean = R::dispense('post');
$bean->title = $data['title'] ?? '';
$bean->content = $data['content'] ?? '';
return new self($bean);
}
/** Charge un post existant (lève une exception sil nexiste pas) */
public static function find(int $id): self
{
$bean = R::load('post', $id);
if ($bean->id === 0) {
throw new \RuntimeException("Post {$id} introuvable");
}
return new self($bean);
}
/** Retourne tous les posts, triés par id décroissant */
public static function allDesc(): array
{
$beans = R::findAll('post', ' ORDER BY id DESC ');
return array_map(fn (OODBBean $b) => new self($b), $beans);
}
/* -------------------------------------------------
PERSISTENCE
------------------------------------------------- */
/** Enregistre (INSERT ou UPDATE) le bean */
public function save(): void
{
R::store($this->bean);
}
/** Supprime le bean de la base */
public function delete(): void
{
R::trash($this->bean);
}
/* -------------------------------------------------
ACCESSEURS / MUTATEURS
------------------------------------------------- */
public function getId(): int
{
return (int) $this->bean->id;
}
public function getTitle(): string
{
return (string) $this->bean->title;
}
public function getContent(): string
{
return (string) $this->bean->content;
}
public function setTitle(string $title): void
{
$this->bean->title = $title;
}
public function setContent(string $content): void
{
$this->bean->content = $content;
}
/* -------------------------------------------------
MISE À JOUR À PARTIR D'UN TABLEAU
------------------------------------------------- */
/**
* Met à jour le bean à partir dun tableau de données.
*
* Seuls les champs connus sont pris en compte ; les clés inconnues
* sont simplement ignorées. Cette méthode facilite lappel depuis
* le contrôleur: `$post->updateFromArray($request->getParsedBody());`
*
* @param array<string,mixed> $data Données du formulaire
*/
public function updateFromArray(array $data): void
{
// titre
if (array_key_exists('title', $data)) {
$this->setTitle((string) $data['title']);
}
// contenu
if (array_key_exists('content', $data)) {
$this->setContent((string) $data['content']);
}
// Si dautres champs sont ajoutés à lentité (ex. author, status),
// il suffit de les gérer ici de la même façon.
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
use Medoo\Medoo;
/**
* Repository pour "post" basé sur Medoo.
* Retourne et consomme des tableaux associatifs.
*/
class PostRepositoryMedoo
{
private Medoo $db;
public function __construct(Medoo $db)
{
$this->db = $db;
}
public function allDesc(): array
{
return $this->db->select('post', ['id', 'title', 'content'], ['ORDER' => ['id' => 'DESC']]);
}
public function find(int $id): ?array
{
$row = $this->db->get('post', ['id', 'title', 'content'], ['id' => $id]);
return $row === null ? null : $row;
}
public function create(array $data): int
{
$this->db->insert('post', [
'title' => $data['title'] ?? '',
'content' => $data['content'] ?? '',
]);
return (int)$this->db->id();
}
public function update(int $id, array $data): void
{
$this->db->update('post', [
'title' => $data['title'] ?? '',
'content' => $data['content'] ?? '',
], ['id' => $id]);
}
public function delete(int $id): void
{
$this->db->delete('post', ['id' => $id]);
}
}

View File

@@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
use Slim\App;
use App\Controllers\PostController;
/**
* Routes d'administration du blog.
*/
return function (App $app): void {
// Tableau de bord admin liste des posts
$app->get('/admin', [PostController::class, 'admin']);
// Formulaire de création
$app->get('/admin/create', [PostController::class, 'form']);
$app->post('/admin/create', [PostController::class, 'create']);
// Formulaire d'édition l'ID doit être fourni dans l'URL
$app->get('/admin/edit/{id}', [PostController::class, 'form']);
$app->post('/admin/edit/{id}', [PostController::class, 'update']);
// Suppression d'un post
$app->post('/admin/delete/{id}', [PostController::class, 'delete']);
};

View File

@@ -1,18 +0,0 @@
<?php
declare(strict_types=1);
use Slim\App;
use App\Controllers\PostController;
/**
* Routes publiques du blog.
*
* Chargé de la même façon que les autres fichiers *.routes.php
* depuis public/index.php.
*/
return function (App $app): void {
// Page d'accueil liste des articles
$app->get('/', [PostController::class, 'index']);
};

19
src/Routes/web.php Normal file
View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
use Slim\App;
use App\Controllers\PostController;
return function (App $app, array $services): void {
$twig = $services['twig'];
$repo = $services['post_repository'];
$controller = new PostController($twig, $repo);
$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']);
};