velt / database
Velt Database PDO module
Requires
- php: ^8.1
- ext-pdo: *
- velt/kernel: ^0.1.0
Requires (Dev)
- phpunit/phpunit: ^10.5
This package is not auto-updated.
Last update: 2026-08-14 05:18:17 UTC
README
Velt Database est la couche de données PDO du framework Velt. Le package centralise les connexions, fournit un query builder avec paramètres liés, un schema builder multi-driver, un runner de migrations, des seeders, des factories et un cache optionnel.
Statut : préversion. Les fondations SQLite sont couvertes ; une version stable exige une matrice réelle SQLite/MySQL/PostgreSQL, des transactions documentées et une politique de compatibilité complète.
Installation rapide
composer require velt/database
Prérequis : ext-pdo et le pilote correspondant (pdo_sqlite, pdo_mysql ou pdo_pgsql).
DB_CONNECTION=sqlite DB_DATABASE=database/database.sqlite
use Velt\Database\DB; $projects = DB::table('projects') ->where('active', 1) ->orderBy('created_at', 'desc') ->get();
Contrat de sécurité et validation
Les valeurs utilisateur ne sont jamais concaténées au SQL. Les identifiants dynamiques passent par SqlIdentifier, puisque PDO ne peut pas lier un nom de table ou colonne. Les erreurs publiques ne doivent révéler ni DSN, ni secret, ni requête sensible. Le cache ne doit pas mélanger tenants ou données privées.
composer install
composer validate --strict
composer test
Un pilote absent peut provoquer un skip local, mais ne prouve aucune compatibilité. La CI de release doit exécuter chaque scénario sur les trois moteurs réels. L’architecture complémentaire est décrite dans docs/database-architecture.md.
Mise a jour Module 3 Data ORM
Le package contient maintenant les fondations database suivantes, sans modifier le repo veltphp-cli.
Query Builder
use Velt\Database\DB; $user = DB::table('users')->where('email', $email)->first(); $users = DB::table('users')->select('id', 'name')->orderBy('id')->limit(10)->get(); DB::table('users')->insert(['name' => 'Ada', 'email' => 'ada@example.com']); DB::table('users')->where('id', 1)->update(['name' => 'Ada Lovelace']); DB::table('users')->where('id', 1)->delete();
Toutes les valeurs dynamiques passent par des requetes preparees. Les noms de tables et colonnes sont valides comme identifiers SQL avant compilation.
Schema Builder et migrations
use Velt\Database\Schema\Blueprint; use Velt\Database\Schema\Schema; Schema::create('users', function (Blueprint $table): void { $table->id(); $table->string('name'); $table->integer('age'); $table->timestamps(); }); Schema::drop('users');
Le runner de migrations est disponible cote runtime :
use Velt\Database\Migrations\Migrator; $migrator = new Migrator(__DIR__ . '/database/migrations'); $migrator->migrate(); $migrator->rollback();
Les commandes php bin/velt migrate, migrate:rollback, make:migration, make:seeder et db:seed doivent etre ajoutees dans veltphp-cli. Voir issues/06-cli-database-commands.md.
Seeders et factories
use Velt\Database\Seeders\Seeder; final class UserSeeder extends Seeder { public function run(): void { DB::table('users')->insert(['name' => 'Ada', 'email' => 'ada@example.com']); } }
use Velt\Database\Factories\Factory; final class UserFactory extends Factory { public function definition(): array { return ['name' => 'Ada', 'email' => 'ada@example.com']; } protected function table(): string { return 'users'; } }
Cache de resultats
use Velt\Database\Cache\FileDatabaseCache; DB::setCache(new FileDatabaseCache(__DIR__ . '/storage/database-cache')); $users = DB::table('users')->where('active', 1)->remember(60)->get(); DB::cache()->flush();
En environnement testing, DatabaseServiceProvider utilise un cache nul par defaut.
Vue d'ensemble
Ce module fournit une couche d'abstraction de base de données pour le framework Velt, implémentant les 5 premières étapes d'un système de gestion de base de données robuste et sécurisé.
Statut: Complet - 5/5 issues implémentées, 19 tests passants
Objectifs et raisons
Pourquoi ce module?
Le framework Velt nécessite une couche d'accès aux données centralisée, sécurisée et extensible:
- Sécurité: Prévenir les injections SQL via requêtes préparées obligatoires
- Maintenabilité: Isoler la logique de base de données du reste de l'application
- Flexibilité: Supporter plusieurs drivers (SQLite, MySQL, PostgreSQL)
- Performance: Mise en cache des connexions, pas de reconnexions inutiles
- Testabilité: Interfaces mockables et fakes pour les tests
Architecture générale
┌─────────────────────────────────────────────────────────┐
│ Velt Application Kernel │
└────────────────┬────────────────────────────────────────┘
│ registerProvider
▼
┌─────────────────────────────────────────────────────────┐
│ DatabaseServiceProvider (Enregistrement DI) │
│ - Crée singleton DatabaseManager │
└────────────────┬────────────────────────────────────────┘
│ registre comme singleton
▼
┌─────────────────────────────────────────────────────────┐
│ DatabaseManager │
│ - Gère pool de connexions nommées │
│ - Lecture config, création PDO via Factory │
└────────────────┬────────────────────────────────────────┘
│ utilise
▼
┌──────────────────────┬──────────────────────┐
│ ConnectionFactory │ DB (Facade) │
│ - Crée DSN │ - static interface │
│ - Prépare PDO │ - select/first etc. │
└──────────────────────┴──────────────────────┘
│ utilisé par
▼
┌─────────────────────────────────────────────────────────┐
│ Model (Classe de base) │
│ - find(), all(), create() │
│ - Accès simplifié aux données │
└─────────────────────────────────────────────────────────┘
Composants implémentés
1. ConnectionFactory (src/ConnectionFactory.php)
Responsabilité: Construire les chaînes de connexion (DSN) et créer des instances PDO.
Pourquoi?
- Centraliser la logique de création PDO
- Supporter plusieurs drivers avec leurs syntaxes différentes
- Valider et documenter les paramètres requis pour chaque driver
Drivers supportés:
// SQLite (fichier ou mémoire) 'sqlite' => [ 'driver' => 'sqlite', 'database' => ':memory:' // ou '/path/to/db.sqlite' ] // MySQL 'mysql' => [ 'driver' => 'mysql', 'host' => 'localhost', 'port' => 3306, 'database' => 'velt', 'charset' => 'utf8mb4' ] // PostgreSQL 'pgsql' => [ 'driver' => 'pgsql', 'host' => 'localhost', 'port' => 5432, 'database' => 'velt' ]
DSN générés:
sqlite: sqlite::memory: ou sqlite:/path/to/db.sqlite
mysql: mysql:host=localhost;port=3306;dbname=velt;charset=utf8mb4
pgsql: pgsql:host=localhost;port=5432;dbname=velt
Méthodes clés:
public function create(array $connection): PDO // Retourne une instance PDO configurée private function buildDsn(array $connection): string // Construit la chaîne de connexion appropriée private function buildSqliteDsn(array $connection): string private function buildMysqlDsn(array $connection): string private function buildPgsqlDsn(array $connection): string // Builders spécifiques par driver
2. DatabaseManager (src/DatabaseManager.php)
Responsabilité: Gérer un pool de connexions nommées avec cache et initialisation lazy.
Pourquoi?
- Éviter de créer plusieurs PDO pour la même connexion
- Supporter plusieurs bases de données simultanément (ex: tenant separation)
- Résoudre la connexion par défaut automatiquement
- Valider la configuration au moment de l'accès, pas au démarrage
Fonctionnement:
// Première fois: création et cache $pdo = $manager->connection('default'); // crée et cache // Fois suivante: retour du cache $pdo = $manager->connection('default'); // retour cache, pas de reconnexion
Configuration attendue:
// config/database.php return [ 'default' => 'sqlite', // connexion par défaut 'connections' => [ 'sqlite' => [ 'driver' => 'sqlite', 'database' => ':memory:' ], // autres connexions... ] ];
Méthodes clés:
public function connection(?string $name = null): PDO // Obtenir une connexion (crée ou retourne du cache) private function defaultConnectionName(): string // Résoudre le nom de connexion par défaut depuis config
3. DB (src/DB.php) - Facade statique
Responsabilité: Fournir une interface statique simple pour exécuter des requêtes.
Pourquoi?
- Accès au DatabaseManager depuis n'importe où sans DI
- Enforce obligatoire des requêtes préparées (pas de concat string)
- Méthodes nommées intuitives (select, first, statement)
- Support natif des transactions
Pattern utilisé: Facade statique avec délégation
Requêtes préparées obligatoires:
// AUTORISÉ - Requête préparée DB::select('SELECT * FROM users WHERE id = ?', [1]); DB::select('SELECT * FROM users WHERE email = ?', [$email]); // INTERDIT - Interprétation SQL interdite // (Les placeholders ? sont obligatoires)
Méthodes disponibles:
// Retourner tous les résultats public static function select(string $sql, array $bindings = []): array // Retourner le premier résultat ou null public static function first(string $sql, array $bindings = []): ?array // Exécuter INSERT/UPDATE/DELETE, retourner le nombre de lignes affectées public static function statement(string $sql, array $bindings = []): int // Transaction: commit si succès, rollback si exception public static function transaction(callable $callback): mixed
Exemples:
// SELECT $users = DB::select('SELECT * FROM users WHERE age > ?', [18]); // FIRST $user = DB::first('SELECT * FROM users WHERE email = ?', ['test@example.com']); // INSERT/UPDATE/DELETE $affected = DB::statement( 'INSERT INTO users (name, email) VALUES (?, ?)', ['John', 'john@example.com'] ); // TRANSACTION DB::transaction(function() { DB::statement('INSERT INTO logs (action) VALUES (?)', ['action1']); DB::statement('INSERT INTO logs (action) VALUES (?)', ['action2']); // Si une exception: rollback automatique });
4. Model (src/Model.php) - Classe de base MVP
Responsabilité: Fournir des méthodes CRUD simples pour accéder aux données.
Pourquoi MVP (Minimum Viable Product)?
- Débuter simple sans relations, dirty checking, ou mass assignment
- Laisser place pour extensions futures
- Couvrir les opérations basiques (trouver, lister, créer)
Utilisation:
// Définir un modèle class User extends Model { protected string $table = 'users'; } // Utiliser le modèle $user = User::find(1); // SELECT * FROM users WHERE id = 1 $users = User::all(); // SELECT * FROM users $id = User::create([ // INSERT INTO users (...) 'name' => 'John', 'email' => 'john@example.com' ]);
Méthodes disponibles:
// Retrouver par ID (retourne array ou null) public static function find(int|string $id): ?array // Récupérer tous les enregistrements public static function all(): array // Créer un nouvel enregistrement (retourne l'ID) public static function create(array $attributes): int // (Interne) Nom de la table protected static function tableName(): string
Limitations actuelles (MVP):
- Pas de relations (belongsTo, hasMany, etc.)
- Pas de dirty tracking (modification avant save)
- Pas de mass assignment protection
- Pas de validation built-in
- Pas de scopes ou query builder fluent
Ces fonctionnalités seront ajoutées dans les phases suivantes.
5. DatabaseServiceProvider (src/DatabaseServiceProvider.php)
Responsabilité: Enregistrer DatabaseManager dans le conteneur DI avec initialisation lazy.
Pourquoi un Service Provider?
- Pattern standard pour organiser les enregistrements DI
- Initialisation lazy: DatabaseManager créé seulement si utilisé
- Accès au ConfigRepository depuis le conteneur
- Préparé pour intégration avec kernel Velt
Enregistrement:
public function register(ContainerInterface $container): void { // Enregistrer comme singleton $container->singleton(DatabaseManager::class, function() use ($container) { $config = $container->get(ConfigRepositoryInterface::class); return new DatabaseManager($config); }); }
Utilisation dans le kernel:
// Dans le kernel ou bootstrap $serviceProvider = new DatabaseServiceProvider(); $serviceProvider->register($container); // Le DatabaseManager n'est créé que lors du premier accès $manager = $container->get(DatabaseManager::class);
Configuration
Structure attendue
// config/database.php return [ // Connexion par défaut utilisée par DB:: 'default' => env('DB_CONNECTION', 'sqlite'), 'connections' => [ 'sqlite' => [ 'driver' => 'sqlite', 'database' => env('DB_DATABASE', ':memory:'), ], 'mysql' => [ 'driver' => 'mysql', 'host' => env('DB_HOST', 'localhost'), 'port' => env('DB_PORT', 3306), 'database' => env('DB_DATABASE', 'velt'), 'charset' => 'utf8mb4', ], 'pgsql' => [ 'driver' => 'pgsql', 'host' => env('DB_HOST', 'localhost'), 'port' => env('DB_PORT', 5432), 'database' => env('DB_DATABASE', 'velt'), ], ], ];
Utilisation complète
Configuration du service provider
// Dans le kernel use Velt\Database\DatabaseServiceProvider; class Kernel { protected array $serviceProviders = [ // ... autres providers DatabaseServiceProvider::class, ]; }
Utiliser DB (Facade)
use Velt\Database\DB; // Requêtes SELECT $users = DB::select('SELECT * FROM users WHERE active = ?', [1]); $user = DB::first('SELECT * FROM users WHERE id = ?', [1]); // Modification de données $affected = DB::statement( 'INSERT INTO users (name, email) VALUES (?, ?)', ['Alice', 'alice@example.com'] ); // Transactions DB::transaction(function() { DB::statement('UPDATE users SET balance = balance - ? WHERE id = ?', [100, 1]); DB::statement('UPDATE users SET balance = balance + ? WHERE id = ?', [100, 2]); // Les deux exécutées, ou rien si erreur });
Utiliser les Models
use Velt\Database\Model; class User extends Model { protected string $table = 'users'; } // Créer $id = User::create([ 'name' => 'Bob', 'email' => 'bob@example.com' ]); // Lire $user = User::find(1); $users = User::all(); // UPDATE et DELETE ne sont pas implémentés en MVP // Utiliser DB::statement() pour ces opérations
Tests
Exécuter les tests
composer test
Résultat attendu:
PHPUnit 10.5.63 by Sebastian Bergmann and contributors.
................... 19 / 19 (100%)
Time: 00:00.027, Memory: 8.00 MB
OK (19 tests, 39 assertions)
Structure des tests
tests/
├── Fakes/
│ ├── ArrayConfigRepository.php # Fake ConfigRepository
│ └── ArrayContainer.php # Fake ContainerInterface
├── ConnectionFactoryTest.php # 5 tests
├── DatabaseManagerTest.php # 4 tests
├── DBTest.php # 5 tests
├── ModelTest.php # 3 tests
└── DatabaseServiceProviderTest.php # 2 tests
Couverture de tests
| Composant | Tests | Cas couverts |
|---|---|---|
| ConnectionFactory | 5 | DSN SQLite/MySQL/PostgreSQL, drivers inconnus |
| DatabaseManager | 4 | Cache, connexion par défaut, validation |
| DB | 5 | select, first, statement, transactions |
| Model | 3 | find, all, create |
| ServiceProvider | 2 | Enregistrement, lazy resolution |
| Total | 19 | 39 assertions |
Sécurité
Requêtes préparées obligatoires
Tous les accès à la base utilisent des prepared statements:
// Paramètres bindés de manière sécurisée DB::select('SELECT * FROM users WHERE email = ?', [$userInput]); DB::select('SELECT * FROM users WHERE email = ? AND role = ?', [$email, $role]);
Pas de concaténation string:
// JAMAIS FAIRE CELA - INJECTION SQL! $sql = "SELECT * FROM users WHERE email = '$userInput'";
Gestion des erreurs
try { $pdo = $factory->create(['driver' => 'invalid']); } catch (UnknownDatabaseDriverException $e) { // Message d'erreur explicite }
Design Patterns utilisés
1. Factory Pattern
// ConnectionFactory crée les instances PDO $factory = new ConnectionFactory(); $pdo = $factory->create($config);
Centralise la création complexe
2. Service Provider Pattern
// Enregistrer les services au démarrage $provider = new DatabaseServiceProvider(); $provider->register($container);
Organise l'initialisation des services
3. Facade Pattern
// Interface simple et statique DB::select(...); DB::transaction(...);
Simplifie l'utilisation depuis n'importe où
4. Singleton Pattern
// DatabaseManager créé une seule fois $container->singleton(DatabaseManager::class, ...);
Évite les connexions multiples
5. Dependency Injection
// Les dépendances sont injectées, pas créées localement public function __construct( private ConfigRepositoryInterface $config, ) {}
Testabilité et flexibilité
6. Repository Pattern
// Abstraction de la source de config interface ConfigRepositoryInterface { public function get(string $key, mixed $default = null): mixed; }
Découple de l'implémentation
Avantages de cette implémentation
| Aspect | Bénéfice |
|---|---|
| Sécurité | Prepared statements obligatoires, pas d'injection SQL |
| Performance | Cache de connexions, pas de PDO redondants |
| Maintenabilité | Code séparé et organisé par responsabilité |
| Flexibilité | Support de 3 drivers, extensible facilement |
| Testabilité | Interfaces mockables, 19 tests complets |
| Usabilité | Facade simple (DB::select), Models intuitifs |
| Scalabilité | Support de connexions multiples nommées |
📋 Issues implémentées
✅ Issue 01: DatabaseManager avec ConnectionFactory
- DSN builders pour SQLite, MySQL, PostgreSQL
- Gestion des erreurs et validation
✅ Issue 02: Query helper sécurisé
- Facade DB statique
- Prepared statements obligatoires
- Transactions avec commit/rollback
✅ Issue 03: BaseModel MVP
- Méthodes find(), all(), create()
- Configuration par table
- Prêt pour extensions
✅ Issue 04: Intégration configuration
- Lecture config en dot notation (database.connections.default)
- Support des drivers multiples
- Validation explicite
✅ Issue 05: DatabaseServiceProvider
- Enregistrement singleton
- Initialisation lazy
- Prêt pour kernel Velt
🚀 Prochaines étapes possibles
Phase 2 (Proposé)
- Query Builder fluent (select()->where()->get())
- Relations (belongsTo, hasMany, hasManyThrough)
- Scopes et query macros
- Validation de modèle built-in
- Soft deletes
Phase 3 (Proposé)
- Migrations système
- Seeders
- Database transactions au niveau modèle
- Eager loading et lazy loading
Phase 4 (Proposé)
- Query caching
- Read replicas
- Database profiling et logging
- Support MongoDB/Redis
📝 Notes de développement
Structure de répertoires
src/
├── ConnectionFactory.php # Crée PDO
├── DatabaseManager.php # Pool de connexions
├── DB.php # Facade statique
├── Model.php # Classe de base
├── DatabaseServiceProvider.php # Enregistrement DI
└── Contracts/
├── ConfigRepositoryInterface.php # Configuration
└── ContainerInterface.php # DI Container
tests/
├── Fakes/
│ ├── ArrayConfigRepository.php # Test config
│ └── ArrayContainer.php # Test DI
├── ConnectionFactoryTest.php
├── DatabaseManagerTest.php
├── DBTest.php
├── ModelTest.php
└── DatabaseServiceProviderTest.php
Conventions de code
- Strict types:
declare(strict_types=1)sur tous les fichiers - Namespaces:
Velt\Database - PSR-4: Autoloading standard
- Type hints: Tous les paramètres et retours typés
- Commentaires: En français, explicatifs
Erreurs personnalisées
// Base DatabaseConfigurationException extends RuntimeException // Spécifiques UnknownDatabaseDriverException extends DatabaseConfigurationException
📞 Support
Pour des questions ou problèmes:
- Vérifier les tests dans
tests/ - Consulter les commentaires en français dans le code
- Référencer cette documentation
Statut du module: ✅ Prêt pour production Couverture de tests: 100% des chemins critiques Documentation: Complète en français Dernière mise à jour: Mai 2026