replaced eloquent by redbeanphp

This commit is contained in:
julien
2026-03-04 03:49:11 +01:00
parent c4acb5226c
commit ab8ac6b7fb
14 changed files with 297 additions and 181 deletions

View File

@@ -2,7 +2,7 @@
* [PHP](https://www.php.net) comme language * [PHP](https://www.php.net) comme language
* [Slim](https://www.slimframework.com) comme framework * [Slim](https://www.slimframework.com) comme framework
* [Eloquent](https://github.com/illuminate/database) comme ORM * [RedBeanPHP](https://www.redbeanphp.com/index.php) comme ORM
* [Twig](https://twig.symfony.com) comme template engine * [Twig](https://twig.symfony.com) comme template engine
## HOWTO ## HOWTO

View File

@@ -12,14 +12,15 @@
], ],
"require": { "require": {
"slim/slim": "4.*", "slim/slim": "4.*",
"slim/psr7":"*", "slim/psr7": "*",
"illuminate/database":"*", "gabordemooij/redbean": "^5.7",
"twig/twig":"*", "twig/twig": "*",
"slim/twig-view":"*" "slim/twig-view": "*",
}, "php-di/php-di": "^7.1"
"autoload": { },
"psr-4": { "autoload": {
"App\\": "src/" "psr-4": {
"App\\": "src/"
}
} }
}
} }

View File

@@ -1,36 +0,0 @@
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use Illuminate\Database\Capsule\Manager as Capsule;
// ---------------------------------------------------------------------
// Connexion SQLite on utilise un chemin absolu (realpath) afin
// d'éviter le helper Laravel `base_path()` qui n'est pas disponible.
// ---------------------------------------------------------------------
$capsule = new Capsule;
$capsule->addConnection([
'driver' => 'sqlite',
// __DIR__ pointe sur le répertoire racine du projet
// le fichier SQLite se trouve dans le sousdossier `database`
'database' => realpath(__DIR__ . '/database/blog.sqlite'),
'prefix' => '',
]);
$capsule->setAsGlobal();
$capsule->bootEloquent();
// ---------------------------------------------------------------------
// Création de la table `posts` si elle n'existe pas déjà
// ---------------------------------------------------------------------
if (!Capsule::schema()->hasTable('posts')) {
Capsule::schema()->create('posts', function ($table) {
$table->increments('id'); // clé primaire autoincrémentée
$table->string('title'); // titre de l'article
$table->text('content'); // contenu de l'article
});
echo "Table 'posts' créée avec succès.\n";
} else {
echo "La table 'posts' existe déjà.\n";
}

View File

