Crud Trait

Le Crud_trait fournit des méthodes réutilisables pour les opérations CRUD standard. Il évite de réécrire les mêmes requêtes SQL dans chaque controller.

Prérequis : Le controller doit avoir protected Database $db et protected Request $request accessibles via $this.

Mise en place

<?php
namespace App\Controllers\Admin;

use River\Controller;
use River\Response;
use River\View;
use River\Request;
use River\Router;
use River\Database;
use River\Traits\Crud_trait;

class Products_ctrl extends Controller {
    use Crud_trait;

    public function __construct(
        View $view,
        Request $request,
        Router $router,
        protected Database $db,  // requis par Crud_trait
    ) {
        parent::__construct($view, $request, $router);
    }
}

C'est tout. Toutes les méthodes crud_* sont maintenant disponibles via $this.

Référence des méthodes

crud_index — Liste paginée

Retourne une liste paginée avec tri et recherche intégrée.

$result = $this->crud_index('products', [
    'per_page'       => 20,
    'order_by'       => 'created_at',
    'order_dir'      => 'DESC',
    'search'         => $this->request->query('search'),
    'search_columns' => ['name', 'description'],
    'base_url'       => '/admin/products',
    'select'         => 'id, name, price, created_at',
]);

// $result contient :
// 'items'     => array d'objets (les produits de la page courante)
// 'paginator' => instance de Paginator (pour le rendu des liens)
// 'total'     => int (nombre total de résultats)
// 'search'    => string|null (terme de recherche actif)

Options disponibles :

OptionTypeDéfautDescription
per_pageint15Nombre d'items par page
order_bystring'id'Colonne de tri
order_dirstring'DESC''ASC' ou 'DESC'
searchstring|nullnullTerme de recherche
search_columnsarray[]Colonnes où chercher (LIKE %...%)
wherestring|nullnullClause WHERE additionnelle
where_paramsarray[]Paramètres pour le WHERE
selectstring'*'Colonnes à sélectionner
base_urlstring''URL de base pour les liens de pagination
Le numéro de page est lu automatiquement via $request->input('page') — fonctionne en GET (?page=2) comme en POST (utile avec HTMX).

Exemple avec filtre personnalisé :

// Lister uniquement les produits actifs d'une catégorie
$result = $this->crud_index('products', [
    'where'        => 'active = ? AND category_id = ?',
    'where_params' => [1, $category_id],
    'search'       => $this->request->query('search'),
    'search_columns' => ['name'],
]);

crud_find — Trouver par ID

$product = $this->crud_find('products', $id);
// Retourne un objet stdClass ou null

if (!$product) {
    return $this->e404();
}
// $product->name, $product->price...

Avec sélection de colonnes :

$product = $this->crud_find('products', $id, 'id, name, price');

crud_find_by — Trouver par champ

$product = $this->crud_find_by('products', 'slug', 'mon-produit');
// Cherche WHERE slug = 'mon-produit'

$user = $this->crud_find_by('users', 'email', 'flo@example.com');

crud_create — Créer

Insère un enregistrement et retourne l'ID. Ajoute automatiquement created_at si non fourni.

$id = $this->crud_create('products', [
    'name'  => 'Nouveau produit',
    'price' => 29.99,
    'slug'  => 'nouveau-produit',
]);
// $id = 42 (dernier ID inséré)
// created_at est ajouté automatiquement

crud_update — Mettre à jour

Met à jour un enregistrement par ID. Ajoute automatiquement updated_at si non fourni. Retourne true si au moins une ligne a été modifiée.

$updated = $this->crud_update('products', $id, [
    'name'  => 'Nom modifié',
    'price' => 39.99,
]);
// $updated = true/false
// updated_at est ajouté automatiquement

crud_delete — Supprimer

$deleted = $this->crud_delete('products', $id);
// $deleted = true si supprimé, false sinon

Helpers

crud_exists — Vérifier l'existence

if (!$this->crud_exists('products', $id)) {
    return $this->e404();
}

crud_count — Compter

// Compter tout
$total = $this->crud_count('products');

// Compter avec condition
$active = $this->crud_count('products', 'active = ?', [1]);

crud_all — Tout récupérer (sans pagination)

Utile pour les selects, les exports, ou les petites tables.

// Tous les produits triés par nom
$products = $this->crud_all('products', 'name', 'ASC');

// Seulement id et name (pour un select)
$categories = $this->crud_all('categories', 'name', 'ASC', 'id, name');

Validation

crud_filter — Filtrer les champs autorisés

Garde uniquement les clés autorisées dans un tableau de données. Indispensable pour éviter le mass assignment.

private const ALLOWED_FIELDS = ['name', 'email', 'role'];

public function store(): Response {
    $data = $this->crud_filter($this->request->all(), self::ALLOWED_FIELDS);
    // Si l'utilisateur envoie 'password' ou 'is_admin' dans le POST,
    // ces champs sont ignorés

    $id = $this->crud_create('users', $data);
}

crud_is_unique — Vérifier l'unicité

