
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.
- 🔍 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
composer require mongoose-studio/phobos-database
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', ''),
],
],
];
<?php
namespace App;
use PhobosFramework\Database\DatabaseServiceProvider;
class AppModule implements ModuleInterface
{
public function providers(): array
{
return [
DatabaseServiceProvider::class,
// ... otros providers
];
}
}
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',
]);
// 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 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 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 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();
<?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;
}
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);
$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
// 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']]
<?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
<?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();
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;
}
$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
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
}
// 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();
// 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);
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
Cuando se usa con Phobos Framework:
- Registra
DatabaseServiceProvider
en 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);
}
}
- 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
Para testing de desarrollo:
{
"repositories": [
{
"type": "path",
"url": "../phobos-database"
}
],
"require": {
"mongoose-studio/phobos-database": "*"
}
}
MIT License - ver el archivo LICENSE para más detalles.
Marcel Rojas
marcelrojas16@gmail.com
Mongoose Studio
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