mongoose-studio / phobos-framework-database
Phobos Framework Database Layer
Installs: 8
Dependents: 1
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/mongoose-studio/phobos-framework-database
Requires
- php: >=8.3
- ext-pdo: *
- mongoose-studio/phobos-framework: ^3.0
Requires (Dev)
- mockery/mockery: ^1.6
- phpunit/phpunit: ^10.5
README
La capa de base de datos de Phobos Framework está diseñada como un componente standalone pensado para integrarse pero no depender del core (a excepción del DatabaseServiceProvider). Viene con un constructor de consultas encadenadas que hace las consultas más legibles y un ORM estilo Active Record para definir y trabajar con modelos de forma directa. Soporta múltiples conexiones simultáneas, transacciones anidadas y adaptadores personalizados, lo que te permite ajustar comportamiento y rendimiento según el caso.
Entre sus virtudes están el soporte para múltiples conexiones simultáneas, transacciones anidadas y la posibilidad de agregar adaptadores personalizados (si necesitas un driver especial o comportamiento distinto). En pocas palabras: te da control y rendimiento cuando lo necesitas, pero sin sacrificar legibilidad ni flexibilidad arquitectónica.
Características
- 🔍 Query Builder Fluido - Interfaz expresiva para SELECT, INSERT, UPDATE, DELETE
- 🏗️ ORM con Active Record - Entidades que combinan datos y operaciones de BD
- 🔄 Gestión de Transacciones - Soporte para transacciones anidadas con savepoints
- 🎯 Change Tracking - Las entidades rastrean cambios para optimizar UPDATEs
- 🔌 Múltiples Conexiones - Manejo de múltiples bases de datos simultáneamente
- 🗂️ Schema Aliasing - Mapeo de aliases para multi-tenant o multi-ambiente
- 🛡️ Prepared Statements - Todas las queries usan parameter binding para seguridad
- 💉 Integración con DI - Compatible con el Container de Phobos Framework
Instalación
composer require mongoose-studio/phobos-database
Configuración
1. Configuración de Base de Datos
Crea config/database.php:
<?php return [ 'default' => 'mysql', 'drivers' => [ 'mysql' => PhobosFramework\Database\Drivers\MySQLDriver::class, 'pgsql' => PhobosFramework\Database\Drivers\PostgreSQLDriver::class, ], 'connections' => [ 'mysql' => [ 'driver' => 'mysql', 'host' => env('DB_HOST', 'localhost'), 'port' => env('DB_PORT', '3306'), 'database' => env('DB_DATABASE', 'myapp'), 'username' => env('DB_USERNAME', 'root'), 'password' => env('DB_PASSWORD', ''), 'charset' => 'utf8mb4', 'collation' => 'utf8mb4_unicode_ci', ], 'analytics' => [ 'driver' => 'mysql', 'host' => env('ANALYTICS_DB_HOST', 'localhost'), 'database' => env('ANALYTICS_DB_DATABASE', 'analytics'), 'username' => env('ANALYTICS_DB_USERNAME', 'root'), 'password' => env('ANALYTICS_DB_PASSWORD', ''), ], ], ];
2. Registro del Service Provider (con Phobos Framework)
<?php namespace App; use PhobosFramework\Database\DatabaseServiceProvider; class AppModule implements ModuleInterface { public function providers(): array { return [ DatabaseServiceProvider::class, // ... otros providers ]; } }
3. Schema Aliasing (Opcional)
Para ambientes multi-tenant o multi-entorno:
// En bootstrap o service provider schemaAlias('app', 'myapp_production'); schemaAlias('analytics', 'myapp_analytics'); // O múltiples a la vez schemaBulkAlias([ 'app' => 'myapp_production', 'analytics' => 'myapp_analytics', 'tenant1' => 'tenant_abc_2024', ]);
Uso
Query Builder
SELECT Queries
// Query simple $users = query() ->select('id', 'username', 'email') ->from('users') ->where(['active = ?' => 1]) ->fetch(); // Con JOINs $posts = query() ->select('p.id', 'p.title', 'u.username', 'u.email') ->from('posts', 'p') ->innerJoin('users', 'u', 'u.id = p.user_id') ->where(['p.published = ?' => true]) ->orderBy('p.created_at DESC') ->limit(10) ->fetch(); // Con múltiples condiciones $results = query() ->select('*') ->from('products') ->where(['category = ?' => 'electronics']) ->where(['price > ?' => 100]) ->where(['stock > ?' => 0]) ->fetch(); // LIKE y operadores $search = query() ->select('*') ->from('articles') ->where(['title LIKE ?' => '%PHP%']) ->orWhere(['content LIKE ?' => '%framework%']) ->fetch(); // Subqueries $subquery = query() ->select('user_id') ->from('orders') ->where(['total > ?' => 1000]); $vipUsers = query() ->select('*') ->from('users') ->where(['id IN' => $subquery]) ->fetch(); // GROUP BY y HAVING $stats = query() ->select('category', 'COUNT(*) as total', 'AVG(price) as avg_price') ->from('products') ->groupBy('category') ->having(['COUNT(*) > ?' => 5]) ->fetch(); // UNION $query1 = query()->select('name')->from('customers'); $query2 = query()->select('name')->from('suppliers'); $all = $query1->union($query2)->fetch(); // Fetch único resultado $user = query() ->select('*') ->from('users') ->where(['id = ?' => 5]) ->fetchOne(); // Fetch columna específica $emails = query() ->select('email') ->from('users') ->fetchColumn('email'); // Contar resultados $count = query() ->select('COUNT(*) as total') ->from('users') ->where(['active = ?' => 1]) ->fetchOne()['total'];
INSERT Queries
// Insert simple insert() ->into('users') ->values([ 'username' => 'john_doe', 'email' => 'john@example.com', 'created_at' => date('Y-m-d H:i:s'), ]) ->execute(); // Insert múltiple insert() ->into('tags') ->values([ ['name' => 'PHP'], ['name' => 'JavaScript'], ['name' => 'Python'], ]) ->execute(); // Con conexión específica insert('analytics') ->into('events') ->values(['event' => 'page_view', 'timestamp' => time()]) ->execute();
UPDATE Queries
// Update simple update() ->table('users') ->set([ 'email' => 'newemail@example.com', 'updated_at' => date('Y-m-d H:i:s'), ]) ->where(['id = ?' => 5]) ->execute(); // Update con límite update() ->table('posts') ->set(['views' => 'views + 1']) // Expresión SQL ->where(['category = ?' => 'news']) ->limit(10) ->execute(); // Update múltiples condiciones update() ->table('products') ->set(['price' => 'price * 0.9']) // 10% descuento ->where(['category = ?' => 'electronics']) ->where(['stock > ?' => 0]) ->execute();
DELETE Queries
// Delete simple delete() ->from('sessions') ->where(['expires_at < ?' => date('Y-m-d H:i:s')]) ->execute(); // Delete con límite delete() ->from('logs') ->where(['level = ?' => 'debug']) ->limit(1000) ->execute(); // Delete con ORDER BY delete() ->from('notifications') ->where(['user_id = ?' => 123]) ->orderBy('created_at ASC') ->limit(50) ->execute();
Entidades (Active Record)
Definir una Entidad
<?php namespace App\Entities; use PhobosFramework\Database\Entity\TableEntity; class User extends TableEntity { // Configuración de la tabla protected static string $schema = 'app'; // Alias del schema protected static string $entity = 'users'; // Nombre de la tabla protected static array $pk = ['id']; // Primary key(s) protected static ?string $connection = null; // null = default connection // Propiedades (mapean a columnas) public int $id; public string $username; public string $email; public string $password; public bool $active; public ?string $created_at; public ?string $updated_at; }
Operaciones CRUD
CREATE:
$user = new User(); $user->username = 'juan_perez'; $user->email = 'juanperez@ejemplo.cl'; $user->password = password_hash('secret', PASSWORD_BCRYPT); $user->active = true; $user->created_at = date('Y-m-d H:i:s'); $user->save(); // INSERT en la BD echo $user->id; // ID auto-incrementado disponible
READ:
// Buscar por primary key $user = User::findByPk(5); // Buscar múltiples registros $activeUsers = User::find( ['active = ?' => true], 'username ASC', 0, 10 ); // Buscar primer resultado $admin = User::findFirst(['username = ?' => 'admin']); // Contar registros $totalUsers = User::count(['active = ?' => true]); // Verificar existencia $exists = User::exists(['email = ?' => 'test@example.com']);
UPDATE:
$user = User::findByPk(5); $user->email = 'newemail@example.com'; $user->updated_at = date('Y-m-d H:i:s'); $user->save(); // UPDATE automático (solo campos modificados) // Verificar si hay cambios if ($user->isDirty()) { $changes = $user->getDirtyFields(); // ['email', 'updated_at'] }
DELETE:
// Delete instancia $user = User::findByPk(5); $user->remove(); // Delete estático User::delete(['active = ?' => false], 100);
Change Tracking
$user = User::findByPk(5); // Estado original $original = $user->getOriginalData(); // Modificar $user->email = 'new@example.com'; $user->username = 'new_username'; // Verificar cambios if ($user->isDirty()) { $dirty = $user->getDirtyFields(); // ['email', 'username'] // Solo campos modificados en UPDATE $changes = $user->toArray(true); // ['email' => 'new@...', 'username' => 'new_...'] } $user->save(); // Solo actualiza campos modificados
Modo Dry-Run (Debug)
// Ver SQL sin ejecutar $dryRun = User::find(['active = ?' => true], 'id ASC', 0, 10, true); // Returns: ['query' => 'SELECT ...', 'bindings' => [1]] $user = new User(); $user->username = 'test'; $dryRun = $user->save(true); // Returns: ['query' => 'INSERT INTO ...', 'bindings' => ['test']]
Entidades de Vista (Read-Only)
<?php namespace App\Entities; use PhobosFramework\Database\Entity\ViewEntity; class UserStats extends ViewEntity { protected static string $schema = 'app'; protected static string $entity = 'v_user_statistics'; public int $user_id; public string $username; public int $total_posts; public int $total_comments; public float $avg_rating; } // Uso (solo lectura) $stats = UserStats::find(); $userStat = UserStats::findFirst(['user_id = ?' => 5]); // save() y remove() lanzarán LogicException
Stored Procedures
<?php namespace App\Entities; use PhobosFramework\Database\Entity\StoredProcedureEntity; class GenerateReport extends StoredProcedureEntity { protected static string $schema = 'app'; protected static string $entity = 'sp_generate_monthly_report'; // Parámetros del SP public int $month; public int $year; public string $report_type; } // Ejecutar $sp = new GenerateReport(); $sp->month = 10; $sp->year = 2024; $sp->report_type = 'sales'; $results = $sp->execute();
Transacciones
Transacciones Manuales
try { beginTransaction(); $user = new User(); $user->username = 'john'; $user->save(); $profile = new Profile(); $profile->user_id = $user->id; $profile->save(); commit(); } catch (\Exception $e) { rollback(); throw $e; }
Transacciones con Helper
$result = transaction(function() { $user = new User(); $user->username = 'john'; $user->save(); $profile = new Profile(); $profile->user_id = $user->id; $profile->save(); return $user; }); // Rollback automático si se lanza excepción
Transacciones Anidadas (Savepoints)
beginTransaction(); // Transacción real try { $user = new User(); $user->save(); beginTransaction(); // Savepoint sp_1 try { $profile = new Profile(); $profile->user_id = $user->id; $profile->save(); commit('sp_1'); // Commit savepoint } catch (\Exception $e) { rollback('sp_1'); // Rollback solo el savepoint } commit(); // Commit transacción principal } catch (\Exception $e) { rollback(); // Rollback todo } // Verificar estado if (inTransaction()) { $level = getTransactionLevel(); // Nivel de anidamiento }
Múltiples Conexiones
// Query con conexión específica $analyticsData = query('analytics') ->select('*') ->from('events') ->where(['date = ?' => date('Y-m-d')]) ->fetch(); // Entidad con conexión específica class AnalyticsEvent extends TableEntity { protected static string $connection = 'analytics'; protected static string $entity = 'events'; // ... } // Transacción en conexión específica transaction(function() { // Operaciones en analytics }, 'analytics'); // Obtener conexión directamente $pdo = db('analytics')->getPdo();
Helpers Globales
// Conexión $connection = db(?string $connection = null); // Query Builders $query = query(?string $connection = null); $insert = insert(?string $connection = null); $update = update(?string $connection = null); $delete = delete(?string $connection = null); // Transacciones beginTransaction(?string $connection = null); commit(?string $savepoint = null, ?string $connection = null); rollback(?string $savepoint = null, ?string $connection = null); transaction(callable $callback, ?string $connection = null); inTransaction(?string $connection = null): bool; getTransactionLevel(?string $connection = null): int; // Schema Registry schemaAlias(string $alias, string $realSchema); schemaBulkAlias(array $aliases);
Arquitectura
Componentes Principales
Connection Layer
ConnectionManager: Singleton que gestiona múltiples conexionesPDOConnection: Implementación basada en PDOTransactionManager: Manejo de transacciones con savepoints- Lazy loading: conexiones se crean solo cuando se usan
Query Builder
QueryBuilder: Interfaz fluida para SELECT con joins, subqueries, unionsInsertQuery,UpdateQuery,DeleteQuery: Builders especializadosClauses/: Implementaciones individuales (WHERE, JOIN, ORDER BY, etc.)- Prepared statements automáticos para seguridad
Entity System
EntityManager: Clase base con hydration y change trackingTableEntity: Active Record para tablas con CRUDViewEntity: Entidades read-only para vistasStoredProcedureEntity: Ejecución de stored procedures- State tracking: new vs persisted, dirty fields, valores originales
Schema Registry
SchemaRegistry: Singleton para mapeo de aliases- Permite cambiar schemas sin modificar código de entidades
Integración con Phobos Framework
Cuando se usa con Phobos Framework:
- Registra
DatabaseServiceProvideren tu módulo - El provider lee
config('database')para configuración - Servicios registrados en el Container:
db→ ConnectionInterface por defectodb.manager→ ConnectionManagerdb.transaction→ TransactionManager
- Acceso vía inyección de dependencias o helpers
// En un controller class UserController { public function __construct( private ConnectionInterface $db, private TransactionManager $transactions ) {} public function index() { $users = User::find(['active = ?' => true]); return Response::json($users); } }
Notas Importantes
- PHP 8.3+ requerido - Usa typed properties, union types, named arguments
- Propiedades reservadas - No uses:
_isNew,_original,_dirty,_reserved,schema,entity,pk - Change tracking automático -
__set()marca campos como dirty automáticamente - Prepared statements siempre - Todas las queries usan parameter binding
- Lazy loading - Conexiones se crean solo cuando se usan por primera vez
- Sin ORM completo - Es Active Record, no un ORM completo como Doctrine
- Schema aliasing - Útil para multi-tenant, multi-ambiente, o testing
Testing
Para testing de desarrollo:
{
"repositories": [
{
"type": "path",
"url": "../phobos-database"
}
],
"require": {
"mongoose-studio/phobos-database": "*"
}
}
Licencia
MIT License - ver el archivo LICENSE para más detalles.
Autor
Marcel Rojas
marcelrojas16@gmail.com
Mongoose Studio
Contribuciones
Las contribuciones son bienvenidas. Por favor:
- Fork el proyecto
- Crea una rama para tu feature (
git checkout -b feature/amazing-feature) - Commit tus cambios (
git commit -m 'Add amazing feature') - Push a la rama (
git push origin feature/amazing-feature) - Abre un Pull Request
Phobos Framework by Mongoose Studio