velt / orm
Active record ORM layer for the Velt framework.
Requires
- php: ^8.2
- velt/database: ^0.1.0
Requires (Dev)
- phpunit/phpunit: ^10.5
This package is not auto-updated.
Last update: 2026-08-14 05:15:53 UTC
README
Velt ORM fournit la couche Active Record du framework Velt. Il transforme les lignes retournées par velt/database en objets métier persistables, expose une API de requête orientée modèle, prend en charge les relations essentielles et retourne des résultats paginés sérialisables.
Le package privilégie une surface réduite et lisible. Il ne réimplémente pas PDO, les migrations ou le compilateur SQL : ces responsabilités appartiennent à velt/database.
Statut : préversion. L’API de base est opérationnelle, mais les relations avancées, les casts, les événements et les garanties de performance doivent être stabilisés avant
1.0.
Installation
composer require velt/orm
Prérequis : PHP 8.2 ou supérieur, ext-pdo, une connexion configurée par velt/database et le pilote PDO correspondant à la base utilisée.
Premier modèle
<?php namespace App\Users\Models; use Velt\Orm\Model; final class User extends Model { protected static string $table = 'users'; protected static array $fillable = [ 'name', 'email', ]; }
$user = User::find(1); $users = User::all(); $active = User::where('active', true)->get(); $ada = User::create([ 'name' => 'Ada Lovelace', 'email' => 'ada@example.com', ]); $ada->name = 'Ada'; $ada->save(); $ada->delete();
Responsabilités et frontières
| Couche | Responsabilité |
|---|---|
velt/database |
connexions PDO, requêtes préparées, query builder, schéma, migrations, seeders |
velt/orm |
hydratation, identité du modèle, persistance, relations, pagination |
velt/cli |
génération de modèles et commandes de migration |
| application | règles métier, validation d’entrée, autorisation et transactions métier |
L’ORM ne doit jamais accéder directement aux variables globales HTTP, générer des réponses ou prendre une décision d’autorisation.
Lecture des données
Recherche par clé primaire
$user = User::find(42); if ($user === null) { // Le modèle n’existe pas. }
Requête par attribut
$user = User::where('email', 'ada@example.com')->first(); $users = User::query() ->where('active', 1) ->orderBy('created_at', 'desc') ->limit(25) ->get();
ModelQueryBuilder adapte le query builder de la couche Database et hydrate chaque résultat dans la classe de modèle appelée.
Création et mise à jour
$user = new User([ 'name' => 'Grace Hopper', 'email' => 'grace@example.com', ]); $user->save(); $user->name = 'Rear Admiral Grace Hopper'; $user->save();
Lors du premier save(), l’ORM insère le modèle. Lorsque sa clé primaire est connue, il met à jour la ligne correspondante. L’application doit entourer les opérations multiples d’une transaction fournie par la couche Database.
Protection des attributs
final class User extends Model { protected static string $table = 'users'; protected static array $fillable = ['name', 'email']; protected static array $guarded = ['id', 'is_admin']; }
La protection de masse est une barrière de programmation, pas une politique d’autorisation. Le fait qu’un attribut soit remplissable ne signifie jamais que l’utilisateur courant a le droit de le modifier. Les payloads entrants doivent être validés et autorisés avant de parvenir au modèle.
Attributs et sérialisation
Les attributs hydratés sont accessibles avec la syntaxe de propriété :
echo $user->name; $user->email = 'new@example.com';
Pour une API, sérialisez uniquement les champs que le contrat public autorise. Ne retournez pas mécaniquement tous les attributs d’une table contenant mots de passe, jetons ou informations personnelles.
Relations
Relation un-à-plusieurs
final class User extends Model { protected static string $table = 'users'; public function posts(): array { return $this->hasMany(Post::class, 'user_id'); } }
Relation inverse
final class Post extends Model { protected static string $table = 'posts'; public function author(): ?Model { return $this->belongsTo(User::class, 'user_id'); } }
Les relations actuelles sont chargées explicitement. Pour éviter le problème N+1, mesurez le nombre de requêtes et utilisez une requête adaptée lorsque vous parcourez une collection importante. Le chargement anticipé et les relations plusieurs-à-plusieurs font partie des travaux restant à stabiliser.
Pagination
$page = User::query() ->orderBy('id') ->paginate(page: 1, perPage: 15); $payload = $page->toArray();
Forme sérialisée :
[
'data' => [...],
'page' => 1,
'total' => 50,
'perPage' => 15,
]
L’application doit imposer une limite maximale à perPage lorsqu’elle accepte ce paramètre depuis une requête publique.
Architecture interne
src/
Model.php cycle de vie Active Record
ModelQueryBuilder.php requêtes et hydratation typée
Pagination/
Paginator.php résultat paginé sérialisable
Le flux principal est :
Model::query()
-> QueryBuilder de velt/database
-> PDO avec paramètres liés
-> ligne associative
-> hydratation Model
-> objet ou collection applicative
Les détails sont présentés dans docs/orm-architecture.md.
Configuration de la connexion
L’ORM utilise la connexion active de velt/database. Dans une application Velt standard, le provider Database initialise cette connexion depuis la configuration :
DB_CONNECTION=sqlite DB_DATABASE=database/database.sqlite
Pour MySQL ou PostgreSQL, installez l’extension PDO appropriée et utilisez les variables d’environnement du skeleton. Les secrets ne doivent pas être committés dans .env.example ou dans les fixtures.
Transactions
Une opération métier qui écrit plusieurs modèles doit rester atomique :
DB::transaction(function () use ($payload): void { $user = User::create($payload['user']); Profile::create(['user_id' => $user->id] + $payload['profile']); });
Si la méthode transactionnelle n’est pas disponible dans la version de velt/database installée, utilisez explicitement la connexion PDO. Une future API ne doit pas simuler l’atomicité en masquant une absence de transaction.
Erreurs et observabilité
- une erreur SQL reste une exception et ne doit pas être convertie silencieusement en liste vide ;
- les logs peuvent inclure la durée et le nom logique de la requête, jamais les secrets liés ;
- une recherche sans résultat retourne
nulllorsque l’API le documente ; - la couche HTTP décide ensuite de produire un
404, pas l’ORM ; - les erreurs de schéma doivent être visibles pendant le développement.
Tests
composer install
composer validate --strict
composer test
La suite SQLite nécessite pdo_sqlite. Un test ignoré faute d’extension n’est pas une validation de cette base : la CI de release doit disposer du pilote et exécuter réellement le scénario.
La matrice cible couvre :
- PHP 8.2, 8.3 et 8.4 ;
- création, lecture, mise à jour et suppression ;
- valeurs nulles, booléens, dates et identifiants ;
- protection
fillable/guarded; - relations avec résultat vide ou clé absente ;
- pagination aux bornes ;
- rollback transactionnel ;
- SQLite, MySQL et PostgreSQL dans les tests d’intégration.
Performance
Active Record privilégie la commodité. Pour des imports massifs ou agrégations complexes, le query builder de velt/database est souvent plus approprié. Toute optimisation doit être étayée par un benchmark reproductible et conserver les requêtes préparées.
Sécurité
- ne concaténez jamais une valeur utilisateur dans un fragment SQL ;
- validez les identifiants dynamiques avec les primitives Database ;
- appliquez validation et autorisation avant l’affectation ;
- masquez les attributs sensibles dans les ressources API ;
- utilisez un compte de base de données aux privilèges minimaux ;
- signalez une vulnérabilité sans publier immédiatement un exploit.
Compatibilité et versionnement
Le package suit SemVer. Avant 1.0, toute préversion peut encore ajuster son API, mais un tag publié doit rester reproductible. velt/framework documente la combinaison de versions supportée entre Kernel, Database, ORM et Skeleton.
Limites avant une version stable
- casts typés et dates non finalisés ;
- événements de modèle et observers absents ;
- eager loading et prévention automatique du N+1 absents ;
- relations many-to-many non stabilisées ;
- stratégie de sérialisation sensible à formaliser ;
- matrice MySQL/PostgreSQL à automatiser en CI.
Contribution
Toute modification doit inclure un test de régression et préserver la séparation avec velt/database. Pour un changement public, documentez l’exemple, l’exception attendue, l’impact de compatibilité et la migration nécessaire. Les travaux planifiés sont suivis par les issues et milestones du dépôt.
Licence
Velt ORM est distribué sous licence MIT.