@@ -1,61 +1,80 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
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 Illuminate\Database\Capsule\Manager as Capsule; use Twig\Loader\FilesystemLoader;
// -------------------------------------------------
// Autoloader Composer
// -------------------------------------------------
require __DIR__ . '/../vendor/autoload.php'; require __DIR__ . '/../vendor/autoload.php';
/* ------------------------------------------------- /* -------------------------------------------------
Instanciation de lapplication Slim Container (DI) configuration explicite
------------------------------------------------- */ ------------------------------------------------- */
$containerBuilder = new ContainerBuilder();
$containerBuilder->addDefinitions([
// -----------------------------------------------------------------
// Twig : création de linstance Slim\Views\Twig
// -----------------------------------------------------------------
Twig::class => function () {
$loader = new FilesystemLoader(__DIR__ . '/../views');
$settings = ['cache' => false]; // désactiver le cache en dev
return new Twig($loader, $settings);
},
// -----------------------------------------------------------------
// Ajoute dautres services ici si besoin (logger, DB, etc.)
// -----------------------------------------------------------------
]);
$container = $containerBuilder->build();
/* -------------------------------------------------
Instanciation de lapplication Slim avec le container
------------------------------------------------- */
AppFactory::setContainer($container);
$app = AppFactory::create(); $app = AppFactory::create();
/* ------------------------------------------------- /* -------------------------------------------------
Middleware derreur (affiche les exceptions) Middleware derreur
------------------------------------------------- */ ------------------------------------------------- */
$app->addErrorMiddleware(true, true, true); $app->addErrorMiddleware(true, true, true);
/* ------------------------------------------------- /* -------------------------------------------------
Twig (templates) Twig middleware on récupère explicitement le service Twig
------------------------------------------------- */ ------------------------------------------------- */
$twig = Twig::create(__DIR__ . '/../views', ['cache' => false]); $twig = $container->get(Twig::class); // <-- récupération directe
$app->add(TwigMiddleware::create($app, $twig)); $app->add(TwigMiddleware::create($app, $twig)); // <-- utilisation de create(), pas createFromContainer()
/* ------------------------------------------------- /* -------------------------------------------------
Vérification / création du fichier SQLite Vérification / création du fichier SQLite
------------------------------------------------- */ ------------------------------------------------- */
$dbFile = __DIR__ . '/../database/blog.sqlite'; $dbFile = __DIR__ . '/../database/blog.sqlite';
if (!file_exists($dbFile)) { if (!file_exists($dbFile)) {
// crée un fichier vide et lui donne les permissions décriture
touch($dbFile); touch($dbFile);
chmod($dbFile, 0664); chmod($dbFile, 0664);
} }
/* ------------------------------------------------- /* -------------------------------------------------
Eloquent (connexion SQLite) RedBeanPHP (connexion SQLite)
------------------------------------------------- */ ------------------------------------------------- */
$capsule = new Capsule; require __DIR__ . '/../src/Config/redbean.php';
$capsule->addConnection([ initRedBean($dbFile);
'driver' => 'sqlite',
// le chemin relatif fonctionne ; SQLite créera le fichier si besoin
'database' => $dbFile,
'prefix' => '',
]);
$capsule->setAsGlobal();
$capsule->bootEloquent();
/* ------------------------------------------------- /* -------------------------------------------------
Chargement des routes Chargement des routes
------------------------------------------------- */ ------------------------------------------------- */
$routerFiles = glob(__DIR__ . '/../src/Routes/*.php'); foreach (glob(__DIR__ . '/../src/Routes/*.routes.php') as $file) {
foreach ($routerFiles as $file) { (require $file)($app);
(require $file)($app); // chaque fichier retourne une fonction qui ajoute ses routes
} }
/* ------------------------------------------------- /* -------------------------------------------------
Démarrage de lapplication Démarrage de lapplication
------------------------------------------------- */ ------------------------------------------------- */
$app->run(); $app->run();

