Switching from NetBean to Medoo
This commit is contained in:
@@ -13,10 +13,8 @@
|
||||
"require": {
|
||||
"slim/slim": "4.*",
|
||||
"slim/psr7": "*",
|
||||
"gabordemooij/redbean": "^5.7",
|
||||
"twig/twig": "*",
|
||||
"slim/twig-view": "*",
|
||||
"php-di/php-di": "^7.1"
|
||||
"catfan/medoo": "^2.1"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
||||
117
public/index.php
117
public/index.php
@@ -2,90 +2,67 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/* -------------------------------------------------
|
||||
* Autoload Composer + notre configuration RedBean
|
||||
* ------------------------------------------------- */
|
||||
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\Views\Twig;
|
||||
use Slim\Views\TwigMiddleware;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
use RedBeanPHP\R; // façade RedBean (nécessaire pour le service DB)
|
||||
use Medoo\Medoo;
|
||||
|
||||
/* -------------------------------------------------
|
||||
* Construction du conteneur DI
|
||||
* ------------------------------------------------- */
|
||||
$containerBuilder = new ContainerBuilder();
|
||||
// -------------------------
|
||||
// DB SQLite + fichier
|
||||
// -------------------------
|
||||
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([
|
||||
|
||||
/* -------------------------------------------------
|
||||
* Service Twig (template engine)
|
||||
* ------------------------------------------------- */
|
||||
Twig::class => function () {
|
||||
$loader = new FilesystemLoader(__DIR__ . '/../views');
|
||||
// En développement on désactive le cache pour voir les changements immédiatement
|
||||
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 l’adaptateur (facultatif, on le garde pour que le service existe)
|
||||
return R::getDatabaseAdapter();
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Ajoutez d’autres services ici (logger, repository, etc.)
|
||||
// -----------------------------------------------------------------
|
||||
// -------------------------
|
||||
// Instancier Medoo (SQLite)
|
||||
// -------------------------
|
||||
$database = new Medoo([
|
||||
'database_type' => 'sqlite',
|
||||
'database_file' => $dbFile,
|
||||
'error' => PDO::ERRMODE_EXCEPTION,
|
||||
'charset' => 'utf8',
|
||||
]);
|
||||
|
||||
/* -------------------------------------------------
|
||||
* Build du conteneur
|
||||
* ------------------------------------------------- */
|
||||
$container = $containerBuilder->build();
|
||||
// Créer la table si nécessaire (schéma minimal)
|
||||
$database->query(
|
||||
<<<'SQL'
|
||||
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
|
||||
* ------------------------------------------------- */
|
||||
$container->get('db'); // déclenche initRedBean() avant le chargement des routes
|
||||
// -------------------------
|
||||
// Services
|
||||
// -------------------------
|
||||
$services = [];
|
||||
$services['twig'] = new Twig(new FilesystemLoader(__DIR__ . '/../views'), ['cache' => false]);
|
||||
$services['db'] = $database;
|
||||
$services['post_repository'] = new App\Repositories\PostRepositoryMedoo($database);
|
||||
|
||||
/* -------------------------------------------------
|
||||
* Instanciation de l’application Slim avec le conteneur
|
||||
* ------------------------------------------------- */
|
||||
AppFactory::setContainer($container);
|
||||
// -------------------------
|
||||
// Slim app
|
||||
// -------------------------
|
||||
$app = AppFactory::create();
|
||||
|
||||
/* -------------------------------------------------
|
||||
* Middleware d’erreur (affiche les traces en dev)
|
||||
* ------------------------------------------------- */
|
||||
$app->addErrorMiddleware(true, true, true);
|
||||
$app->add(TwigMiddleware::create($app, $services['twig']));
|
||||
|
||||
/* -------------------------------------------------
|
||||
* Middleware Twig – rend les vues disponibles
|
||||
* ------------------------------------------------- */
|
||||
$twig = $container->get(Twig::class);
|
||||
$app->add(TwigMiddleware::create($app, $twig));
|
||||
// Charger routes et injecter services
|
||||
(require __DIR__ . '/../src/Routes/web.php')($app, $services);
|
||||
|
||||
/* -------------------------------------------------
|
||||
* 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 l’application
|
||||
* ------------------------------------------------- */
|
||||
$app->run();
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use RedBeanPHP\R;
|
||||
|
||||
/**
|
||||
* -----------------------------------------------------------------
|
||||
* Helper – crée le fichier SQLite s’il n’existe 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 rw‑rw‑r-- (0664) – suffisantes pour le serveur web
|
||||
chmod($path, 0664);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* -----------------------------------------------------------------
|
||||
* Initialise RedBeanPHP avec le driver SQLite.
|
||||
* -----------------------------------------------------------------
|
||||
*
|
||||
* Cette fonction :
|
||||
* 1. S’assure 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 d’environnement
|
||||
* DB_FILE (ou via un .env chargé par l’application). 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;
|
||||
}
|
||||
@@ -7,74 +7,56 @@ namespace App\Controllers;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Slim\Views\Twig;
|
||||
use App\Models\Post;
|
||||
use App\Repositories\PostRepositoryMedoo as PostRepository;
|
||||
|
||||
/**
|
||||
* Contrôleur dédié à la gestion des articles.
|
||||
*/
|
||||
class PostController
|
||||
{
|
||||
private Twig $view;
|
||||
private PostRepository $repo;
|
||||
|
||||
public function __construct(Twig $view)
|
||||
public function __construct(Twig $view, PostRepository $repo)
|
||||
{
|
||||
$this->view = $view;
|
||||
$this->repo = $repo;
|
||||
}
|
||||
|
||||
/** Page publique – liste des articles */
|
||||
public function index(Request $request, Response $response): Response
|
||||
public function index(Request $req, Response $res): Response
|
||||
{
|
||||
$posts = Post::allDesc();
|
||||
return $this->view->render($response, 'pages/home.twig', ['posts' => $posts]);
|
||||
$posts = $this->repo->allDesc();
|
||||
return $this->view->render($res, 'pages/home.twig', ['posts' => $posts]);
|
||||
}
|
||||
|
||||
/** Tableau de bord admin */
|
||||
public function admin(Request $request, Response $response): Response
|
||||
public function admin(Request $req, Response $res): Response
|
||||
{
|
||||
$posts = Post::allDesc();
|
||||
return $this->view->render($response, 'pages/admin.twig', ['posts' => $posts]);
|
||||
$posts = $this->repo->allDesc();
|
||||
return $this->view->render($res, 'pages/admin.twig', ['posts' => $posts]);
|
||||
}
|
||||
|
||||
/** Formulaire création / édition */
|
||||
public function form(Request $request, Response $response, array $args): Response
|
||||
public function form(Request $req, Response $res, array $args): Response
|
||||
{
|
||||
$id = (int)($args['id'] ?? 0);
|
||||
$post = $id ? Post::find($id) : null; // id=0 → création
|
||||
|
||||
$id = (int)($args['id'] ?? 0);
|
||||
$post = $id ? $this->repo->find($id) : null;
|
||||
$action = $id ? "/admin/edit/{$id}" : "/admin/create";
|
||||
|
||||
return $this->view->render($response, 'pages/post_form.twig', [
|
||||
'action' => $action,
|
||||
'post' => $post,
|
||||
]);
|
||||
return $this->view->render($res, 'pages/post_form.twig', ['post' => $post, 'action' => $action]);
|
||||
}
|
||||
|
||||
/** Enregistrement d’un nouvel article */
|
||||
public function create(Request $request, Response $response): Response
|
||||
public function create(Request $req, Response $res): Response
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
$post = Post::make($data);
|
||||
$post->save();
|
||||
|
||||
return $response->withHeader('Location', '/admin')->withStatus(302);
|
||||
$data = $req->getParsedBody();
|
||||
$this->repo->create($data);
|
||||
return $res->withHeader('Location', '/admin')->withStatus(302);
|
||||
}
|
||||
|
||||
/** Mise à jour d’un article existant */
|
||||
public function update(Request $request, Response $response, array $args): Response
|
||||
public function update(Request $req, Response $res, array $args): Response
|
||||
{
|
||||
$post = Post::find((int)$args['id']);
|
||||
$post->updateFromArray($request->getParsedBody());
|
||||
$post->save();
|
||||
|
||||
return $response->withHeader('Location', '/admin')->withStatus(302);
|
||||
$id = (int)$args['id'];
|
||||
$this->repo->update($id, $req->getParsedBody());
|
||||
return $res->withHeader('Location', '/admin')->withStatus(302);
|
||||
}
|
||||
|
||||
/** Suppression d’un article */
|
||||
public function delete(Request $request, Response $response, array $args): Response
|
||||
public function delete(Request $req, Response $res, array $args): Response
|
||||
{
|
||||
$post = Post::find((int)$args['id']);
|
||||
$post->delete();
|
||||
|
||||
return $response->withHeader('Location', '/admin')->withStatus(302);
|
||||
$this->repo->delete((int)$args['id']);
|
||||
return $res->withHeader('Location', '/admin')->withStatus(302);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<?php
|
||||
|
||||
// Non utilisé pour l'instant
|
||||
@@ -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 d’un 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 s’il n’existe 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 d’un tableau de données.
|
||||
*
|
||||
* Seuls les champs connus sont pris en compte ; les clés inconnues
|
||||
* sont simplement ignorées. Cette méthode facilite l’appel 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 d’autres champs sont ajoutés à l’entité (ex. author, status),
|
||||
// il suffit de les gérer ici de la même façon.
|
||||
}
|
||||
}
|
||||
54
src/Repositories/PostRepositoryMedoo.php
Normal file
54
src/Repositories/PostRepositoryMedoo.php
Normal 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]);
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
};
|
||||
@@ -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
19
src/Routes/web.php
Normal 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']);
|
||||
};
|
||||
Reference in New Issue
Block a user