// Création : vérifier que l'email n'existe pas
if (!$this->crud_is_unique('users', 'email', $data['email'])) {
    // Erreur : email déjà utilisé
}

// Update : exclure l'enregistrement en cours de modification
if (!$this->crud_is_unique('users', 'email', $data['email'], $id)) {
    // Erreur : email déjà utilisé par un autre utilisateur
}

Fetch Mode

Par défaut, toutes les méthodes retournent des objets stdClass ($item->name). Pour utiliser des tableaux associatifs ($item['name']), définissez la propriété $crud_fetch_mode :

use PDO;

class Products_ctrl extends Controller {
    use Crud_trait;

    protected int $crud_fetch_mode = PDO::FETCH_ASSOC;
    // Maintenant : $item['name'] au lieu de $item->name
}

Exemple complet

Un controller CRUD typique pour une gestion d'utilisateurs :

<?php
namespace App\Controllers\Admin;

use River\Controller;
use River\Response;
use River\View;
use River\Request;
use River\Router;
use River\Database;
use River\Csrf;
use River\Session;
use River\Traits\Crud_trait;

class Users_ctrl extends Controller {
    use Crud_trait;

    private const ALLOWED = ['name', 'email', 'role'];

    public function __construct(
        View $view,
        Request $request,
        Router $router,
        protected Database $db,
        private Csrf $csrf,
        private Session $session,
    ) {
        parent::__construct($view, $request, $router);
    }

    public function index(): Response {
        $result = $this->crud_index('users', [
            'per_page'       => 15,
            'order_by'       => 'created_at',
            'order_dir'      => 'DESC',
            'search'         => $this->request->query('search'),
            'search_columns' => ['name', 'email'],
            'base_url'       => '/admin/users',
            'select'         => 'id, name, email, role, created_at',
        ]);

        return $this->render('admin/users/index', [
            'users'     => $result['items'],
            'paginator' => $result['paginator'],
            'search'    => $result['search'],
            'total'     => $result['total'],
        ]);
    }

    public function store(): Response {
        $data = $this->crud_filter($this->request->all(), self::ALLOWED);

        if (!$this->crud_is_unique('users', 'email', $data['email'])) {
            $this->session->flash('error', 'Email déjà utilisé');
            return Response::redirect('/admin/users/create');
        }

        $data['password'] = password_hash(
            $this->request->post('password'), PASSWORD_DEFAULT
        );

        $id = $this->crud_create('users', $data);

        $this->session->flash('success', 'Utilisateur créé');
        return Response::redirect('/admin/users/' . $id);
    }

    public function show(): Response {
        $user = $this->crud_find('users', (int) $this->param('id'));

        if (!$user) return $this->e404();

        return $this->render('admin/users/show', ['user' => $user]);
    }

    public function update(): Response {
        $id = (int) $this->param('id');
        $data = $this->crud_filter($this->request->all(), self::ALLOWED);

        if (!$this->crud_is_unique('users', 'email', $data['email'], $id)) {
            $this->session->flash('error', 'Email déjà utilisé');
            return Response::redirect('/admin/users/' . $id . '/edit');
        }

        $this->crud_update('users', $id, $data);

        $this->session->flash('success', 'Utilisateur mis à jour');
        return Response::redirect('/admin/users/' . $id);
    }

    public function destroy(): Response {
        $this->crud_delete('users', (int) $this->param('id'));

        $this->session->flash('success', 'Utilisateur supprimé');
        return Response::redirect('/admin/users');
    }
}

Routes associées :

$router->group(['prefix' => 'admin', 'middleware' => Auth_middleware::class], function($r) {
    $r->get('users', 'Admin\Users_ctrl::index')->name('admin.users');
    $r->get('users/create', 'Admin\Users_ctrl::create')->name('admin.users.create');
    $r->post('users', 'Admin\Users_ctrl::store')->name('admin.users.store');
    $r->get('users/{id}', 'Admin\Users_ctrl::show')->where('id', '\d+')->name('admin.users.show');
    $r->get('users/{id}/edit', 'Admin\Users_ctrl::edit')->where('id', '\d+')->name('admin.users.edit');
    $r->post('users/{id}', 'Admin\Users_ctrl::update')->where('id', '\d+');
    $r->post('users/{id}/delete', 'Admin\Users_ctrl::destroy')->where('id', '\d+');
});

Récapitulatif

MéthodeRetourDescription
crud_index($table, $options)arrayListe paginée avec recherche
crud_find($table, $id)object|nullUn item par ID
crud_find_by($table, $col, $val)object|nullUn item par champ
crud_create($table, $data)intCrée, retourne l'ID
crud_update($table, $id, $data)boolMet à jour
crud_delete($table, $id)boolSupprime
crud_exists($table, $id)boolVérifie l'existence
crud_count($table, $where?)intCompte les items
crud_all($table, ...)arrayTout sans pagination
crud_filter($data, $allowed)arrayFiltre les champs
crud_is_unique($table, $col, $val)boolVérifie l'unicité