19
src/Config/redbean.php Normal file
View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
use RedBeanPHP\R;
/**
* Initialise RedBeanPHP avec SQLite.
* Appelé depuis public/index.php avant le chargement des routes.
*/
function initRedBean(string $dbPath): void
{
// Le préfixe « sqlite: » indique le driver SQLite
R::setup('sqlite:' . $dbPath);
// En mode production on désactive le «freeze» pour que RedBean
// ne crée plus de tables automatiquement.
// Ici on garde le freeze à false pendant le développement.
R::freeze(false);
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
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;
/**
* Contrôleur dédié à la gestion des articles.
*/
class PostController
{
private Twig $view;
public function __construct(Twig $view)
{
$this->view = $view;
}
/** Page publique liste des articles */
public function index(Request $request, Response $response): Response
{
$posts = Post::allDesc();
return $this->view->render($response, 'pages/home.twig', ['posts' => $posts]);
}
/** Tableau de bord admin */
public function admin(Request $request, Response $response): Response
{
$posts = Post::allDesc();
return $this->view->render($response, 'pages/admin.twig', ['posts' => $posts]);
}
/** Formulaire création / édition */
public function form(Request $request, Response $response, array $args): Response
{
$id = (int)($args['id'] ?? 0);
$post = $id ? Post::find($id) : null; // id=0 → création
$action = $id ? "/admin/edit/{$id}" : "/admin/create";
return $this->view->render($response, 'pages/post_form.twig', [
'action' => $action,
'post' => $post,
]);
}
/** Enregistrement dun nouvel article */
public function create(Request $request, Response $response): Response
{
$data = $request->getParsedBody();
$post = Post::make($data);
$post->save();
return $response->withHeader('Location', '/admin')->withStatus(302);
}
/** Mise à jour dun article existant */
public function update(Request $request, Response $response, array $args): Response
{
$post = Post::find((int)$args['id']);
$post->updateFromArray($request->getParsedBody());
$post->save();
return $response->withHeader('Location', '/admin')->withStatus(302);
}
/** Suppression dun article */
public function delete(Request $request, Response $response, array $args): Response
{
$post = Post::find((int)$args['id']);
$post->delete();
return $response->withHeader('Location', '/admin')->withStatus(302);
}
}

View File

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

View File

@@ -1,16 +1,112 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace App\Models; // ← namespace attendu par routes.php namespace App\Models;
use Illuminate\Database\Eloquent\Model; use RedBeanPHP\R;
use RedBeanPHP\OODBBean;
/** /**
* Modèle Eloquent représentant un article. * 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 extends Model class Post
{ {
protected $table = 'posts'; /** @var OODBBean le bean réel géré par RedBean */
protected $fillable = ['title', 'content']; private OODBBean $bean;
public $timestamps = false;
/** 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

@@ -1,83 +0,0 @@
<?php
declare(strict_types=1);
use Slim\App;
use Slim\Psr7\Request;
use Slim\Psr7\Response;
use App\Models\Post;
use Slim\Views\Twig;
return function (App $app) {
// -------------------------------------------------
// Page admin tableau de bord
// -------------------------------------------------
$app->get('/admin', function (Request $request, Response $response) use ($app) {
$posts = Post::orderByDesc('id')->get();
/** @var Twig $view */
$view = $request->getAttribute('view');
return $view->render($response, 'pages/admin.twig', ['posts' => $posts]);
});
// -------------------------------------------------
// Création d'un article (POST depuis admin)
// -------------------------------------------------
$app->post('/admin/create', function (Request $request, Response $response) {
$data = $request->getParsedBody();
Post::create([
'title' => $data['title'],
'content' => $data['content'],
]);
return $response
->withHeader('Location', '/admin')
->withStatus(302);
});
// -------------------------------------------------
// Formulaire d'édition (GET depuis admin)
// -------------------------------------------------
$app->get('/admin/edit/{id}', function (Request $request, Response $response, $args) use ($app) {
$id = (int)$args['id'];
$post = $id ? Post::findOrFail($id) : null; // id=0 → création
/** @var Twig $view */
$view = $request->getAttribute('view');
return $view->render($response, 'pages/post_form.twig', [
'action' => $id ? "/admin/edit/{$id}" : "/admin/create",
'post' => $post,
]);
});
// -------------------------------------------------
// Enregistrement des modifications (POST depuis admin)
// -------------------------------------------------
$app->post('/admin/edit/{id}', function (Request $request, Response $response, $args) {
$post = Post::findOrFail($args['id']);
$data = $request->getParsedBody();
$post->update([
'title' => $data['title'],
'content' => $data['content'],
]);
return $response
->withHeader('Location', '/admin')
->withStatus(302);
});
// -------------------------------------------------
// Suppression d'un article (POST depuis admin)
// -------------------------------------------------
$app->post('/admin/delete/{id}', function (Request $request, Response $response, $args) {
$post = Post::findOrFail($args['id']);
$post->delete();
return $response
->withHeader('Location', '/admin')
->withStatus(302);
});
};

View File

@@ -0,0 +1,24 @@
<?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,22 +0,0 @@
<?php
declare(strict_types=1);
use Slim\App;
use Slim\Psr7\Request;
use Slim\Psr7\Response;
use App\Models\Post;
use Slim\Views\Twig;
return function (App $app) {
// -------------------------------------------------
// Page publique liste des articles
// -------------------------------------------------
$app->get('/', function (Request $request, Response $response) use ($app) {
$posts = Post::orderByDesc('id')->get();
/** @var Twig $view */
$view = $request->getAttribute('view'); // <-- récupération correcte
return $view->render($response, 'pages/home.twig', ['posts' => $posts]);
});
};

View File

@@ -0,0 +1,17 @@
<?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']);
};